From 296dd91005af8ae672c2a57a399421cbd7624d19 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 18 Oct 2023 00:33:34 +0200 Subject: [PATCH 001/771] buildsys: Fix version tag formatting not accepted by regex. --- buildsystem/DetectProjectVersion.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildsystem/DetectProjectVersion.cmake b/buildsystem/DetectProjectVersion.cmake index 8c45b1db64..67782509ec 100644 --- a/buildsystem/DetectProjectVersion.cmake +++ b/buildsystem/DetectProjectVersion.cmake @@ -12,7 +12,7 @@ endif() if(IS_DIRECTORY "${CMAKE_SOURCE_DIR}/.git") message(STATUS "Set PROJECT_VERSION from git.") execute_process( - COMMAND git describe --tags + COMMAND git describe --tags --long WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" RESULT_VARIABLE _RES OUTPUT_VARIABLE PROJECT_VERSION From 80eb80f5757ca92a6de65ba186b3db9578fcb21a Mon Sep 17 00:00:00 2001 From: fabiobarkoski Date: Fri, 20 Oct 2023 19:22:24 -0300 Subject: [PATCH 002/771] doc: Fix typos in converter fixed typos in the architecture and workflow files --- doc/code/converter/architecture_overview.md | 2 +- doc/code/converter/workflow.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/code/converter/architecture_overview.md b/doc/code/converter/architecture_overview.md index 9247952246..22f46d1695 100644 --- a/doc/code/converter/architecture_overview.md +++ b/doc/code/converter/architecture_overview.md @@ -30,7 +30,7 @@ Value objects are used to store primitive data or as definition of how to read p In the converter, the parsers utilize these objects to extract attributes from the original Genie Engine file formats. Extracted attributes are also saved as value objects. -Value objects are treated as *immutable*. Operations on the objects's values will therefore always return +Value objects are treated as *immutable*. Operations on the objects values will therefore always return a new object instance with the result and leave the original values as-is. ### Entity Object diff --git a/doc/code/converter/workflow.md b/doc/code/converter/workflow.md index 5ddd2db621..b4dc4ead9e 100644 --- a/doc/code/converter/workflow.md +++ b/doc/code/converter/workflow.md @@ -66,7 +66,7 @@ content can be accessed in the same way as "loose" files from the source folder. After all relevant source folders are mounted, the converter will begin the main conversion process by reading the available data. Every source file format has -its own reader that is adpated to the constraints and features of the format. +its own reader that is adapted to the constraints and features of the format. However, reading always follows this general workflow: * **Registration**: Enumerates all files with the requested format. @@ -100,7 +100,7 @@ attribute values (e.g. `"attack"`). For example, a `uint32_t` value could be used for a normal integer value or define an ID to a graphics/sound resource. Every unique value type is associated with a Python object type used for storing the attribute. -* **Output mode**: Dtetermines whether the attribute is part of the reader output +* **Output mode**: Determines whether the attribute is part of the reader output or can be skipped (e.g. `SKIP`). The Reader parses attributes one by one and stores them in a `ValueMember` subclass @@ -162,7 +162,7 @@ In general, it involves these 3 steps: 1. Check if a concept group has a certain property 2. If true, create and assign nyan API objects associated with that property -3. Map values from concept group data to the objects' member values +3. Map values from concept group data to the objects member values This is repeated for every property and for every concept group. Most values can be mapped 1-to-1, although some require additional property checks. @@ -173,7 +173,7 @@ that contains the ID and the desired target filename. In the Export stage, the source filename for the given ID is then resolved and the file is parsed, converted and saved at the target location. -At the end of the mappping stage, the resulting nyan objects are put into nyan files +At the end of the mapping stage, the resulting nyan objects are put into nyan files and -- together will the media export requests -- are organized into modpacks which are passed to the exporter. From 08c4521569f73d7329967120fc9c2ec5725b9868 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 22 Oct 2023 17:44:49 +0200 Subject: [PATCH 003/771] assets: Remove dangling refs in modpack info load. --- libopenage/assets/modpack.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libopenage/assets/modpack.cpp b/libopenage/assets/modpack.cpp index acc3865696..e2854280ed 100644 --- a/libopenage/assets/modpack.cpp +++ b/libopenage/assets/modpack.cpp @@ -15,7 +15,7 @@ ModpackInfo parse_modepack_def(const util::Path &info_file) { const auto modpack_def = toml::parse(info_file.resolve_native_path()); // info table - const toml::table &info = toml::find(modpack_def, "info"); + const toml::table info = toml::find(modpack_def, "info"); if (info.contains("name")) { def.id = info.at("name").as_string(); } @@ -63,7 +63,7 @@ ModpackInfo parse_modepack_def(const util::Path &info_file) { } // assets table - const toml::table &assets = toml::find(modpack_def, "assets"); + const toml::table assets = toml::find(modpack_def, "assets"); std::vector includes{}; for (const auto &include : assets.at("include").as_array()) { includes.push_back(include.as_string()); @@ -81,7 +81,7 @@ ModpackInfo parse_modepack_def(const util::Path &info_file) { // dependency table if (modpack_def.contains("dependency")) { - const toml::table &dependency = toml::find(modpack_def, "dependency"); + const toml::table dependency = toml::find(modpack_def, "dependency"); std::vector deps{}; if (not dependency.contains("modpacks")) { @@ -97,7 +97,7 @@ ModpackInfo parse_modepack_def(const util::Path &info_file) { // conflicts table if (modpack_def.contains("conflict")) { - const toml::table &conflict = toml::find(modpack_def, "conflict"); + const toml::table conflict = toml::find(modpack_def, "conflict"); std::vector conflicts{}; if (not conflict.contains("modpacks")) { @@ -113,7 +113,7 @@ ModpackInfo parse_modepack_def(const util::Path &info_file) { // authors table if (modpack_def.contains("authors")) { - const toml::table &authors = toml::find(modpack_def, "authors"); + const toml::table authors = toml::find(modpack_def, "authors"); std::vector author_infos{}; for (const auto &author : authors) { AuthorInfo author_info{}; @@ -143,7 +143,7 @@ ModpackInfo parse_modepack_def(const util::Path &info_file) { author_info.roles = roles; } if (author.second.contains("contact")) { - const toml::table &contact = toml::find(author.second, "contact"); + const toml::table contact = toml::find(author.second, "contact"); std::unordered_map contacts{}; for (const auto &contact_info : contact) { contacts[contact_info.first] = contact_info.second.as_string(); @@ -158,7 +158,7 @@ ModpackInfo parse_modepack_def(const util::Path &info_file) { // authorgroups table if (modpack_def.contains("authorgroups")) { - const toml::table &authorgroups = toml::find(modpack_def, "authorgroups"); + const toml::table authorgroups = toml::find(modpack_def, "authorgroups"); std::vector author_group_infos{}; for (const auto &authorgroup : authorgroups) { AuthorGroupInfo author_group_info{}; From bcacaccdfd4dc56dfb74356a0e571f35f8b7d6a8 Mon Sep 17 00:00:00 2001 From: Leet <36166244+leetfin@users.noreply.github.com> Date: Mon, 23 Oct 2023 17:29:06 -0400 Subject: [PATCH 004/771] Use gender-neutral language --- doc/ideas/gameplay.md | 4 ++-- doc/media/original-metadata.md | 2 +- doc/nyan/api_reference/reference_resistance.md | 2 +- doc/reverse_engineering/game_mechanics/formations.md | 2 +- doc/reverse_engineering/game_mechanics/monk_conversion.md | 2 +- doc/reverse_engineering/game_mechanics/selection.md | 4 ++-- .../game_mechanics/switching_villager_tasks.md | 2 +- doc/reverse_engineering/game_mechanics/town_bell.md | 2 +- doc/reverse_engineering/game_mechanics/wolves.md | 2 +- doc/reverse_engineering/networking/02-header.md | 2 +- .../networking/06-chat_message_spoofing.md | 2 +- doc/reverse_engineering/networking/08-movement.md | 2 +- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/doc/ideas/gameplay.md b/doc/ideas/gameplay.md index e4e26becd2..29e25e2532 100644 --- a/doc/ideas/gameplay.md +++ b/doc/ideas/gameplay.md @@ -144,7 +144,7 @@ A mode similar to *Trouble in Terrorist Town* and *Secret Hitler*. The game star ### Pure Battle Mode -No buildings, just units. The game generates a map and players can choose a starting position. Then they have a few minutes and a set amount of resources to select an army composition and some techs. After the first phase is over they place their units on the battlefield and have to use what they assembled to destroy their opponent. Utilizing height advantages, microing and tactical positioning contrast the strategic decisions of creating the army. The player who destroys his opponent, inflicts the most resource damage to others or holds strategic positions wins the battle. +No buildings, just units. The game generates a map and players can choose a starting position. Then they have a few minutes and a set amount of resources to select an army composition and some techs. After the first phase is over they place their units on the battlefield and have to use what they assembled to destroy their opponent. Utilizing height advantages, microing and tactical positioning contrast the strategic decisions of creating the army. The player who destroys their opponent, inflicts the most resource damage to others or holds strategic positions wins the battle. ### Micro-nerd Mode (or Mod) @@ -373,7 +373,7 @@ Relics & Kings - e.g. they can have attack bonuses for special units or economic/military bonuses - special abilities for every relic could either be generated when the map is generated or when the relic is discovered based on the actual needs of the player - so the later you go out to get the relic the more it could get useful for you, because it could be better shaped on your personal military/economy but the risk is higher, that another player was going out before you - - if a player scouts the relic first and the ability gets generated in this moment, it will be for the player who scouted it first, so he knows, that this relic could help their own economy/military alot so the player will try to fight about this relic against the enemy heavily -> new gameplay aspect + - if a player scouts the relic first and the ability gets generated in this moment, it will be for the player who scouted it first, so they know, that this relic could help their own economy/military a lot so the player will try to fight about this relic against the enemy heavily -> new gameplay aspect - Relics could have ranged attributes - idea of the devs of AoE II diff --git a/doc/media/original-metadata.md b/doc/media/original-metadata.md index 5e5540fe44..b29583b44a 100644 --- a/doc/media/original-metadata.md +++ b/doc/media/original-metadata.md @@ -2,7 +2,7 @@ Original Metadata ================= All data relevant for the game (e.g. how much costs building the castle? -What cultures exist? Can my priest overclock his "Wololo?") +What cultures exist? Can my priest overclock their "Wololo?") are stored in a binary format in the file `empires2_x1_p1.dat`. The format is described in the [huge struct definition](/openage/convert/value_object/read/media/datfile/empiresdat.py). diff --git a/doc/nyan/api_reference/reference_resistance.md b/doc/nyan/api_reference/reference_resistance.md index e38f838119..1a0b7a5344 100644 --- a/doc/nyan/api_reference/reference_resistance.md +++ b/doc/nyan/api_reference/reference_resistance.md @@ -317,7 +317,7 @@ Resistance to the `MakeHarvestable` effect. Resource spot that should be made harvestable. Effects of type `effect.discrete.make_harvestable.type.MakeHarvestable` are matched to this resistance if they store the same `ResourceSpot` object in their `resource_spot` member. Additionally, the target needs to have a `Harvestable` ability that contains the resource spot. **resist_condition** -Condition which must he fulfilled to make the resource spot harvestable. +Condition which they must fulfill to make the resource spot harvestable. ## resistance.discrete.send_to_container.type.SendToContainer diff --git a/doc/reverse_engineering/game_mechanics/formations.md b/doc/reverse_engineering/game_mechanics/formations.md index dd39371ccd..b93de426e1 100644 --- a/doc/reverse_engineering/game_mechanics/formations.md +++ b/doc/reverse_engineering/game_mechanics/formations.md @@ -123,7 +123,7 @@ The same rules apply, if more than two unit types are used. We will now have a l .............. ``` -Which unit type is sorted into the line first depends on the order in which the player selected the unit types (or how they are ordered inside the selection queue). In the first example of this section, the player selected the archers first and added the skirmishers to his selection. The second and above example would be the result of selecting longbowman first, archers second, skirmishers third and throwing axeman last. +Which unit type is sorted into the line first depends on the order in which the player selected the unit types (or how they are ordered inside the selection queue). In the first example of this section, the player selected the archers first and added the skirmishers to their selection. The second and above example would be the result of selecting longbowman first, archers second, skirmishers third and throwing axeman last. #### Distance Between Units diff --git a/doc/reverse_engineering/game_mechanics/monk_conversion.md b/doc/reverse_engineering/game_mechanics/monk_conversion.md index 583cb61ce1..cf101a942f 100644 --- a/doc/reverse_engineering/game_mechanics/monk_conversion.md +++ b/doc/reverse_engineering/game_mechanics/monk_conversion.md @@ -32,5 +32,5 @@ Units inside a building are not converted and will ungarrison. The same is true [Source](https://www.youtube.com/watch?v=_gjpDWfzaM0) * If a converted unit is "replaced" by the game, e.g. a villager changing to a farmer, the stats are not frozen anymore. -* Converted siege units are only frozen in tech level until the new owner researches a technology that changes the units stats, e.g. chemistry. As soon as the research finishes, the units are replaced with ones that are on the same tech level as the player. Weirdly enough, Onagers are replaced if the player researches Heavy Scorpions but not if he researches Siege/Capped Rams. +* Converted siege units are only frozen in tech level until the new owner researches a technology that changes the units stats, e.g. chemistry. As soon as the research finishes, the units are replaced with ones that are on the same tech level as the player. Weirdly enough, Onagers are replaced if the player researches Heavy Scorpions but not if they research Siege/Capped Rams. * The flaming projectile caused by chemistry research is tied to the tech level of the owner but the +1 damage is kept, if the unit is converted. diff --git a/doc/reverse_engineering/game_mechanics/selection.md b/doc/reverse_engineering/game_mechanics/selection.md index de08156fff..0016ed4ea4 100644 --- a/doc/reverse_engineering/game_mechanics/selection.md +++ b/doc/reverse_engineering/game_mechanics/selection.md @@ -8,7 +8,7 @@ This documents shows the methods of selecting and deselecting units. The player doesn't have to click directly on the sprite of a unit because there is a tolerance factor involved. The tolerance factor is roughly `1 unit width` horizontally and `1 unit height` vertically measured from the center of the unit. This means that clicking in the general area of a unit will be accepted as a valid selection most of the time. -If two units' areas of tolerance overlap, the unit that is in the front seems to be preferred, but clicking directly on the sprite of one of the player's own units will select the unit that is pointed at. When the area of tolerance overlaps with an enemy (or ally) unit, the unit of the player will always be preferred, even if he clicks directly on the sprite of the enemy unit. +If two units' areas of tolerance overlap, the unit that is in the front seems to be preferred, but clicking directly on the sprite of one of the player's own units will select the unit that is pointed at. When the area of tolerance overlaps with an enemy (or ally) unit, the unit of the player will always be preferred, even if they click directly on the sprite of the enemy unit. Relics and animals also have an area of tolerance regarding selection, while resource spots like trees, bushes, gold/stone mines as well as buildings don't. @@ -25,7 +25,7 @@ All of these have a specific purpose and will now be explained further. ### Selection Box -The selection box is the easiest way to select multiple units of any type in AoE2. When the player draws the box around the units he wants to select, the units inside the box will be added to the selection queue, **going from the top to the bottom of the box** until the limit of 40 units is reached. +The selection box is the easiest way to select multiple units of any type in AoE2. When the player draws the box around the units they want to select, the units inside the box will be added to the selection queue, **going from the top to the bottom of the box** until the limit of 40 units is reached. The tolerance factor is also used here, which can result in units which are slightly outside of the selection box to be selected. diff --git a/doc/reverse_engineering/game_mechanics/switching_villager_tasks.md b/doc/reverse_engineering/game_mechanics/switching_villager_tasks.md index 8e1db4838f..b82c84ae69 100644 --- a/doc/reverse_engineering/game_mechanics/switching_villager_tasks.md +++ b/doc/reverse_engineering/game_mechanics/switching_villager_tasks.md @@ -22,4 +22,4 @@ Note: This only happens if the builder was actively working on the construction ## Weird AoE2 Quirks -* If the cheat `aegis` (villagers gather instantly from a resource) is used in a game and the role of a villager changes from "hunter" to "shepard", he will dump all of the resources he is carrying "into" the sheep. +* If the cheat `aegis` (villagers gather instantly from a resource) is used in a game and the role of a villager changes from "hunter" to "shepard", they will dump all of the resources they are carrying "into" the sheep. diff --git a/doc/reverse_engineering/game_mechanics/town_bell.md b/doc/reverse_engineering/game_mechanics/town_bell.md index c3ee341498..c8748b061c 100644 --- a/doc/reverse_engineering/game_mechanics/town_bell.md +++ b/doc/reverse_engineering/game_mechanics/town_bell.md @@ -48,4 +48,4 @@ As soon as the town bell is triggered, the algorithm searches for villagers that In the next step, the algorithm tries to find the nearest garrison for a villager by calculating the distances between the villager and all buildings on the second list. The closest distance calculated and the corresponding garrison are then saved. Afterwards the villager is put into a third list, which is sorted by the *closest distance to any garrison*. This step is repeated for every villager. -Last but not least, the villagers are assigned to a garrison. Since the third list is sorted by closest distance, villagers that are the nearest to a building are guaranteed to get a place, which satisfies the condition that towers prefer the villagers close to them. If the villager is assigned to a building which is already full, the algorithm recalculates the distances to other garrisons and sorts him back into the list. +Last but not least, the villagers are assigned to a garrison. Since the third list is sorted by closest distance, villagers that are the nearest to a building are guaranteed to get a place, which satisfies the condition that towers prefer the villagers close to them. If the villager is assigned to a building which is already full, the algorithm recalculates the distances to other garrisons and sorts them back into the list. diff --git a/doc/reverse_engineering/game_mechanics/wolves.md b/doc/reverse_engineering/game_mechanics/wolves.md index 667a219956..9c467a1cd4 100644 --- a/doc/reverse_engineering/game_mechanics/wolves.md +++ b/doc/reverse_engineering/game_mechanics/wolves.md @@ -14,7 +14,7 @@ Line of sight depends on the selected difficulty of the game. Hard 12 tiles Hardest 12 tiles -As soon as a unit moves into the LOS of a wolf, he will chase and attack the unit. However some unit types are ignored, including: +As soon as a unit moves into the LOS of a wolf, they will chase and attack the unit. However some unit types are ignored, including: * King * Trade Cart diff --git a/doc/reverse_engineering/networking/02-header.md b/doc/reverse_engineering/networking/02-header.md index 4e7e57e67b..a361f5a4e4 100644 --- a/doc/reverse_engineering/networking/02-header.md +++ b/doc/reverse_engineering/networking/02-header.md @@ -20,7 +20,7 @@ end ## Description *:network_source_id*
-The *:network_id* of the person who sent the packet. A *:network_id* is different for every game, but is not generated randomly for all players. When joining the lobby, every player gets assigned `last_network_id - 2` as his own *:network_id* where *last_network_id* is the ID of the person who joined before him. +The *:network_id* of the person who sent the packet. A *:network_id* is different for every game, but is not generated randomly for all players. When joining the lobby, every player gets assigned `last_network_id - 2` as their own *:network_id* where *last_network_id* is the ID of the person who joined before them. *:network_dest_id*
The *:network_id* of the person who should receive the packet. Is only used for sync packets and remains unused for most commands. diff --git a/doc/reverse_engineering/networking/06-chat_message_spoofing.md b/doc/reverse_engineering/networking/06-chat_message_spoofing.md index 4c4d15092c..75d0516b08 100644 --- a/doc/reverse_engineering/networking/06-chat_message_spoofing.md +++ b/doc/reverse_engineering/networking/06-chat_message_spoofing.md @@ -23,7 +23,7 @@ To construct a message the following parameters are needed: * Player number of the spoofed sender * Player number of the receiver(s) -An attacker that is in game with other players will have no problems getting to know the player numbers. As the game is constantly synced, he can easily get the value for *:communication_turn*. +An attacker that is in game with other players will have no problems getting to know the player numbers. As the game is constantly synced, they can easily get the value for *:communication_turn*. Deriving the valid Sender ID of the receiving player is more difficult and depends on the attacker's ability to capture network traffic. The easiest way to discover all Player IDs is by capturing a few packets of normal gameplay beforehand. The IDs of Players 1-8 have a fixed byte position in the data stream. diff --git a/doc/reverse_engineering/networking/08-movement.md b/doc/reverse_engineering/networking/08-movement.md index 4909d82413..6c52684e60 100644 --- a/doc/reverse_engineering/networking/08-movement.md +++ b/doc/reverse_engineering/networking/08-movement.md @@ -74,7 +74,7 @@ end Always has the value `0x03`. *:player_id*
-The ID of the player who moves his units (`0x01` - `0x08`). +The ID of the player who moves their units (`0x01` - `0x08`). *:zero*
The two bytes following the *:player_id* are unused. From db6fa9a8e6447da751636983f304bf1f6a62f46a Mon Sep 17 00:00:00 2001 From: Leet <36166244+leetfin@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:40:00 -0400 Subject: [PATCH 005/771] Fix typo --- doc/nyan/api_reference/reference_resistance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/nyan/api_reference/reference_resistance.md b/doc/nyan/api_reference/reference_resistance.md index 1a0b7a5344..61a2aa06cb 100644 --- a/doc/nyan/api_reference/reference_resistance.md +++ b/doc/nyan/api_reference/reference_resistance.md @@ -317,7 +317,7 @@ Resistance to the `MakeHarvestable` effect. Resource spot that should be made harvestable. Effects of type `effect.discrete.make_harvestable.type.MakeHarvestable` are matched to this resistance if they store the same `ResourceSpot` object in their `resource_spot` member. Additionally, the target needs to have a `Harvestable` ability that contains the resource spot. **resist_condition** -Condition which they must fulfill to make the resource spot harvestable. +Condition which must be fulfilled to make the resource spot harvestable. ## resistance.discrete.send_to_container.type.SendToContainer From 682f52487a7d5276dcee3b77d6c6d477e7650f46 Mon Sep 17 00:00:00 2001 From: zoli111 Date: Sat, 28 Oct 2023 20:55:44 +0200 Subject: [PATCH 006/771] Fix typos --- openage/convert/entity_object/conversion/ror/genie_unit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openage/convert/entity_object/conversion/ror/genie_unit.py b/openage/convert/entity_object/conversion/ror/genie_unit.py index 86e6b4986c..c2fe46267f 100644 --- a/openage/convert/entity_object/conversion/ror/genie_unit.py +++ b/openage/convert/entity_object/conversion/ror/genie_unit.py @@ -50,7 +50,7 @@ def __init__( def is_garrison(self, civ_id: int = -1) -> bool: """ - Only transport shis can garrison in RoR. + Only transport ships can garrison in RoR. :returns: True if the unit has the unload command (ID: 12). """ @@ -229,7 +229,7 @@ def __init__( def is_garrison(self, civ_id: int = -1) -> bool: """ - Only transport shis can garrison in RoR. + Only transport ships can garrison in RoR. :returns: True if the unit has the unload command (ID: 12). """ From 3bd0674baa59ef268473b498ab55aec0c157c878 Mon Sep 17 00:00:00 2001 From: Ashhar-24 Date: Wed, 1 Nov 2023 02:21:24 +0530 Subject: [PATCH 007/771] util: Temporary file/directory support. --- copying.md | 1 + openage/util/fslike/path.py | 53 +++++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/copying.md b/copying.md index 310076a011..e1df2d97bb 100644 --- a/copying.md +++ b/copying.md @@ -148,6 +148,7 @@ _the openage authors_ are: | Zoltán Ács | zoli111 | acszoltan111 à gmail dawt com | | Trevor Slocum | tslocum | trevor à rocket9labs dawt com | | Munawar Hafiz | munahaf | munawar dawt hafiz à gmail dawt com | +| Md Ashhar | ashhar | mdashhar01 à gmail dawt com | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. diff --git a/openage/util/fslike/path.py b/openage/util/fslike/path.py index f69479360e..7532ec3e0d 100644 --- a/openage/util/fslike/path.py +++ b/openage/util/fslike/path.py @@ -4,9 +4,12 @@ Provides Path, which is analogous to pathlib.Path, and the type of FSLikeObject.root. """ +from typing import NoReturn from io import UnsupportedOperation, TextIOWrapper -from typing import NoReturn +import os +import pathlib +import tempfile class Path: @@ -32,7 +35,7 @@ class Path: # lower. # pylint: disable=too-many-public-methods - def __init__(self, fsobj, parts=None): + def __init__(self, fsobj, parts: str | bytes | bytearray | list | tuple = None): if isinstance(parts, str): parts = parts.encode() @@ -63,6 +66,9 @@ def __init__(self, fsobj, parts=None): self.fsobj = fsobj + # Set to True by create_temp_file or create_temp_dir + self.is_temp: bool = False + # use tuple instead of list to prevent accidential modification self.parts = tuple(result) @@ -330,3 +336,46 @@ def mount(self, pathobj, priority=0) -> NoReturn: # pylint: disable=no-self-use,unused-argument # TODO: https://github.com/PyCQA/pylint/issues/2329 raise PermissionError("Do not call mount on Path instances!") + + @staticmethod + def get_temp_file(): + """ + Creates a temporary file. + """ + temp_fd, temp_file = tempfile.mkstemp() + + # Close the file descriptor to release resources + os.close(temp_fd) + + # Wrap the temporary file path in a Path object and return it + path = Path(pathlib.Path(temp_file)) + path.is_temp = True + + return path + + @staticmethod + def get_temp_dir(): + """ + Creates a temporary directory. + """ + # Create a temporary directory using tempfile.mkdtemp + temp_dir = tempfile.mkdtemp() + + # Wrap the temporary directory path in a Path object and return it + path = Path(pathlib.Path(temp_dir)) + path.is_temp = True + + return path + + def __del__(self): + """ + Destructor used for temp files and directories. + """ + if self.is_temp: + # Cleanup temp file + if self.exists(): + if self.is_file(): + self.unlink() + + elif self.is_dir(): + self.removerecursive() From 2e10795d7b3649d0870efb60196abc5649555a35 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 1 Nov 2023 01:42:24 +0100 Subject: [PATCH 008/771] util: Remove __del__ because it doesn't work as destructor. --- openage/util/fslike/path.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/openage/util/fslike/path.py b/openage/util/fslike/path.py index 7532ec3e0d..c34a5d6a6c 100644 --- a/openage/util/fslike/path.py +++ b/openage/util/fslike/path.py @@ -366,16 +366,3 @@ def get_temp_dir(): path.is_temp = True return path - - def __del__(self): - """ - Destructor used for temp files and directories. - """ - if self.is_temp: - # Cleanup temp file - if self.exists(): - if self.is_file(): - self.unlink() - - elif self.is_dir(): - self.removerecursive() From fdba30869b37fe56f84dc43e88417fc932a9c66b Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 1 Nov 2023 03:40:37 +0100 Subject: [PATCH 009/771] util: Append mode for paths/unions. --- openage/util/fslike/path.py | 4 ++++ openage/util/fslike/union.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/openage/util/fslike/path.py b/openage/util/fslike/path.py index c34a5d6a6c..422b9f176b 100644 --- a/openage/util/fslike/path.py +++ b/openage/util/fslike/path.py @@ -149,6 +149,10 @@ def open_w(self): """ open with mode='wb' """ return self.fsobj.open_w(self.parts) + def open_a(self): + """ open with mode='ab' """ + return self.fsobj.open_a(self.parts) + def _get_native_path(self): """ return the native path (usable by your kernel) of this path, diff --git a/openage/util/fslike/union.py b/openage/util/fslike/union.py index eb28129d01..7b2ca8a8f2 100644 --- a/openage/util/fslike/union.py +++ b/openage/util/fslike/union.py @@ -116,6 +116,14 @@ def open_w(self, parts): raise UnsupportedOperation( "not writable: " + b'/'.join(parts).decode(errors='replace')) + def open_a(self, parts): + for path in self.candidate_paths(parts): + if path.writable(): + return path.open_a() + + raise UnsupportedOperation( + "not appendable: " + b'/'.join(parts).decode(errors='replace')) + def resolve_r(self, parts): for path in self.candidate_paths(parts): if path.is_file() or path.is_dir(): From 6c48b8437a9fcfe7ff87b5adc84260ec450ee8ec Mon Sep 17 00:00:00 2001 From: fabiobarkoski Date: Sat, 28 Oct 2023 00:46:53 -0300 Subject: [PATCH 010/771] Add condition for SLD files added condition to now supported SLD files --- openage/convert/processor/export/media_exporter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 8984cbd4bf..1bb6c4faa0 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -462,6 +462,10 @@ def _get_media_cache( from ...value_object.read.media.smx import SMX image = SMX(media_file.read()) + elif source_file.suffix.lower() == ".sld": + from ...value_object.read.media.sld import SLD + image = SLD(media_file.read()) + from .texture_merge import merge_frames texture = Texture(image, palettes) merge_frames(texture) From 40c5b7e4fbd10dd7f7334b281e55cc376317c430 Mon Sep 17 00:00:00 2001 From: fabiobarkoski Date: Sat, 28 Oct 2023 00:48:48 -0300 Subject: [PATCH 011/771] Create debug output for execution time per stage and not found sounds created debug output for sounds not found on exporting stage and for execution time for each stage --- copying.md | 1 + .../processor/export/media_exporter.py | 12 +++++ openage/convert/service/debug_info.py | 44 +++++++++++++++++++ openage/convert/tool/driver.py | 20 ++++++++- 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/copying.md b/copying.md index e1df2d97bb..b524367fb0 100644 --- a/copying.md +++ b/copying.md @@ -149,6 +149,7 @@ _the openage authors_ are: | Trevor Slocum | tslocum | trevor à rocket9labs dawt com | | Munawar Hafiz | munahaf | munawar dawt hafiz à gmail dawt com | | Md Ashhar | ashhar | mdashhar01 à gmail dawt com | +| Fábio Barkoski | fabiobarkoski | fabiobarkoskii à gmail dawt com | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 1bb6c4faa0..7005e262c9 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -80,6 +80,8 @@ def export( info("-- Exporting graphics files...") elif media_type is MediaType.SOUNDS: + kwargs["loglevel"] = args.debug_info + kwargs["debugdir"] = args.debugdir export_func = MediaExporter._export_sound info("-- Exporting sound files...") @@ -225,6 +227,10 @@ def _export_graphics( from ...value_object.read.media.sld import SLD image = SLD(media_file.read()) + else: + raise SyntaxError(f"Source file {source_file.name} has an unrecognized extension: " + f"{source_file.suffix.lower()}") + packer_cache = None compr_cache = None if cache_info: @@ -284,6 +290,7 @@ def _export_sound( export_request: MediaExportRequest, sourcedir: Path, exportdir: Path, + **kwargs ) -> None: """ Convert and export a sound file. @@ -308,6 +315,7 @@ def _export_sound( else: # TODO: Filter files that do not exist out sooner + debug_info.debug_not_found_sounds(kwargs["debugdir"], kwargs["loglevel"], source_file) return from ...service.export.opus.opusenc import encode @@ -466,6 +474,10 @@ def _get_media_cache( from ...value_object.read.media.sld import SLD image = SLD(media_file.read()) + else: + raise SyntaxError(f"Source file {source_file.name} has an unrecognized extension: " + f"{source_file.suffix.lower()}") + from .texture_merge import merge_frames texture = Texture(image, palettes) merge_frames(texture) diff --git a/openage/convert/service/debug_info.py b/openage/convert/service/debug_info.py index 50ffd72bc1..39c8b92d3d 100644 --- a/openage/convert/service/debug_info.py +++ b/openage/convert/service/debug_info.py @@ -677,3 +677,47 @@ def debug_media_cache( with logfile.open("w") as log: log.write(logtext) + + +def debug_execution_time(debugdir: Directory, loglevel: int, stages_time: dict[str, float]) -> None: + """ + Create debug output for execution time for each stage + + :param debugdir: Output directory for the debug info. + :type debugdir: Directory + :param loglevel: Determines how detailed the output is. + :type loglevel: int + :param stages_time: Dict with execution time for each stage. + :type stages_time: dict + """ + if loglevel < 1: + return + + logfile = debugdir["execution_time"] + logtext = "".join(f"{k}: {v}\n" for k, v in stages_time.items()) + + with logfile.open("w") as log: + log.write(logtext) + + +def debug_not_found_sounds(debugdir: Directory, loglevel: int, sound: Path) -> None: + """ + Create debug output for sounds not found + + :param debugdir: Output directory for the debug info. + :type debugdir: Directory + :param loglevel: Determines how detailed the output is. + :type loglevel: int + :param sound: Sound object with path and name values. + :type sound: Path + """ + if loglevel < 6: + return + + logfile = debugdir.joinpath("export/not_found_sounds")[sound.stem] + + path = [part.decode() for part in sound.parts] + logtext = f"name: {sound.name}\npath: {'/'.join(path)}" + + with logfile.open("w") as log: + log.write(logtext) diff --git a/openage/convert/tool/driver.py b/openage/convert/tool/driver.py index a1b8bbfddf..d7d664d16e 100644 --- a/openage/convert/tool/driver.py +++ b/openage/convert/tool/driver.py @@ -8,13 +8,14 @@ """ from __future__ import annotations import typing +import timeit from ...log import info, dbg from ..processor.export.modpack_exporter import ModpackExporter from ..service.debug_info import debug_gamedata_format from ..service.debug_info import debug_string_resources, \ - debug_registered_graphics, debug_modpack + debug_registered_graphics, debug_modpack, debug_execution_time from ..service.init.changelog import (ASSET_VERSION) from ..service.read.gamedata import get_gamespec from ..service.read.palette import get_palettes @@ -64,7 +65,7 @@ def convert_metadata(args: Namespace) -> None: gamedata_path = args.targetdir.joinpath('gamedata') if gamedata_path.exists(): gamedata_path.removerecursive() - + read_start = timeit.default_timer() # Read .dat debug_gamedata_format(args.debugdir, args.debug_info, args.game_version) gamespec = get_gamespec(args.srcdir, args.game_version, not args.flag("no_pickle_cache")) @@ -84,16 +85,31 @@ def convert_metadata(args: Namespace) -> None: existing_graphics = get_existing_graphics(args) debug_registered_graphics(args.debugdir, args.debug_info, existing_graphics) + read_end = timeit.default_timer() + + conversion_start = timeit.default_timer() # Convert modpacks = args.converter.convert(gamespec, args, string_resources, existing_graphics) + conversion_end = timeit.default_timer() + + export_start = timeit.default_timer() for modpack in modpacks: ModpackExporter.export(modpack, args) debug_modpack(args.debugdir, args.debug_info, modpack) + export_end = timeit.default_timer() + + stages_time = { + "read": read_end - read_start, + "convert": conversion_end - conversion_start, + "export": export_end - export_start, + } + debug_execution_time(args.debugdir, args.debug_info, stages_time) + # TODO: player palettes # player_palette = PlayerColorTable(palette) # data_formatter.add_data(player_palette.dump("player_palette")) From d900aabbbe759c8a695002b8a4a651a7113cc7f2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 2 Nov 2023 23:24:57 +0100 Subject: [PATCH 012/771] renderer: Set format correctly on Wayland. --- libopenage/renderer/opengl/context.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libopenage/renderer/opengl/context.cpp b/libopenage/renderer/opengl/context.cpp index d65fe4aae1..91e2fb431b 100644 --- a/libopenage/renderer/opengl/context.cpp +++ b/libopenage/renderer/opengl/context.cpp @@ -29,10 +29,11 @@ gl_context_spec GlContext::find_spec() { for (size_t i_ver = 0; i_ver < gl_versions.size(); ++i_ver) { QOpenGLContext test_context{}; - auto tf = test_format; + test_format.setMajorVersion(gl_versions[i_ver].first); test_format.setMinorVersion(gl_versions[i_ver].second); + test_context.setFormat(test_format); test_context.create(); if (!test_context.isValid()) { @@ -49,6 +50,7 @@ gl_context_spec GlContext::find_spec() { } QOpenGLContext test_context{}; + test_context.setFormat(test_format); test_context.create(); if (!test_context.isValid()) { throw Error(MSG(err) << "Failed to create OpenGL context which previously succeeded. This should not happen!"); From 388511610aa4c706f2211588b7448029ad3ca0d9 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 4 Nov 2023 03:27:52 +0100 Subject: [PATCH 013/771] renderer: Dynamically fetch default fbo. --- libopenage/renderer/opengl/context.cpp | 4 + libopenage/renderer/opengl/context.h | 11 +++ libopenage/renderer/opengl/framebuffer.cpp | 32 +++++++- libopenage/renderer/opengl/framebuffer.h | 73 +++++++++++++---- libopenage/renderer/opengl/render_target.cpp | 34 ++++---- libopenage/renderer/opengl/render_target.h | 82 +++++++++++++++----- libopenage/renderer/opengl/renderer.cpp | 4 +- 7 files changed, 181 insertions(+), 59 deletions(-) diff --git a/libopenage/renderer/opengl/context.cpp b/libopenage/renderer/opengl/context.cpp index 91e2fb431b..e02992d25e 100644 --- a/libopenage/renderer/opengl/context.cpp +++ b/libopenage/renderer/opengl/context.cpp @@ -156,6 +156,10 @@ std::shared_ptr GlContext::get_raw_context() const { return this->gl_context; } +GLuint GlContext::get_default_framebuffer_id() { + return this->gl_context->defaultFramebufferObject(); +} + gl_context_spec GlContext::get_specs() const { return this->specs; } diff --git a/libopenage/renderer/opengl/context.h b/libopenage/renderer/opengl/context.h index 68e3e39c1f..ba5459ed5f 100644 --- a/libopenage/renderer/opengl/context.h +++ b/libopenage/renderer/opengl/context.h @@ -66,6 +66,17 @@ class GlContext { */ std::shared_ptr get_raw_context() const; + /** + * Get the ID of the default framebuffer used for displaying to + * the window. + * + * This value may change on every frame, so it should be called every + * time the default framebuffer is bound. + * + * @return ID of the default (display) framebuffer. + */ + unsigned int get_default_framebuffer_id(); + /** * Get the capabilities of this context. */ diff --git a/libopenage/renderer/opengl/framebuffer.cpp b/libopenage/renderer/opengl/framebuffer.cpp index e9862ff7ce..477c628f90 100644 --- a/libopenage/renderer/opengl/framebuffer.cpp +++ b/libopenage/renderer/opengl/framebuffer.cpp @@ -2,17 +2,25 @@ #include "framebuffer.h" +#include "renderer/opengl/context.h" #include "renderer/opengl/texture.h" namespace openage::renderer::opengl { +GlFramebuffer::GlFramebuffer(const std::shared_ptr &context) : + GlSimpleObject(context, + [](GLuint /*handle*/) {}), + type{gl_framebuffer_t::display} { +} + // TODO the validity of this object is contingent // on its texture existing. use shared_ptr? GlFramebuffer::GlFramebuffer(const std::shared_ptr &context, std::vector> const &textures) : GlSimpleObject(context, - [](GLuint handle) { glDeleteFramebuffers(1, &handle); }) { + [](GLuint handle) { glDeleteFramebuffers(1, &handle); }), + type{gl_framebuffer_t::textures} { GLuint handle; glGenFramebuffers(1, &handle); this->handle = handle; @@ -21,6 +29,10 @@ GlFramebuffer::GlFramebuffer(const std::shared_ptr &context, std::vector drawBuffers; + if (textures.empty()) { + throw Error{ERR << "At least 1 texture must be assigned to texture framebuffer."}; + } + size_t colorTextureCount = 0; for (auto const &texture : textures) { // TODO figure out attachment points from pixel formats @@ -41,12 +53,26 @@ GlFramebuffer::GlFramebuffer(const std::shared_ptr &context, } } +gl_framebuffer_t GlFramebuffer::get_type() const { + return this->type; +} + void GlFramebuffer::bind_read() const { - glBindFramebuffer(GL_READ_FRAMEBUFFER, *this->handle); + if (this->type == gl_framebuffer_t::textures) { + glBindFramebuffer(GL_READ_FRAMEBUFFER, *this->handle); + } + else { + glBindFramebuffer(GL_READ_FRAMEBUFFER, this->context->get_default_framebuffer_id()); + } } void GlFramebuffer::bind_write() const { - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, *this->handle); + if (this->type == gl_framebuffer_t::textures) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, *this->handle); + } + else { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, this->context->get_default_framebuffer_id()); + } } } // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/framebuffer.h b/libopenage/renderer/opengl/framebuffer.h index ccd544f57e..cd37ccb76a 100644 --- a/libopenage/renderer/opengl/framebuffer.h +++ b/libopenage/renderer/opengl/framebuffer.h @@ -7,30 +7,75 @@ #include "renderer/opengl/simple_object.h" -namespace openage { -namespace renderer { -namespace opengl { +namespace openage::renderer::opengl { class GlTexture2d; -/// Represents an OpenGL Framebuffer Object. -/// It is a collection of bitmap targets that can be drawn into -/// and read from. +/** + * The type of OpenGL framebuffer. + */ +enum class gl_framebuffer_t { + /** + * The actual window. This is visible to the user after swapping front and back buffers. + */ + display, + /** + * A bunch of textures. These can be color texture, depth textures, etc. + */ + textures, +}; + +/** + * Represents an OpenGL Framebuffer Object. + * It is a collection of bitmap targets that can be drawn into + * and read from. + */ class GlFramebuffer final : public GlSimpleObject { public: - /// Construct a framebuffer pointing at the given textures. - /// Texture are attached to points specific to their pixel format, - /// e.g. a depth texture will be set as the depth target. + /** + * Construct a framebuffer pointing at the default framebuffer - the window. + * + * Drawing into this framebuffer draws onto the screen. + * + * @param context OpenGL context used for drawing. + */ + GlFramebuffer(const std::shared_ptr &context); + + /** + * Construct a framebuffer pointing at the given textures. + * + * Texture are attached to points specific to their pixel format, + * e.g. a depth texture will be set as the depth target. + * + * @param context OpenGL context used for drawing. + * @param textures Textures targeted by the framebuffer. They are automatically + * attached to the correct attachement points depending on their type. + */ GlFramebuffer(const std::shared_ptr &context, std::vector> const &textures); - /// Bind this framebuffer to GL_READ_FRAMEBUFFER. + /** + * Get the type of this framebuffer. + * + * @return Framebuffer type. + */ + gl_framebuffer_t get_type() const; + + /** + * Bind this framebuffer to \p GL_READ_FRAMEBUFFER. + */ void bind_read() const; - /// Bind this framebuffer to GL_DRAW_FRAMEBUFFER. + /** + * Bind this framebuffer to \p GL_DRAW_FRAMEBUFFER. + */ void bind_write() const; + +private: + /** + * Type of this framebuffer. + */ + gl_framebuffer_t type; }; -} // namespace opengl -} // namespace renderer -} // namespace openage +} // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/render_target.cpp b/libopenage/renderer/opengl/render_target.cpp index b2f844d83b..c0da3a22a8 100644 --- a/libopenage/renderer/opengl/render_target.cpp +++ b/libopenage/renderer/opengl/render_target.cpp @@ -2,17 +2,20 @@ #include "render_target.h" +#include "error/error.h" + #include "renderer/opengl/texture.h" namespace openage::renderer::opengl { -GlRenderTarget::GlRenderTarget(size_t width, size_t height) : - type(gl_render_target_t::display), - size(width, height) {} +GlRenderTarget::GlRenderTarget(const std::shared_ptr &context, size_t width, size_t height) : + type(gl_render_target_t::framebuffer), + size(width, height), + framebuffer(context) {} GlRenderTarget::GlRenderTarget(const std::shared_ptr &context, const std::vector> &textures) : - type(gl_render_target_t::textures), + type(gl_render_target_t::framebuffer), framebuffer({context, textures}), textures(textures) { // TODO: Check if the textures are all the same size @@ -21,7 +24,7 @@ GlRenderTarget::GlRenderTarget(const std::shared_ptr &context, std::vector> GlRenderTarget::get_texture_targets() { std::vector> textures{}; - if (this->type == gl_render_target_t::display) { + if (this->framebuffer->get_type() == gl_framebuffer_t::display) { return textures; } //else upcast pointers @@ -33,8 +36,9 @@ std::vector> GlRenderTarget::get_texture_targets() { } void GlRenderTarget::resize(size_t width, size_t height) { - if (this->type != gl_render_target_t::display) { - throw Error{ERR << "Texture render target should not be resized. Create a new one instead."}; + if (this->framebuffer->get_type() == gl_framebuffer_t::textures) { + throw Error{ERR << "Render target with textured framebuffer should not be resized. " + << "Create a new one instead."}; } this->size = std::make_pair(width, height); @@ -46,23 +50,11 @@ void GlRenderTarget::bind_write() const { // different sizes glViewport(0, 0, size.first, size.second); - if (this->type == gl_render_target_t::textures) { - this->framebuffer->bind_write(); - } - else { - // 0 is the default, window framebuffer - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - } + this->framebuffer->bind_write(); } void GlRenderTarget::bind_read() const { - if (this->type == gl_render_target_t::textures) { - this->framebuffer->bind_read(); - } - else { - // 0 is the default, window framebuffer - glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); - } + this->framebuffer->bind_read(); } } // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/render_target.h b/libopenage/renderer/opengl/render_target.h index 546ac1e8ca..5246b03cb7 100644 --- a/libopenage/renderer/opengl/render_target.h +++ b/libopenage/renderer/opengl/render_target.h @@ -17,50 +17,92 @@ namespace opengl { class GlTexture2d; -/// The type of OpenGL render target +/** + * The type of OpenGL render target. + */ enum class gl_render_target_t { - /// The actual window. This is visible to the user after swapping front and back buffers - display, - /// A bunch of textures - textures, + /** + * Render into a framebuffer. + */ + framebuffer, // TODO renderbuffers mixed with textures }; -/// Represents an OpenGL target that can be drawn into. -/// It can be either a framebuffer or the display (the window). +/** + * Represents an OpenGL target that can be drawn into. + * It can be either a framebuffer with texture attachements or the display (the window). + */ class GlRenderTarget final : public RenderTarget { public: - /// Construct a render target pointed at the default framebuffer - the window. - GlRenderTarget(size_t width, size_t height); - - /// Construct a render target pointing at the given textures. - /// Texture are attached to points specific to their pixel format, - /// e.g. a depth texture will be set as the depth target. + /** + * Construct a render target pointed at the default framebuffer - the window. + * + * @param context OpenGL context used for drawing. + * @param width Current width of the window. + * @param height Current height of the window. + */ + GlRenderTarget(const std::shared_ptr &context, + size_t width, + size_t height); + + /** + * Construct a render target pointing at the given textures. + * Texture are attached to points specific to their pixel format, + * e.g. a depth texture will be set as the depth target. + * + * @param context OpenGL context used for drawing. + * @param textures Texture attachements. + */ GlRenderTarget(const std::shared_ptr &context, std::vector> const &textures); - // Get the targeted textures + /** + * Get the targeted textures. + * + * @return Textures drawn into by the render target. + */ std::vector> get_texture_targets() override; - // Resize the render target for scaling viewport correctly. + /** + * Resize the render target to the specified dimensions. + * + * This is used to scale the viewport to the correct size when + * binding the render target with write access. + * + * @param width New width. + * @param height New height. + */ void resize(size_t width, size_t height); - /// Bind this render target to be drawn into. + /** + * Bind this render target to be drawn into. + */ void bind_write() const; - /// Bind this render target to be read from. + /** + * Bind this render target to be read from. + */ void bind_read() const; private: + /** + * Type of this render target. + */ gl_render_target_t type; - // Size of the window or the texture target + /** + * Size of the window or the texture targets. + */ std::pair size; - /// For textures target type, the framebuffer. + /** + * For framebuffer target type, the framebuffer. + */ std::optional framebuffer; - // target textures if the render target is an fbo + /** + * Target textures if the render target is a textured fbo. + */ std::optional>> textures; }; diff --git a/libopenage/renderer/opengl/renderer.cpp b/libopenage/renderer/opengl/renderer.cpp index f0e513f14c..91cc5b35ee 100644 --- a/libopenage/renderer/opengl/renderer.cpp +++ b/libopenage/renderer/opengl/renderer.cpp @@ -24,7 +24,9 @@ namespace openage::renderer::opengl { GlRenderer::GlRenderer(const std::shared_ptr &ctx, const util::Vector2s &viewport_size) : gl_context{ctx}, - display{std::make_shared(viewport_size[0], viewport_size[1])} { + display{std::make_shared(ctx, + viewport_size[0], + viewport_size[1])} { // global GL alpha blending settings glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); From d63d33e6918b4dba12b77d0055cd732a62ad0dee Mon Sep 17 00:00:00 2001 From: askastitva Date: Fri, 20 Oct 2023 16:02:41 +0530 Subject: [PATCH 014/771] Add atan2 to FixedPoint --- copying.md | 1 + libopenage/util/fixed_point.h | 9 +++++++++ libopenage/util/fixed_point_test.cpp | 3 ++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/copying.md b/copying.md index b524367fb0..cd024a0a13 100644 --- a/copying.md +++ b/copying.md @@ -150,6 +150,7 @@ _the openage authors_ are: | Munawar Hafiz | munahaf | munawar dawt hafiz à gmail dawt com | | Md Ashhar | ashhar | mdashhar01 à gmail dawt com | | Fábio Barkoski | fabiobarkoski | fabiobarkoskii à gmail dawt com | +| Astitva Kamble | askastitva | astitvakamble5 à gmail dawt com | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. diff --git a/libopenage/util/fixed_point.h b/libopenage/util/fixed_point.h index 196ca17f54..1e27d15082 100644 --- a/libopenage/util/fixed_point.h +++ b/libopenage/util/fixed_point.h @@ -370,6 +370,10 @@ class FixedPoint { constexpr double sqrt() { return std::sqrt(this->to_double()); } + + constexpr double atan2(const FixedPoint &n) { + return std::atan2(this->to_double(), n.to_double()); + } }; @@ -481,6 +485,11 @@ constexpr double sqrt(openage::util::FixedPoint n) { return n.sqrt(); } +template +constexpr double atan2(openage::util::FixedPoint x, openage::util::FixedPoint y) { + return x.atan2(y); +} + template constexpr openage::util::FixedPoint min(openage::util::FixedPoint x, openage::util::FixedPoint y) { return openage::util::FixedPoint::from_raw_value( diff --git a/libopenage/util/fixed_point_test.cpp b/libopenage/util/fixed_point_test.cpp index 569d177e42..e5728f518c 100644 --- a/libopenage/util/fixed_point_test.cpp +++ b/libopenage/util/fixed_point_test.cpp @@ -1,4 +1,4 @@ -// Copyright 2016-2018 the openage authors. See copying.md for legal info. +// Copyright 2016-2023 the openage authors. See copying.md for legal info. #include "fixed_point.h" @@ -58,6 +58,7 @@ void fixed_point() { TESTEQUALS_FLOAT((e * 10).to_double(), 108.3 * 10, 1e-7); TESTEQUALS_FLOAT((e / 10).to_double(), 108.3 / 10, 1e-7); TESTEQUALS_FLOAT(std::sqrt(e), sqrt(108.3), 1e-7); + TESTEQUALS_FLOAT(std::atan2(e, f), atan2(108.3, -12.4), 1e-7); TESTEQUALS_FLOAT(std::abs(-e).to_double(), 108.3, 1e-7); TESTEQUALS_FLOAT(std::hypot(e, f), hypot(108.3, -12.4), 1e-7); TESTEQUALS_FLOAT(std::min(e, f), -12.4, 1e-7); From f82a89fd6de19c309ec0167fdae23fa129e9ac64 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 18 Nov 2023 15:57:37 +0100 Subject: [PATCH 015/771] assets: Mirror anchor offset when flipping is active. --- assets/shaders/world2d.vert.glsl | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/assets/shaders/world2d.vert.glsl b/assets/shaders/world2d.vert.glsl index 7e01b5a414..76cd6db34d 100644 --- a/assets/shaders/world2d.vert.glsl +++ b/assets/shaders/world2d.vert.glsl @@ -40,9 +40,14 @@ void main() { // this is the position where we want to draw the subtex in 2D vec4 obj_clip_pos = proj * view * model * vec4(obj_world_position, 1.0); + // if the subtex is flipped, we also need to flip the anchor offset + // essentially, we invert the coordinates for the flipped axis + float anchor_x = float(flip_x) * -1.0 * anchor_offset.x + float(!flip_x) * anchor_offset.x; + float anchor_y = float(flip_y) * -1.0 * anchor_offset.y + float(!flip_y) * anchor_offset.y; + // offset the clip position by the offset of the subtex anchor - // this basically aligns the object position and the subtex anchor - obj_clip_pos += vec4(anchor_offset.xy, 0.0, 0.0); + // imagine this as pinning the subtex to the object position at the subtex anchor point + obj_clip_pos += vec4(anchor_x, anchor_y, 0.0, 0.0); // create a move matrix for positioning the vertices // uses the scale and the transformed object position in clip space @@ -51,9 +56,15 @@ void main() { 0.0, 0.0, 1.0, 0.0, obj_clip_pos.x, obj_clip_pos.y, obj_clip_pos.z, 1.0); - // finally calculate the vertex position + // calculate the final vertex position gl_Position = move * vec4(v_position, 0.0, 1.0); + + // if the subtex is flipped, we also need to flip the uv tex coordinates + // essentially, we invert the coordinates for the flipped axis + + // !flip_x is default because OpenGL uses bottom-left as its origin float uv_x = float(!flip_x) * uv.x + float(flip_x) * (1.0 - uv.x); float uv_y = float(flip_y) * uv.y + float(!flip_y) * (1.0 - uv.y); + vert_uv = vec2(uv_x, uv_y); } From d493c800f6b795d018895b8479d32ab29e51fed7 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 Nov 2023 21:02:20 +0100 Subject: [PATCH 016/771] doc: PR templates. --- .../PULL_REQUEST_TEMPLATES/pull_request_template.md | 13 +++++++++++++ .github/PULL_REQUEST_TEMPLATES/release_template.md | 7 +++++++ 2 files changed, 20 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATES/pull_request_template.md create mode 100644 .github/PULL_REQUEST_TEMPLATES/release_template.md diff --git a/.github/PULL_REQUEST_TEMPLATES/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATES/pull_request_template.md new file mode 100644 index 0000000000..ed3dfa558f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATES/pull_request_template.md @@ -0,0 +1,13 @@ +### Merge Checklist + + + + +- [ ] I have read the [contribution guide](doc/contributing.md) +- [ ] I have added my info to [copying.md](copying.md) (only first time contributors) +- [ ] I have run `make checkall` and fixed all mentioned problems + + +### Description + + diff --git a/.github/PULL_REQUEST_TEMPLATES/release_template.md b/.github/PULL_REQUEST_TEMPLATES/release_template.md new file mode 100644 index 0000000000..96661d5de3 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATES/release_template.md @@ -0,0 +1,7 @@ +### Checklist + +- [ ] Changelog + - [ ] Release date + - [ ] Version number in title + - [ ] Full commit log +- [ ] Bump version in `openage_version` From 417aa6d1bd5b36964ae1eb11269c19d93f5146e2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 22 Nov 2023 05:11:13 +0100 Subject: [PATCH 017/771] Use block indent for aligning after open brackets. --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index b870e0ee95..911b0baec5 100644 --- a/.clang-format +++ b/.clang-format @@ -4,7 +4,7 @@ # see documentation in doc/code_style/ for details and explainations. Language: Cpp AccessModifierOffset: -4 -AlignAfterOpenBracket: Align +AlignAfterOpenBracket: BlockIndent AlignArrayOfStructures: None AlignConsecutiveAssignments: false AlignConsecutiveBitFields: false From 7abc01d28289627571fb4221364b15d6e8399027 Mon Sep 17 00:00:00 2001 From: AyiStar Date: Mon, 27 Nov 2023 23:18:37 +0800 Subject: [PATCH 018/771] complete if-else branch to fix pylint error --- copying.md | 1 + openage/convert/entity_object/conversion/aoc/genie_unit.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/copying.md b/copying.md index cd024a0a13..afad82f953 100644 --- a/copying.md +++ b/copying.md @@ -151,6 +151,7 @@ _the openage authors_ are: | Md Ashhar | ashhar | mdashhar01 à gmail dawt com | | Fábio Barkoski | fabiobarkoski | fabiobarkoskii à gmail dawt com | | Astitva Kamble | askastitva | astitvakamble5 à gmail dawt com | +| Haoyang Bi | AyiStar | ayistar à outlook dawt com | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. diff --git a/openage/convert/entity_object/conversion/aoc/genie_unit.py b/openage/convert/entity_object/conversion/aoc/genie_unit.py index e719c5f24f..52ded9b659 100644 --- a/openage/convert/entity_object/conversion/aoc/genie_unit.py +++ b/openage/convert/entity_object/conversion/aoc/genie_unit.py @@ -479,6 +479,8 @@ def is_unique(self) -> bool: else: # AoE1 return False + else: + raise ValueError(f"Unknown group type for {repr(self)}") enabling_research_id = head_unit_connection["enabling_research"].value From 85a76747f75ccff568b7bec8f3a078c7269cad83 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 28 Nov 2023 00:54:52 +0100 Subject: [PATCH 019/771] cli: Make 'main' default entrypoint. --- openage/__main__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openage/__main__.py b/openage/__main__.py index c002490739..a2bac73496 100644 --- a/openage/__main__.py +++ b/openage/__main__.py @@ -99,15 +99,15 @@ def main(argv=None): # pylint: disable=reimported from .main.main import init_subparser - init_subparser(subparsers.add_parser( + main_cli = subparsers.add_parser( "main", - parents=[global_cli, cfg_cli])) + parents=[global_cli, cfg_cli]) + init_subparser(main_cli) from .game.main import init_subparser - game_cli = subparsers.add_parser( + init_subparser(subparsers.add_parser( "game", - parents=[global_cli, cfg_cli]) - init_subparser(game_cli) + parents=[global_cli, cfg_cli])) from .testing.main import init_subparser init_subparser(subparsers.add_parser( @@ -143,8 +143,8 @@ def main(argv=None): print_version() if not args.subcommand: - # the user didn't specify a subcommand. default to 'game'. - args = game_cli.parse_args(argv) + # the user didn't specify a subcommand. default to 'main'. + args = main_cli.parse_args(argv) # process the shared args set_loglevel(verbosity_to_level(args.verbose - args.quiet)) From c0505b67001f025cb9465764669fc10079a2aa48 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 27 Nov 2023 23:49:24 +0100 Subject: [PATCH 020/771] Revert "Merge pull request #1602 from heinezen/clang-format-align" This reverts commit bfa99b5a90e72e78e5340a6456c60094398e8b76, reversing changes made to 4334fffbf6330c82bbdfb430ad5656d2ef15d687. --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index 911b0baec5..b870e0ee95 100644 --- a/.clang-format +++ b/.clang-format @@ -4,7 +4,7 @@ # see documentation in doc/code_style/ for details and explainations. Language: Cpp AccessModifierOffset: -4 -AlignAfterOpenBracket: BlockIndent +AlignAfterOpenBracket: Align AlignArrayOfStructures: None AlignConsecutiveAssignments: false AlignConsecutiveBitFields: false From 0a6c454abb3f1c830036e95c76bbf776845e105d Mon Sep 17 00:00:00 2001 From: AyiStar Date: Thu, 30 Nov 2023 23:28:20 +0800 Subject: [PATCH 021/771] try adding two math constants E and PI for FixedPoint --- libopenage/util/fixed_point.h | 25 +++++++++++++++++++++---- libopenage/util/fixed_point_test.cpp | 9 +++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/libopenage/util/fixed_point.h b/libopenage/util/fixed_point.h index 1e27d15082..3623091218 100644 --- a/libopenage/util/fixed_point.h +++ b/libopenage/util/fixed_point.h @@ -53,8 +53,8 @@ constexpr static /** - * Helper function that performs either a safe shift-right (amount > 0), - * or a safe shift-left (amount < 0). + * Helper function that performs either a safe shift-right (amount < 0), + * or a safe shift-left (amount >= 0). */ template constexpr static @@ -169,6 +169,17 @@ class FixedPoint { return FixedPoint::from_int(0); } + /** + * Constants + */ + static constexpr FixedPoint e() { + return from_fixedpoint(FixedPoint::from_raw_value(6267931151224907085ll)); + } + + static constexpr FixedPoint pi() { + return from_fixedpoint(FixedPoint::from_raw_value(7244019458077122842ll)); + } + /** * Factory function to get a fixed-point number from an integer. */ @@ -193,10 +204,16 @@ class FixedPoint { /** * Factory function to get a fixed-point number from a fixed-point number of different type. */ - template + 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> static constexpr FixedPoint from_fixedpoint(const FixedPoint &other) { return FixedPoint::from_raw_value( - safe_shift(other.get_raw_value())); + static_cast(other.get_raw_value() / safe_shiftleft(1))); } /** diff --git a/libopenage/util/fixed_point_test.cpp b/libopenage/util/fixed_point_test.cpp index e5728f518c..72a0a0f088 100644 --- a/libopenage/util/fixed_point_test.cpp +++ b/libopenage/util/fixed_point_test.cpp @@ -6,6 +6,7 @@ #include "../testing/testing.h" #include "stringformatter.h" +#include "math_constants.h" namespace openage { namespace util { @@ -98,6 +99,14 @@ void fixed_point() { std::stringstream sstr("1234.5678"); sstr >> e; TESTEQUALS_FLOAT(e.to_double(), 1234.5678, 1e-7); + + TESTEQUALS_FLOAT(TestType::e().to_double(), math::E, 1e-7); + TESTEQUALS_FLOAT(TestType::pi().to_double(), math::PI, 1e-7); + + using TestTypeShort = FixedPoint; + TESTEQUALS_FLOAT(TestTypeShort::e().to_double(), math::E, 1e-3); + TESTEQUALS_FLOAT(TestTypeShort::pi().to_double(), math::PI, 1e-3); + } }}} // openage::util::tests From 83dc9d54d2710121822dc93f1fe10ffe638c5394 Mon Sep 17 00:00:00 2001 From: AyiStar Date: Mon, 4 Dec 2023 09:14:29 +0000 Subject: [PATCH 022/771] Complete full set of math constants --- libopenage/util/fixed_point.h | 59 +++++++++++++++++++++++++++- libopenage/util/fixed_point_test.cpp | 15 +++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/libopenage/util/fixed_point.h b/libopenage/util/fixed_point.h index 3623091218..39717540bf 100644 --- a/libopenage/util/fixed_point.h +++ b/libopenage/util/fixed_point.h @@ -170,16 +170,73 @@ class FixedPoint { } /** - * Constants + * Math constants represented in FixedPoint */ + // naming, definition and value are kept compatible with `math_constants.h` static constexpr FixedPoint e() { return from_fixedpoint(FixedPoint::from_raw_value(6267931151224907085ll)); } + static constexpr FixedPoint log2e() { + return from_fixedpoint(FixedPoint::from_raw_value(3326628274461080622ll)); + } + + static constexpr FixedPoint log10e() { + return from_fixedpoint(FixedPoint::from_raw_value(1001414895036696345ll)); + } + + static constexpr FixedPoint ln2() { + return from_fixedpoint(FixedPoint::from_raw_value(1598288580650331957ll)); + } + + static constexpr FixedPoint ln10() { + return from_fixedpoint(FixedPoint::from_raw_value(5309399739799983627ll)); + } + static constexpr FixedPoint pi() { return from_fixedpoint(FixedPoint::from_raw_value(7244019458077122842ll)); } + static constexpr FixedPoint pi_2() { + return from_fixedpoint(FixedPoint::from_raw_value(3622009729038561421ll)); + } + + static constexpr FixedPoint pi_4() { + return from_fixedpoint(FixedPoint::from_raw_value(1811004864519280710ll)); + } + + static constexpr FixedPoint inv_pi() { + return from_fixedpoint(FixedPoint::from_raw_value(733972625820500306ll)); + } + + static constexpr FixedPoint inv2_pi() { + return from_fixedpoint(FixedPoint::from_raw_value(1467945251641000613ll)); + } + + static constexpr FixedPoint inv2_sqrt_pi() { + return from_fixedpoint(FixedPoint::from_raw_value(2601865214189558307ll)); + } + + static constexpr FixedPoint tau() { + return from_fixedpoint(FixedPoint::from_raw_value(7244019458077122842ll)); + } + + static constexpr FixedPoint degs_per_rad() { + return from_fixedpoint(FixedPoint::from_raw_value(40244552544872904ll)); + } + + static constexpr FixedPoint rads_per_deg() { + return from_fixedpoint(FixedPoint::from_raw_value(8257192040480628449ll)); + } + + static constexpr FixedPoint sqrt_2() { + return from_fixedpoint(FixedPoint::from_raw_value(3260954456333195553ll)); + } + + static constexpr FixedPoint inv_sqrt_2() { + return from_fixedpoint(FixedPoint::from_raw_value(1630477228166597776ll)); + } + /** * Factory function to get a fixed-point number from an integer. */ diff --git a/libopenage/util/fixed_point_test.cpp b/libopenage/util/fixed_point_test.cpp index 72a0a0f088..7a7fb2bc0c 100644 --- a/libopenage/util/fixed_point_test.cpp +++ b/libopenage/util/fixed_point_test.cpp @@ -101,7 +101,22 @@ void fixed_point() { TESTEQUALS_FLOAT(e.to_double(), 1234.5678, 1e-7); TESTEQUALS_FLOAT(TestType::e().to_double(), math::E, 1e-7); + TESTEQUALS_FLOAT(TestType::log2e().to_double(), math::LOG2E, 1e-7); + TESTEQUALS_FLOAT(TestType::log10e().to_double(), math::LOG10E, 1e-7); + TESTEQUALS_FLOAT(TestType::ln2().to_double(), math::LN2, 1e-7); + TESTEQUALS_FLOAT(TestType::ln10().to_double(), math::LN10, 1e-7); TESTEQUALS_FLOAT(TestType::pi().to_double(), math::PI, 1e-7); + TESTEQUALS_FLOAT(TestType::pi_2().to_double(), math::PI_2, 1e-7); + TESTEQUALS_FLOAT(TestType::pi_4().to_double(), math::PI_4, 1e-7); + TESTEQUALS_FLOAT(TestType::inv_pi().to_double(), math::INV_PI, 1e-7); + TESTEQUALS_FLOAT(TestType::inv2_pi().to_double(), math::INV2_PI, 1e-7); + TESTEQUALS_FLOAT(TestType::inv2_sqrt_pi().to_double(), math::INV2_SQRT_PI, 1e-7); + TESTEQUALS_FLOAT(TestType::tau().to_double(), math::TAU, 1e-7); + TESTEQUALS_FLOAT(TestType::degs_per_rad().to_double(), math::DEGSPERRAD, 1e-7); + TESTEQUALS_FLOAT(TestType::rads_per_deg().to_double(), math::RADSPERDEG, 1e-7); + TESTEQUALS_FLOAT(TestType::sqrt_2().to_double(), math::SQRT_2, 1e-7); + TESTEQUALS_FLOAT(TestType::inv_sqrt_2().to_double(), math::INV_SQRT_2, 1e-7); + using TestTypeShort = FixedPoint; TESTEQUALS_FLOAT(TestTypeShort::e().to_double(), math::E, 1e-3); From 35e9ffb5a5f0dc69604821a8ce817869d9d4aca6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 15 Dec 2023 17:37:37 +0100 Subject: [PATCH 023/771] ci: Fix macOS build. --- .github/workflows/macosx-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/macosx-ci.yml b/.github/workflows/macosx-ci.yml index c60d76a4da..d7b86776db 100644 --- a/.github/workflows/macosx-ci.yml +++ b/.github/workflows/macosx-ci.yml @@ -55,7 +55,7 @@ jobs: - name: Brew install DeJaVu fonts run: brew tap homebrew/cask-fonts && brew install font-dejavu - name: Remove python's 2to3 link so that 'brew link' does not fail - run: rm '/usr/local/bin/2to3' && rm '/usr/local/bin/2to3-3.11' + run: rm /usr/local/bin/2to3* && rm /usr/local/bin/idle3* - name: Install environment helpers with homebrew run: brew install ccache - name: Install dependencies with homebrew @@ -66,7 +66,7 @@ jobs: # cython, numpy and pygments are in homebrew, # but "cython is keg-only, which means it was not symlinked into /usr/local" # numpy pulls gcc as dep? and pygments doesn't work. - run: pip3 install --upgrade cython numpy mako lz4 pillow pygments toml + run: pip3 install --upgrade cython numpy mako lz4 pillow pygments setuptools toml - name: Configure run: | CLANG_PATH="$HOME/clang-15.0.0/bin/clang++" From cd135ae19854a6c5fa0f257133ed27af788b68c9 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 15 Dec 2023 18:32:09 +0100 Subject: [PATCH 024/771] doc: Add 'setuptools' as conditional dependency. --- doc/building.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/building.md b/doc/building.md index 81d075eebb..c5b8af6180 100644 --- a/doc/building.md +++ b/doc/building.md @@ -32,9 +32,10 @@ Dependency list: C cython >=0.29.31 C cmake >=3.16 A numpy + A lz4 A python imaging library (PIL) -> pillow - RA toml - RA lz4 + RA setuptools (for python>=3.12 and cython<3.1) + A toml CR opengl >=3.3 CR libepoxy CR libpng From 27b05b49327cddb6f9b36f1e9cbb2d91ea194663 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 22 Sep 2023 21:13:19 +0200 Subject: [PATCH 025/771] renderer: Deprecate old renderer code. --- libopenage/assets/assetmanager.h | 2 +- libopenage/assets/legacy_assetmanager.h | 2 +- libopenage/handlers.h | 10 +++---- libopenage/options.h | 29 ++++++++----------- libopenage/presenter/assets/asset_manager.h | 2 +- libopenage/presenter/legacy/game_control.h | 12 ++++---- libopenage/presenter/legacy/legacy.h | 2 +- libopenage/presenter/legacy/legacy_renderer.h | 2 +- libopenage/screenshot.h | 9 +++--- libopenage/texture.h | 23 ++++++++------- 10 files changed, 44 insertions(+), 49 deletions(-) diff --git a/libopenage/assets/assetmanager.h b/libopenage/assets/assetmanager.h index 482f2ac672..5fff6b0328 100644 --- a/libopenage/assets/assetmanager.h +++ b/libopenage/assets/assetmanager.h @@ -26,7 +26,7 @@ class GameSimulation; * Container class for all available assets. * Responsible for loading, providing and updating requested files. */ -class AssetManager final { +class [[deprecated]] AssetManager final { public: explicit AssetManager(qtsdl::GuiItemLink *gui_link); diff --git a/libopenage/assets/legacy_assetmanager.h b/libopenage/assets/legacy_assetmanager.h index 284719b1e1..c4436007e9 100644 --- a/libopenage/assets/legacy_assetmanager.h +++ b/libopenage/assets/legacy_assetmanager.h @@ -24,7 +24,7 @@ class Texture; * Container class for all available assets. * Responsible for loading, providing and updating requested files. */ -class LegacyAssetManager final { +class [[deprecated]] LegacyAssetManager final { public: explicit LegacyAssetManager(qtsdl::GuiItemLink *gui_link); diff --git a/libopenage/handlers.h b/libopenage/handlers.h index 7df7300d45..f695a2589e 100644 --- a/libopenage/handlers.h +++ b/libopenage/handlers.h @@ -14,7 +14,7 @@ class LegacyEngine; /** * superclass for all possible drawing operations in the game. */ -class DrawHandler { +class [[deprecated]] DrawHandler { public: virtual ~DrawHandler() = default; @@ -27,7 +27,7 @@ class DrawHandler { /** * superclass for all possible drawing operations in the game. */ -class HudHandler { +class [[deprecated]] HudHandler { public: virtual ~HudHandler() = default; @@ -40,7 +40,7 @@ class HudHandler { /** * superclass for all calculations being done on engine tick. */ -class TickHandler { +class [[deprecated]] TickHandler { public: virtual ~TickHandler() = default; @@ -53,7 +53,7 @@ class TickHandler { /** * superclass for handling any input event. */ -class InputHandler { +class [[deprecated]] InputHandler { public: virtual ~InputHandler() = default; @@ -66,7 +66,7 @@ class InputHandler { /** * superclass for handling a window resize event. */ -class ResizeHandler { +class [[deprecated]] ResizeHandler { public: virtual ~ResizeHandler() = default; diff --git a/libopenage/options.h b/libopenage/options.h index 320b14a54d..0798007f7b 100644 --- a/libopenage/options.h +++ b/libopenage/options.h @@ -36,10 +36,11 @@ using option_list = std::vector; /** * stores a type and value + * + * TODO: What is this used for? */ class OptionValue { public: - /** * value ownership managed by this */ @@ -71,13 +72,13 @@ class OptionValue { /** * Checks equality */ - bool operator ==(const OptionValue &other) const; + bool operator==(const OptionValue &other) const; /** * Assignment, reference values share their values * non reference values are copied */ - const OptionValue &operator =(const OptionValue &other); + const OptionValue &operator=(const OptionValue &other); /** * Value converted to a string @@ -87,7 +88,7 @@ class OptionValue { /** * read inner type - the templated type must match */ - template + template const T &value() const { return var->get(); } @@ -95,13 +96,12 @@ class OptionValue { const option_type type; private: - /** * set the value */ void set(const OptionValue &other); - template + template void set_value(const OptionValue &other) { const T &other_value = other.value(); if (this->var) { @@ -125,7 +125,6 @@ class OptionValue { */ bool owner; util::VariableBase *var; - }; OptionValue parse(option_type t, const std::string &s); @@ -152,7 +151,6 @@ class OptionAction { private: const opt_func_t function; - }; /** @@ -162,8 +160,9 @@ class OptionAction { * with console interaction or gui elements */ class OptionNode { - template + template friend class Var; + public: OptionNode(std::string panel_name); virtual ~OptionNode(); @@ -171,7 +170,7 @@ class OptionNode { /** * lists all available options in a readable format */ - std::vector list_options(bool recurse=false, const std::string &indent=""); + std::vector list_options(bool recurse = false, const std::string &indent = ""); /** * shows all available variable names @@ -189,7 +188,7 @@ class OptionNode { /** * shortcut for get_variable(name).value() */ - template + template const T &getv(const std::string &name) { return this->get_variable(name).value(); } @@ -217,7 +216,6 @@ class OptionNode { const std::string name; protected: - /** * add types to the interface */ @@ -226,7 +224,6 @@ class OptionNode { void add_action(const OptionAction &action); private: - /** * add child nodes */ @@ -264,13 +261,11 @@ class OptionNode { * option node allowing reflection, while also * being directly accessable as a typed member */ -template +template class Var : public util::Variable { public: - Var(OptionNode *owner, const std::string &name, const T &init) - : + Var(OptionNode *owner, const std::string &name, const T &init) : util::Variable{init} { - owner->add(name, this->value); } }; diff --git a/libopenage/presenter/assets/asset_manager.h b/libopenage/presenter/assets/asset_manager.h index 48075eeaeb..fa3461501b 100644 --- a/libopenage/presenter/assets/asset_manager.h +++ b/libopenage/presenter/assets/asset_manager.h @@ -24,7 +24,7 @@ namespace openage::presenter { * Container class for all available assets. * Responsible for loading, providing and updating requested files. */ -class AssetManager final { +class [[deprecated]] AssetManager final { public: AssetManager(); AssetManager(const util::Path &asset_dir); diff --git a/libopenage/presenter/legacy/game_control.h b/libopenage/presenter/legacy/game_control.h index cad9deab7b..2732ce0345 100644 --- a/libopenage/presenter/legacy/game_control.h +++ b/libopenage/presenter/legacy/game_control.h @@ -32,7 +32,7 @@ class GameControl; /** * Signals for a gui mode. */ -class OutputModeSignals : public QObject { +class [[deprecated]] OutputModeSignals : public QObject { Q_OBJECT public: @@ -55,7 +55,7 @@ class OutputModeSignals : public QObject { * A target for input handling and gui rendering. * This allows to switch to different display UIs. */ -class OutputMode : public input::legacy::InputContext { +class [[deprecated]] OutputMode : public input::legacy::InputContext { public: explicit OutputMode(qtsdl::GuiItemLink *gui_link); virtual ~OutputMode(); @@ -117,7 +117,7 @@ class OutputMode : public input::legacy::InputContext { * This is mainly the game editor. * Shows menus to choose units to build. */ -class CreateMode : public OutputMode { +class [[deprecated]] CreateMode : public OutputMode { public: CreateMode(qtsdl::GuiItemLink *gui_link); @@ -165,7 +165,7 @@ public slots: * Used to control units, issue commands, basically this is where you * sink your time in when playing. */ -class ActionMode : public OutputMode { +class [[deprecated]] ActionMode : public OutputMode { public: ActionMode(qtsdl::GuiItemLink *gui_link); @@ -279,7 +279,7 @@ public slots: /** * UI mode to provide an interface for map editing. */ -class EditorMode : public OutputMode { +class [[deprecated]] EditorMode : public OutputMode { public: explicit EditorMode(qtsdl::GuiItemLink *gui_link); @@ -354,7 +354,7 @@ public slots: * * hud rendering and input handling is redirected to the active mode */ -class GameControl : public openage::HudHandler { +class [[deprecated]] GameControl : public openage::HudHandler { public: explicit GameControl(qtsdl::GuiItemLink *gui_link); diff --git a/libopenage/presenter/legacy/legacy.h b/libopenage/presenter/legacy/legacy.h index b4a82443fd..9ee18ab1c9 100644 --- a/libopenage/presenter/legacy/legacy.h +++ b/libopenage/presenter/legacy/legacy.h @@ -49,7 +49,7 @@ namespace presenter { /** * Temporary container class for the legacy renderer implementation. */ -class LegacyDisplay final : public ResizeHandler +class [[deprecated]] LegacyDisplay final : public ResizeHandler , public options::OptionNode { public: LegacyDisplay(const util::Path &path, LegacyEngine *engine); diff --git a/libopenage/presenter/legacy/legacy_renderer.h b/libopenage/presenter/legacy/legacy_renderer.h index ea60a76623..cde40b869d 100644 --- a/libopenage/presenter/legacy/legacy_renderer.h +++ b/libopenage/presenter/legacy/legacy_renderer.h @@ -25,7 +25,7 @@ class GameMain; * * TODO include fog drawing etc */ -class RenderOptions : public options::OptionNode { +class [[deprecated]] RenderOptions : public options::OptionNode { public: RenderOptions(); diff --git a/libopenage/screenshot.h b/libopenage/screenshot.h index 221f0b43ab..45e2660f5b 100644 --- a/libopenage/screenshot.h +++ b/libopenage/screenshot.h @@ -2,9 +2,9 @@ #pragma once -#include #include #include +#include #include "coord/pixel.h" @@ -19,12 +19,12 @@ class JobManager; * * TODO: move into renderer! */ -class ScreenshotManager { +class [[deprecated]] ScreenshotManager { public: /** * Initializes the screenshot manager with the given job manager. */ - ScreenshotManager(job::JobManager* job_mgr); + ScreenshotManager(job::JobManager *job_mgr); ~ScreenshotManager(); @@ -39,7 +39,6 @@ class ScreenshotManager { coord::viewport_delta window_size; private: - /** to be called to get the next screenshot filename into the array */ std::string gen_next_filename(); @@ -53,4 +52,4 @@ class ScreenshotManager { job::JobManager *job_manager; }; -} // openage +} // namespace openage diff --git a/libopenage/texture.h b/libopenage/texture.h index 5469fe11e3..64283e86cc 100644 --- a/libopenage/texture.h +++ b/libopenage/texture.h @@ -1,14 +1,14 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. +// Copyright 2013-2023 the openage authors. See copying.md for legal info. #pragma once #include -#include #include +#include -#include "gamedata/texture_dummy.h" #include "coord/pixel.h" #include "coord/tile.h" +#include "gamedata/texture_dummy.h" #include "shader/program.h" #include "shader/shader.h" #include "util/path.h" @@ -43,12 +43,12 @@ extern GLint base_texture, mask_texture, base_coord, mask_coord, show_mask; // bitmasks for shader modes constexpr int PLAYERCOLORED = 1 << 0; -constexpr int ALPHAMASKED = 1 << 1; +constexpr int ALPHAMASKED = 1 << 1; /** * enables transfer of data to opengl */ -struct gl_texture_buffer { +struct [[deprecated]] gl_texture_buffer { GLuint id, vertbuf; // this requires loading on the main thread @@ -67,8 +67,10 @@ struct gl_texture_buffer { * * The class supports subtextures, so that one big texture can contain * several small images. These are the ones actually to be rendered. + * + * TODO: Deprecated, replaced by new renderer */ -class Texture { +class [[deprecated]] Texture { public: int w; int h; @@ -83,23 +85,23 @@ class Texture { * Create a texture from a existing image file. * For supported image file types, see the SDL_Image initialization in the engine. */ - Texture(const util::Path &filename, bool use_metafile=false); + Texture(const util::Path &filename, bool use_metafile = false); ~Texture(); /** * Draws the texture at hud coordinates. */ - void draw(const coord::CoordManager &mgr, coord::camhud pos, unsigned int mode=0, bool mirrored=false, int subid=0, unsigned player=0) const; + void draw(const coord::CoordManager &mgr, coord::camhud pos, unsigned int mode = 0, bool mirrored = false, int subid = 0, unsigned player = 0) const; /** * Draws the texture at game coordinates. */ - void draw(const coord::CoordManager &mgr, coord::camgame pos, unsigned int mode=0, bool mirrored=false, int subid=0, unsigned player=0) const; + void draw(const coord::CoordManager &mgr, coord::camgame pos, unsigned int mode = 0, bool mirrored = false, int subid = 0, unsigned player = 0) const; /** * Draws the texture at phys coordinates. */ - void draw(const coord::CoordManager &mgr, coord::phys3 pos, unsigned int mode=0, bool mirrored=false, int subid=0, unsigned player=0) const; + void draw(const coord::CoordManager &mgr, coord::phys3 pos, unsigned int mode = 0, bool mirrored = false, int subid = 0, unsigned player = 0) const; /** * Draws the texture at tile coordinates. @@ -181,7 +183,6 @@ class Texture { void load_in_glthread() const; GLuint make_gl_texture(int iformat, int oformat, int w, int h, void *) const; void unload(); - }; } // namespace openage From 96d08f5d5f566091fde85a33313d23cad7325001 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 22 Sep 2023 21:22:42 +0200 Subject: [PATCH 026/771] renderer: Remove legacy renderer. --- libopenage/presenter/legacy/CMakeLists.txt | 1 - .../presenter/legacy/legacy_renderer.cpp | 214 ------------------ libopenage/presenter/legacy/legacy_renderer.h | 65 ------ 3 files changed, 280 deletions(-) delete mode 100644 libopenage/presenter/legacy/legacy_renderer.cpp delete mode 100644 libopenage/presenter/legacy/legacy_renderer.h diff --git a/libopenage/presenter/legacy/CMakeLists.txt b/libopenage/presenter/legacy/CMakeLists.txt index d4add0acd2..5a57295ab3 100644 --- a/libopenage/presenter/legacy/CMakeLists.txt +++ b/libopenage/presenter/legacy/CMakeLists.txt @@ -1,7 +1,6 @@ add_sources(libopenage game_control.cpp legacy.cpp - legacy_renderer.cpp ) pxdgen( diff --git a/libopenage/presenter/legacy/legacy_renderer.cpp b/libopenage/presenter/legacy/legacy_renderer.cpp deleted file mode 100644 index 3e65693f36..0000000000 --- a/libopenage/presenter/legacy/legacy_renderer.cpp +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "legacy_renderer.h" - -#include -#include -#include -#include -#include - -#include "../../console/console.h" -#include "../../gamedata/color_dummy.h" -#include "../../gamestate/old/game_main.h" -#include "../../gamestate/old/game_spec.h" -#include "../../input/legacy/input_manager.h" -#include "../../legacy_engine.h" -#include "../../log/log.h" -#include "../../renderer/text.h" -#include "../../unit/action.h" -#include "../../unit/command.h" -#include "../../unit/producer.h" -#include "../../unit/unit.h" -#include "../../unit/unit_texture.h" -#include "../../util/externalprofiler.h" -#include "../../util/timer.h" -#include "legacy.h" - -namespace openage { - - -RenderOptions::RenderOptions() : - OptionNode{"RendererOptions"}, - draw_debug{this, "draw_debug", false}, - terrain_blending{this, "terrain_blending", true} {} - - -LegacyRenderer::LegacyRenderer(LegacyEngine *e, presenter::LegacyDisplay *d) : - engine{e}, - display{d} { - // set options structure - this->settings.set_parent(this->display); - - // display callbacks - this->display->register_draw_action(this); - - // fetch asset loading dir - util::Path asset_dir = this->engine->get_root_dir()["assets"]; - - // load textures and stuff - gaben = new Texture{asset_dir["test"]["textures"]["gaben.png"]}; - - std::vector player_color_lines = util::read_csv_file( - asset_dir["converted/player_palette.docx"]); - - std::unique_ptr playercolors = std::make_unique(player_color_lines.size() * 4); - for (size_t i = 0; i < player_color_lines.size(); i++) { - auto line = &player_color_lines[i]; - playercolors[i * 4] = line->r / 255.0; - playercolors[i * 4 + 1] = line->g / 255.0; - playercolors[i * 4 + 2] = line->b / 255.0; - playercolors[i * 4 + 3] = line->a / 255.0; - } - - // shader initialisation - // read shader source codes and create shader objects for wrapping them. - const char *shader_header_code = "#version 120\n"; - std::string equals_epsilon_code = asset_dir["shaders/equalsEpsilon.glsl"].open().read(); - std::string texture_vert_code = asset_dir["shaders/maptexture.vert.glsl"].open().read(); - auto plaintexture_vert = std::make_unique( - GL_VERTEX_SHADER, - std::initializer_list{shader_header_code, texture_vert_code.c_str()}); - - std::string texture_frag_code = asset_dir["shaders/maptexture.frag.glsl"].open().read(); - auto plaintexture_frag = std::make_unique( - GL_FRAGMENT_SHADER, - std::initializer_list{shader_header_code, texture_frag_code.c_str()}); - - std::string teamcolor_frag_code = asset_dir["shaders/teamcolors.frag.glsl"].open().read(); - std::stringstream ss; - ss << player_color_lines.size(); - auto teamcolor_frag = std::make_unique( - GL_FRAGMENT_SHADER, - std::initializer_list{ - shader_header_code, - ("#define NUM_OF_PLAYER_COLORS " + ss.str() + "\n").c_str(), - equals_epsilon_code.c_str(), - teamcolor_frag_code.c_str()}); - - std::string alphamask_vert_code = asset_dir["shaders/alphamask.vert.glsl"].open().read(); - auto alphamask_vert = std::make_unique( - GL_VERTEX_SHADER, - std::initializer_list{shader_header_code, alphamask_vert_code.c_str()}); - - std::string alphamask_frag_code = asset_dir["shaders/alphamask.frag.glsl"].open().read(); - auto alphamask_frag = std::make_unique( - GL_FRAGMENT_SHADER, - std::initializer_list{shader_header_code, alphamask_frag_code.c_str()}); - - std::string texturefont_vert_code = asset_dir["shaders/texturefont.vert.glsl"].open().read(); - auto texturefont_vert = std::make_unique( - GL_VERTEX_SHADER, - std::initializer_list{shader_header_code, texturefont_vert_code.c_str()}); - - std::string texturefont_frag_code = asset_dir["shaders/texturefont.frag.glsl"].open().read(); - auto texturefont_frag = std::make_unique( - GL_FRAGMENT_SHADER, - std::initializer_list{shader_header_code, texturefont_frag_code.c_str()}); - - // create program for rendering simple textures - texture_shader::program = new shader::Program(plaintexture_vert.get(), plaintexture_frag.get()); - texture_shader::program->link(); - texture_shader::texture = texture_shader::program->get_uniform_id("texture"); - texture_shader::tex_coord = texture_shader::program->get_attribute_id("tex_coordinates"); - texture_shader::program->use(); - glUniform1i(texture_shader::texture, 0); - texture_shader::program->stopusing(); - - - // create program for tinting textures at alpha-marked pixels - // with team colors - teamcolor_shader::program = new shader::Program(plaintexture_vert.get(), teamcolor_frag.get()); - teamcolor_shader::program->link(); - teamcolor_shader::texture = teamcolor_shader::program->get_uniform_id("texture"); - teamcolor_shader::tex_coord = teamcolor_shader::program->get_attribute_id("tex_coordinates"); - teamcolor_shader::player_id_var = teamcolor_shader::program->get_uniform_id("player_number"); - teamcolor_shader::alpha_marker_var = teamcolor_shader::program->get_uniform_id("alpha_marker"); - teamcolor_shader::player_color_var = teamcolor_shader::program->get_uniform_id("player_color"); - teamcolor_shader::program->use(); - glUniform1i(teamcolor_shader::texture, 0); - glUniform1f(teamcolor_shader::alpha_marker_var, 254.0 / 255.0); - // fill the teamcolor shader's player color table: - glUniform4fv(teamcolor_shader::player_color_var, 64, playercolors.get()); - teamcolor_shader::program->stopusing(); - - - // create program for drawing textures that are alpha-masked before - alphamask_shader::program = new shader::Program(alphamask_vert.get(), alphamask_frag.get()); - alphamask_shader::program->link(); - alphamask_shader::base_coord = alphamask_shader::program->get_attribute_id("base_tex_coordinates"); - alphamask_shader::mask_coord = alphamask_shader::program->get_attribute_id("mask_tex_coordinates"); - alphamask_shader::show_mask = alphamask_shader::program->get_uniform_id("show_mask"); - alphamask_shader::base_texture = alphamask_shader::program->get_uniform_id("base_texture"); - alphamask_shader::mask_texture = alphamask_shader::program->get_uniform_id("mask_texture"); - alphamask_shader::program->use(); - glUniform1i(alphamask_shader::base_texture, 0); - glUniform1i(alphamask_shader::mask_texture, 1); - alphamask_shader::program->stopusing(); - - // Create program for texture based font rendering - texturefont_shader::program = new shader::Program(texturefont_vert.get(), texturefont_frag.get()); - texturefont_shader::program->link(); - texturefont_shader::texture = texturefont_shader::program->get_uniform_id("texture"); - texturefont_shader::color = texturefont_shader::program->get_uniform_id("color"); - texturefont_shader::tex_coord = texturefont_shader::program->get_attribute_id("tex_coordinates"); - texturefont_shader::program->use(); - glUniform1i(texturefont_shader::texture, 0); - texturefont_shader::program->stopusing(); - - // Renderer keybinds - // TODO: a renderer settings struct - // would allow these to be put somewhere better - input::legacy::ActionManager &action = this->display->get_action_manager(); - auto &global_input_context = this->display->get_input_manager().get_global_context(); - global_input_context.bind(action.get("TOGGLE_BLENDING"), [this](const input::legacy::action_arg_t &) { - this->settings.terrain_blending.value = !this->settings.terrain_blending.value; - }); - global_input_context.bind(action.get("TOGGLE_UNIT_DEBUG"), [this](const input::legacy::action_arg_t &) { - this->settings.draw_debug.value = !this->settings.draw_debug.value; - - log::log(MSG(dbg) << "Toggle debug grid"); - - // TODO remove this hack, use render settings instead - UnitAction::show_debug = !UnitAction::show_debug; - }); - - log::log(MSG(dbg) << "Loaded Renderer"); -} - -LegacyRenderer::~LegacyRenderer() { - // oh noes, release hl3 before that! - delete this->gaben; - - delete texture_shader::program; - delete teamcolor_shader::program; - delete alphamask_shader::program; - delete texturefont_shader::program; -} - - -bool LegacyRenderer::on_draw() { - // draw terrain - GameMain *game = this->display->get_game(); - - if (game) { - // draw gaben, our great and holy protector, bringer of the half-life 3. - gaben->draw(this->display->coord, coord::camgame{0, 0}); - - // TODO move render code out of terrain - if (game->terrain) { - game->terrain->draw(this->display, &this->settings); - } - } - return true; -} - -GameMain *LegacyRenderer::game() const { - return this->display->get_game(); -} - -GameSpec *LegacyRenderer::game_spec() const { - return this->game()->get_spec(); -} - -} // namespace openage diff --git a/libopenage/presenter/legacy/legacy_renderer.h b/libopenage/presenter/legacy/legacy_renderer.h deleted file mode 100644 index cde40b869d..0000000000 --- a/libopenage/presenter/legacy/legacy_renderer.h +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include -#include - -#include "../../coord/tile.h" -#include "../../gamestate/old/game_spec.h" -#include "../../handlers.h" -#include "../../options.h" -#include "legacy.h" - -namespace openage { - -class GameMain; - -/** - * Options for the renderer. - * These will be included in the user interface - * via reflection, so adding new members will - * always be visible - * - * TODO include fog drawing etc - */ -class [[deprecated]] RenderOptions : public options::OptionNode { -public: - RenderOptions(); - - options::Var draw_debug; - options::Var terrain_blending; -}; - -/** - * renders the editor and action views - */ -class LegacyRenderer : DrawHandler { -public: - LegacyRenderer(LegacyEngine *e, presenter::LegacyDisplay *d); - ~LegacyRenderer(); - - bool on_draw() override; - - /** - * the game this renderer is using - */ - GameMain *game() const; - - /** - * GameSpec used by this renderer - */ - GameSpec *game_spec() const; - - Texture *gaben; - - RenderOptions settings; - -private: - LegacyEngine *engine; - presenter::LegacyDisplay *display; -}; - -} // namespace openage From 4f8e4e6e5dfe5c4250db4035809744bd83673889 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 22 Sep 2023 21:58:52 +0200 Subject: [PATCH 027/771] renderer: Remove legacy asset management. --- libopenage/assets/CMakeLists.txt | 1 - libopenage/assets/assetmanager.cpp | 207 ------------------ libopenage/assets/assetmanager.h | 139 ------------ libopenage/presenter/CMakeLists.txt | 1 - libopenage/presenter/assets/CMakeLists.txt | 3 - libopenage/presenter/assets/asset_manager.cpp | 194 ---------------- libopenage/presenter/assets/asset_manager.h | 105 --------- libopenage/renderer/gui/CMakeLists.txt | 1 - libopenage/renderer/gui/assetmanager_link.cpp | 57 ----- libopenage/renderer/gui/assetmanager_link.h | 60 ----- 10 files changed, 768 deletions(-) delete mode 100644 libopenage/assets/assetmanager.cpp delete mode 100644 libopenage/assets/assetmanager.h delete mode 100644 libopenage/presenter/assets/CMakeLists.txt delete mode 100644 libopenage/presenter/assets/asset_manager.cpp delete mode 100644 libopenage/presenter/assets/asset_manager.h delete mode 100644 libopenage/renderer/gui/assetmanager_link.cpp delete mode 100644 libopenage/renderer/gui/assetmanager_link.h diff --git a/libopenage/assets/CMakeLists.txt b/libopenage/assets/CMakeLists.txt index 681d9882b9..8384d7d988 100644 --- a/libopenage/assets/CMakeLists.txt +++ b/libopenage/assets/CMakeLists.txt @@ -1,5 +1,4 @@ add_sources(libopenage - assetmanager.cpp legacy_assetmanager.cpp mod_manager.cpp modpack.cpp diff --git a/libopenage/assets/assetmanager.cpp b/libopenage/assets/assetmanager.cpp deleted file mode 100644 index a62f0c2d34..0000000000 --- a/libopenage/assets/assetmanager.cpp +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -/** - * TODO: Deprecated in favor of presenter/assets/asset_manager.h - * - */ - -#include "assetmanager.h" - -#if WITH_INOTIFY -#include /* for NAME_MAX */ -#include -#include -#endif - -#include "error/error.h" -#include "log/log.h" -#include "util/compiler.h" -#include "util/file.h" - -#include "texture.h" - -namespace openage { - -AssetManager::AssetManager(qtsdl::GuiItemLink *gui_link) : - missing_tex{nullptr}, - gui_link{gui_link} { -#if WITH_INOTIFY - // initialize the inotify instance - this->inotify_fd = inotify_init1(IN_NONBLOCK); - if (this->inotify_fd < 0) { - throw Error{MSG(err) << "Failed to initialize inotify!"}; - } -#endif -} - - -const util::Path &AssetManager::get_asset_dir() { - return this->asset_path; -} - - -void AssetManager::set_asset_dir(const util::Path &new_path) { - if (this->asset_path != new_path) { - this->asset_path = new_path; - this->clear(); - } -} - - -void AssetManager::set_display(presenter::LegacyDisplay *display) { - this->display = display; -} - - -presenter::LegacyDisplay *AssetManager::get_display() const { - return this->display; -} - -void AssetManager::set_engine(gamestate::GameSimulation *engine) { - this->engine = engine; -} - -gamestate::GameSimulation *AssetManager::get_engine() const { - return this->engine; -} - - -std::shared_ptr AssetManager::load_texture(const std::string &name, - bool use_metafile, - bool null_if_missing) { - // the texture to be associated with the given filename - std::shared_ptr tex; - - util::Path tex_path = this->asset_path[name]; - - // try to open the texture filename. - if (not tex_path.is_file()) { - // TODO: add/fetch inotify watch on the containing folder - // to display the tex as soon at it exists. - - if (null_if_missing) { - return nullptr; - } - else { - // return the big X texture instead - tex = this->get_missing_tex(); - } - } - else { - // create the texture! - tex = std::make_shared(tex_path, use_metafile); - -#if WITH_INOTIFY - std::string native_path = tex_path.resolve_native_path(); - - if (native_path.size() > 0) { - // create inotify update trigger for the requested file - - // TODO: let util::Path do the file watching - int wd = inotify_add_watch( - this->inotify_fd, - native_path.c_str(), - IN_CLOSE_WRITE); - - if (wd < 0) { - log::log(WARN << "Failed to add inotify watch for " << native_path); - } - else { - this->watch_fds[wd] = tex; - } - } -#endif - } - - // pass back the shared_ptr - return tex; -} - - -Texture *AssetManager::get_texture(const std::string &name, bool use_metafile, bool null_if_missing) { - // check whether the requested texture was loaded already - auto tex_it = this->textures.find(name); - - // the texture was not loaded yet: - if (tex_it == this->textures.end()) { - auto tex = this->load_texture(name, use_metafile, null_if_missing); - - if (tex.get() != nullptr) { - // insert the texture into the map - this->textures.insert(std::make_pair(name, tex)); - } - - // and return the texture pointer. - return tex.get(); - } - - return tex_it->second.get(); -} - - -void AssetManager::check_updates() { -#if WITH_INOTIFY - // buffer for at least 4 inotify events - char buf[4 * (sizeof(struct inotify_event) + NAME_MAX + 1)]; - ssize_t len; - - while (true) { - // fetch all events, the kernel won't write "half" structs. - len = read(this->inotify_fd, buf, sizeof(buf)); - - if (len == -1) { - if (errno == EAGAIN) { - // no events, nothing to do. - break; - } - else { - // something went wrong - log::log(WARN << "Failed to read inotify events!"); - break; - } - } - - // process fetched events, - // the kernel guarantees complete events in the buffer. - char *ptr = buf; - while (ptr < buf + len) { - auto *event = reinterpret_cast(ptr); - - if (event->mask & IN_CLOSE_WRITE) { - // TODO: this should invoke callback functions - this->watch_fds[event->wd]->reload(); - } - - // move the buffer ptr to the next event. - ptr += sizeof(struct inotify_event) + event->len; - } - } -#endif -} - -std::shared_ptr AssetManager::get_missing_tex() { - // if not loaded, fetch the "missing" texture (big red X). - if (this->missing_tex.get() == nullptr) [[unlikely]] { - this->missing_tex = std::make_shared( - this->asset_path["test"]["textures"]["missing.png"], - false); - } - - return this->missing_tex; -} - -void AssetManager::clear() { -#if WITH_INOTIFY - for (auto &watch_fd : this->watch_fds) { - int result = inotify_rm_watch(this->inotify_fd, watch_fd.first); - if (result < 0) { - log::log(WARN << "Failed to remove inotify watches"); - } - } - this->watch_fds.clear(); -#endif - - this->textures.clear(); -} - -} // namespace openage diff --git a/libopenage/assets/assetmanager.h b/libopenage/assets/assetmanager.h deleted file mode 100644 index 5fff6b0328..0000000000 --- a/libopenage/assets/assetmanager.h +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include "config.h" - -#include -#include -#include - -#include "presenter/legacy/legacy.h" -#include "util/path.h" - -namespace qtsdl { -class GuiItemLink; -} // namespace qtsdl - -namespace openage { -class Texture; - -namespace gamestate { -class GameSimulation; -} - -/** - * Container class for all available assets. - * Responsible for loading, providing and updating requested files. - */ -class [[deprecated]] AssetManager final { -public: - explicit AssetManager(qtsdl::GuiItemLink *gui_link); - - /** - * Return the path where assets are found in. - */ - const util::Path &get_asset_dir(); - - /** - * Set the asset search path. - */ - void set_asset_dir(const util::Path &asset_dir); - - /** - * Set the game display of this asset manager. - * Called from QML. - */ - void set_display(presenter::LegacyDisplay *display); - - /** - * Return the display responsible for this asset manager. - */ - presenter::LegacyDisplay *get_display() const; - - /** - * Set the game engine of this asset manager. - * Called from QML. - */ - void set_engine(gamestate::GameSimulation *engine); - - /** - * Return the engine responsible for this asset manager. - */ - gamestate::GameSimulation *get_engine() const; - - /** - * Query the Texture for a given filename. - * - * @param name: the asset file name relative to the asset root. - * @param use_metafile: load subtexture information from meta file - * @param null_if_missing: instead of providing the "missing texture", - * return nullptr. - * @returns the queried texture handle. - */ - Texture *get_texture(const std::string &name, bool use_metafile = true, bool null_if_missing = false); - - /** - * Ask the kernel whether there were updates to watched files. - */ - void check_updates(); - -protected: - /** - * Create an internal texture handle. - */ - std::shared_ptr load_texture(const std::string &name, - bool use_metafile = true, - bool null_if_missing = false); - - /** - * Retrieves the texture for missing textures. - */ - std::shared_ptr get_missing_tex(); - -private: - void clear(); - - /** - * The display this asset manager is attached to. - */ - presenter::LegacyDisplay *display; - - /** - * The engine this asset manager is attached to. - */ - gamestate::GameSimulation *engine; - - /** - * The root directory for the available assets. - */ - util::Path asset_path; - - /** - * The replacement texture for missing textures. - */ - std::shared_ptr missing_tex; - - /** - * Map from texture filename to texture instance ptr. - */ - std::unordered_map> textures; - -#if WITH_INOTIFY - /** - * The file descriptor pointing to the inotify instance. - */ - int inotify_fd; - - /** - * Map from inotify watch handle fd to texture instance ptr. - * The kernel returns the handle fd when events are triggered. - */ - std::unordered_map> watch_fds; -#endif - -public: - qtsdl::GuiItemLink *gui_link; -}; - -} // namespace openage diff --git a/libopenage/presenter/CMakeLists.txt b/libopenage/presenter/CMakeLists.txt index d0fc81f7be..5041bacc2c 100644 --- a/libopenage/presenter/CMakeLists.txt +++ b/libopenage/presenter/CMakeLists.txt @@ -3,4 +3,3 @@ add_sources(libopenage ) add_subdirectory("legacy") -add_subdirectory("assets") diff --git a/libopenage/presenter/assets/CMakeLists.txt b/libopenage/presenter/assets/CMakeLists.txt deleted file mode 100644 index 59346cbc3e..0000000000 --- a/libopenage/presenter/assets/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -add_sources(libopenage - asset_manager.cpp -) diff --git a/libopenage/presenter/assets/asset_manager.cpp b/libopenage/presenter/assets/asset_manager.cpp deleted file mode 100644 index 0077d0db95..0000000000 --- a/libopenage/presenter/assets/asset_manager.cpp +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#include "asset_manager.h" - -#if WITH_INOTIFY -#include /* for NAME_MAX */ -#include -#include -#endif - -#include "error/error.h" -#include "log/log.h" -#include "texture.h" -#include "util/compiler.h" -#include "util/file.h" - -namespace openage::presenter { - -AssetManager::AssetManager() : - missing_tex{nullptr} { -#if WITH_INOTIFY - // initialize the inotify instance - this->inotify_fd = inotify_init1(IN_NONBLOCK); - if (this->inotify_fd < 0) { - throw Error{MSG(err) << "Failed to initialize inotify!"}; - } -#endif -} - -AssetManager::AssetManager(const util::Path &asset_dir) : - asset_dir{asset_dir}, - missing_tex{nullptr} { -#if WITH_INOTIFY - // initialize the inotify instance - this->inotify_fd = inotify_init1(IN_NONBLOCK); - if (this->inotify_fd < 0) { - throw Error{MSG(err) << "Failed to initialize inotify!"}; - } -#endif -} - - -const util::Path &AssetManager::get_asset_dir() { - return this->asset_dir; -} - - -void AssetManager::set_asset_dir(const util::Path &new_path) { - if (this->asset_dir != new_path) { - this->asset_dir = new_path; - this->clear(); - } -} - - -std::shared_ptr AssetManager::load_texture(const std::string &name, - bool use_metafile, - bool null_if_missing) { - // the texture to be associated with the given filename - std::shared_ptr tex; - - util::Path tex_path = this->asset_dir[name]; - - // try to open the texture filename. - if (not tex_path.is_file()) { - // TODO: add/fetch inotify watch on the containing folder - // to display the tex as soon at it exists. - - if (null_if_missing) { - return nullptr; - } - else { - // return the big X texture instead - tex = this->get_missing_tex(); - } - } - else { - // create the texture! - tex = std::make_shared(tex_path, use_metafile); - -#if WITH_INOTIFY - std::string native_path = tex_path.resolve_native_path(); - - if (native_path.size() > 0) { - // create inotify update trigger for the requested file - - // TODO: let util::Path do the file watching - int wd = inotify_add_watch( - this->inotify_fd, - native_path.c_str(), - IN_CLOSE_WRITE); - - if (wd < 0) { - log::log(WARN << "Failed to add inotify watch for " << native_path); - } - else { - this->watch_fds[wd] = tex; - } - } -#endif - } - - // pass back the shared_ptr - return tex; -} - - -Texture *AssetManager::get_texture(const std::string &name, bool use_metafile, bool null_if_missing) { - // check whether the requested texture was loaded already - auto tex_it = this->textures.find(name); - - // the texture was not loaded yet: - if (tex_it == this->textures.end()) { - auto tex = this->load_texture(name, use_metafile, null_if_missing); - - if (tex.get() != nullptr) { - // insert the texture into the map - this->textures.insert(std::make_pair(name, tex)); - } - - // and return the texture pointer. - return tex.get(); - } - - return tex_it->second.get(); -} - - -void AssetManager::check_updates() { -#if WITH_INOTIFY - // buffer for at least 4 inotify events - char buf[4 * (sizeof(struct inotify_event) + NAME_MAX + 1)]; - ssize_t len; - - while (true) { - // fetch all events, the kernel won't write "half" structs. - len = read(this->inotify_fd, buf, sizeof(buf)); - - if (len == -1) { - if (errno == EAGAIN) { - // no events, nothing to do. - break; - } - else { - // something went wrong - log::log(WARN << "Failed to read inotify events!"); - break; - } - } - - // process fetched events, - // the kernel guarantees complete events in the buffer. - char *ptr = buf; - while (ptr < buf + len) { - auto *event = reinterpret_cast(ptr); - - if (event->mask & IN_CLOSE_WRITE) { - // TODO: this should invoke callback functions - this->watch_fds[event->wd]->reload(); - } - - // move the buffer ptr to the next event. - ptr += sizeof(struct inotify_event) + event->len; - } - } -#endif -} - -std::shared_ptr AssetManager::get_missing_tex() { - // if not loaded, fetch the "missing" texture (big red X). - if (this->missing_tex.get() == nullptr) [[unlikely]] { - this->missing_tex = std::make_shared( - this->asset_dir["test"]["textures"]["missing.png"], - false); - } - - return this->missing_tex; -} - -void AssetManager::clear() { -#if WITH_INOTIFY - for (auto &watch_fd : this->watch_fds) { - int result = inotify_rm_watch(this->inotify_fd, watch_fd.first); - if (result < 0) { - log::log(WARN << "Failed to remove inotify watches"); - } - } - this->watch_fds.clear(); -#endif - - this->textures.clear(); -} - -} // namespace openage::presenter diff --git a/libopenage/presenter/assets/asset_manager.h b/libopenage/presenter/assets/asset_manager.h deleted file mode 100644 index fa3461501b..0000000000 --- a/libopenage/presenter/assets/asset_manager.h +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include "config.h" - -#include -#include -#include - -#include "util/path.h" - -namespace qtsdl { -class GuiItemLink; -} // namespace qtsdl - -namespace openage { -class Texture; -} // namespace openage - -namespace openage::presenter { - -/** - * Container class for all available assets. - * Responsible for loading, providing and updating requested files. - */ -class [[deprecated]] AssetManager final { -public: - AssetManager(); - AssetManager(const util::Path &asset_dir); - - /** - * Return the path where assets are found in. - */ - const util::Path &get_asset_dir(); - - /** - * Set the asset search path. - */ - void set_asset_dir(const util::Path &new_path); - - /** - * Query the Texture for a given filename. - * - * @param name: the asset file name relative to the asset root. - * @param use_metafile: load subtexture information from meta file - * @param null_if_missing: instead of providing the "missing texture", - * return nullptr. - * @returns the queried texture handle. - */ - Texture *get_texture(const std::string &name, - bool use_metafile = true, - bool null_if_missing = false); - - /** - * Ask the kernel whether there were updates to watched files. - */ - void check_updates(); - -protected: - /** - * Create an internal texture handle. - */ - std::shared_ptr load_texture(const std::string &name, - bool use_metafile = true, - bool null_if_missing = false); - - /** - * Retrieves the texture for missing textures. - */ - std::shared_ptr get_missing_tex(); - -private: - void clear(); - - /** - * The root directory for the available assets. - */ - util::Path asset_dir; - - /** - * The replacement texture for missing textures. - */ - std::shared_ptr missing_tex; - - /** - * Map from texture filename to texture instance ptr. - */ - std::unordered_map> textures; - -#if WITH_INOTIFY - /** - * The file descriptor pointing to the inotify instance. - */ - int inotify_fd; - - /** - * Map from inotify watch handle fd to texture instance ptr. - * The kernel returns the handle fd when events are triggered. - */ - std::unordered_map> watch_fds; -#endif -}; - -} // namespace openage::presenter diff --git a/libopenage/renderer/gui/CMakeLists.txt b/libopenage/renderer/gui/CMakeLists.txt index 60a1676d76..a0a61a9b62 100644 --- a/libopenage/renderer/gui/CMakeLists.txt +++ b/libopenage/renderer/gui/CMakeLists.txt @@ -1,5 +1,4 @@ add_sources(libopenage - assetmanager_link.cpp engine_link.cpp gui.cpp qml_info.cpp diff --git a/libopenage/renderer/gui/assetmanager_link.cpp b/libopenage/renderer/gui/assetmanager_link.cpp deleted file mode 100644 index 600cf51ffb..0000000000 --- a/libopenage/renderer/gui/assetmanager_link.cpp +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "assetmanager_link.h" - -#include - -#include "renderer/gui/engine_link.h" - -namespace openage { - -class LegacyEngine; - -namespace renderer { -namespace gui { - -namespace { -const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "AssetManager"); -} - -AssetManagerLink::AssetManagerLink(QObject *parent) : - GuiItemQObject{parent}, - GuiItem{this} { - Q_UNUSED(registration); -} - -AssetManagerLink::~AssetManagerLink() = default; - - -const util::Path &AssetManagerLink::get_asset_dir() const { - return this->asset_dir; -} - - -void AssetManagerLink::set_asset_dir(const util::Path &asset_dir) { - static auto f = [](AssetManager *_this, const util::Path &dir) { - _this->set_asset_dir(dir); - }; - this->s(f, this->asset_dir, asset_dir); -} - - -EngineLink *AssetManagerLink::get_engine() const { - return this->engine; -} - - -void AssetManagerLink::set_engine(EngineLink *engine_link) { - static auto f = [](AssetManager *_this, gamestate::GameSimulation *engine) { - _this->set_engine(engine); - }; - this->s(f, this->engine, engine_link); -} - - -} // namespace gui -} // namespace renderer -} // namespace openage diff --git a/libopenage/renderer/gui/assetmanager_link.h b/libopenage/renderer/gui/assetmanager_link.h deleted file mode 100644 index 3378558855..0000000000 --- a/libopenage/renderer/gui/assetmanager_link.h +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include "assets/assetmanager.h" -#include "gui/guisys/link/gui_item.h" -#include "util/path.h" - - -namespace openage { -namespace renderer::gui { -class AssetManagerLink; -class EngineLink; -} // namespace renderer::gui -} // namespace openage - -namespace qtsdl { -template <> -struct Wrap { - using Type = openage::renderer::gui::AssetManagerLink; -}; - -template <> -struct Unwrap { - using Type = openage::AssetManager; -}; - -} // namespace qtsdl - - -namespace openage { -namespace renderer { -namespace gui { - -class AssetManagerLink : public qtsdl::GuiItemQObject - , public qtsdl::GuiItem { - Q_OBJECT - - Q_PROPERTY(openage::util::Path assetDir READ get_asset_dir WRITE set_asset_dir) - Q_MOC_INCLUDE("renderer/gui/engine_link.h") - Q_PROPERTY(EngineLink *engine READ get_engine WRITE set_engine) - -public: - explicit AssetManagerLink(QObject *parent = nullptr); - virtual ~AssetManagerLink(); - - const util::Path &get_asset_dir() const; - void set_asset_dir(const util::Path &data_dir); - - EngineLink *get_engine() const; - void set_engine(EngineLink *engine); - -private: - util::Path asset_dir; - EngineLink *engine; -}; - -} // namespace gui -} // namespace renderer -} // namespace openage From c3c4a22dcf5dd93a6b51197dd1fdaf24b099c446 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 22 Sep 2023 23:03:54 +0200 Subject: [PATCH 028/771] assets: Remove old asset management entirely. --- libopenage/assets/CMakeLists.txt | 1 - libopenage/assets/legacy_assetmanager.cpp | 207 ------------ libopenage/assets/legacy_assetmanager.h | 137 -------- libopenage/gamestate/old/game_spec.cpp | 364 ++++++++++------------ libopenage/gamestate/old/game_spec.h | 22 +- libopenage/gui/CMakeLists.txt | 1 - libopenage/gui/assetmanager_link.cpp | 55 ---- libopenage/gui/assetmanager_link.h | 58 ---- libopenage/gui/game_spec_link.cpp | 25 +- libopenage/gui/game_spec_link.h | 6 - libopenage/terrain/terrain.h | 2 +- 11 files changed, 174 insertions(+), 704 deletions(-) delete mode 100644 libopenage/assets/legacy_assetmanager.cpp delete mode 100644 libopenage/assets/legacy_assetmanager.h delete mode 100644 libopenage/gui/assetmanager_link.cpp delete mode 100644 libopenage/gui/assetmanager_link.h diff --git a/libopenage/assets/CMakeLists.txt b/libopenage/assets/CMakeLists.txt index 8384d7d988..234ac3b1b0 100644 --- a/libopenage/assets/CMakeLists.txt +++ b/libopenage/assets/CMakeLists.txt @@ -1,5 +1,4 @@ add_sources(libopenage - legacy_assetmanager.cpp mod_manager.cpp modpack.cpp ) diff --git a/libopenage/assets/legacy_assetmanager.cpp b/libopenage/assets/legacy_assetmanager.cpp deleted file mode 100644 index 1058aee26e..0000000000 --- a/libopenage/assets/legacy_assetmanager.cpp +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -/** - * TODO: Deprecated in favor of presenter/assets/asset_manager.h - * - */ - -#include "legacy_assetmanager.h" - -#if WITH_INOTIFY -#include /* for NAME_MAX */ -#include -#include -#endif - -#include "error/error.h" -#include "log/log.h" -#include "util/compiler.h" -#include "util/file.h" - -#include "texture.h" - -namespace openage { - -LegacyAssetManager::LegacyAssetManager(qtsdl::GuiItemLink *gui_link) : - missing_tex{nullptr}, - gui_link{gui_link} { -#if WITH_INOTIFY - // initialize the inotify instance - this->inotify_fd = inotify_init1(IN_NONBLOCK); - if (this->inotify_fd < 0) { - throw Error{MSG(err) << "Failed to initialize inotify!"}; - } -#endif -} - - -const util::Path &LegacyAssetManager::get_asset_dir() { - return this->asset_path; -} - - -void LegacyAssetManager::set_asset_dir(const util::Path &new_path) { - if (this->asset_path != new_path) { - this->asset_path = new_path; - this->clear(); - } -} - - -void LegacyAssetManager::set_display(presenter::LegacyDisplay *display) { - this->display = display; -} - - -presenter::LegacyDisplay *LegacyAssetManager::get_display() const { - return this->display; -} - -void LegacyAssetManager::set_engine(LegacyEngine *engine) { - this->engine = engine; -} - -LegacyEngine *LegacyAssetManager::get_engine() const { - return this->engine; -} - - -std::shared_ptr LegacyAssetManager::load_texture(const std::string &name, - bool use_metafile, - bool null_if_missing) { - // the texture to be associated with the given filename - std::shared_ptr tex; - - util::Path tex_path = this->asset_path[name]; - - // try to open the texture filename. - if (not tex_path.is_file()) { - // TODO: add/fetch inotify watch on the containing folder - // to display the tex as soon at it exists. - - if (null_if_missing) { - return nullptr; - } - else { - // return the big X texture instead - tex = this->get_missing_tex(); - } - } - else { - // create the texture! - tex = std::make_shared(tex_path, use_metafile); - -#if WITH_INOTIFY - std::string native_path = tex_path.resolve_native_path(); - - if (native_path.size() > 0) { - // create inotify update trigger for the requested file - - // TODO: let util::Path do the file watching - int wd = inotify_add_watch( - this->inotify_fd, - native_path.c_str(), - IN_CLOSE_WRITE); - - if (wd < 0) { - log::log(WARN << "Failed to add inotify watch for " << native_path); - } - else { - this->watch_fds[wd] = tex; - } - } -#endif - } - - // pass back the shared_ptr - return tex; -} - - -Texture *LegacyAssetManager::get_texture(const std::string &name, bool use_metafile, bool null_if_missing) { - // check whether the requested texture was loaded already - auto tex_it = this->textures.find(name); - - // the texture was not loaded yet: - if (tex_it == this->textures.end()) { - auto tex = this->load_texture(name, use_metafile, null_if_missing); - - if (tex.get() != nullptr) { - // insert the texture into the map - this->textures.insert(std::make_pair(name, tex)); - } - - // and return the texture pointer. - return tex.get(); - } - - return tex_it->second.get(); -} - - -void LegacyAssetManager::check_updates() { -#if WITH_INOTIFY - // buffer for at least 4 inotify events - char buf[4 * (sizeof(struct inotify_event) + NAME_MAX + 1)]; - ssize_t len; - - while (true) { - // fetch all events, the kernel won't write "half" structs. - len = read(this->inotify_fd, buf, sizeof(buf)); - - if (len == -1) { - if (errno == EAGAIN) { - // no events, nothing to do. - break; - } - else { - // something went wrong - log::log(WARN << "Failed to read inotify events!"); - break; - } - } - - // process fetched events, - // the kernel guarantees complete events in the buffer. - char *ptr = buf; - while (ptr < buf + len) { - auto *event = reinterpret_cast(ptr); - - if (event->mask & IN_CLOSE_WRITE) { - // TODO: this should invoke callback functions - this->watch_fds[event->wd]->reload(); - } - - // move the buffer ptr to the next event. - ptr += sizeof(struct inotify_event) + event->len; - } - } -#endif -} - -std::shared_ptr LegacyAssetManager::get_missing_tex() { - // if not loaded, fetch the "missing" texture (big red X). - if (this->missing_tex.get() == nullptr) [[unlikely]] { - this->missing_tex = std::make_shared( - this->asset_path["test"]["textures"]["missing.png"], - false); - } - - return this->missing_tex; -} - -void LegacyAssetManager::clear() { -#if WITH_INOTIFY - for (auto &watch_fd : this->watch_fds) { - int result = inotify_rm_watch(this->inotify_fd, watch_fd.first); - if (result < 0) { - log::log(WARN << "Failed to remove inotify watches"); - } - } - this->watch_fds.clear(); -#endif - - this->textures.clear(); -} - -} // namespace openage diff --git a/libopenage/assets/legacy_assetmanager.h b/libopenage/assets/legacy_assetmanager.h deleted file mode 100644 index c4436007e9..0000000000 --- a/libopenage/assets/legacy_assetmanager.h +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include "config.h" - -#include -#include -#include - -#include "presenter/legacy/legacy.h" -#include "util/path.h" - -namespace qtsdl { -class GuiItemLink; -} // namespace qtsdl - -namespace openage { - -class LegacyEngine; -class Texture; - -/** - * Container class for all available assets. - * Responsible for loading, providing and updating requested files. - */ -class [[deprecated]] LegacyAssetManager final { -public: - explicit LegacyAssetManager(qtsdl::GuiItemLink *gui_link); - - /** - * Return the path where assets are found in. - */ - const util::Path &get_asset_dir(); - - /** - * Set the asset search path. - */ - void set_asset_dir(const util::Path &asset_dir); - - /** - * Set the game display of this asset manager. - * Called from QML. - */ - void set_display(presenter::LegacyDisplay *display); - - /** - * Return the display responsible for this asset manager. - */ - presenter::LegacyDisplay *get_display() const; - - /** - * Set the game engine of this asset manager. - * Called from QML. - */ - void set_engine(LegacyEngine *engine); - - /** - * Return the engine responsible for this asset manager. - */ - LegacyEngine *get_engine() const; - - /** - * Query the Texture for a given filename. - * - * @param name: the asset file name relative to the asset root. - * @param use_metafile: load subtexture information from meta file - * @param null_if_missing: instead of providing the "missing texture", - * return nullptr. - * @returns the queried texture handle. - */ - Texture *get_texture(const std::string &name, bool use_metafile = true, bool null_if_missing = false); - - /** - * Ask the kernel whether there were updates to watched files. - */ - void check_updates(); - -protected: - /** - * Create an internal texture handle. - */ - std::shared_ptr load_texture(const std::string &name, - bool use_metafile = true, - bool null_if_missing = false); - - /** - * Retrieves the texture for missing textures. - */ - std::shared_ptr get_missing_tex(); - -private: - void clear(); - - /** - * The display this asset manager is attached to. - */ - presenter::LegacyDisplay *display; - - /** - * The engine this asset manager is attached to. - */ - LegacyEngine *engine; - - /** - * The root directory for the available assets. - */ - util::Path asset_path; - - /** - * The replacement texture for missing textures. - */ - std::shared_ptr missing_tex; - - /** - * Map from texture filename to texture instance ptr. - */ - std::unordered_map> textures; - -#if WITH_INOTIFY - /** - * The file descriptor pointing to the inotify instance. - */ - int inotify_fd; - - /** - * Map from inotify watch handle fd to texture instance ptr. - * The kernel returns the handle fd when events are triggered. - */ - std::unordered_map> watch_fds; -#endif - -public: - qtsdl::GuiItemLink *gui_link; -}; - -} // namespace openage diff --git a/libopenage/gamestate/old/game_spec.cpp b/libopenage/gamestate/old/game_spec.cpp index c35528d62f..048fbc4ce9 100644 --- a/libopenage/gamestate/old/game_spec.cpp +++ b/libopenage/gamestate/old/game_spec.cpp @@ -6,24 +6,22 @@ #include "../../audio/error.h" #include "../../audio/resource_def.h" -#include "../../legacy_engine.h" #include "../../gamedata/blending_mode_dummy.h" #include "../../gamedata/string_resource_dummy.h" #include "../../gamedata/terrain_dummy.h" +#include "../../legacy_engine.h" #include "../../log/log.h" #include "../../rng/global_rng.h" #include "../../unit/producer.h" #include "../../util/compiler.h" #include "../../util/strings.h" #include "../../util/timer.h" -#include "assets/legacy_assetmanager.h" #include "civilisation.h" namespace openage { -GameSpec::GameSpec(LegacyAssetManager *am) : - assetmanager{am}, +GameSpec::GameSpec() : gamedata_loaded{false} { } @@ -34,38 +32,38 @@ bool GameSpec::initialize() { util::Timer load_timer; load_timer.start(); - const util::Path &asset_dir = this->assetmanager->get_asset_dir(); + // const util::Path &asset_dir = this->assetmanager->get_asset_dir(); - log::log(MSG(info) << "Loading game specification files..."); + // log::log(MSG(info) << "Loading game specification files..."); - std::vector string_resources = util::read_csv_file( - asset_dir["converted/string_resources.docx"]); + // std::vector string_resources = util::read_csv_file( + // asset_dir["converted/string_resources.docx"]); - try { - // read the packed csv file - util::CSVCollection raw_gamedata{ - asset_dir["converted/gamedata/gamedata.docx"]}; + // try { + // // read the packed csv file + // util::CSVCollection raw_gamedata{ + // asset_dir["converted/gamedata/gamedata.docx"]}; - // parse the original game description files - this->gamedata = raw_gamedata.read( - "gamedata-empiresdat.docx"); + // // parse the original game description files + // this->gamedata = raw_gamedata.read( + // "gamedata-empiresdat.docx"); - this->load_terrain(this->gamedata[0]); + // this->load_terrain(this->gamedata[0]); - // process and load the game description files - this->on_gamedata_loaded(this->gamedata[0]); - this->gamedata_loaded = true; - } - catch (Error &exc) { - // rethrow allmighty openage exceptions - throw; - } - catch (std::exception &exc) { - // unfortunately we have no idea of the std::exception backtrace - throw Error{ERR << "gamedata could not be loaded: " - << util::typestring(exc) - << ": " << exc.what()}; - } + // // process and load the game description files + // this->on_gamedata_loaded(this->gamedata[0]); + // this->gamedata_loaded = true; + // } + // catch (Error &exc) { + // // rethrow allmighty openage exceptions + // throw; + // } + // catch (std::exception &exc) { + // // unfortunately we have no idea of the std::exception backtrace + // throw Error{ERR << "gamedata could not be loaded: " + // << util::typestring(exc) + // << ": " << exc.what()}; + // } log::log(MSG(info).fmt("Loading time [data]: %5.3f s", load_timer.getval() / 1e9)); @@ -105,7 +103,8 @@ Texture *GameSpec::get_texture(index_t graphic_id) const { Texture *GameSpec::get_texture(const std::string &file_name, bool use_metafile) const { // return nullptr if the texture wasn't found (3rd param) - return this->assetmanager->get_texture(file_name, use_metafile, true); + // return this->assetmanager->get_texture(file_name, use_metafile, true); + return nullptr; } std::shared_ptr GameSpec::get_unit_texture(index_t unit_id) const { @@ -179,85 +178,79 @@ void GameSpec::create_unit_types(unit_meta_list &objects, int civ_id) const { } } +void GameSpec::on_gamedata_loaded(const gamedata::empiresdat &gamedata) { + // const util::Path &asset_dir = this->assetmanager->get_asset_dir(); + // util::Path sound_dir = asset_dir["converted/sounds"]; -LegacyAssetManager *GameSpec::get_asset_manager() const { - return this->assetmanager; -} + // // create graphic id => graphic map + // for (auto &graphic : gamedata.graphics.data) { + // this->graphics[graphic.graphic_id] = &graphic; + // this->slp_to_graphic[graphic.slp_id] = graphic.graphic_id; + // } + // log::log(INFO << "Loading textures..."); -void GameSpec::on_gamedata_loaded(const gamedata::empiresdat &gamedata) { - const util::Path &asset_dir = this->assetmanager->get_asset_dir(); - util::Path sound_dir = asset_dir["converted/sounds"]; + // // create complete set of unit textures + // for (auto &g : this->graphics) { + // this->unit_textures.insert({g.first, std::make_shared(*this, g.second)}); + // } - // create graphic id => graphic map - for (auto &graphic : gamedata.graphics.data) { - this->graphics[graphic.graphic_id] = &graphic; - this->slp_to_graphic[graphic.slp_id] = graphic.graphic_id; - } + // log::log(INFO << "Loading sounds..."); - log::log(INFO << "Loading textures..."); + // // playable sound files for the audio manager + // std::vector load_sound_files; - // create complete set of unit textures - for (auto &g : this->graphics) { - this->unit_textures.insert({g.first, std::make_shared(*this, g.second)}); - } + // // all sounds defined in the game specification + // for (const gamedata::sound &sound : gamedata.sounds.data) { + // std::vector sound_items; - log::log(INFO << "Loading sounds..."); - - // playable sound files for the audio manager - std::vector load_sound_files; - - // all sounds defined in the game specification - for (const gamedata::sound &sound : gamedata.sounds.data) { - std::vector sound_items; - - // each sound may have multiple variation, - // processed in this loop - // these are the single sound files. - for (const gamedata::sound_item &item : sound.sound_items.data) { - if (item.resource_id < 0) { - log::log(SPAM << " Invalid sound resource id < 0"); - continue; - } - - std::string snd_filename = util::sformat("%d.opus", item.resource_id); - util::Path snd_path = sound_dir[snd_filename]; - - if (not snd_path.is_file()) { - continue; - } - - // single items for a sound (so that we can ramdomize it) - sound_items.push_back(item.resource_id); - - // the single sound will be loaded in the audio system. - audio::resource_def resource{ - audio::category_t::GAME, - item.resource_id, - snd_path, - audio::format_t::OPUS, - audio::loader_policy_t::DYNAMIC}; - load_sound_files.push_back(resource); - } + // // each sound may have multiple variation, + // // processed in this loop + // // these are the single sound files. + // for (const gamedata::sound_item &item : sound.sound_items.data) { + // if (item.resource_id < 0) { + // log::log(SPAM << " Invalid sound resource id < 0"); + // continue; + // } + // std::string snd_filename = util::sformat("%d.opus", item.resource_id); + // util::Path snd_path = sound_dir[snd_filename]; - // create test sound objects that can be played later - this->available_sounds.insert({sound.sound_id, - Sound{ - this, - std::move(sound_items)}}); - } + // if (not snd_path.is_file()) { + // continue; + // } + + // // single items for a sound (so that we can ramdomize it) + // sound_items.push_back(item.resource_id); + + // // the single sound will be loaded in the audio system. + // audio::resource_def resource{ + // audio::category_t::GAME, + // item.resource_id, + // snd_path, + // audio::format_t::OPUS, + // audio::loader_policy_t::DYNAMIC}; + // load_sound_files.push_back(resource); + // } + + + // // create test sound objects that can be played later + // this->available_sounds.insert({sound.sound_id, + // Sound{ + // this, + // std::move(sound_items)}}); + // } - // TODO: move out the loading of the sound. - // this class only provides the names and locations + // // TODO: move out the loading of the sound. + // // this class only provides the names and locations - // load the requested sounds. - audio::AudioManager &am = this->assetmanager->get_display()->get_audio_manager(); - am.load_resources(load_sound_files); + // // load the requested sounds. + // audio::AudioManager &am = this->assetmanager->get_display()->get_audio_manager(); + // am.load_resources(load_sound_files); - // this final step occurs after loading media - // as producers require both the graphics and sounds - this->create_abilities(gamedata); + // // this final step occurs after loading media + // // as producers require both the graphics and sounds + // this->create_abilities(gamedata); } bool GameSpec::valid_graphic_id(index_t graphic_id) const { @@ -313,69 +306,69 @@ void GameSpec::load_missile(const gamedata::missile_unit &proj, unit_meta_list & void GameSpec::load_terrain(const gamedata::empiresdat &gamedata) { // fetch blending modes - util::Path convert_dir = this->assetmanager->get_asset_dir()["converted"]; - std::vector blending_meta = util::read_csv_file( - convert_dir["blending_modes.docx"]); - - // copy the terrain metainformation - std::vector terrain_meta = gamedata.terrains.data; - - // remove any disabled textures - terrain_meta.erase( - std::remove_if( - terrain_meta.begin(), - terrain_meta.end(), - [](const gamedata::terrain_type &t) { - return not t.enabled; - }), - terrain_meta.end()); - - // result attributes - this->terrain_data.terrain_id_count = terrain_meta.size(); - this->terrain_data.blendmode_count = blending_meta.size(); - this->terrain_data.textures.resize(terrain_data.terrain_id_count); - this->terrain_data.blending_masks.reserve(terrain_data.blendmode_count); - this->terrain_data.terrain_id_priority_map = std::make_unique( - this->terrain_data.terrain_id_count); - this->terrain_data.terrain_id_blendmode_map = std::make_unique( - this->terrain_data.terrain_id_count); - this->terrain_data.influences_buf = std::make_unique( - this->terrain_data.terrain_id_count); - - - log::log(MSG(dbg) << "Terrain prefs: " - << "tiletypes=" << terrain_data.terrain_id_count << ", " - "blendmodes=" - << terrain_data.blendmode_count); - - // create tile textures (snow, ice, grass, whatever) - for (size_t terrain_id = 0; - terrain_id < terrain_data.terrain_id_count; - terrain_id++) { - auto line = &terrain_meta[terrain_id]; - - // TODO: terrain double-define check? - terrain_data.terrain_id_priority_map[terrain_id] = line->blend_priority; - terrain_data.terrain_id_blendmode_map[terrain_id] = line->blend_mode; - - // TODO: remove hardcoding and rely on nyan data - auto terraintex_filename = util::sformat("converted/terrain/%d.slp.png", - line->slp_id); - - auto new_texture = this->assetmanager->get_texture(terraintex_filename, true); - - terrain_data.textures[terrain_id] = new_texture; - } - - // create blending masks (see doc/media/blendomatic) - for (size_t i = 0; i < terrain_data.blendmode_count; i++) { - auto line = &blending_meta[i]; - - // TODO: remove hardcodingn and use nyan data - std::string mask_filename = util::sformat("converted/blendomatic/mode%02d.png", - line->blend_mode); - terrain_data.blending_masks[i] = this->assetmanager->get_texture(mask_filename); - } + // util::Path convert_dir = this->assetmanager->get_asset_dir()["converted"]; + // std::vector blending_meta = util::read_csv_file( + // convert_dir["blending_modes.docx"]); + + // // copy the terrain metainformation + // std::vector terrain_meta = gamedata.terrains.data; + + // // remove any disabled textures + // terrain_meta.erase( + // std::remove_if( + // terrain_meta.begin(), + // terrain_meta.end(), + // [](const gamedata::terrain_type &t) { + // return not t.enabled; + // }), + // terrain_meta.end()); + + // // result attributes + // this->terrain_data.terrain_id_count = terrain_meta.size(); + // this->terrain_data.blendmode_count = blending_meta.size(); + // this->terrain_data.textures.resize(terrain_data.terrain_id_count); + // this->terrain_data.blending_masks.reserve(terrain_data.blendmode_count); + // this->terrain_data.terrain_id_priority_map = std::make_unique( + // this->terrain_data.terrain_id_count); + // this->terrain_data.terrain_id_blendmode_map = std::make_unique( + // this->terrain_data.terrain_id_count); + // this->terrain_data.influences_buf = std::make_unique( + // this->terrain_data.terrain_id_count); + + + // log::log(MSG(dbg) << "Terrain prefs: " + // << "tiletypes=" << terrain_data.terrain_id_count << ", " + // "blendmodes=" + // << terrain_data.blendmode_count); + + // // create tile textures (snow, ice, grass, whatever) + // for (size_t terrain_id = 0; + // terrain_id < terrain_data.terrain_id_count; + // terrain_id++) { + // auto line = &terrain_meta[terrain_id]; + + // // TODO: terrain double-define check? + // terrain_data.terrain_id_priority_map[terrain_id] = line->blend_priority; + // terrain_data.terrain_id_blendmode_map[terrain_id] = line->blend_mode; + + // // TODO: remove hardcoding and rely on nyan data + // auto terraintex_filename = util::sformat("converted/terrain/%d.slp.png", + // line->slp_id); + + // auto new_texture = this->assetmanager->get_texture(terraintex_filename, true); + + // terrain_data.textures[terrain_id] = new_texture; + // } + + // // create blending masks (see doc/media/blendomatic) + // for (size_t i = 0; i < terrain_data.blendmode_count; i++) { + // auto line = &blending_meta[i]; + + // // TODO: remove hardcodingn and use nyan data + // std::string mask_filename = util::sformat("converted/blendomatic/mode%02d.png", + // line->blend_mode); + // terrain_data.blending_masks[i] = this->assetmanager->get_texture(mask_filename); + // } } @@ -413,25 +406,24 @@ void Sound::play() const { int rand = rng::random_range(0, this->sound_items.size()); int sndid = this->sound_items.at(rand); - try { - // TODO: buhuuuu gnargghh this has to be moved to the asset loading subsystem hnnnng - audio::AudioManager &am = this->game_spec->get_asset_manager()->get_display()->get_audio_manager(); + // try { + // // TODO: buhuuuu gnargghh this has to be moved to the asset loading subsystem hnnnng + // audio::AudioManager &am = this->game_spec->get_asset_manager()->get_display()->get_audio_manager(); - if (not am.is_available()) { - return; - } + // if (not am.is_available()) { + // return; + // } - audio::Sound sound = am.get_sound(audio::category_t::GAME, sndid); - sound.play(); - } - catch (audio::Error &e) { - log::log(MSG(warn) << "cannot play: " << e); - } + // audio::Sound sound = am.get_sound(audio::category_t::GAME, sndid); + // sound.play(); + // } + // catch (audio::Error &e) { + // log::log(MSG(warn) << "cannot play: " << e); + // } } GameSpecHandle::GameSpecHandle(qtsdl::GuiItemLink *gui_link) : active{}, - asset_manager{}, gui_signals{std::make_shared()}, gui_link{gui_link} { } @@ -442,14 +434,6 @@ void GameSpecHandle::set_active(bool active) { this->start_loading_if_needed(); } -void GameSpecHandle::set_asset_manager(LegacyAssetManager *asset_manager) { - if (this->asset_manager != asset_manager) { - this->asset_manager = asset_manager; - - this->start_loading_if_needed(); - } -} - bool GameSpecHandle::is_ready() const { return this->spec && this->spec->load_complete(); } @@ -457,9 +441,6 @@ bool GameSpecHandle::is_ready() const { void GameSpecHandle::invalidate() { this->spec = nullptr; - if (this->asset_manager) - this->asset_manager->check_updates(); - this->start_loading_if_needed(); } @@ -473,13 +454,6 @@ std::shared_ptr GameSpecHandle::get_spec() { } void GameSpecHandle::start_loading_if_needed() { - if (this->active && this->asset_manager && !this->spec) { - // create the game specification - this->spec = std::make_shared(this->asset_manager); - - // the load the data - this->start_load_job(); - } } void GameSpecHandle::start_load_job() { @@ -512,12 +486,6 @@ void GameSpecHandle::start_load_job() { emit gui_signals_ptr->load_job_finished(); } }; - - job::JobManager *job_mgr = this->asset_manager->get_engine()->get_job_manager(); - - std::get>(*spec_and_job_ptr) = job_mgr->enqueue( - perform_load, - load_finished); } } // namespace openage diff --git a/libopenage/gamestate/old/game_spec.h b/libopenage/gamestate/old/game_spec.h index c5a9a66348..0fc34dcd40 100644 --- a/libopenage/gamestate/old/game_spec.h +++ b/libopenage/gamestate/old/game_spec.h @@ -10,8 +10,8 @@ #include "terrain/terrain.h" #include "types.h" -#include #include +#include #include @@ -65,7 +65,7 @@ class Sound { */ class GameSpec { public: - GameSpec(LegacyAssetManager *am); + GameSpec(); virtual ~GameSpec(); /** @@ -133,12 +133,6 @@ class GameSpec { */ void create_unit_types(unit_meta_list &objects, int civ_id) const; - /** - * Return the asset manager used for loading resources - * of this game specification. - */ - LegacyAssetManager *get_asset_manager() const; - private: /** * check graphic id is valid @@ -181,11 +175,6 @@ class GameSpec { */ void on_gamedata_loaded(const gamedata::empiresdat &gamedata); - /** - * Asset management entity that is responsible for textures, sounds, etc. - */ - LegacyAssetManager *assetmanager; - /** * The full original gamedata tree. */ @@ -253,11 +242,6 @@ class GameSpecHandle { */ void set_active(bool active); - /** - * invoked from qml when the asset_manager member is set. - */ - void set_asset_manager(LegacyAssetManager *asset_manager); - /** * Return if the specification was fully loaded. */ @@ -305,8 +289,6 @@ class GameSpecHandle { */ bool active; - LegacyAssetManager *asset_manager; - public: std::shared_ptr gui_signals; qtsdl::GuiItemLink *gui_link; diff --git a/libopenage/gui/CMakeLists.txt b/libopenage/gui/CMakeLists.txt index e63046c401..1697bc8d78 100644 --- a/libopenage/gui/CMakeLists.txt +++ b/libopenage/gui/CMakeLists.txt @@ -1,5 +1,4 @@ add_sources(libopenage - assetmanager_link.cpp actions_list_model.cpp category_contents_list_model.cpp engine_info.cpp diff --git a/libopenage/gui/assetmanager_link.cpp b/libopenage/gui/assetmanager_link.cpp deleted file mode 100644 index 7d05bd5564..0000000000 --- a/libopenage/gui/assetmanager_link.cpp +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "assetmanager_link.h" - -#include - -#include "engine_link.h" - -namespace openage { - -class LegacyEngine; - -namespace gui { - -namespace { -const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "LegacyAssetManager"); -} - -AssetManagerLink::AssetManagerLink(QObject *parent) : - GuiItemQObject{parent}, - GuiItem{this} { - Q_UNUSED(registration); -} - -AssetManagerLink::~AssetManagerLink() = default; - - -const util::Path &AssetManagerLink::get_asset_dir() const { - return this->asset_dir; -} - - -void AssetManagerLink::set_asset_dir(const util::Path &asset_dir) { - static auto f = [](LegacyAssetManager *_this, const util::Path &dir) { - _this->set_asset_dir(dir); - }; - this->s(f, this->asset_dir, asset_dir); -} - - -EngineLink *AssetManagerLink::get_engine() const { - return this->engine; -} - - -void AssetManagerLink::set_engine(EngineLink *engine_link) { - static auto f = [](LegacyAssetManager *_this, LegacyEngine *engine) { - _this->set_engine(engine); - }; - this->s(f, this->engine, engine_link); -} - - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/assetmanager_link.h b/libopenage/gui/assetmanager_link.h deleted file mode 100644 index effa0abc33..0000000000 --- a/libopenage/gui/assetmanager_link.h +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include "../util/path.h" -#include "assets/legacy_assetmanager.h" -#include "guisys/link/gui_item.h" - - -namespace openage { -namespace gui { -class AssetManagerLink; -class EngineLink; -} // namespace gui -} // namespace openage - -namespace qtsdl { -template <> -struct Wrap { - using Type = openage::gui::AssetManagerLink; -}; - -template <> -struct Unwrap { - using Type = openage::LegacyAssetManager; -}; - -} // namespace qtsdl - - -namespace openage { -namespace gui { - -class AssetManagerLink : public qtsdl::GuiItemQObject - , public qtsdl::GuiItem { - Q_OBJECT - - Q_PROPERTY(openage::util::Path assetDir READ get_asset_dir WRITE set_asset_dir) - Q_MOC_INCLUDE("gui/engine_link.h") - Q_PROPERTY(openage::gui::EngineLink *engine READ get_engine WRITE set_engine) - -public: - explicit AssetManagerLink(QObject *parent = nullptr); - virtual ~AssetManagerLink(); - - const util::Path &get_asset_dir() const; - void set_asset_dir(const util::Path &data_dir); - - EngineLink *get_engine() const; - void set_engine(EngineLink *engine); - -private: - util::Path asset_dir; - EngineLink *engine; -}; - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/game_spec_link.cpp b/libopenage/gui/game_spec_link.cpp index dea041ce5c..be8ebf6bb1 100644 --- a/libopenage/gui/game_spec_link.cpp +++ b/libopenage/gui/game_spec_link.cpp @@ -4,23 +4,19 @@ #include -#include "assetmanager_link.h" - namespace openage::gui { namespace { const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "GameSpec"); const int registration_of_ptr = qRegisterMetaType>("shared_ptr"); -} +} // namespace -GameSpecLink::GameSpecLink(QObject *parent) - : +GameSpecLink::GameSpecLink(QObject *parent) : GuiItemQObject{parent}, QQmlParserStatus{}, GuiItem{this}, state{}, active{}, - asset_manager{}, terrain_id_count{} { Q_UNUSED(registration); Q_UNUSED(registration_of_ptr); @@ -42,7 +38,7 @@ void GameSpecLink::componentComplete() { } void GameSpecLink::on_load_job_finished() { - static auto f = [] (GameSpecHandle *_this) { + static auto f = [](GameSpecHandle *_this) { _this->announce_spec(); }; this->i(f); @@ -69,7 +65,7 @@ GameSpecLink::State GameSpecLink::get_state() const { } void GameSpecLink::invalidate() { - static auto f = [] (GameSpecHandle *_this) { + static auto f = [](GameSpecHandle *_this) { _this->invalidate(); }; this->i(f); @@ -82,7 +78,7 @@ bool GameSpecLink::get_active() const { } void GameSpecLink::set_active(bool active) { - static auto f = [] (GameSpecHandle *_this, bool active) { + static auto f = [](GameSpecHandle *_this, bool active) { _this->set_active(active); }; this->s(f, this->active, active); @@ -90,17 +86,6 @@ void GameSpecLink::set_active(bool active) { this->set_state(this->active && this->state == State::Null ? State::Loading : this->state); } -AssetManagerLink* GameSpecLink::get_asset_manager() const { - return this->asset_manager; -} - -void GameSpecLink::set_asset_manager(AssetManagerLink *asset_manager) { - static auto f = [] (GameSpecHandle *_this, LegacyAssetManager *asset_manager) { - _this->set_asset_manager(asset_manager); - }; - this->s(f, this->asset_manager, asset_manager); -} - int GameSpecLink::get_terrain_id_count() const { return this->terrain_id_count; } diff --git a/libopenage/gui/game_spec_link.h b/libopenage/gui/game_spec_link.h index ac2b2dd96e..c896227d23 100644 --- a/libopenage/gui/game_spec_link.h +++ b/libopenage/gui/game_spec_link.h @@ -17,7 +17,6 @@ class GameSpec; namespace gui { -class AssetManagerLink; class GameSpecLink; } // namespace gui @@ -48,7 +47,6 @@ class GameSpecLink : public qtsdl::GuiItemQObject Q_ENUMS(State) Q_PROPERTY(State state READ get_state NOTIFY state_changed) Q_PROPERTY(bool active READ get_active WRITE set_active) - Q_PROPERTY(openage::gui::AssetManagerLink *assetManager READ get_asset_manager WRITE set_asset_manager) Q_PROPERTY(int terrainIdCount READ get_terrain_id_count NOTIFY terrain_id_count_changed) public: @@ -66,9 +64,6 @@ class GameSpecLink : public qtsdl::GuiItemQObject bool get_active() const; void set_active(bool active); - AssetManagerLink *get_asset_manager() const; - void set_asset_manager(AssetManagerLink *asset_manager); - int get_terrain_id_count() const; Q_INVOKABLE void invalidate(); @@ -100,7 +95,6 @@ private slots: State state; bool active; - AssetManagerLink *asset_manager; int terrain_id_count; std::shared_ptr loaded_game_spec; diff --git a/libopenage/terrain/terrain.h b/libopenage/terrain/terrain.h index bb1d876843..c33cd43dee 100644 --- a/libopenage/terrain/terrain.h +++ b/libopenage/terrain/terrain.h @@ -13,9 +13,9 @@ #include "../coord/phys.h" #include "../coord/pixel.h" #include "../coord/tile.h" +#include "../presenter/legacy/legacy.h" #include "../texture.h" #include "../util/misc.h" -#include "assets/legacy_assetmanager.h" namespace openage { From ec0d748e3f256e9e230a00f6122439bf96fba555 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 22 Sep 2023 23:30:02 +0200 Subject: [PATCH 029/771] presenter: Remove legacy game control. --- libopenage/gui/CMakeLists.txt | 1 - libopenage/gui/actions_list_model.cpp | 139 +-- libopenage/gui/actions_list_model.h | 28 +- .../gui/category_contents_list_model.cpp | 39 +- libopenage/gui/category_contents_list_model.h | 7 - libopenage/gui/game_control_link.cpp | 456 --------- libopenage/gui/game_control_link.h | 333 ------- libopenage/gui/resources_list_model.cpp | 24 +- libopenage/gui/resources_list_model.h | 22 +- libopenage/presenter/legacy/CMakeLists.txt | 1 - libopenage/presenter/legacy/game_control.cpp | 916 ------------------ libopenage/presenter/legacy/game_control.h | 395 -------- 12 files changed, 26 insertions(+), 2335 deletions(-) delete mode 100644 libopenage/gui/game_control_link.cpp delete mode 100644 libopenage/gui/game_control_link.h delete mode 100644 libopenage/presenter/legacy/game_control.cpp delete mode 100644 libopenage/presenter/legacy/game_control.h diff --git a/libopenage/gui/CMakeLists.txt b/libopenage/gui/CMakeLists.txt index 1697bc8d78..a1dd56541b 100644 --- a/libopenage/gui/CMakeLists.txt +++ b/libopenage/gui/CMakeLists.txt @@ -3,7 +3,6 @@ add_sources(libopenage category_contents_list_model.cpp engine_info.cpp engine_link.cpp - game_control_link.cpp game_creator.cpp game_main_link.cpp game_saver.cpp diff --git a/libopenage/gui/actions_list_model.cpp b/libopenage/gui/actions_list_model.cpp index 67f2d2780f..a8fa45fa46 100644 --- a/libopenage/gui/actions_list_model.cpp +++ b/libopenage/gui/actions_list_model.cpp @@ -1,9 +1,8 @@ -// Copyright 2016-2019 the openage authors. See copying.md for legal info. +// Copyright 2016-2023 the openage authors. See copying.md for legal info. #include "actions_list_model.h" #include "../log/log.h" -#include "game_control_link.h" #include #include @@ -15,132 +14,13 @@ namespace { const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "ActionsListModel"); } -ActionsListModel::ActionsListModel(QObject *parent) - : - QAbstractListModel{parent}, - action_mode{} { +ActionsListModel::ActionsListModel(QObject *parent) : + QAbstractListModel{parent} { Q_UNUSED(registration); } ActionsListModel::~ActionsListModel() = default; -ActionButtonsType ActionsListModel::get_active_buttons() const { - return this->active_buttons; -} - -void ActionsListModel::set_active_buttons(const ActionButtonsType &active_buttons) { - if (this->active_buttons == active_buttons) { - return; - } - this->active_buttons = active_buttons; - - switch (active_buttons) { - case ActionButtonsType::None: - this->clear_buttons(); - break; - - case ActionButtonsType::MilitaryUnits: - this->clear_buttons(); - this->set_icons_source("image://by-filename/converted/interface/hudactions.slp.png"); - this->beginResetModel(); - this->add_button(6, -1, static_cast(GroupIDs::NoGroup), "SET_ABILITY_PATROL"); - this->add_button(7, -1, static_cast(GroupIDs::NoGroup), "SET_ABILITY_GUARD"); - this->add_button(8, -1, static_cast(GroupIDs::NoGroup), "SET_ABILITY_FOLLOW"); - this->add_button(59, -1, static_cast(GroupIDs::NoGroup), "KILL_UNIT"); - this->add_button(2, -1, static_cast(GroupIDs::NoGroup), "SET_ABILITY_GARRISON"); - - this->add_button(9, 53, static_cast(GroupIDs::StanceGroup), "AGGRESSIVE_STANCE"); - this->add_button(10, 52, static_cast(GroupIDs::StanceGroup), "DEFENSIVE_STANCE"); - this->add_button(11, 51, static_cast(GroupIDs::StanceGroup), "HOLD_STANCE"); - this->add_button(50, 54, static_cast(GroupIDs::StanceGroup), "PASSIVE_STANCE"); - this->add_button(3, -1, static_cast(GroupIDs::NoGroup), "STOP"); - this->endResetModel(); - break; - - case ActionButtonsType::CivilianUnits: - this->clear_buttons(); - this->set_icons_source("image://by-filename/converted/interface/hudactions.slp.png"); - this->beginResetModel(); - this->add_button(30, -1, static_cast(GroupIDs::NoGroup), "BUILD_MENU"); - this->add_button(31, -1, static_cast(GroupIDs::NoGroup), "BUILD_MENU_MIL"); - this->add_button(28, -1, static_cast(GroupIDs::NoGroup), "SET_ABILITY_REPAIR"); - this->add_button(59, -1, static_cast(GroupIDs::NoGroup), "KILL_UNIT"); - this->add_button(2, -1, static_cast(GroupIDs::NoGroup), "SET_ABILITY_GARRISON"); - - this->add_button(-1, -1, static_cast(GroupIDs::NoGroup), ""); - this->add_button(-1, -1, static_cast(GroupIDs::NoGroup), ""); - this->add_button(-1, -1, static_cast(GroupIDs::NoGroup), ""); - this->add_button(-1, -1, static_cast(GroupIDs::NoGroup), ""); - this->add_button(3, -1, static_cast(GroupIDs::NoGroup), "STOP"); - this->endResetModel(); - break; - - case ActionButtonsType::BuildMenu: - this->clear_buttons(); - this->set_icons_source("image://by-filename/converted/interface/50705.slp.png"); - this->beginResetModel(); - this->add_button(34, -1, static_cast(GroupIDs::NoGroup), "BUILDING_HOUS"); - this->add_button(20, -1, static_cast(GroupIDs::NoGroup), "BUILDING_MILL"); - this->add_button(39, -1, static_cast(GroupIDs::NoGroup), "BUILDING_MINE"); - this->add_button(40, -1, static_cast(GroupIDs::NoGroup), "BUILDING_SMIL"); - this->add_button(13, -1, static_cast(GroupIDs::NoGroup), "BUILDING_DOCK"); - this->add_button(35, -1, static_cast(GroupIDs::NoGroup), "BUILDING_FARM"); - this->add_button(4, -1, static_cast(GroupIDs::NoGroup), "BUILDING_BLAC"); - this->add_button(16, -1, static_cast(GroupIDs::NoGroup), "BUILDING_MRKT"); - this->add_button(10, -1, static_cast(GroupIDs::NoGroup), "BUILDING_CRCH"); - this->add_button(32, -1, static_cast(GroupIDs::NoGroup), "BUILDING_UNIV"); - this->add_button(28, -1, static_cast(GroupIDs::NoGroup), "BUILDING_RTWC"); - this->add_button(37, -1, static_cast(GroupIDs::NoGroup), "BUILDING_WNDR"); - // the go back button is not in this slp (is in hudactions.slp.png) - this->endResetModel(); - break; - - case ActionButtonsType::MilBuildMenu: - this->clear_buttons(); - this->set_icons_source("image://by-filename/converted/interface/50705.slp.png"); - this->beginResetModel(); - this->add_button(2, -1, static_cast(GroupIDs::NoGroup), "BUILDING_BRKS"); - this->add_button(0, -1, static_cast(GroupIDs::NoGroup), "BUILDING_ARRG"); - this->add_button(23, -1, static_cast(GroupIDs::NoGroup), "BUILDING_STBL"); - this->add_button(22, -1, static_cast(GroupIDs::NoGroup), "BUILDING_SIWS"); - this->add_button(38, -1, static_cast(GroupIDs::NoGroup), "BUILDING_WCTWX"); - this->add_button(30, -1, static_cast(GroupIDs::NoGroup), "BUILDING_WALL"); - this->add_button(29, -1, static_cast(GroupIDs::NoGroup), "BUILDING_WALL2"); - this->add_button(25, -1, static_cast(GroupIDs::NoGroup), "BUILDING_WCTW"); - this->add_button(42, -1, static_cast(GroupIDs::NoGroup), "BUILDING_WCTW4"); - this->add_button(36, -1, static_cast(GroupIDs::NoGroup), "BUILDING_GTCA2"); - this->add_button(7, -1, static_cast(GroupIDs::NoGroup), "BUILDING_CSTL"); - // the go back button is not in this slp (is in hudactions.slp.png) - this->endResetModel(); - break; - - default: - log::log(MSG(warn) << "Unknown action mode selection"); - } -} - -ActionModeLink* ActionsListModel::get_action_mode() const { - return this->action_mode; -} - -void ActionsListModel::set_action_mode(ActionModeLink *action_mode) { - if (this->action_mode != action_mode) { - if (this->action_mode != nullptr) { - QObject::disconnect(this->action_mode, - &ActionModeLink::buttons_type_changed, - this, - &ActionsListModel::on_buttons_type_changed); - } - - this->action_mode = action_mode; - - QObject::connect(this->action_mode, - &ActionModeLink::buttons_type_changed, - this, - &ActionsListModel::on_buttons_type_changed); - } -} - QUrl ActionsListModel::get_icons_source() const { return QUrl(this->icons_source); } @@ -154,14 +34,6 @@ void ActionsListModel::set_icons_source(const std::string &icons_source) { emit this->icons_source_changed(this->icons_source); } -Q_INVOKABLE void ActionsListModel::set_initial_buttons() { - this->set_active_buttons(ActionButtonsType::None); -} - -void ActionsListModel::on_buttons_type_changed(const ActionButtonsType buttons_type) { - this->set_active_buttons(buttons_type); -} - QHash ActionsListModel::roleNames() const { QHash roles; roles[static_cast(ActionsRoles::IconRole)] = "ico"; @@ -171,7 +43,7 @@ QHash ActionsListModel::roleNames() const { return roles; } -int ActionsListModel::rowCount(const QModelIndex&) const { +int ActionsListModel::rowCount(const QModelIndex &) const { return this->buttons.size(); } @@ -198,4 +70,5 @@ void ActionsListModel::add_button(int ico, int ico_chk, int grp_id, const char * this->buttons.push_back(map); } -}} // namespace openage::gui +} // namespace gui +} // namespace openage diff --git a/libopenage/gui/actions_list_model.h b/libopenage/gui/actions_list_model.h index e0ce30f605..7ffff6a752 100644 --- a/libopenage/gui/actions_list_model.h +++ b/libopenage/gui/actions_list_model.h @@ -1,11 +1,9 @@ -// Copyright 2016-2019 the openage authors. See copying.md for legal info. +// Copyright 2016-2023 the openage authors. See copying.md for legal info. #pragma once #include -#include "game_control_link.h" - #include #include #include @@ -20,25 +18,15 @@ namespace gui { class ActionsListModel : public QAbstractListModel { Q_OBJECT - Q_PROPERTY(ActionButtonsType active_buttons READ get_active_buttons WRITE set_active_buttons) - Q_PROPERTY(ActionModeLink* action_mode READ get_action_mode WRITE set_action_mode) Q_PROPERTY(QUrl iconsSource READ get_icons_source WRITE set_icons_source NOTIFY icons_source_changed) public: - ActionsListModel(QObject *parent=nullptr); + ActionsListModel(QObject *parent = nullptr); virtual ~ActionsListModel(); - ActionButtonsType get_active_buttons() const; - void set_active_buttons(const ActionButtonsType &active_buttons); - - ActionModeLink* get_action_mode() const; - void set_action_mode(ActionModeLink* action_mode); - QUrl get_icons_source() const; void set_icons_source(QUrl icons_source); - Q_INVOKABLE void set_initial_buttons(); - enum class ActionsRoles { IconRole = Qt::UserRole + 1, IconCheckedRole, @@ -54,13 +42,10 @@ class ActionsListModel : public QAbstractListModel { signals: void icons_source_changed(const QUrl icons_source); -private slots: - void on_buttons_type_changed(const ActionButtonsType buttons_type); - private: virtual QHash roleNames() const override; - virtual int rowCount(const QModelIndex&) const override; - virtual QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override; + virtual int rowCount(const QModelIndex &) const override; + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; virtual QMap itemData(const QModelIndex &index) const override; /** @@ -78,10 +63,9 @@ private slots: */ void add_button(int ico, int ico_chk, int grp_id, const char *name); - ActionButtonsType active_buttons; - ActionModeLink *action_mode; QUrl icons_source; std::vector> buttons; }; -}} // namespace openage::gui +} // namespace gui +} // namespace openage diff --git a/libopenage/gui/category_contents_list_model.cpp b/libopenage/gui/category_contents_list_model.cpp index 334bf7570f..88408ee458 100644 --- a/libopenage/gui/category_contents_list_model.cpp +++ b/libopenage/gui/category_contents_list_model.cpp @@ -1,11 +1,9 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "category_contents_list_model.h" #include -#include "game_control_link.h" - namespace openage { namespace gui { @@ -13,10 +11,8 @@ namespace { const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "Category"); } -CategoryContentsListModel::CategoryContentsListModel(QObject *parent) - : - QAbstractListModel{parent}, - editor_mode{} { +CategoryContentsListModel::CategoryContentsListModel(QObject *parent) : + QAbstractListModel{parent} { Q_UNUSED(registration); } @@ -34,28 +30,6 @@ void CategoryContentsListModel::set_name(const QString &name) { } } -EditorModeLink* CategoryContentsListModel::get_editor_mode() const { - return this->editor_mode; -} - -void CategoryContentsListModel::set_editor_mode(EditorModeLink *editor_mode) { - if (this->editor_mode != editor_mode) { - if (this->editor_mode) { - QObject::disconnect(this->editor_mode, &EditorModeLink::categories_content_changed, this, &CategoryContentsListModel::on_categories_content_changed); - QObject::disconnect(this->editor_mode, &EditorModeLink::category_content_changed, this, &CategoryContentsListModel::on_category_content_changed); - } - - this->editor_mode = editor_mode; - - if (this->editor_mode) { - QObject::connect(this->editor_mode, &EditorModeLink::categories_content_changed, this, &CategoryContentsListModel::on_categories_content_changed); - QObject::connect(this->editor_mode, &EditorModeLink::category_content_changed, this, &CategoryContentsListModel::on_category_content_changed); - } - - this->on_categories_content_changed(); - } -} - void CategoryContentsListModel::on_category_content_changed(const std::string &category_name, std::vector> type_and_texture) { if (this->name == QString::fromStdString(category_name)) { this->beginResetModel(); @@ -65,8 +39,6 @@ void CategoryContentsListModel::on_category_content_changed(const std::string &c } void CategoryContentsListModel::on_categories_content_changed() { - if (this->editor_mode) - this->editor_mode->announce_category_content(this->name.toStdString()); } QHash CategoryContentsListModel::roleNames() const { @@ -75,7 +47,7 @@ QHash CategoryContentsListModel::roleNames() const { return names; } -int CategoryContentsListModel::rowCount(const QModelIndex&) const { +int CategoryContentsListModel::rowCount(const QModelIndex &) const { return this->type_and_texture.size(); } @@ -94,4 +66,5 @@ QVariant CategoryContentsListModel::data(const QModelIndex &index, int role) con return QVariant{}; } -}} // namespace openage::gui +} // namespace gui +} // namespace openage diff --git a/libopenage/gui/category_contents_list_model.h b/libopenage/gui/category_contents_list_model.h index b8dde68526..e3845a44db 100644 --- a/libopenage/gui/category_contents_list_model.h +++ b/libopenage/gui/category_contents_list_model.h @@ -14,8 +14,6 @@ namespace openage { namespace gui { -class EditorModeLink; - /** * Adaptor for the contents of a category of the Civilisation. */ @@ -23,7 +21,6 @@ class CategoryContentsListModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QString name READ get_name WRITE set_name) - Q_PROPERTY(openage::gui::EditorModeLink *editorMode READ get_editor_mode WRITE set_editor_mode) public: CategoryContentsListModel(QObject *parent = nullptr); @@ -32,9 +29,6 @@ class CategoryContentsListModel : public QAbstractListModel { QString get_name() const; void set_name(const QString &name); - EditorModeLink *get_editor_mode() const; - void set_editor_mode(EditorModeLink *editor_mode); - private slots: void on_category_content_changed(const std::string &category_name, std::vector> type_and_texture); void on_categories_content_changed(); @@ -47,7 +41,6 @@ private slots: std::vector> type_and_texture; QString name; - EditorModeLink *editor_mode; }; } // namespace gui diff --git a/libopenage/gui/game_control_link.cpp b/libopenage/gui/game_control_link.cpp deleted file mode 100644 index 4c41678892..0000000000 --- a/libopenage/gui/game_control_link.cpp +++ /dev/null @@ -1,456 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "game_control_link.h" - -#include - -#include - -#include "../legacy_engine.h" -#include "../unit/action.h" -#include "../unit/unit.h" -#include "engine_link.h" -#include "game_main_link.h" - -namespace openage::gui { - -namespace { -const int registration_mode = qmlRegisterUncreatableType("yay.sfttech.openage", 1, 0, "OutputMode", "OutputMode is an abstract interface for the concrete modes like EditorMode or ActionMode."); -const int registration_create = qmlRegisterType("yay.sfttech.openage", 1, 0, "CreateMode"); -const int registration_action = qmlRegisterType("yay.sfttech.openage", 1, 0, "ActionMode"); -const int registration_editor = qmlRegisterType("yay.sfttech.openage", 1, 0, "EditorMode"); -const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "GameControl"); -} // namespace - -OutputModeLink::OutputModeLink(QObject *parent) : - GuiItemQObject{parent}, - QQmlParserStatus{}, - GuiItemInterface{} { -} - -OutputModeLink::~OutputModeLink() = default; - -QString OutputModeLink::get_name() const { - return this->name; -} - -QStringList OutputModeLink::get_binds() const { - return this->binds; -} - -void OutputModeLink::on_announced(const std::string &name) { - auto new_name = QString::fromStdString(name); - - if (this->name != new_name) { - this->name = new_name; - emit this->name_changed(); - } -} - -void OutputModeLink::on_binds_changed( - const std::vector &binds) { - QStringList new_binds; - std::transform( - std::begin(binds), - std::end(binds), - std::back_inserter(new_binds), - [](const std::string &s) { - return QString::fromStdString(s); - }); - - if (this->binds != new_binds) { - this->binds = new_binds; - emit this->binds_changed(); - } -} - -void OutputModeLink::classBegin() { -} - -void OutputModeLink::on_core_adopted() { - QObject::connect(&unwrap(this)->gui_signals, - &OutputModeSignals::announced, - this, - &OutputModeLink::on_announced); - - QObject::connect(&unwrap(this)->gui_signals, - &OutputModeSignals::binds_changed, - this, - &OutputModeLink::on_binds_changed); -} - -void OutputModeLink::componentComplete() { - static auto f = [](OutputMode *_this) { - _this->announce(); - }; - this->i(f); -} - -CreateModeLink::CreateModeLink(QObject *parent) : - Inherits{parent} { - Q_UNUSED(registration_create); -} - -CreateModeLink::~CreateModeLink() = default; - -ActionModeLink::ActionModeLink(QObject *parent) : - Inherits{parent} { - Q_UNUSED(registration_action); -} - -ActionModeLink::~ActionModeLink() = default; - -QString ActionModeLink::get_ability() const { - return this->ability; -} - -QString ActionModeLink::get_population() const { - return this->population; -} - -int ActionModeLink::get_selection_size() const { - return this->selection ? this->selection->get_units_count() : 0; -} - -bool ActionModeLink::get_population_warn() const { - return this->population_warn; -} - -void ActionModeLink::act(const QString &action) { - emit this->action_triggered(action.toStdString()); -} - -void ActionModeLink::on_ability_changed(const std::string &ability) { - this->ability = QString::fromStdString(ability); - emit this->ability_changed(); -} - -void ActionModeLink::on_buttons_type_changed(const ActionButtonsType buttons_type) { - emit this->buttons_type_changed(buttons_type); -} - -void ActionModeLink::on_population_changed(int demand, int capacity, bool warn) { - this->population = QString::number(demand) + "/" + QString::number(capacity); - this->population_warn = warn; - emit this->population_changed(); -} - -void ActionModeLink::on_selection_changed(const UnitSelection *unit_selection, const Player *player) { - this->selection = unit_selection; - - if (this->selection->get_units_count() == 1) { - auto &ref = this->selection->get_first_unit(); - if (ref.is_valid()) { - Unit *u = ref.get(); - - this->selection_name = QString::fromStdString(u->unit_type->name()); - // the icons are split into two sprites - if (u->unit_type->unit_class == gamedata::unit_classes::BUILDING) { - this->selection_icon = QString::fromStdString("50706.slp.png." + std::to_string(u->unit_type->icon)); - } - else { - this->selection_icon = QString::fromStdString("50730.slp.png." + std::to_string(u->unit_type->icon)); - } - this->selection_type = QString::fromStdString("(type: " + std::to_string(u->unit_type->id()) + " " + u->top()->name() + ")"); - - if (u->has_attribute(attr_type::owner)) { - auto &own_attr = u->get_attribute(); - if (own_attr.player.civ->civ_id != 0) { // not gaia - this->selection_owner = QString::fromStdString( - own_attr.player.name + "\n" + own_attr.player.civ->civ_name + "\n" + (!player || *player == own_attr.player ? "" : player->is_ally(own_attr.player) ? "Ally" : - "Enemy")); - // TODO find the team status of the player - } - else { - this->selection_owner = QString::fromStdString(" "); - } - } - - if (u->has_attribute(attr_type::hitpoints) && u->has_attribute(attr_type::damaged)) { - auto &hp = u->get_attribute(); - auto &dm = u->get_attribute(); - // TODO replace ascii health bar with real one - if (hp.hp >= 200) { - this->selection_hp = QString::fromStdString(progress(dm.hp * 1.0f / hp.hp, 8) + " " + std::to_string(dm.hp) + "/" + std::to_string(hp.hp)); - } - else { - this->selection_hp = QString::fromStdString(progress(dm.hp * 1.0f / hp.hp, 4) + " " + std::to_string(dm.hp) + "/" + std::to_string(hp.hp)); - } - } - else { - this->selection_hp = QString::fromStdString(" "); - } - - std::string lines; - if (u->has_attribute(attr_type::resource)) { - auto &res_attr = u->get_attribute(); - lines += std::to_string((int)res_attr.amount) + " " + std::to_string(res_attr.resource_type) + "\n"; - } - if (u->has_attribute(attr_type::building)) { - auto &build_attr = u->get_attribute(); - if (build_attr.completed < 1) { - lines += "Building: " + progress(build_attr.completed, 16) + " " + std::to_string((int)(100 * build_attr.completed)) + "%\n"; - } - } - if (u->has_attribute(attr_type::garrison)) { - auto &garrison_attr = u->get_attribute(); - if (garrison_attr.content.size() > 0) { - lines += "Garrison: " + std::to_string(garrison_attr.content.size()) + " units\n"; - } - } - lines += "\n"; - if (u->has_attribute(attr_type::population)) { - auto &population_attr = u->get_attribute(); - if (population_attr.demand > 1) { - lines += "Population demand: " + std::to_string(population_attr.demand) + " units\n"; - } - if (population_attr.capacity > 0) { - lines += "Population capacity: " + std::to_string(population_attr.capacity) + " units\n"; - } - } - this->selection_attrs = QString::fromStdString(lines); - } - } - else if (this->selection->get_units_count() > 1) { - this->selection_name = QString::fromStdString( - std::to_string(this->selection->get_units_count()) + " units"); - } - - - emit this->selection_changed(); -} - -// TODO remove -std::string ActionModeLink::progress(float progress, int size) { - std::string bar = "["; - for (int i = 0; i < size; i++) { - bar += (i < progress * size ? "=" : " "); - } - return bar + "]"; -} - -void ActionModeLink::on_core_adopted() { - this->Inherits::on_core_adopted(); - QObject::connect(&unwrap(this)->gui_signals, - &ActionModeSignals::ability_changed, - this, - &ActionModeLink::on_ability_changed); - QObject::connect(&unwrap(this)->gui_signals, - &ActionModeSignals::population_changed, - this, - &ActionModeLink::on_population_changed); - QObject::connect(&unwrap(this)->gui_signals, - &ActionModeSignals::selection_changed, - this, - &ActionModeLink::on_selection_changed); - QObject::connect(&unwrap(this)->gui_signals, - &ActionModeSignals::buttons_type_changed, - this, - &ActionModeLink::on_buttons_type_changed); - QObject::connect(this, - &ActionModeLink::action_triggered, - &unwrap(this)->gui_signals, - &ActionModeSignals::on_action); -} - -EditorModeLink::EditorModeLink(QObject *parent) : - Inherits{parent}, - current_type_id{-1}, - current_terrain_id{-1}, - paint_terrain{} { - Q_UNUSED(registration_editor); -} - -EditorModeLink::~EditorModeLink() = default; - -int EditorModeLink::get_current_type_id() const { - return this->current_type_id; -} - -void EditorModeLink::set_current_type_id(int current_type_id) { - static auto f = [](EditorMode *_this, int current_type_id) { - _this->set_current_type_id(current_type_id); - }; - this->s(f, this->current_type_id, current_type_id); -} - -int EditorModeLink::get_current_terrain_id() const { - return this->current_terrain_id; -} - -void EditorModeLink::set_current_terrain_id(int current_terrain_id) { - static auto f = [](EditorMode *_this, int current_terrain_id) { - _this->set_current_terrain_id(current_terrain_id); - }; - this->s(f, this->current_terrain_id, current_terrain_id); -} - -bool EditorModeLink::get_paint_terrain() const { - return this->paint_terrain; -} - -void EditorModeLink::set_paint_terrain(bool paint_terrain) { - static auto f = [](EditorMode *_this, int paint_terrain) { - _this->set_paint_terrain(paint_terrain); - }; - this->s(f, this->paint_terrain, paint_terrain); -} - -QStringList EditorModeLink::get_categories() const { - return this->categories; -} - -void EditorModeLink::announce_category_content(const std::string &category_name) { - static auto f = [](EditorMode *_this, const std::string &category_name) { - _this->announce_category_content(category_name); - }; - this->i(f, category_name); -} - -void EditorModeLink::on_categories_changed(const std::vector &categories) { - this->categories.clear(); - std::transform(std::begin(categories), std::end(categories), std::back_inserter(this->categories), [](const std::string &s) { return QString::fromStdString(s); }); - emit this->categories_changed(); -} - -void EditorModeLink::on_core_adopted() { - this->Inherits::on_core_adopted(); - QObject::connect(&unwrap(this)->gui_signals, &EditorModeSignals::toggle, this, &EditorModeLink::toggle); - QObject::connect(&unwrap(this)->gui_signals, &EditorModeSignals::categories_changed, this, &EditorModeLink::on_categories_changed); - QObject::connect(&unwrap(this)->gui_signals, &EditorModeSignals::categories_content_changed, this, &EditorModeLink::categories_content_changed); - QObject::connect(&unwrap(this)->gui_signals, &EditorModeSignals::category_content_changed, this, &EditorModeLink::category_content_changed); -} - -GameControlLink::GameControlLink(QObject *parent) : - GuiItemQObject{parent}, - QQmlParserStatus{}, - GuiItem{this}, - mode{}, - effective_mode_index{-1}, - mode_index{-1}, - engine{}, - game{}, - current_civ_index{} { - Q_UNUSED(registration_mode); - Q_UNUSED(registration); -} - -GameControlLink::~GameControlLink() = default; - -void GameControlLink::classBegin() { -} - -void GameControlLink::on_core_adopted() { - QObject::connect(&unwrap(this)->gui_signals, &GameControlSignals::mode_changed, this, &GameControlLink::on_mode_changed); - QObject::connect(&unwrap(this)->gui_signals, &GameControlSignals::modes_changed, this, &GameControlLink::on_modes_changed); - QObject::connect(&unwrap(this)->gui_signals, &GameControlSignals::current_player_name_changed, this, &GameControlLink::on_current_player_name_changed); - QObject::connect(&unwrap(this)->gui_signals, &GameControlSignals::current_civ_index_changed, this, &GameControlLink::on_current_civ_index_changed); -} - -void GameControlLink::componentComplete() { - static auto f = [](GameControl *_this) { - _this->announce_mode(); - _this->announce_current_player_name(); - }; - this->i(f); -} - -OutputModeLink *GameControlLink::get_mode() const { - return this->mode; -} - -int GameControlLink::get_effective_mode_index() const { - return this->effective_mode_index; -} - -int GameControlLink::get_mode_index() const { - return this->mode_index; -} - -void GameControlLink::set_mode_index(int mode) { - static auto f = [](GameControl *_this, int mode) { - _this->set_mode(mode, true); - }; - - this->sf(f, this->mode_index, mode); -} - -QVariantList GameControlLink::get_modes() const { - return this->modes; -} - -void GameControlLink::set_modes(const QVariantList &modes) { - static auto f = [](GameControl *_this, const QVariantList &modes) { - std::vector new_modes; - - for (auto m : modes) - if (m.canConvert()) - new_modes.push_back(unwrap(m.value())); - - _this->set_modes(new_modes); - }; - - this->s(f, this->modes, modes); -} - -EngineLink *GameControlLink::get_engine() const { - return this->engine; -} - -void GameControlLink::set_engine(EngineLink *engine) { - static auto f = [](GameControl *_this, LegacyEngine *engine) { - _this->set_engine(engine); - }; - this->s(f, this->engine, engine); -} - -GameMainLink *GameControlLink::get_game() const { - return this->game; -} - -void GameControlLink::set_game(GameMainLink *game) { - static auto f = [](GameControl *_this, GameMainHandle *game) { - _this->set_game(game); - }; - this->s(f, this->game, game); -} - -QString GameControlLink::get_current_player_name() const { - return this->current_player_name; -} - -int GameControlLink::get_current_civ_index() const { - return this->current_civ_index; -} - -void GameControlLink::on_mode_changed(OutputMode *mode, int mode_index) { - auto new_mode = qtsdl::wrap(mode); - - if (this->mode != new_mode || this->effective_mode_index != mode_index) { - this->effective_mode_index = mode_index; - this->mode = new_mode; - emit this->mode_changed(); - } -} - -void GameControlLink::on_modes_changed(OutputMode *mode, int mode_index) { - static auto f = [](GameControl *_this, int mode) { - _this->set_mode(mode); - }; - this->i(f, this->mode_index); - - this->on_mode_changed(mode, mode_index); - emit this->modes_changed(); -} - -void GameControlLink::on_current_player_name_changed(const std::string ¤t_player_name) { - this->current_player_name = QString::fromStdString(current_player_name); - emit this->current_player_name_changed(); -} - -void GameControlLink::on_current_civ_index_changed(int current_civ_index) { - this->current_civ_index = current_civ_index; - emit this->current_civ_index_changed(); -} - -} // namespace openage::gui diff --git a/libopenage/gui/game_control_link.h b/libopenage/gui/game_control_link.h deleted file mode 100644 index 9dde68e924..0000000000 --- a/libopenage/gui/game_control_link.h +++ /dev/null @@ -1,333 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "../presenter/legacy/game_control.h" -#include "guisys/link/gui_item.h" - -namespace openage { -namespace gui { - -class GameControlLink; - -class OutputModeLink; - -} // namespace gui -} // namespace openage - -namespace qtsdl { -template <> -struct Wrap { - using Type = openage::gui::OutputModeLink; -}; - -template <> -struct Unwrap { - using Type = openage::OutputMode; -}; - -} // namespace qtsdl - -namespace openage { -namespace gui { - -class OutputModeLink : public qtsdl::GuiItemQObject - , public QQmlParserStatus - , public qtsdl::GuiItemInterface { - Q_OBJECT - - Q_INTERFACES(QQmlParserStatus) - Q_PROPERTY(QString name READ get_name NOTIFY name_changed) - Q_PROPERTY(QStringList binds READ get_binds NOTIFY binds_changed) - -public: - OutputModeLink(QObject *parent = nullptr); - virtual ~OutputModeLink(); - - QString get_name() const; - QStringList get_binds() const; - -signals: - void name_changed(); - void binds_changed(); - -private slots: - void on_announced(const std::string &name); - void on_binds_changed(const std::vector &binds); - -protected: - virtual void classBegin() override; - virtual void on_core_adopted() override; - virtual void componentComplete() override; - -private: - QString name; - QStringList binds; -}; - -class CreateModeLink; - -} // namespace gui -} // namespace openage - -namespace qtsdl { -template <> -struct Wrap { - using Type = openage::gui::CreateModeLink; -}; - -template <> -struct Unwrap { - using Type = openage::CreateMode; -}; - -} // namespace qtsdl - -namespace openage { -namespace gui { - -class CreateModeLink : public qtsdl::Inherits { - Q_OBJECT - -public: - CreateModeLink(QObject *parent = nullptr); - virtual ~CreateModeLink(); -}; - -class ActionModeLink; - -} // namespace gui -} // namespace openage - -namespace qtsdl { -template <> -struct Wrap { - using Type = openage::gui::ActionModeLink; -}; - -template <> -struct Unwrap { - using Type = openage::ActionMode; -}; - -} // namespace qtsdl - -namespace openage { -namespace gui { - -class ActionModeLink : public qtsdl::Inherits { - Q_OBJECT - - Q_PROPERTY(QString ability READ get_ability NOTIFY ability_changed) - Q_PROPERTY(QString population READ get_population NOTIFY population_changed) - Q_PROPERTY(bool population_warn READ get_population_warn NOTIFY population_changed) - - Q_PROPERTY(int selection_size READ get_selection_size NOTIFY selection_changed) - - Q_PROPERTY(QString selection_name MEMBER selection_name NOTIFY selection_changed) - Q_PROPERTY(QString selection_icon MEMBER selection_icon NOTIFY selection_changed) - Q_PROPERTY(QString selection_type MEMBER selection_type NOTIFY selection_changed) - Q_PROPERTY(QString selection_owner MEMBER selection_owner NOTIFY selection_changed) - Q_PROPERTY(QString selection_hp MEMBER selection_hp NOTIFY selection_changed) - Q_PROPERTY(QString selection_attrs MEMBER selection_attrs NOTIFY selection_changed) - -public: - ActionModeLink(QObject *parent = nullptr); - virtual ~ActionModeLink(); - - QString get_ability() const; - QString get_population() const; - bool get_population_warn() const; - int get_selection_size() const; - - Q_INVOKABLE void act(const QString &action); - -signals: - void ability_changed(); - void action_triggered(const std::string &ability); - void buttons_type_changed(const ActionButtonsType buttons_type); - void population_changed(); - void selection_changed(); - -private slots: - void on_ability_changed(const std::string &ability); - void on_buttons_type_changed(const ActionButtonsType buttons_type); - void on_population_changed(int demand, int capacity, bool warn); - void on_selection_changed(const UnitSelection *unit_selection, const Player *player); - -private: - virtual void on_core_adopted() override; - - QString ability; - QString population; - bool population_warn; - const UnitSelection *selection = nullptr; - - QString selection_name; - QString selection_icon; - QString selection_type; - QString selection_owner; - QString selection_hp; - QString selection_attrs; - - std::string progress(float progress, int size); -}; - -class EditorModeLink; - -} // namespace gui -} // namespace openage - -namespace qtsdl { -template <> -struct Wrap { - using Type = openage::gui::EditorModeLink; -}; - -template <> -struct Unwrap { - using Type = openage::EditorMode; -}; - -} // namespace qtsdl - -namespace openage { -namespace gui { - -class EditorModeLink : public qtsdl::Inherits { - Q_OBJECT - - Q_PROPERTY(int currentTypeId READ get_current_type_id WRITE set_current_type_id) - Q_PROPERTY(int currentTerrainId READ get_current_terrain_id WRITE set_current_terrain_id) - Q_PROPERTY(bool paintTerrain READ get_paint_terrain WRITE set_paint_terrain) - Q_PROPERTY(QStringList categories READ get_categories NOTIFY categories_changed) - -public: - EditorModeLink(QObject *parent = nullptr); - virtual ~EditorModeLink(); - - int get_current_type_id() const; - void set_current_type_id(int current_type_id); - - int get_current_terrain_id() const; - void set_current_terrain_id(int current_terrain_id); - - bool get_paint_terrain() const; - void set_paint_terrain(bool paint_terrain); - - QStringList get_categories() const; - - void announce_category_content(const std::string &category_name); - -signals: - void toggle(); - void categories_changed(); - void categories_content_changed(); - void category_content_changed(const std::string &category_name, std::vector> type_and_texture); - -private slots: - void on_categories_changed(const std::vector &categories); - -private: - virtual void on_core_adopted() override; - - int current_type_id; - int current_terrain_id; - bool paint_terrain; - QStringList categories; -}; - -class EngineLink; -class GameMainLink; -class GameControlLink; - -} // namespace gui -} // namespace openage - -namespace qtsdl { -template <> -struct Wrap { - using Type = openage::gui::GameControlLink; -}; - -template <> -struct Unwrap { - using Type = openage::GameControl; -}; - -} // namespace qtsdl - -namespace openage { -namespace gui { - -class GameControlLink : public qtsdl::GuiItemQObject - , public QQmlParserStatus - , public qtsdl::GuiItem { - Q_OBJECT - - Q_INTERFACES(QQmlParserStatus) - Q_PROPERTY(openage::gui::OutputModeLink *mode READ get_mode NOTIFY mode_changed) - Q_PROPERTY(int effectiveModeIndex READ get_effective_mode_index NOTIFY mode_changed) - Q_PROPERTY(int modeIndex READ get_mode_index WRITE set_mode_index) - Q_PROPERTY(QVariantList modes READ get_modes WRITE set_modes NOTIFY modes_changed) - Q_MOC_INCLUDE("gui/engine_link.h") - Q_MOC_INCLUDE("gui/game_main_link.h") - Q_PROPERTY(openage::gui::EngineLink *engine READ get_engine WRITE set_engine) - Q_PROPERTY(openage::gui::GameMainLink *game READ get_game WRITE set_game) - Q_PROPERTY(QString currentPlayerName READ get_current_player_name NOTIFY current_player_name_changed) - Q_PROPERTY(int currentCivIndex READ get_current_civ_index NOTIFY current_civ_index_changed) - -public: - explicit GameControlLink(QObject *parent = nullptr); - virtual ~GameControlLink(); - - OutputModeLink *get_mode() const; - int get_effective_mode_index() const; - - int get_mode_index() const; - void set_mode_index(int mode); - - QVariantList get_modes() const; - void set_modes(const QVariantList &modes); - - EngineLink *get_engine() const; - void set_engine(EngineLink *engine); - - GameMainLink *get_game() const; - void set_game(GameMainLink *game); - - QString get_current_player_name() const; - int get_current_civ_index() const; - -signals: - void mode_changed(); - void modes_changed(); - void current_player_name_changed(); - void current_civ_index_changed(); - -private slots: - void on_mode_changed(OutputMode *mode, int mode_index); - void on_modes_changed(OutputMode *mode, int mode_index); - void on_current_player_name_changed(const std::string ¤t_player_name); - void on_current_civ_index_changed(int current_civ_index); - -private: - virtual void classBegin() override; - virtual void on_core_adopted() override; - virtual void componentComplete() override; - - OutputModeLink *mode = nullptr; - int effective_mode_index; - int mode_index; - QVariantList modes; - // TODO: remove engine because it's already accessible through the game - EngineLink *engine = nullptr; - GameMainLink *game = nullptr; - QString current_player_name; - int current_civ_index; -}; - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/resources_list_model.cpp b/libopenage/gui/resources_list_model.cpp index ad0106663d..d804d147b4 100644 --- a/libopenage/gui/resources_list_model.cpp +++ b/libopenage/gui/resources_list_model.cpp @@ -9,9 +9,6 @@ #include "../error/error.h" -#include "../presenter/legacy/game_control.h" -#include "game_control_link.h" - namespace openage::gui { namespace { @@ -23,31 +20,12 @@ const auto resource_type_count = static_cast ResourcesListModel::ResourcesListModel(QObject *parent) : QAbstractListModel(parent), - amounts(resource_type_count), - action_mode() { + amounts(resource_type_count) { Q_UNUSED(registration); } ResourcesListModel::~ResourcesListModel() = default; -ActionModeLink *ResourcesListModel::get_action_mode() const { - return this->action_mode; -} - -void ResourcesListModel::set_action_mode(ActionModeLink *action_mode) { - if (this->action_mode != action_mode) { - if (this->action_mode) { - QObject::disconnect(&unwrap(this->action_mode)->gui_signals, &ActionModeSignals::resource_changed, this, &ResourcesListModel::on_resource_changed); - } - - this->action_mode = action_mode; - - if (this->action_mode) { - QObject::connect(&unwrap(this->action_mode)->gui_signals, &ActionModeSignals::resource_changed, this, &ResourcesListModel::on_resource_changed); - } - } -} - void ResourcesListModel::on_resource_changed(game_resource resource, int amount) { int i = static_cast(resource); ENSURE(i >= 0 && i < std::distance(std::begin(this->amounts), std::end(this->amounts)), "Res type index is out of range: '" << i << "'."); diff --git a/libopenage/gui/resources_list_model.h b/libopenage/gui/resources_list_model.h index 574618fb78..51ef6425bb 100644 --- a/libopenage/gui/resources_list_model.h +++ b/libopenage/gui/resources_list_model.h @@ -1,9 +1,9 @@ -// Copyright 2016-2018 the openage authors. See copying.md for legal info. +// Copyright 2016-2023 the openage authors. See copying.md for legal info. #pragma once -#include #include +#include #include @@ -12,33 +12,25 @@ namespace openage { namespace gui { -class ActionModeLink; - /** * Resource table for the gui. */ class ResourcesListModel : public QAbstractListModel { Q_OBJECT - Q_PROPERTY(openage::gui::ActionModeLink* actionMode READ get_action_mode WRITE set_action_mode) - public: - ResourcesListModel(QObject *parent=nullptr); + ResourcesListModel(QObject *parent = nullptr); virtual ~ResourcesListModel(); - ActionModeLink* get_action_mode() const; - void set_action_mode(ActionModeLink *action_mode); - private slots: void on_resource_changed(game_resource resource, int amount); private: - virtual int rowCount(const QModelIndex&) const override; - virtual QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override; + virtual int rowCount(const QModelIndex &) const override; + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; std::vector amounts; - - ActionModeLink *action_mode; }; -}} // namespace openage::gui +} // namespace gui +} // namespace openage diff --git a/libopenage/presenter/legacy/CMakeLists.txt b/libopenage/presenter/legacy/CMakeLists.txt index 5a57295ab3..0c5905d0b5 100644 --- a/libopenage/presenter/legacy/CMakeLists.txt +++ b/libopenage/presenter/legacy/CMakeLists.txt @@ -1,5 +1,4 @@ add_sources(libopenage - game_control.cpp legacy.cpp ) diff --git a/libopenage/presenter/legacy/game_control.cpp b/libopenage/presenter/legacy/game_control.cpp deleted file mode 100644 index 91255b1b54..0000000000 --- a/libopenage/presenter/legacy/game_control.cpp +++ /dev/null @@ -1,916 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "game_control.h" - -#include "error/error.h" -#include "gamestate/old/game_spec.h" -#include "legacy_engine.h" -#include "log/log.h" -#include "renderer/color.h" -#include "terrain/terrain_chunk.h" -#include "util/strings.h" - - -namespace openage { - -OutputMode::OutputMode(qtsdl::GuiItemLink *gui_link) : - game_control{nullptr}, - gui_link{gui_link} { -} - -OutputMode::~OutputMode() { - // An empty deconstructor prevents our clients from needing - // to implement one. - // - // The deconstructor is needed in the first place to stop - // the compiler from generating ud2 (undefined) instructors - // for the body of this method. -} - -void OutputMode::announce() { - emit this->gui_signals.announced(this->name()); - - emit this->gui_signals.binds_changed(this->active_binds()); -} - -void OutputMode::set_game_control(GameControl *game_control) { - this->game_control = game_control; - - this->on_game_control_set(); - this->announce(); -} - -CreateMode::CreateMode(qtsdl::GuiItemLink *gui_link) : - OutputMode{gui_link} {} - -bool CreateMode::available() const { - return true; -} - -std::string CreateMode::name() const { - return "Creation Mode"; -} - -void CreateMode::on_game_control_set() {} - -void CreateMode::on_enter() {} - -void CreateMode::on_exit() {} - -void CreateMode::render() {} - - -ActionModeSignals::ActionModeSignals(ActionMode *action_mode) : - action_mode(action_mode) { -} - -void ActionModeSignals::on_action(const std::string &action_name) { - presenter::LegacyDisplay *display = this->action_mode->game_control->get_display(); - - const input::legacy::action_t &action = display->get_action_manager().get(action_name); - input::legacy::InputContext *top_ctxt = &display->get_input_manager().get_top_context(); - - if (top_ctxt == this->action_mode || top_ctxt == &this->action_mode->building_context || top_ctxt == &this->action_mode->build_menu_context || top_ctxt == &this->action_mode->build_menu_mil_context) { - // create an action that just relays the action. - input::legacy::action_arg_t action_arg{ - input::legacy::Event{input::legacy::event_class::ANY, 0, input::legacy::modset_t{}}, - coord::input{0, 0}, - coord::input_delta{0, 0}, - {action}}; - top_ctxt->execute_if_bound(action_arg); - } -} - -ActionMode::ActionMode(qtsdl::GuiItemLink *gui_link) : - OutputMode{gui_link}, - selection{nullptr}, - use_set_ability{false}, - type_focus{nullptr}, - selecting{false}, - rng{rng::random_seed()}, - gui_signals{this} {} - - -void ActionMode::on_game_control_set() { - ENSURE(this->game_control != nullptr, "no control was actually set"); - - LegacyEngine *engine = this->game_control->get_engine(); - presenter::LegacyDisplay *display = this->game_control->get_display(); - ENSURE(engine != nullptr, "engine must be known!"); - - auto &action = display->get_action_manager(); - auto input = &display->get_input_manager(); - coord::CoordManager &coord = display->coord; - - // TODO: the selection should not be used in here! - this->selection = display->get_unit_selection(); - ENSURE(this->selection != nullptr, "selection must be fetched!"); - - this->bind(action.get("TRAIN_OBJECT"), - [this](const input::legacy::action_arg_t &) { - // attempt to train editor selected object - - // randomly select between male and female villagers - auto player = this->game_control->get_current_player(); - auto type = player->get_type(this->rng.probability(0.5) ? 83 : 293); - - Command cmd(*player, type); - cmd.add_flag(command_flag::interrupt); - this->selection->all_invoke(cmd); - }); - - this->bind(action.get("ENABLE_BUILDING_PLACEMENT"), - [](const input::legacy::action_arg_t &) { - // this->building_placement = true; - }); - - this->bind(action.get("DISABLE_SET_ABILITY"), - [this](const input::legacy::action_arg_t &) { - this->use_set_ability = false; - }); - - this->bind(action.get("SET_ABILITY_MOVE"), - [this](const input::legacy::action_arg_t &) { - this->use_set_ability = true; - this->ability = ability_type::move; - emit this->gui_signals.ability_changed(std::to_string(this->ability)); - }); - - this->bind(action.get("SET_ABILITY_GATHER"), - [this](const input::legacy::action_arg_t &) { - this->use_set_ability = true; - this->ability = ability_type::gather; - emit this->gui_signals.ability_changed(std::to_string(this->ability)); - }); - - this->bind(action.get("SET_ABILITY_GARRISON"), - [this](const input::legacy::action_arg_t &) { - this->use_set_ability = true; - this->ability = ability_type::garrison; - emit this->gui_signals.ability_changed(std::to_string(this->ability)); - }); - - this->bind(action.get("SET_ABILITY_REPAIR"), [this](const input::legacy::action_arg_t &) { - this->use_set_ability = true; - this->ability = ability_type::repair; - emit this->gui_signals.ability_changed(std::to_string(this->ability)); - }); - - this->bind(action.get("SPAWN_VILLAGER"), - [this, display](const input::legacy::action_arg_t &) { - auto player = this->game_control->get_current_player(); - - if (player->type_count() > 0) { - UnitType &type = *player->get_type(590); - - // TODO tile position - display->get_game()->placed_units.new_unit(type, *player, this->mousepos_phys3); - } - }); - - this->bind(action.get("KILL_UNIT"), - [this](const input::legacy::action_arg_t &) { - this->selection->kill_unit(*this->game_control->get_current_player()); - }); - - this->bind(action.get("BUILD_MENU"), - [this, input](const input::legacy::action_arg_t &) { - log::log(MSG(dbg) << "Opening build menu"); - input->push_context(&this->build_menu_context); - this->announce_buttons_type(); - }); - - this->bind(action.get("BUILD_MENU_MIL"), - [this, input](const input::legacy::action_arg_t &) { - log::log(MSG(dbg) << "Opening military build menu"); - input->push_context(&this->build_menu_mil_context); - this->announce_buttons_type(); - }); - - this->build_menu_context.bind(action.get("CANCEL"), - [this, input](const input::legacy::action_arg_t &) { - input->remove_context(&this->build_menu_context); - this->announce_buttons_type(); - }); - - this->build_menu_mil_context.bind(action.get("CANCEL"), - [this, input](const input::legacy::action_arg_t &) { - input->remove_context(&this->build_menu_mil_context); - this->announce_buttons_type(); - }); - - // Villager build commands - auto bind_building_key = [this, input](input::legacy::action_t action, int building, input::legacy::InputContext *ctxt) { - ctxt->bind(action, [this, building, ctxt, input](const input::legacy::action_arg_t &) { - auto player = this->game_control->get_current_player(); - if (this->selection->contains_builders(*player)) { - auto player = this->game_control->get_current_player(); - this->type_focus = player->get_type(building); - - if (player->can_make(*this->type_focus)) { - if (&input->get_top_context() != &this->building_context) { - input->remove_context(ctxt); - input->push_context(&this->building_context); - this->announce_buttons_type(); - } - } - else { - // TODO show in game error message - } - } - }); - }; - - bind_building_key(action.get("BUILDING_HOUS"), 70, &this->build_menu_context); // House - bind_building_key(action.get("BUILDING_MILL"), 68, &this->build_menu_context); // Mill - bind_building_key(action.get("BUILDING_MINE"), 584, &this->build_menu_context); // Mining Camp - bind_building_key(action.get("BUILDING_SMIL"), 562, &this->build_menu_context); // Lumber Camp - bind_building_key(action.get("BUILDING_DOCK"), 47, &this->build_menu_context); // Dock - // TODO: Doesn't show until it is placed - bind_building_key(action.get("BUILDING_FARM"), 50, &this->build_menu_context); // Farm - bind_building_key(action.get("BUILDING_BLAC"), 103, &this->build_menu_context); // Blacksmith - bind_building_key(action.get("BUILDING_MRKT"), 84, &this->build_menu_context); // Market - bind_building_key(action.get("BUILDING_CRCH"), 104, &this->build_menu_context); // Monastery - bind_building_key(action.get("BUILDING_UNIV"), 209, &this->build_menu_context); // University - bind_building_key(action.get("BUILDING_RTWC"), 109, &this->build_menu_context); // Town Center - bind_building_key(action.get("BUILDING_WNDR"), 276, &this->build_menu_context); // Wonder - - bind_building_key(action.get("BUILDING_BRKS"), 12, &this->build_menu_mil_context); // Barracks - bind_building_key(action.get("BUILDING_ARRG"), 87, &this->build_menu_mil_context); // Archery Range - bind_building_key(action.get("BUILDING_STBL"), 101, &this->build_menu_mil_context); // Stable - bind_building_key(action.get("BUILDING_SIWS"), 49, &this->build_menu_mil_context); // Siege Workshop - bind_building_key(action.get("BUILDING_WCTWX"), 598, &this->build_menu_mil_context); // Outpost - // TODO for palisade and stone wall: Drag walls, automatically adjust orientation - // TODO: This just cycles through all palisade textures - bind_building_key(action.get("BUILDING_WALL"), 72, &this->build_menu_mil_context); // Palisade Wall - // TODO: Fortified wall has a different ID - bind_building_key(action.get("BUILDING_WALL2"), 117, &this->build_menu_mil_context); // Stone Wall - // TODO: Upgraded versions have different IDs - bind_building_key(action.get("BUILDING_WCTW"), 79, &this->build_menu_mil_context); // Watch Tower - bind_building_key(action.get("BUILDING_WCTW4"), 236, &this->build_menu_mil_context); // Bombard Tower - // TODO: Gate placement - 659 is horizontal closed - bind_building_key(action.get("BUILDING_GTCA2"), 659, &this->build_menu_mil_context); // Gate - bind_building_key(action.get("BUILDING_CSTL"), 82, &this->build_menu_mil_context); // Castle - - this->building_context.bind(action.get("CANCEL"), - [this, input](const input::legacy::action_arg_t &) { - input->remove_context(&this->building_context); - this->announce_buttons_type(); - this->type_focus = nullptr; - }); - - auto bind_build = [this, input, &coord](input::legacy::action_t action, const bool increase) { - this->building_context.bind(action, [this, increase, input, &coord](const input::legacy::action_arg_t &arg) { - this->mousepos_phys3 = arg.mouse.to_phys3(coord, 0); - this->mousepos_tile = this->mousepos_phys3.to_tile(); - - bool placed = this->place_selection(this->mousepos_phys3); - - if (placed && !increase) { - this->type_focus = nullptr; - input->remove_context(&this->building_context); - this->announce_buttons_type(); - } - }); - }; - - bind_build(action.get("BUILD"), false); - bind_build(action.get("KEEP_BUILDING"), true); - - auto bind_select = [this, input, display](input::legacy::action_t action, const bool increase) { - this->bind(action, [this, increase, input, display](const input::legacy::action_arg_t &arg) { - auto mousepos_camgame = arg.mouse.to_camgame(display->coord); - Terrain *terrain = display->get_game()->terrain.get(); - - this->selection->drag_update(mousepos_camgame); - this->selection->drag_release(*this->game_control->get_current_player(), terrain, increase); - InputContext *top_ctxt = &input->get_top_context(); - - if ((this->selection->get_selection_type() != selection_type_t::own_units or this->selection->contains_military(*this->game_control->get_current_player())) - and (top_ctxt == &this->build_menu_context || top_ctxt == &this->build_menu_mil_context)) { - input->remove_context(top_ctxt); - } - this->announce_buttons_type(); - this->announce_current_selection(); - }); - }; - - bind_select(action.get("SELECT"), false); - bind_select(action.get("INCREASE_SELECTION"), true); - - this->bind(action.get("ORDER_SELECT"), [this, input, &coord](const input::legacy::action_arg_t &arg) { - if (this->type_focus) { - // right click can cancel building placement - this->type_focus = nullptr; - input->remove_context(&this->building_context); - this->announce_buttons_type(); - } - - auto cmd = this->get_action(arg.mouse.to_phys3(coord)); - cmd.add_flag(command_flag::interrupt); - this->selection->all_invoke(cmd); - this->use_set_ability = false; - }); - - this->bind(action.get("BEGIN_SELECTION"), [this](const input::legacy::action_arg_t &) { - this->selecting = true; - }); - - this->bind(action.get("END_SELECTION"), [this](const input::legacy::action_arg_t &) { - this->selecting = false; - }); - - this->bind(input::legacy::event_class::MOUSE, [this, &coord](const input::legacy::action_arg_t &arg) { - auto mousepos_camgame = arg.mouse.to_camgame(coord); - this->mousepos_phys3 = mousepos_camgame.to_phys3(coord); - this->mousepos_tile = this->mousepos_phys3.to_tile(); - - // drag selection box - if (arg.e.cc == input::legacy::ClassCode(input::legacy::event_class::MOUSE_MOTION, 0) && this->selecting && !this->type_focus) { - this->selection->drag_update(mousepos_camgame); - this->announce_current_selection(); - return true; - } - return false; - }); -} - -bool ActionMode::available() const { - if (this->game_control == nullptr) { - log::log(MSG(warn) << "ActionMode::available() queried without " - "game control being attached."); - return false; - } - - presenter::LegacyDisplay *display = this->game_control->get_display(); - if (display->get_game() != nullptr) { - return true; - } - else { - log::log(MSG(warn) << "Cannot enter action mode without a game"); - return false; - } -} - -void ActionMode::on_enter() {} - -void ActionMode::on_exit() { - // Since on_exit is called after removing the active mode, if the top context isn't the global one then it must - // be either a build menu or the building context - presenter::LegacyDisplay *display = this->game_control->get_display(); - auto *input_manager = &display->get_input_manager(); - InputContext *top_ctxt = &input_manager->get_top_context(); - if (top_ctxt != &input_manager->get_global_context()) { - if (top_ctxt == &this->building_context) { - this->type_focus = nullptr; - } - input_manager->remove_context(top_ctxt); - this->announce_buttons_type(); - } - this->selecting = false; -} - -Command ActionMode::get_action(const coord::phys3 &pos) const { - presenter::LegacyDisplay *display = this->game_control->get_display(); - GameMain *game = display->get_game(); - - auto obj = game->terrain->obj_at_point(pos); - if (obj) { - Command c(*this->game_control->get_current_player(), &obj->unit, pos); - if (this->use_set_ability) { - c.set_ability(ability); - } - return c; - } - else { - Command c(*this->game_control->get_current_player(), pos); - if (this->use_set_ability) { - c.set_ability(ability); - } - return c; - } -} - -bool ActionMode::place_selection(coord::phys3 point) { - if (this->type_focus) { - auto player = this->game_control->get_current_player(); - - if (player->can_make(*this->type_focus)) { - // confirm building placement with left click - // first create foundation using the producer - presenter::LegacyDisplay *display = this->game_control->get_display(); - - UnitContainer *container = &display->get_game()->placed_units; - UnitReference new_building = container->new_unit(*this->type_focus, *player, point); - - // task all selected villagers to build - // TODO: editor placed objects are completed already - if (new_building.is_valid()) { - player->deduct(this->type_focus->cost.get(*player)); // TODO change, move elsewheres - - Command cmd(*player, new_building.get()); - cmd.set_ability(ability_type::build); - cmd.add_flag(command_flag::interrupt); - this->selection->all_invoke(cmd); - return true; - } - } - else { - // TODO show in game error message - return false; - } - } - return false; -} - -void ActionMode::render() { - ENSURE(this->game_control != nullptr, "game_control is unset"); - LegacyEngine *engine = this->game_control->get_engine(); - presenter::LegacyDisplay *display = this->game_control->get_display(); - - ENSURE(engine != nullptr, "engine is needed to render ActionMode"); - - if (GameMain *game = display->get_game()) { - Player *player = this->game_control->get_current_player(); - - this->announce_resources(); - - if (this->selection && this->selection->get_units_count() > 0) { - this->announce_current_selection(); - } - - // when a building is being placed - if (this->type_focus) { - auto txt = this->type_focus->default_texture(); - auto size = this->type_focus->foundation_size; - - tile_range center = building_center(this->mousepos_phys3, size, *game->terrain); - txt->sample(display->coord, center.draw.to_camhud(display->coord), player->color); - } - } - else { - display->render_text({0, 140}, 12, renderer::Colors::WHITE, "Action Mode requires a game"); - } - - ENSURE(this->selection != nullptr, "selection not set"); - - this->selection->on_drawhud(); -} - -std::string ActionMode::name() const { - return "Action Mode"; -} - -void ActionMode::announce() { - this->OutputMode::announce(); - - this->announce_resources(); - emit this->gui_signals.ability_changed( - this->use_set_ability ? std::to_string(this->ability) : ""); -} - -void ActionMode::announce_resources() { - if (this->game_control) { - if (Player *player = this->game_control->get_current_player()) { - for (auto i = static_cast::type>(game_resource::RESOURCE_TYPE_COUNT); i != 0; --i) { - auto resource_type = static_cast(i - 1); - - emit this->gui_signals.resource_changed( - resource_type, - static_cast(player->amount(resource_type))); - } - emit this->gui_signals.population_changed(player->population.get_demand(), player->population.get_capacity(), player->population.get_space() <= 0 && !player->population.is_capacity_maxed()); - } - } -} - -void ActionMode::announce_buttons_type() { - ActionButtonsType buttons_type; - presenter::LegacyDisplay *display = this->game_control->get_display(); - InputContext *top_ctxt = &display->get_input_manager().get_top_context(); - if (top_ctxt == &this->build_menu_context) { - buttons_type = ActionButtonsType::BuildMenu; - } - else if (top_ctxt == &this->build_menu_mil_context) { - buttons_type = ActionButtonsType::MilBuildMenu; - } - else if (top_ctxt == &this->building_context || this->selection->get_selection_type() != selection_type_t::own_units) { - buttons_type = ActionButtonsType::None; - } - else if (this->selection->contains_military(*this->game_control->get_current_player())) { - buttons_type = ActionButtonsType::MilitaryUnits; - } - else { - buttons_type = ActionButtonsType::CivilianUnits; - } - - if (buttons_type != this->buttons_type) { - this->buttons_type = buttons_type; - emit this->gui_signals.buttons_type_changed(buttons_type); - - // announce the changed input context - this->announce(); - } -} - -EditorModeSignals::EditorModeSignals(EditorMode *editor_mode) : - editor_mode{editor_mode} { -} - -void EditorModeSignals::on_current_player_name_changed() { - this->editor_mode->announce_categories(); -} - -EditorMode::EditorMode(qtsdl::GuiItemLink *gui_link) : - OutputMode{gui_link}, - editor_current_terrain{-1}, - current_type_id{-1}, - paint_terrain{}, - gui_signals{this} {} - - -void EditorMode::on_game_control_set() { - presenter::LegacyDisplay *display = this->game_control->get_display(); - auto &action = display->get_action_manager(); - - // bind required hotkeys - this->bind(action.get("ENABLE_BUILDING_PLACEMENT"), [this](const input::legacy::action_arg_t &) { - log::log(MSG(dbg) << "change category"); - emit this->gui_signals.toggle(); - }); - - this->bind(input::legacy::event_class::MOUSE, [this, display](const input::legacy::action_arg_t &arg) { - if (arg.e.cc == input::legacy::ClassCode(input::legacy::event_class::MOUSE_BUTTON, 1) || display->get_input_manager().is_down(input::legacy::event_class::MOUSE_BUTTON, 1)) { - if (this->paint_terrain) { - this->paint_terrain_at(arg.mouse.to_viewport(display->coord)); - } - else { - this->paint_entity_at(arg.mouse.to_viewport(display->coord), false); - } - return true; - } - else if (arg.e.cc == input::legacy::ClassCode(input::legacy::event_class::MOUSE_BUTTON, 3) || display->get_input_manager().is_down(input::legacy::event_class::MOUSE_BUTTON, 3)) { - if (!this->paint_terrain) { - this->paint_entity_at(arg.mouse.to_viewport(display->coord), true); - } - return true; - } - return false; - }); -} - -bool EditorMode::available() const { - if (this->game_control == nullptr) { - log::log(MSG(warn) << "game_control not yet linked to EditorMode"); - return false; - } - else if (this->game_control->get_display()->get_game() != nullptr) { - return true; - } - else { - log::log(MSG(warn) << "Cannot enter editor mode without a game"); - return false; - } -} - -void EditorMode::on_enter() {} - -void EditorMode::on_exit() {} - -void EditorMode::render() {} - -std::string EditorMode::name() const { - return "Editor mode"; -} - -void EditorMode::set_current_type_id(int current_type_id) { - this->current_type_id = current_type_id; -} - -void EditorMode::set_current_terrain_id(terrain_t current_terrain_id) { - this->editor_current_terrain = current_terrain_id; -} - -void EditorMode::set_paint_terrain(bool paint_terrain) { - this->paint_terrain = paint_terrain; -} - -void EditorMode::paint_terrain_at(const coord::viewport &point) { - presenter::LegacyDisplay *display = this->game_control->get_display(); - Terrain *terrain = display->get_game()->terrain.get(); - - auto mousepos_tile = point.to_tile(display->coord); - - TerrainChunk *chunk = terrain->get_create_chunk(mousepos_tile); - chunk->get_data(mousepos_tile.get_pos_on_chunk())->terrain_id = editor_current_terrain; -} - -void EditorMode::paint_entity_at(const coord::viewport &point, const bool del) { - presenter::LegacyDisplay *display = this->game_control->get_display(); - GameMain *game = display->get_game(); - Terrain *terrain = game->terrain.get(); - - auto mousepos_phys3 = point.to_phys3(display->coord); - auto mousepos_tile = mousepos_phys3.to_tile(); - - TerrainChunk *chunk = terrain->get_create_chunk(mousepos_tile); - // TODO : better detection of presence of unit - if (!chunk->get_data(mousepos_tile.get_pos_on_chunk())->obj.empty()) { - if (del) { - // delete first object currently standing at the clicked position - TerrainObject *obj = chunk->get_data(mousepos_tile.get_pos_on_chunk())->obj[0]; - obj->remove(); - } - } - else if (!del && this->current_type_id != -1) { - Player *player = this->game_control->get_current_player(); - UnitType *selected_type = player->get_type(this->current_type_id); - - // tile is empty so try creating a unit - UnitContainer *container = &game->placed_units; - container->new_unit(*selected_type, *this->game_control->get_current_player(), mousepos_phys3); - } -} - -void EditorMode::announce_categories() { - if (this->game_control) - if (auto player = this->game_control->get_current_player()) - emit this->gui_signals.categories_changed(player->civ->get_type_categories()); - - emit this->gui_signals.categories_content_changed(); -} - -void EditorMode::announce_category_content(const std::string &category_name) { - if (this->game_control) - if (auto player = this->game_control->get_current_player()) { - auto inds = player->civ->get_category(category_name); - std::vector> type_and_texture(inds.size()); - - auto it = std::begin(type_and_texture); - - std::for_each(std::begin(inds), std::end(inds), [player, &it](auto index) { - *it++ = std::make_tuple(index, player->get_type(index)->default_texture()->id); - }); - - emit this->gui_signals.category_content_changed(category_name, type_and_texture); - } -} - -void EditorMode::announce() { - OutputMode::announce(); - this->announce_categories(); -} - -void EditorMode::set_game_control(GameControl *game_control) { - if (this->game_control != game_control) { - if (this->game_control) - QObject::disconnect( - &this->game_control->gui_signals, - &GameControlSignals::current_player_name_changed, - &this->gui_signals, - &EditorModeSignals::on_current_player_name_changed); - - // actually set the control - OutputMode::set_game_control(game_control); - - if (this->game_control) - QObject::connect( - &this->game_control->gui_signals, - &GameControlSignals::current_player_name_changed, - &this->gui_signals, - &EditorModeSignals::on_current_player_name_changed); - - this->announce_categories(); - } - else { - // just set the control - OutputMode::set_game_control(game_control); - } -} - -GameControlSignals::GameControlSignals(GameControl *game_control) : - game_control{game_control} { -} - -void GameControlSignals::on_game_running(bool running) { - if (running) - this->game_control->announce_current_player_name(); -} - -GameControl::GameControl(qtsdl::GuiItemLink *gui_link) : - engine{nullptr}, - game{nullptr}, - active_mode{nullptr}, - active_mode_index{-1}, - current_player{1}, - gui_signals{this}, - gui_link{gui_link} { -} - -void GameControl::set_engine(LegacyEngine *engine) { - // TODO: decide to either go for a full Engine QML-singleton or for a regular object - ENSURE(!this->engine || this->engine == engine, "relinking GameControl to another engine is not supported and not caught properly"); - - if (not this->engine) { - this->engine = engine; - - // add handlers - this->display->register_drawhud_action(this, -1); - - auto &action = this->display->get_action_manager(); - - auto &global_input_context = display->get_input_manager().get_global_context(); - - // advance the active mode - global_input_context.bind(action.get("TOGGLE_CONSTRUCT_MODE"), [this](const input::legacy::action_arg_t &) { - this->set_mode((this->active_mode_index + 1) % this->modes.size()); - }); - - // Switching between players with the 1-8 keys - auto bind_player_switch = [this, &global_input_context](input::legacy::action_t action, size_t player_index) { - global_input_context.bind(action, [this, player_index](const input::legacy::action_arg_t &) { - if (this->current_player != player_index) - if (auto game = this->display->get_game()) - if (player_index < game->player_count()) { - this->current_player = player_index; - this->announce_current_player_name(); - } - }); - }; - - bind_player_switch(action.get("SWITCH_TO_PLAYER_1"), 0); - bind_player_switch(action.get("SWITCH_TO_PLAYER_2"), 1); - bind_player_switch(action.get("SWITCH_TO_PLAYER_3"), 2); - bind_player_switch(action.get("SWITCH_TO_PLAYER_4"), 3); - bind_player_switch(action.get("SWITCH_TO_PLAYER_5"), 4); - bind_player_switch(action.get("SWITCH_TO_PLAYER_6"), 5); - bind_player_switch(action.get("SWITCH_TO_PLAYER_7"), 6); - bind_player_switch(action.get("SWITCH_TO_PLAYER_8"), 7); - - this->display->announce_global_binds(); - } -} -void GameControl::set_display(presenter::LegacyDisplay *display) { - this->display = display; -} - -void GameControl::set_game(GameMainHandle *game) { - if (this->game != game) { - if (this->game) - QObject::disconnect(&this->game->gui_signals, - &GameMainSignals::game_running, - &this->gui_signals, - &GameControlSignals::on_game_running); - - this->game = game; - - if (this->game) - QObject::connect(&this->game->gui_signals, - &GameMainSignals::game_running, - &this->gui_signals, - &GameControlSignals::on_game_running); - } -} - -void GameControl::set_modes(const std::vector &modes) { - const int old_mode_index = this->active_mode_index; - - this->set_mode(-1); - this->modes = modes; - - for (auto mode : this->modes) { - // link the controller to the mode - mode->set_game_control(this); - } - - // announce the newly set modes - emit this->gui_signals.modes_changed(this->active_mode, - this->active_mode_index); - - // try to enter the mode we were in before - // assuming its index didn't change. - if (old_mode_index != -1) { - if (old_mode_index < std::distance(std::begin(this->modes), - std::end(this->modes))) { - this->set_mode(old_mode_index, true); - } - else { - log::log(MSG(warn) << "couldn't enter previous gui mode #" - << old_mode_index << " as it vanished."); - } - } -} - -void GameControl::announce_mode() { - emit this->gui_signals.mode_changed(this->active_mode, - this->active_mode_index); - - if (this->active_mode != nullptr) { - emit this->active_mode->announce(); - } -} - -void GameControl::announce_current_player_name() { - if (Player *player = this->get_current_player()) { - emit this->gui_signals.current_player_name_changed( - "[" + std::to_string(player->color) + "] " + player->name); - emit this->gui_signals.current_civ_index_changed(player->civ->civ_id); - } -} - -void ActionMode::announce_current_selection() { - Player *player = this->game_control->get_current_player(); - emit this->gui_signals.selection_changed(this->selection, player); -} - -bool GameControl::on_drawhud() { - // render the active mode - if (this->active_mode) - this->active_mode->render(); - - return true; -} - -Player *GameControl::get_current_player() const { - if (auto game = this->display->get_game()) { - unsigned int number = game->player_count(); - return game->get_player(this->current_player % number); - } - return nullptr; -} - -void GameControl::set_mode(int mode_index, bool signal_if_unchanged) { - bool need_to_signal = signal_if_unchanged; - - // do we wanna set a new mode? - if (mode_index != -1) { - // if the new mode is valid, available and not active at the moment - if (mode_index < std::distance(std::begin(this->modes), - std::end(this->modes)) - && this->modes[mode_index]->available() - && this->active_mode_index != mode_index) { - ENSURE(!this->active_mode == (this->active_mode_index == -1), - "inconsistency between the active mode index and pointer"); - - // exit from the old mode - if (this->active_mode) { - this->display->get_input_manager().remove_context( - this->active_mode); - - // trigger the exit callback of the mode - if (this->active_mode) { - this->active_mode->on_exit(); - } - } - - // set the new active mode - this->active_mode_index = mode_index; - this->active_mode = this->modes[mode_index]; - - // trigger the entry callback - this->active_mode->on_enter(); - - // add the mode-local input context - this->display->get_input_manager().push_context( - this->active_mode); - - need_to_signal = true; - } - } - else { - // unassign the active mode - if (this->active_mode) { - // remove the input context - this->display->get_input_manager().remove_context( - this->active_mode); - - this->active_mode_index = -1; - this->active_mode = nullptr; - - need_to_signal = true; - } - } - - if (need_to_signal) { - this->announce_mode(); - } -} - -LegacyEngine *GameControl::get_engine() const { - if (this->engine == nullptr) { - throw Error{MSG(err) << "game control doesn't have a valid engine pointer yet"}; - } - - return this->engine; -} - -presenter::LegacyDisplay *GameControl::get_display() const { - if (this->engine == nullptr) { - throw Error{MSG(err) << "game control doesn't have a valid engine pointer yet"}; - } - - return this->display; -} - - -} // namespace openage diff --git a/libopenage/presenter/legacy/game_control.h b/libopenage/presenter/legacy/game_control.h deleted file mode 100644 index 2732ce0345..0000000000 --- a/libopenage/presenter/legacy/game_control.h +++ /dev/null @@ -1,395 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include - -#include "coord/pixel.h" -#include "gamestate/old/game_main.h" -#include "gui/guisys/link/gui_item_link.h" -#include "handlers.h" -#include "input/legacy/input_context.h" -#include "presenter/legacy/legacy.h" -#include "rng/rng.h" -#include "unit/command.h" -#include "unit/selection.h" -#include "unit/unit_type.h" - -namespace qtsdl { -class GuiItemLink; -} // namespace qtsdl - -namespace openage { - -class ActionMode; -class EditorMode; -class LegacyEngine; -class GameControl; - - -/** - * Signals for a gui mode. - */ -class [[deprecated]] OutputModeSignals : public QObject { - Q_OBJECT - -public: -signals: - /** - * Signal is triggered to anounce a new output mode. - * name: name of the gui mode. - */ - void announced(const std::string &name); - - /** - * Signal is triggered when the bindings of the mode change. - * binds: list of strings that describe keybindings. - */ - void binds_changed(const std::vector &binds); -}; - - -/** - * A target for input handling and gui rendering. - * This allows to switch to different display UIs. - */ -class [[deprecated]] OutputMode : public input::legacy::InputContext { -public: - explicit OutputMode(qtsdl::GuiItemLink *gui_link); - virtual ~OutputMode(); - - /** - * Is this mode able to be used? - */ - virtual bool available() const = 0; - - /** - * Called when switching to this mode. - */ - virtual void on_enter() = 0; - - /** - * Called when the mode is left. - */ - virtual void on_exit() = 0; - - /** - * Display this mode. - */ - virtual void render() = 0; - - /** - * Used for displaying the current mode. - */ - virtual std::string name() const = 0; - - /** - * Emit the "announced" signal with (name, InputContext::active_binds). - */ - virtual void announce(); - - /** - * Called when the GameControl is set in QML. - */ - virtual void set_game_control(GameControl *game_control); - - /** - * Called after GameControl has been set by QML from `set_game_control`. - */ - virtual void on_game_control_set() = 0; - -protected: - GameControl *game_control; - -public: - /** - * Signals to be triggered when the mode canges. - */ - OutputModeSignals gui_signals; - - qtsdl::GuiItemLink *gui_link; -}; - - -/** - * This is mainly the game editor. - * Shows menus to choose units to build. - */ -class [[deprecated]] CreateMode : public OutputMode { -public: - CreateMode(qtsdl::GuiItemLink *gui_link); - - bool available() const override; - void on_enter() override; - void on_exit() override; - void render() override; - std::string name() const override; - void on_game_control_set() override; -}; - -enum class ActionButtonsType { - None, - MilitaryUnits, - CivilianUnits, - BuildMenu, - MilBuildMenu -}; - - -class ActionModeSignals : public QObject { - Q_OBJECT - -public: - explicit ActionModeSignals(ActionMode *action_mode); - -public slots: - void on_action(const std::string &action_name); - -signals: - void resource_changed(game_resource resource, int amount); - void population_changed(int demand, int capacity, bool warn); - void selection_changed(const UnitSelection *unit_selection, const Player *player); - void ability_changed(const std::string &ability); - void buttons_type_changed(const ActionButtonsType type); - -private: - ActionMode *action_mode; -}; - - -/** - * This is the main game UI. - * - * Used to control units, issue commands, basically this is where you - * sink your time in when playing. - */ -class [[deprecated]] ActionMode : public OutputMode { -public: - ActionMode(qtsdl::GuiItemLink *gui_link); - - bool available() const override; - void on_enter() override; - void on_exit() override; - void render() override; - std::string name() const override; - void on_game_control_set() override; - -private: - friend ActionModeSignals; - /** - * sends to gui the properties that it needs - */ - virtual void announce() override; - - /** - * sends to gui the amounts of resources - */ - void announce_resources(); - - /** - * sends to gui the buttons it should use for the action buttons - * (if changed) - */ - void announce_buttons_type(); - - void announce_current_selection(); - - /** - * decides which type of right mouse click command - * to issue based on position. - * - * if a unit is at the position the command should target the unit, - * otherwise target ground position - */ - Command get_action(const coord::phys3 &pos) const; - - /** - * Register the keybindings. - */ - void create_binds(); - - /** - * used after opening the build menu - */ - InputContext build_menu_context; - - /** - * used after opening the military build menu - */ - InputContext build_menu_mil_context; - - /** - * used when selecting the building placement - */ - InputContext building_context; - - /** - * places hovering building - */ - bool place_selection(coord::phys3 point); - - // currently selected units - UnitSelection *selection; - - // restrict command abilities - bool use_set_ability; - ability_type ability; - - // a selected type for placement - UnitType *type_focus; - - // TODO these shouldn't be here. remove them ASAP. - // they are used to carry over mouse information - // into some of the game control lambda functions - coord::phys3 mousepos_phys3{0, 0, 0}; - coord::tile mousepos_tile{0, 0}; - bool selecting; - - ActionButtonsType buttons_type; - - // used for random type creation - rng::RNG rng; - -public: - ActionModeSignals gui_signals; -}; - - -class EditorModeSignals : public QObject { - Q_OBJECT - -public: - explicit EditorModeSignals(EditorMode *editor_mode); - -public slots: - void on_current_player_name_changed(); - -signals: - void toggle(); - void categories_changed(const std::vector &categories); - void categories_content_changed(); - void category_content_changed(const std::string &category_name, std::vector> &type_and_texture); - -private: - EditorMode *editor_mode; -}; - -/** - * UI mode to provide an interface for map editing. - */ -class [[deprecated]] EditorMode : public OutputMode { -public: - explicit EditorMode(qtsdl::GuiItemLink *gui_link); - - bool available() const override; - void on_enter() override; - void on_exit() override; - void render() override; - std::string name() const override; - void on_game_control_set() override; - - void set_current_type_id(int current_type_id); - void set_current_terrain_id(openage::terrain_t current_terrain_id); - void set_paint_terrain(bool paint_terrain); - - bool on_single_click(int button, coord::viewport point); - - void announce_categories(); - void announce_category_content(const std::string &category_name); - -private: - virtual void announce() override; - virtual void set_game_control(GameControl *game_control) override; - - void paint_terrain_at(const coord::viewport &point); - void paint_entity_at(const coord::viewport &point, const bool del); - - /** - * currently selected terrain id - */ - terrain_t editor_current_terrain; - int current_type_id; - std::string category; - - /** - * mouse click mode: - * true = terrain painting, - * false = unit placement - */ - bool paint_terrain; - -public: - EditorModeSignals gui_signals; -}; - - -class GameControlSignals : public QObject { - Q_OBJECT - -public: - explicit GameControlSignals(GameControl *game_control); - -public slots: - void on_game_running(bool running); - -signals: - void mode_changed(OutputMode *mode, int mode_index); - void modes_changed(OutputMode *mode, int mode_index); - - void current_player_name_changed(const std::string ¤t_player_name); - void current_civ_index_changed(int current_civ_index); - void is_selected_unit_changed(bool is_selected_unit); - -private: - GameControl *game_control; -}; - - -/** - * connects the gui system with the game engine - * switches between contexts such as editor mode and - * action mode - * - * hud rendering and input handling is redirected to the active mode - */ -class [[deprecated]] GameControl : public openage::HudHandler { -public: - explicit GameControl(qtsdl::GuiItemLink *gui_link); - - void set_engine(LegacyEngine *engine); - void set_display(presenter::LegacyDisplay *engine); - void set_game(GameMainHandle *game); - - void set_modes(const std::vector &modes); - - void set_mode(int mode, bool signal_if_unchanged = false); - void announce_mode(); - void announce_current_player_name(); - - bool on_drawhud() override; - - Player *get_current_player() const; - LegacyEngine *get_engine() const; - presenter::LegacyDisplay *get_display() const; - -private: - LegacyEngine *engine; - presenter::LegacyDisplay *display; - GameMainHandle *game; - - // control modes - std::vector modes; - - OutputMode *active_mode; - int active_mode_index; - - size_t current_player; - -public: - GameControlSignals gui_signals; - qtsdl::GuiItemLink *gui_link; -}; - -} // namespace openage From 3cea703dcf060b8d4fea353794c6b33f61090343 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Sep 2023 00:04:40 +0200 Subject: [PATCH 030/771] presenter: Remove legacy display code. --- libopenage/console/console.cpp | 114 +++-- libopenage/console/console.h | 6 +- libopenage/console/draw.cpp | 134 ++--- libopenage/console/draw.h | 3 +- libopenage/gamestate/old/game_main.cpp | 4 +- libopenage/gamestate/old/game_main.h | 8 +- libopenage/gui/engine_info.h | 10 - libopenage/gui/gui.cpp | 6 +- libopenage/legacy_engine.cpp | 10 - libopenage/legacy_engine.h | 11 - libopenage/presenter/CMakeLists.txt | 2 - libopenage/presenter/legacy/CMakeLists.txt | 7 - libopenage/presenter/legacy/legacy.cpp | 550 --------------------- libopenage/presenter/legacy/legacy.h | 302 ----------- libopenage/terrain/terrain.cpp | 49 -- libopenage/terrain/terrain.h | 7 - 16 files changed, 138 insertions(+), 1085 deletions(-) delete mode 100644 libopenage/presenter/legacy/CMakeLists.txt delete mode 100644 libopenage/presenter/legacy/legacy.cpp delete mode 100644 libopenage/presenter/legacy/legacy.h diff --git a/libopenage/console/console.cpp b/libopenage/console/console.cpp index ae9c2cae77..f2350a01ed 100644 --- a/libopenage/console/console.cpp +++ b/libopenage/console/console.cpp @@ -22,8 +22,8 @@ namespace console { * log console, command console */ -Console::Console(presenter::LegacyDisplay *engine) : - engine{engine}, +Console::Console(/* presenter::LegacyDisplay *display */) : + // display{display}, bottomleft{0, 0}, topright{1, 1}, charsize{1, 1}, @@ -56,64 +56,68 @@ void Console::load_colors(std::vector &colortable) { } void Console::register_to_engine() { - this->engine->register_input_action(this); - this->engine->register_tick_action(this); - this->engine->register_drawhud_action(this); - this->engine->register_resize_action(this); + // TODO: Use new renderer + + // this->display->register_input_action(this); + // this->display->register_tick_action(this); + // this->display->register_drawhud_action(this); + // this->display->register_resize_action(this); // Bind the console toggle key globally - auto &action = this->engine->get_action_manager(); - auto &global = this->engine->get_input_manager().get_global_context(); + // auto &action = this->display->get_action_manager(); + // auto &global = this->display->get_input_manager().get_global_context(); - global.bind(action.get("TOGGLE_CONSOLE"), [this](const input::legacy::action_arg_t &) { - this->set_visible(!this->visible); - }); + // global.bind(action.get("TOGGLE_CONSOLE"), [this](const input::legacy::action_arg_t &) { + // this->set_visible(!this->visible); + // }); // TODO: bind any needed input to InputContext // toggle console will take highest priority - this->input_context.bind(action.get("TOGGLE_CONSOLE"), [this](const input::legacy::action_arg_t &) { - this->set_visible(false); - }); - this->input_context.bind(input::legacy::event_class::UTF8, [this](const input::legacy::action_arg_t &arg) { - // a single char typed into the console - std::string utf8 = arg.e.as_utf8(); - this->buf.write(utf8.c_str()); - command += utf8; - return true; - }); - this->input_context.bind(input::legacy::event_class::NONPRINT, [this](const input::legacy::action_arg_t &arg) { - switch (arg.e.as_char()) { - case 8: // remove a single UTF-8 character - if (this->command.size() > 0) { - util::utf8_pop_back(this->command); - this->buf.pop_last_char(); - } - return true; - - case 13: // interpret command - this->buf.write('\n'); - this->interpret(this->command); - this->command = ""; - return true; - - default: - return false; - } - }); - this->input_context.utf8_mode = true; + // this->input_context.bind(action.get("TOGGLE_CONSOLE"), [this](const input::legacy::action_arg_t &) { + // this->set_visible(false); + // }); + // this->input_context.bind(input::legacy::event_class::UTF8, [this](const input::legacy::action_arg_t &arg) { + // // a single char typed into the console + // std::string utf8 = arg.e.as_utf8(); + // this->buf.write(utf8.c_str()); + // command += utf8; + // return true; + // }); + // this->input_context.bind(input::legacy::event_class::NONPRINT, [this](const input::legacy::action_arg_t &arg) { + // switch (arg.e.as_char()) { + // case 8: // remove a single UTF-8 character + // if (this->command.size() > 0) { + // util::utf8_pop_back(this->command); + // this->buf.pop_last_char(); + // } + // return true; + + // case 13: // interpret command + // this->buf.write('\n'); + // this->interpret(this->command); + // this->command = ""; + // return true; + + // default: + // return false; + // } + // }); + // this->input_context.utf8_mode = true; } void Console::set_visible(bool make_visible) { - if (make_visible) { - this->engine->get_input_manager().push_context(&this->input_context); - this->visible = true; - } - else { - this->engine->get_input_manager().remove_context(&this->input_context); - this->visible = false; - } + // TODO: Use new renderer + + // if (make_visible) { + // this->display->get_input_manager().push_context(&this->input_context); + // this->visible = true; + // } + // else { + // this->display->get_input_manager().remove_context(&this->input_context); + // this->visible = false; + // } } void Console::write(const char *text) { @@ -126,9 +130,11 @@ void Console::interpret(const std::string &command) { this->set_visible(false); } else if (command == "list") { - for (auto &line : this->engine->list_options()) { - this->write(line.c_str()); - } + // TODO: Use new renderer + + // for (auto &line : this->display->list_options()) { + // this->write(line.c_str()); + // } } else if (command.substr(0, 3) == "set") { std::size_t first_space = command.find(" "); @@ -170,7 +176,9 @@ bool Console::on_drawhud() { return true; } - draw::to_opengl(this->engine, this); + // TODO: Use new renderer + + // draw::to_opengl(this->display, this); return true; } diff --git a/libopenage/console/console.h b/libopenage/console/console.h index 8585373eb6..afe4095b51 100644 --- a/libopenage/console/console.h +++ b/libopenage/console/console.h @@ -9,7 +9,6 @@ #include "../gamedata/color_dummy.h" #include "../handlers.h" #include "../input/legacy/input_manager.h" -#include "../presenter/legacy/legacy.h" #include "../renderer/font/font.h" #include "../util/color.h" #include "buf.h" @@ -28,7 +27,7 @@ class Console : InputHandler , HudHandler , ResizeHandler { public: - Console(presenter::LegacyDisplay *renderer); + Console(/* presenter::LegacyDisplay *display */); ~Console(); /** @@ -60,7 +59,8 @@ class Console : InputHandler bool on_resize(coord::viewport_delta new_size) override; protected: - presenter::LegacyDisplay *engine; + // TODO: Replace with new renderer + // presenter::LegacyDisplay *display; public: coord::camhud bottomleft; diff --git a/libopenage/console/draw.cpp b/libopenage/console/draw.cpp index 237c16d91c..187079f86f 100644 --- a/libopenage/console/draw.cpp +++ b/libopenage/console/draw.cpp @@ -18,73 +18,73 @@ namespace openage { namespace console { namespace draw { -void to_opengl(presenter::LegacyDisplay *engine, Console *console) { - coord::camhud topleft{ - console->bottomleft.x, - // TODO This should probably just be console->topright.y - console->bottomleft.y + console->charsize.y * console->buf.dims.y}; - coord::pixel_t ascender = static_cast(console->font.get_ascender()); - - renderer::TextRenderer *text_renderer = engine->get_text_renderer(); - text_renderer->set_font(&console->font); - - int64_t monotime = timing::get_monotonic_time(); - - bool fastblinking_visible = (monotime % 600000000 < 300000000); - bool slowblinking_visible = (monotime % 300000000 < 150000000); - - for (coord::term_t x = 0; x < console->buf.dims.x; x++) { - coord::camhud chartopleft{topleft.x + console->charsize.x * x, 0}; - - for (coord::term_t y = 0; y < console->buf.dims.y; y++) { - chartopleft.y = topleft.y - console->charsize.y * y; - buf_char p = *(console->buf.chrdataptr({x, y - console->buf.scrollback_pos})); - - int fgcolid, bgcolid; - - bool cursor_visible_at_current_pos = (console->buf.cursorpos == coord::term{x, y - console->buf.scrollback_pos}); - - cursor_visible_at_current_pos &= console->buf.cursor_visible; - - if (((p.flags & CHR_NEGATIVE) != 0) xor cursor_visible_at_current_pos) { - bgcolid = p.fgcol; - fgcolid = p.bgcol; - } - else { - bgcolid = p.bgcol; - fgcolid = p.fgcol; - } - - if ((p.flags & CHR_INVISIBLE) - or (p.flags & CHR_BLINKING and not slowblinking_visible) - or (p.flags & CHR_BLINKINGFAST and not fastblinking_visible)) { - fgcolid = bgcolid; - } - - console->termcolors[bgcolid].use(0.8); - - glBegin(GL_QUADS); - { - glVertex3f(chartopleft.x, chartopleft.y, 0); - glVertex3f(chartopleft.x, chartopleft.y - console->charsize.y, 0); - glVertex3f(chartopleft.x + console->charsize.x, chartopleft.y - console->charsize.y, 0); - glVertex3f(chartopleft.x + console->charsize.x, chartopleft.y, 0); - } - glEnd(); - - console->termcolors[fgcolid].use(1); - - char utf8buf[5]; - if (util::utf8_encode(p.cp, utf8buf) == 0) { - //unrepresentable character (question mark in black rhombus) - text_renderer->draw(chartopleft.x, chartopleft.y - ascender, "\uFFFD"); - } - else { - text_renderer->draw(chartopleft.x, chartopleft.y - ascender, utf8buf); - } - } - } -} +// void to_opengl(presenter::LegacyDisplay *engine, Console *console) { +// coord::camhud topleft{ +// console->bottomleft.x, +// // TODO This should probably just be console->topright.y +// console->bottomleft.y + console->charsize.y * console->buf.dims.y}; +// coord::pixel_t ascender = static_cast(console->font.get_ascender()); + +// renderer::TextRenderer *text_renderer = engine->get_text_renderer(); +// text_renderer->set_font(&console->font); + +// int64_t monotime = timing::get_monotonic_time(); + +// bool fastblinking_visible = (monotime % 600000000 < 300000000); +// bool slowblinking_visible = (monotime % 300000000 < 150000000); + +// for (coord::term_t x = 0; x < console->buf.dims.x; x++) { +// coord::camhud chartopleft{topleft.x + console->charsize.x * x, 0}; + +// for (coord::term_t y = 0; y < console->buf.dims.y; y++) { +// chartopleft.y = topleft.y - console->charsize.y * y; +// buf_char p = *(console->buf.chrdataptr({x, y - console->buf.scrollback_pos})); + +// int fgcolid, bgcolid; + +// bool cursor_visible_at_current_pos = (console->buf.cursorpos == coord::term{x, y - console->buf.scrollback_pos}); + +// cursor_visible_at_current_pos &= console->buf.cursor_visible; + +// if (((p.flags & CHR_NEGATIVE) != 0) xor cursor_visible_at_current_pos) { +// bgcolid = p.fgcol; +// fgcolid = p.bgcol; +// } +// else { +// bgcolid = p.bgcol; +// fgcolid = p.fgcol; +// } + +// if ((p.flags & CHR_INVISIBLE) +// or (p.flags & CHR_BLINKING and not slowblinking_visible) +// or (p.flags & CHR_BLINKINGFAST and not fastblinking_visible)) { +// fgcolid = bgcolid; +// } + +// console->termcolors[bgcolid].use(0.8); + +// glBegin(GL_QUADS); +// { +// glVertex3f(chartopleft.x, chartopleft.y, 0); +// glVertex3f(chartopleft.x, chartopleft.y - console->charsize.y, 0); +// glVertex3f(chartopleft.x + console->charsize.x, chartopleft.y - console->charsize.y, 0); +// glVertex3f(chartopleft.x + console->charsize.x, chartopleft.y, 0); +// } +// glEnd(); + +// console->termcolors[fgcolid].use(1); + +// char utf8buf[5]; +// if (util::utf8_encode(p.cp, utf8buf) == 0) { +// //unrepresentable character (question mark in black rhombus) +// text_renderer->draw(chartopleft.x, chartopleft.y - ascender, "\uFFFD"); +// } +// else { +// text_renderer->draw(chartopleft.x, chartopleft.y - ascender, utf8buf); +// } +// } +// } +// } void to_terminal(Buf *buf, util::FD *fd, bool clear) { //move cursor, draw top left corner diff --git a/libopenage/console/draw.h b/libopenage/console/draw.h index 2e35555f05..a359556fb4 100644 --- a/libopenage/console/draw.h +++ b/libopenage/console/draw.h @@ -2,7 +2,6 @@ #pragma once -#include "../presenter/legacy/legacy.h" namespace openage { @@ -22,7 +21,7 @@ namespace draw { /** * experimental and totally inefficient opengl draw of a terminal buffer. */ -void to_opengl(presenter::LegacyDisplay *engine, Console *console); +// void to_opengl(presenter::LegacyDisplay *engine, Console *console); /** * very early and inefficient printing of the console to a pty. diff --git a/libopenage/gamestate/old/game_main.cpp b/libopenage/gamestate/old/game_main.cpp index bd26fe6e48..0fd3624f47 100644 --- a/libopenage/gamestate/old/game_main.cpp +++ b/libopenage/gamestate/old/game_main.cpp @@ -81,7 +81,7 @@ void GameMainHandle::set_engine(LegacyEngine *engine) { void GameMainHandle::clear() { if (this->engine) { this->game = nullptr; - this->display->end_game(); + // this->display->end_game(); announce_running(); } } @@ -94,7 +94,7 @@ void GameMainHandle::set_game(std::unique_ptr &&game) { this->game = game.get(); // then pass on the game to the engine - this->display->start_game(std::move(game)); + // this->display->start_game(std::move(game)); announce_running(); } diff --git a/libopenage/gamestate/old/game_main.h b/libopenage/gamestate/old/game_main.h index 13d1ceb9d1..a7318f25dc 100644 --- a/libopenage/gamestate/old/game_main.h +++ b/libopenage/gamestate/old/game_main.h @@ -2,14 +2,13 @@ #pragma once -#include #include +#include #include #include #include "../../options.h" -#include "../../presenter/legacy/legacy.h" #include "../../terrain/terrain.h" #include "../../unit/unit_container.h" #include "../../util/timing.h" @@ -170,11 +169,6 @@ class GameMainHandle { */ LegacyEngine *engine; - /** - * The engine the main game handle is attached to. - */ - presenter::LegacyDisplay *display; - public: GameMainSignals gui_signals; qtsdl::GuiItemLink *gui_link; diff --git a/libopenage/gui/engine_info.h b/libopenage/gui/engine_info.h index 145821f483..999360ac2a 100644 --- a/libopenage/gui/engine_info.h +++ b/libopenage/gui/engine_info.h @@ -2,17 +2,12 @@ #pragma once -#include "../presenter/legacy/legacy.h" #include "../util/path.h" #include "guisys/public/gui_singleton_items_info.h" namespace openage { class LegacyEngine; -namespace presenter { -class LegacyDisplay; -} - namespace gui { /** @@ -33,11 +28,6 @@ class EngineQMLInfo : public qtsdl::GuiSingletonItemsInfo { */ LegacyEngine *engine; - /** - * The openage display. - */ - presenter::LegacyDisplay *display; - /** * Search path for finding assets n stuff. */ diff --git a/libopenage/gui/gui.cpp b/libopenage/gui/gui.cpp index b9b600d8c5..ebaf20b24a 100644 --- a/libopenage/gui/gui.cpp +++ b/libopenage/gui/gui.cpp @@ -45,9 +45,9 @@ GUI::GUI(SDL_Window *window, source, rootdir}, input{&renderer, &game_logic_updater} { - info->display->register_resize_action(this); - info->display->register_input_action(this); - info->display->register_drawhud_action(this); + // info->display->register_resize_action(this); + // info->display->register_input_action(this); + // info->display->register_drawhud_action(this); util::Path shader_dir = info->asset_dir / "shaders"; diff --git a/libopenage/legacy_engine.cpp b/libopenage/legacy_engine.cpp index af5480032b..8b972316d1 100644 --- a/libopenage/legacy_engine.cpp +++ b/libopenage/legacy_engine.cpp @@ -13,7 +13,6 @@ #include "config.h" #include "error/error.h" #include "log/log.h" -#include "presenter/legacy/legacy.h" #include "version.h" #include "versions/compiletime.h" @@ -30,11 +29,6 @@ LegacyEngine::LegacyEngine(enum mode mode, cvar_manager{cvar_manager}, profiler{this}, gui_link{} { - if (mode == mode::LEGACY) { - this->old_display = std::make_unique(root_dir, this); - return; - } - // TODO: implement FULL and HEADLESS mode :) } @@ -47,10 +41,6 @@ void LegacyEngine::run() { this->job_manager.start(); this->running = true; - if (this->run_mode == mode::LEGACY) { - this->old_display->loop(); - } - this->running = false; } catch (...) { diff --git a/libopenage/legacy_engine.h b/libopenage/legacy_engine.h index bc8b187dff..532a5e17d1 100644 --- a/libopenage/legacy_engine.h +++ b/libopenage/legacy_engine.h @@ -37,11 +37,6 @@ namespace gui { class GuiItemLink; } // namespace gui -namespace presenter { -class LegacyDisplay; -} // namespace presenter - - /** * Qt signals for the engine. */ @@ -201,12 +196,6 @@ class LegacyEngine final { */ std::unique_ptr logsink_file; - /** - * old, deprecated, and initial implementation of the renderer and game simulation. - * TODO: remove - */ - std::unique_ptr old_display; - public: /** * Signal emitting capability for the engine. diff --git a/libopenage/presenter/CMakeLists.txt b/libopenage/presenter/CMakeLists.txt index 5041bacc2c..5a824fab5a 100644 --- a/libopenage/presenter/CMakeLists.txt +++ b/libopenage/presenter/CMakeLists.txt @@ -1,5 +1,3 @@ add_sources(libopenage presenter.cpp ) - -add_subdirectory("legacy") diff --git a/libopenage/presenter/legacy/CMakeLists.txt b/libopenage/presenter/legacy/CMakeLists.txt deleted file mode 100644 index 0c5905d0b5..0000000000 --- a/libopenage/presenter/legacy/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -add_sources(libopenage - legacy.cpp -) - -pxdgen( - legacy.h -) diff --git a/libopenage/presenter/legacy/legacy.cpp b/libopenage/presenter/legacy/legacy.cpp deleted file mode 100644 index a959b1da35..0000000000 --- a/libopenage/presenter/legacy/legacy.cpp +++ /dev/null @@ -1,550 +0,0 @@ -// Copyright 2020-2023 the openage authors. See copying.md for legal info. - -#include "legacy.h" -#include - -#include "../../config.h" -#include "../../error/error.h" -#include "../../gamestate/old/game_main.h" -#include "../../gamestate/old/generator.h" -#include "../../gui/gui.h" -#include "../../log/log.h" -#include "../../texture.h" -#include "../../unit/selection.h" -#include "../../util/color.h" -#include "../../util/fps.h" -#include "../../util/opengl.h" -#include "../../util/strings.h" -#include "../../util/timer.h" -#include "../../version.h" -#include "legacy_engine.h" - -namespace openage::presenter { - - -LegacyDisplay::LegacyDisplay(const util::Path &path, LegacyEngine *engine) : - OptionNode{"Engine"}, - drawing_debug_overlay{this, "drawing_debug_overlay", false}, - drawing_huds{this, "drawing_huds", true}, - screenshot_manager{engine->get_job_manager()}, - action_manager{&this->input_manager, std::shared_ptr(engine->get_cvar_manager())}, - audio_manager{engine->get_job_manager()}, - input_manager{&this->action_manager} { - // TODO get this from config system (cvar) - int32_t fps_limit = 0; - if (fps_limit > 0) { - this->ns_per_frame = 1e9 / fps_limit; - } - else { - this->ns_per_frame = 0; - } - - this->font_manager = std::make_unique(); - for (uint32_t size : {12, 14, 20}) { - fonts[size] = this->font_manager->get_font("DejaVu Serif", "Book", size); - } - - // enqueue the engine's own input handler to the - // execution list. - this->register_resize_action(this); - - if (SDL_Init(SDL_INIT_VIDEO) < 0) { - throw Error(MSG(err) << "SDL video initialization: " << SDL_GetError()); - } - else { - log::log(MSG(info) << "Initialized SDL video subsystems."); - } - - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); - SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1); - SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); - SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); - - int32_t window_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_MAXIMIZED; - this->window = SDL_CreateWindow( - "openage", - SDL_WINDOWPOS_CENTERED, - SDL_WINDOWPOS_CENTERED, - this->coord.viewport_size.x, - this->coord.viewport_size.y, - window_flags); - - if (this->window == nullptr) { - throw Error(MSG(err) << "Failed to create SDL window: " << SDL_GetError()); - } - - // load support for the PNG image formats, jpg bit: IMG_INIT_JPG - int wanted_image_formats = IMG_INIT_PNG; - int sdlimg_inited = IMG_Init(wanted_image_formats); - if ((sdlimg_inited & wanted_image_formats) != wanted_image_formats) { - throw Error(MSG(err) << "Failed to init PNG support: " << IMG_GetError()); - } - - if (false) { - this->glcontext = error::create_debug_context(this->window); - } - else { - this->glcontext = SDL_GL_CreateContext(this->window); - } - - if (this->glcontext == nullptr) { - throw Error(MSG(err) << "Failed creating OpenGL context: " << SDL_GetError()); - } - - // check the OpenGL version, for shaders n stuff - if (!epoxy_is_desktop_gl() || epoxy_gl_version() < 21) { - throw Error(MSG(err) << "OpenGL 2.1 not available"); - } - - // to quote the standard doc: - // 'The value gives a rough estimate - // of the largest texture that the GL can handle' - // -> wat? - // anyways, we need at least 1024x1024. - int max_texture_size; - glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size); - log::log(MSG(dbg) << "Maximum supported texture size: " << max_texture_size); - if (max_texture_size < 1024) { - throw Error(MSG(err) << "Maximum supported texture size too small: " << max_texture_size); - } - - int max_texture_units; - glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &max_texture_units); - log::log(MSG(dbg) << "Maximum supported texture units: " << max_texture_units); - if (max_texture_units < 2) { - throw Error(MSG(err) << "Your GPU has not enough texture units: " << max_texture_units); - } - - // vsync on - SDL_GL_SetSwapInterval(1); - - // enable alpha blending - glEnable(GL_BLEND); - - // order of drawing relevant for depth - // what gets drawn last is displayed on top. - glDisable(GL_DEPTH_TEST); - - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - //// -- initialize the gui - util::Path qml_root = engine->get_root_dir() / "assets" / "qml"; - if (not qml_root.is_dir()) { - throw Error{ERR << "could not find qml root folder " << qml_root}; - } - - util::Path qml_root_file = qml_root / "main.qml"; - if (not qml_root_file.is_file()) { - throw Error{ERR << "could not find main.qml file " << qml_root_file}; - } - - // TODO: in order to support qml-mods, the fslike and filelike - // library has to be integrated into qt. For now, - // figure out the absolute paths here and pass them in. - - std::string qml_root_str = qml_root.resolve_native_path(); - std::string qml_root_file_str = qml_root_file.resolve_native_path(); - - this->gui = std::make_unique( - this->window, // sdl window for the gui - qml_root_file_str, // entry qml file, absolute path. - qml_root_str // directory to watch for qml file changes - // &engine->get_qml_info() // qml data: Engine *, the data directory, ... - ); - //// -- gui initialization - - - // register the engines input manager - this->register_input_action(&this->input_manager); - - - // initialize engine related global keybinds - auto &global_input_context = this->get_input_manager().get_global_context(); - - input::legacy::ActionManager &action = this->get_action_manager(); - global_input_context.bind(action.get("STOP_GAME"), [engine](const input::legacy::action_arg_t &) { - engine->stop(); - }); - global_input_context.bind(action.get("TOGGLE_HUD"), [this](const input::legacy::action_arg_t &) { - this->drawing_huds.value = !this->drawing_huds.value; - }); - global_input_context.bind(action.get("SCREENSHOT"), [this](const input::legacy::action_arg_t &) { - this->get_screenshot_manager().save_screenshot(this->coord.viewport_size); - }); - global_input_context.bind(action.get("TOGGLE_DEBUG_OVERLAY"), [this](const input::legacy::action_arg_t &) { - this->drawing_debug_overlay.value = !this->drawing_debug_overlay.value; - }); - global_input_context.bind(action.get("TOGGLE_PROFILER"), [engine](const input::legacy::action_arg_t &) { - if (engine->external_profiler.currently_profiling) { - engine->external_profiler.stop(); - engine->external_profiler.show_results(); - } - else { - engine->external_profiler.start(); - } - }); - global_input_context.bind(input::legacy::event_class::MOUSE, [this](const input::legacy::action_arg_t &arg) { - if (arg.e.cc.has_class(input::legacy::event_class::MOUSE_MOTION) && this->get_input_manager().is_down(input::legacy::event_class::MOUSE_BUTTON, 2)) { - this->move_phys_camera(arg.motion.x, arg.motion.y); - return true; - } - return false; - }); - - this->text_renderer = std::make_unique(); - this->unit_selection = std::make_unique(engine); -} - - -LegacyDisplay::~LegacyDisplay() { - // deallocate the gui system - // this looses the opengl context in the qtsdl::GuiRenderer - // deallocation (of the QtOpenGLContext). - // so no gl* functions will be called after the gl context is gone. - this->gui.reset(nullptr); - - SDL_GL_DeleteContext(this->glcontext); - SDL_DestroyWindow(this->window); - IMG_Quit(); - SDL_Quit(); -} - - -bool LegacyDisplay::draw_debug_overlay() { - // Draw FPS counter in the lower right corner - this->render_text( - {this->coord.viewport_size.x - 70, 15}, - 20, - renderer::Colors::WHITE, - "%.0f fps", - this->fps_counter.display_fps); - - // Draw version string in the lower left corner - this->render_text( - {5, 35}, - 20, - renderer::Colors::WHITE, - "openage %s", - version::version); - this->render_text( - {5, 15}, - 12, - renderer::Colors::WHITE, - "%s", - config::config_option_string); - - // this->profiler.show(true); - - return true; -} - - -void LegacyDisplay::register_input_action(InputHandler *handler) { - this->on_input_event.push_back(handler); -} - - -void LegacyDisplay::register_tick_action(TickHandler *handler) { - this->on_engine_tick.push_back(handler); -} - - -void LegacyDisplay::register_drawhud_action(HudHandler *handler, int order) { - if (order < 0) { - this->on_drawhud.insert(this->on_drawhud.begin(), handler); - } - else { - this->on_drawhud.push_back(handler); - } -} - - -void LegacyDisplay::register_draw_action(DrawHandler *handler) { - this->on_drawgame.push_back(handler); -} - - -void LegacyDisplay::register_resize_action(ResizeHandler *handler) { - this->on_resize_handler.push_back(handler); -} - - -bool LegacyDisplay::on_resize(coord::viewport_delta new_size) { - // TODO: move all this to the renderer! - - log::log(MSG(dbg) << "engine window resize to " - << new_size.x << "x" << new_size.y); - - // update engine window size - this->coord.viewport_size = new_size; - - // update camgame window position, set it to center. - this->coord.camgame_viewport = coord::viewport{0, 0} + (this->coord.viewport_size / 2); - - // update camhud window position, set to lower left corner - this->coord.camhud_viewport = {0, coord::pixel_t{this->coord.viewport_size.y}}; - - // reset previous projection matrix - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - - // update OpenGL viewport: the rendering area - glViewport(0, 0, this->coord.viewport_size.x, this->coord.viewport_size.y); - - // set orthographic projection: left, right, bottom, top, near_val, far_val - glOrtho(0, this->coord.viewport_size.x, 0, this->coord.viewport_size.y, 9001, -1); - - // reset the modelview matrix - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - - return true; -} - - -renderer::TextRenderer *LegacyDisplay::get_text_renderer() { - return this->text_renderer.get(); -} - - -time_nsec_t LegacyDisplay::lastframe_duration_nsec() const { - return this->fps_counter.nsec_lastframe; -} - - -void LegacyDisplay::announce_global_binds() { - // emit this->gui_signals.global_binds_changed( - // this->get_input_manager().get_global_context().active_binds()); -} - - -UnitSelection *LegacyDisplay::get_unit_selection() { - return this->unit_selection.get(); -} - - -void LegacyDisplay::render_text(coord::viewport position, size_t size, const renderer::Color &color, const char *format, ...) { - auto it = this->fonts.find(size); - if (it == this->fonts.end()) { - throw Error(MSG(err) << "Unknown font size requested: " << size); - } - - renderer::Font *font = it->second; - - std::string buf; - va_list vl; - va_start(vl, format); - util::vsformat(format, vl, buf); - va_end(vl); - - this->text_renderer->set_font(font); - this->text_renderer->set_color(color); - this->text_renderer->draw(position.x, position.y, buf); -} - - -void LegacyDisplay::move_phys_camera(float x, float y, float amount) { - // calculate camera position delta from velocity and frame duration - coord::camgame_delta cam_delta{ - static_cast(+x * amount), - static_cast(-y * amount)}; - - // update camera phys position - this->coord.camgame_phys += cam_delta.to_phys3(this->coord, 0); -} - - -GameMain *LegacyDisplay::get_game() { - return this->game.get(); -} - - -void LegacyDisplay::start_game(std::unique_ptr &&game) { - // TODO: maybe implement a proper 1-to-1 connection - ENSURE(game, "linking game to engine problem"); - - this->game = std::move(game); - this->game->set_parent(this); -} - - -void LegacyDisplay::start_game(const Generator &generator) { - this->game = std::make_unique(generator); - this->game->set_parent(this); -} - - -void LegacyDisplay::end_game() { - this->game = nullptr; - this->unit_selection->clear(); -} - - -void LegacyDisplay::loop() { - //SDL_Event event; - //util::Timer cap_timer; - // - //while (this->running) { - // this->profiler.start_frame_measure(); - // this->fps_counter.frame(); - // cap_timer.reset(false); - // - // this->job_manager.execute_callbacks(); - // - // this->profiler.start_measure("events", {1.0, 0.0, 0.0}); - // // top level input handling - // while (SDL_PollEvent(&event)) { - // switch (event.type) { - // case SDL_QUIT: - // this->stop(); - // break; - // - // case SDL_WINDOWEVENT: { - // if (event.window.event == SDL_WINDOWEVENT_RESIZED) { - // coord::viewport_delta new_size{event.window.data1, event.window.data2}; - // - // // call additional handlers for the resize event - // for (auto &handler : on_resize_handler) { - // if (!handler->on_resize(new_size)) { - // break; - // } - // } - // } - // break; - // } - // - // default: - // for (auto &action : this->on_input_event) { - // if (false == action->on_input(&event)) { - // break; - // } - // } - // } // switch event - // } - // - // // here, call to Qt and process all the gui events. - // this->gui->process_events(); - // - // if (this->game) { - // // read camera movement input keys and/or cursor location, - // // and move camera accordingly. - // - // // camera movement speed, in pixels per millisecond - // // one pixel per millisecond equals 14.3 tiles/second - // float mov_x = 0.0, mov_y = 0.0, cam_movement_speed_keyboard = 0.5; - // Uint32 win_flags = SDL_GetWindowFlags(this->window); - // bool inp_focus = win_flags & SDL_WINDOW_INPUT_FOCUS; - // - // input::InputManager &input = this->get_input_manager(); - // using Edge = input::InputManager::Edge; - // - // if (inp_focus and (input.is_down(SDLK_LEFT) or input.is_mouse_at_edge(Edge::LEFT, coord.viewport_size.x))) { - // mov_x = -cam_movement_speed_keyboard; - // } - // if (inp_focus and (input.is_down(SDLK_RIGHT) or input.is_mouse_at_edge(Edge::RIGHT, coord.viewport_size.x))) { - // mov_x = cam_movement_speed_keyboard; - // } - // if (inp_focus and (input.is_down(SDLK_DOWN) or input.is_mouse_at_edge(Edge::DOWN, coord.viewport_size.y))) { - // mov_y = cam_movement_speed_keyboard; - // } - // if (inp_focus and (input.is_down(SDLK_UP) or input.is_mouse_at_edge(Edge::UP, coord.viewport_size.y))) { - // mov_y = -cam_movement_speed_keyboard; - // } - // - // // perform camera movement - // this->move_phys_camera( - // mov_x, - // mov_y, - // static_cast(this->lastframe_duration_nsec()) / 1e6); - // - // // update the currently running game - // this->game->update(this->lastframe_duration_nsec()); - // } - // this->profiler.end_measure("events"); - // - // // call engine tick callback methods - // for (auto &action : this->on_engine_tick) { - // if (false == action->on_tick()) { - // break; - // } - // } - // - // this->profiler.start_measure("rendering", {0.0, 1.0, 0.0}); - // - // // clear the framebuffer to black - // // in the future, we might disable it for lazy drawing - // glClearColor(0.0, 0.0, 0.0, 0.0); - // glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - // - // // invoke all game drawing handlers - // for (auto &action : this->on_drawgame) { - // if (false == action->on_draw()) { - // break; - // } - // } - // - // util::gl_check_error(); - // - // // draw the fps overlay - // if (this->drawing_debug_overlay.value) { - // this->draw_debug_overlay(); - // } - // - // // invoke all hud drawing callback methods - // if (this->drawing_huds.value) { - // for (auto &action : this->on_drawhud) { - // if (false == action->on_drawhud()) { - // break; - // } - // } - // } - // - // this->text_renderer->render(); - // - // util::gl_check_error(); - // - // this->profiler.end_measure("rendering"); - // - // this->profiler.start_measure("idle", {0.0, 0.0, 1.0}); - // - // // the rendering is done - // // swap the drawing buffers to actually show the frame - // SDL_GL_SwapWindow(window); - // - // if (this->ns_per_frame != 0) { - // uint64_t ns_for_current_frame = cap_timer.getval(); - // if (ns_for_current_frame < this->ns_per_frame) { - // SDL_Delay((this->ns_per_frame - ns_for_current_frame) / 1e6); - // } - // } - // - // this->profiler.end_measure("idle"); - // - // this->profiler.end_frame_measure(); - //} -} - - -audio::AudioManager &LegacyDisplay::get_audio_manager() { - return this->audio_manager; -} - - -ScreenshotManager &LegacyDisplay::get_screenshot_manager() { - return this->screenshot_manager; -} - - -input::legacy::ActionManager &LegacyDisplay::get_action_manager() { - return this->action_manager; -} - - -input::legacy::InputManager &LegacyDisplay::get_input_manager() { - return this->input_manager; -} - - -} // namespace openage::presenter diff --git a/libopenage/presenter/legacy/legacy.h b/libopenage/presenter/legacy/legacy.h deleted file mode 100644 index 9ee18ab1c9..0000000000 --- a/libopenage/presenter/legacy/legacy.h +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright 2020-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include "../../audio/audio_manager.h" -// pxd: from libopenage.coord.coordmanager cimport CoordManager -#include "../../coord/coordmanager.h" -#include "../../error/gl_debug.h" -#include "../../handlers.h" -// pxd: from libopenage.input.input_manager cimport InputManager -#include "../../input/legacy/input_manager.h" -#include "../../options.h" -#include "../../renderer/font/font.h" -#include "../../renderer/font/font_manager.h" -#include "../../renderer/text.h" -#include "../../screenshot.h" -#include "../../util/fps.h" -#include "../../util/path.h" -#include "../../util/timing.h" - - -namespace openage { - -class DrawHandler; -class TickHandler; -class ResizeHandler; - -class Generator; -class GameSpec; -class GameMain; -class UnitSelection; - -namespace gui { -class GUI; -} // namespace gui - -namespace renderer { - -class Font; -class FontManager; -class TextRenderer; -class Color; - -} // namespace renderer - - -namespace presenter { - -/** - * Temporary container class for the legacy renderer implementation. - */ -class [[deprecated]] LegacyDisplay final : public ResizeHandler - , public options::OptionNode { -public: - LegacyDisplay(const util::Path &path, LegacyEngine *engine); - - ~LegacyDisplay(); - - /** - * Draw the game version and the current FPS on screen. - */ - bool draw_debug_overlay(); - - /** - * register a new input event handler, run for each input event. - */ - void register_input_action(InputHandler *handler); - - /** - * register a tick action, executed upon engine tick. - */ - void register_tick_action(TickHandler *handler); - - /** - * register a hud drawing handler, drawn in hud coordinates. - * order: 1 above, -1 below - */ - void register_drawhud_action(HudHandler *handler, int order = 1); - - /** - * register a draw handler, run in game coordinates. - */ - void register_draw_action(DrawHandler *handler); - - /** - * register a resize handler, run when the window size changes. - */ - void register_resize_action(ResizeHandler *handler); - - /** - * window resize handler function. - * recalculates opengl settings like viewport and projection matrices. - */ - bool on_resize(coord::viewport_delta new_size) override; - - /** - * return this engine's text renderer. - */ - renderer::TextRenderer *get_text_renderer(); - - /** - * return the number of nanoseconds that have passed - * for rendering the last frame. - * - * use that for fps-independent input actions. - */ - time_nsec_t lastframe_duration_nsec() const; - - /** - * send keybindings help string to gui. - */ - void announce_global_binds(); - - /** - * return this engine's unit selection. - */ - UnitSelection *get_unit_selection(); - - /** - * render text at a position with the specified font size - */ - void render_text(coord::viewport position, size_t size, const renderer::Color &color, const char *format, ...) ATTRIBUTE_FORMAT(5, 6); - - /** - * move the phys3 camera incorporated in the engine - */ - void move_phys_camera(float x, float y, float amount = 1.0); - - /** - * Start a game with the given game generator. - */ - void start_game(const Generator &generator); - - /** - * Start a game with the given initialized game. - */ - void start_game(std::unique_ptr &&game); - - /** - * Stop the running game. - */ - void end_game(); - - /** - * return currently running game or null if a game is not - * currently running - */ - GameMain *get_game(); - - /** - * legacy engine loop function. - * this will be looped once per frame when the game is running. - * - * the loop invokes fps counting, SDL event handling, - * view translation, and calling the main draw_method. - */ - void loop(); - -public: - /** - * return this engine's audio manager. - */ - audio::AudioManager &get_audio_manager(); - - /** - * return this engine's screenshot manager. - */ - ScreenshotManager &get_screenshot_manager(); - - /** - * return this engine's action manager. - */ - input::legacy::ActionManager &get_action_manager(); - - /** - * return this engine's keybind manager. - */ - input::legacy::InputManager &get_input_manager(); - - /** - * FPS and game version are drawn when this is true. - */ - options::Var drawing_debug_overlay; - - /** - * this allows to disable drawing of every registered hud. - */ - options::Var drawing_huds; - - /** - * The coordinate state. - */ - coord::CoordManager coord; - -private: - /** - * the currently running game - */ - std::unique_ptr game; - - /** - * how many nanoseconds are in a frame (1e9 / fps_limit). - * 0 if there is no fps limit. - */ - time_nsec_t ns_per_frame; - - /** - * input event processor objects. - * called for each captured sdl input event. - */ - std::vector on_input_event; - - /** - * run every time the game is being drawn, - * with the renderer set to the camgame system - */ - std::vector on_drawgame; - - /** - * run every time the hud is being drawn, - * with the renderer set to the camhud system - */ - std::vector on_drawhud; - - /** - * list of handlers that are executed upon a resize event. - */ - std::vector on_resize_handler; - - /** - * run on every engine tick, after input handling, before rendering - */ - std::vector on_engine_tick; - - /** - * the frame counter measuring fps. - */ - util::FrameCounter fps_counter; - - /** - * the engine's screenshot manager. - */ - ScreenshotManager screenshot_manager; - - /** - * the engine's action manager. - */ - input::legacy::ActionManager action_manager; - - /** - * the engine's audio manager. - */ - audio::AudioManager audio_manager; - - /** - * the engine's keybind manager. - */ - input::legacy::InputManager input_manager; - - /** - * the engine's unit selection. - */ - std::unique_ptr unit_selection; - - /** - * the text fonts to be used for (can you believe it?) texts. - * maps fontsize -> font - */ - std::unordered_map fonts; - - /** - * SDL window where everything is displayed within. - */ - SDL_Window *window; - - /** - * SDL OpenGL context, we'll only have one, - * but it would allow having multiple ones. - * - * This is actually a void * but sdl2 thought it was a good idea to - * name it like a differently. - */ - SDL_GLContext glcontext; - - /** - * Qt GUI system - */ - std::unique_ptr gui; - - /** - * ttf font loading manager - */ - std::unique_ptr font_manager; - - /** - * 2d text renderer - */ - std::unique_ptr text_renderer; -}; - -} // namespace presenter -} // namespace openage diff --git a/libopenage/terrain/terrain.cpp b/libopenage/terrain/terrain.cpp index a360878984..325db0a5d5 100644 --- a/libopenage/terrain/terrain.cpp +++ b/libopenage/terrain/terrain.cpp @@ -271,55 +271,6 @@ bool Terrain::check_tile_position(const coord::tile & /*pos*/) { } } -void Terrain::draw(presenter::LegacyDisplay *display, RenderOptions *settings) { - // TODO: move this draw invokation to a render manager. - // it can reorder the draw instructions and minimize texture switching. - - // query the window coordinates from the display first - coord::viewport wbl = coord::viewport{0, 0}; - coord::viewport wbr = coord::viewport{display->coord.viewport_size.x, 0}; - coord::viewport wtl = coord::viewport{0, display->coord.viewport_size.y}; - coord::viewport wtr = coord::viewport{display->coord.viewport_size.x, display->coord.viewport_size.y}; - - // top left, bottom right tile coordinates - // that are currently visible in the window - // then convert them to tile coordinates. - coord::tile tl = wtl.to_tile(display->coord); - coord::tile tr = wtr.to_tile(display->coord); - coord::tile bl = wbl.to_tile(display->coord); - coord::tile br = wbr.to_tile(display->coord); - - // main terrain calculation call: get the `terrain_render_data` - auto draw_data = this->create_draw_advice(tl, tr, br, bl, true); - - // TODO: the following loop is totally inefficient and shit. - // it reloads the drawing texture to the gpu FOR EACH TILE! - // nevertheless, currently it works. - - // draw the terrain ground - for (auto &tile : draw_data.tiles) { - // iterate over all layers to be drawn - for (int i = 0; i < tile.count; i++) { - struct tile_data *layer = &tile.data[i]; - - // position, where the tile is drawn - coord::tile tile_pos = layer->pos; - - int mask_id = layer->mask_id; - Texture *texture = layer->tex; - int subtexture_id = layer->subtexture_id; - Texture *mask_texture = layer->mask_tex; - - texture->draw(display->coord, *this, tile_pos, ALPHAMASKED, subtexture_id, mask_texture, mask_id); - } - } - - // TODO: drawing buildings can't be the job of the terrain.. - // draw the buildings - for (auto &object : draw_data.objects) { - // object->draw(*display); - } -} struct terrain_render_data Terrain::create_draw_advice(const coord::tile &ab, const coord::tile &cd, diff --git a/libopenage/terrain/terrain.h b/libopenage/terrain/terrain.h index c33cd43dee..fcde460ad7 100644 --- a/libopenage/terrain/terrain.h +++ b/libopenage/terrain/terrain.h @@ -13,7 +13,6 @@ #include "../coord/phys.h" #include "../coord/pixel.h" #include "../coord/tile.h" -#include "../presenter/legacy/legacy.h" #include "../texture.h" #include "../util/misc.h" @@ -323,12 +322,6 @@ class Terrain { */ int get_blending_mode(terrain_t base_id, terrain_t neighbor_id); - /** - * draw the currently visible terrain area on screen. - * @param display: the display where the terrain should be drawn to. - */ - void draw(presenter::LegacyDisplay *display, RenderOptions *settings); - /** * create the drawing instruction data. * From 5236d44bea6b651a5b71e7c615cbe5e68493b26b Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Sep 2023 01:43:03 +0200 Subject: [PATCH 031/771] renderer: Remove old texture code. --- libopenage/CMakeLists.txt | 1 - libopenage/game_renderer.cpp | 210 -------- libopenage/gamestate/old/game_spec.cpp | 25 - libopenage/gamestate/old/game_spec.h | 10 - libopenage/gui/gui.cpp | 13 +- libopenage/gui/gui.h | 4 - .../gui/integration/private/CMakeLists.txt | 4 - ...e_spec_image_provider_by_filename_impl.cpp | 50 -- ...ame_spec_image_provider_by_filename_impl.h | 27 -- ...spec_image_provider_by_graphic_id_impl.cpp | 31 -- ...e_spec_image_provider_by_graphic_id_impl.h | 27 -- ...ui_game_spec_image_provider_by_id_impl.cpp | 45 -- .../gui_game_spec_image_provider_by_id_impl.h | 25 - ...spec_image_provider_by_terrain_id_impl.cpp | 32 -- ...e_spec_image_provider_by_terrain_id_impl.h | 27 -- .../gui_game_spec_image_provider_impl.cpp | 32 +- .../gui_game_spec_image_provider_impl.h | 21 +- .../private/gui_image_provider_link.cpp | 40 +- .../private/gui_image_provider_link.h | 14 +- .../gui/integration/private/gui_texture.cpp | 38 +- .../private/gui_texture_factory.cpp | 13 +- .../private/gui_texture_handle.cpp | 28 +- .../integration/private/gui_texture_handle.h | 8 +- .../gui/integration/public/CMakeLists.txt | 1 - .../public/gui_game_spec_image_provider.cpp | 37 -- .../public/gui_game_spec_image_provider.h | 26 - libopenage/pathfinding/path.cpp | 47 +- libopenage/pathfinding/path.h | 40 +- libopenage/renderer/gui/gui.h | 3 - libopenage/shader/program.h | 7 +- libopenage/shader/shader.h | 9 +- libopenage/terrain/CMakeLists.txt | 1 - libopenage/terrain/terrain.cpp | 30 +- libopenage/terrain/terrain.h | 16 - libopenage/terrain/terrain_chunk.cpp | 1 - libopenage/terrain/terrain_chunk.h | 3 +- libopenage/terrain/terrain_object.cpp | 16 - libopenage/terrain/terrain_object.h | 13 - libopenage/terrain/terrain_outline.cpp | 60 --- libopenage/terrain/terrain_outline.h | 23 - libopenage/texture.cpp | 447 ------------------ libopenage/texture.h | 188 -------- libopenage/unit/producer.cpp | 33 +- libopenage/unit/producer.h | 17 +- libopenage/unit/unit_texture.cpp | 122 +---- libopenage/unit/unit_texture.h | 34 +- 46 files changed, 127 insertions(+), 1772 deletions(-) delete mode 100644 libopenage/game_renderer.cpp delete mode 100644 libopenage/gui/integration/private/gui_game_spec_image_provider_by_filename_impl.cpp delete mode 100644 libopenage/gui/integration/private/gui_game_spec_image_provider_by_filename_impl.h delete mode 100644 libopenage/gui/integration/private/gui_game_spec_image_provider_by_graphic_id_impl.cpp delete mode 100644 libopenage/gui/integration/private/gui_game_spec_image_provider_by_graphic_id_impl.h delete mode 100644 libopenage/gui/integration/private/gui_game_spec_image_provider_by_id_impl.cpp delete mode 100644 libopenage/gui/integration/private/gui_game_spec_image_provider_by_id_impl.h delete mode 100644 libopenage/gui/integration/private/gui_game_spec_image_provider_by_terrain_id_impl.cpp delete mode 100644 libopenage/gui/integration/private/gui_game_spec_image_provider_by_terrain_id_impl.h delete mode 100644 libopenage/gui/integration/public/gui_game_spec_image_provider.cpp delete mode 100644 libopenage/gui/integration/public/gui_game_spec_image_provider.h delete mode 100644 libopenage/terrain/terrain_outline.cpp delete mode 100644 libopenage/terrain/terrain_outline.h delete mode 100644 libopenage/texture.cpp delete mode 100644 libopenage/texture.h diff --git a/libopenage/CMakeLists.txt b/libopenage/CMakeLists.txt index f1772bfc36..8c17eaa9d5 100644 --- a/libopenage/CMakeLists.txt +++ b/libopenage/CMakeLists.txt @@ -327,7 +327,6 @@ add_sources(libopenage main.cpp options.cpp screenshot.cpp - texture.cpp ${CMAKE_CURRENT_BINARY_DIR}/config.cpp ${CMAKE_CURRENT_BINARY_DIR}/version.cpp ${CODEGEN_SCU_FILE} diff --git a/libopenage/game_renderer.cpp b/libopenage/game_renderer.cpp deleted file mode 100644 index 530d87ad92..0000000000 --- a/libopenage/game_renderer.cpp +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include -#include -#include -#include -#include - -#include "console/console.h" -#include "game_renderer.h" -#include "gamedata/color_dummy.h" -#include "gamestate/old/game_main.h" -#include "gamestate/old/game_spec.h" -#include "input/input_manager.h" -#include "legacy_engine.h" -#include "log/log.h" -#include "renderer/text.h" -#include "terrain/terrain.h" -#include "unit/action.h" -#include "unit/command.h" -#include "unit/producer.h" -#include "unit/unit.h" -#include "unit/unit_texture.h" -#include "util/externalprofiler.h" -#include "util/timer.h" - -namespace openage { - -RenderOptions::RenderOptions() : - OptionNode{"RendererOptions"}, - draw_debug{this, "draw_debug", false}, - terrain_blending{this, "terrain_blending", true} {} - -GameRenderer::GameRenderer(LegacyEngine *e) : - engine{e} { - // set options structure - this->settings.set_parent(this->engine); - - // engine callbacks - this->engine->register_draw_action(this); - - // fetch asset loading dir - util::Path asset_dir = engine->get_root_dir()["assets"]; - - // load textures and stuff - gaben = new Texture{asset_dir["test"]["textures"]["gaben.png"]}; - - std::vector player_color_lines = util::read_csv_file( - asset_dir["converted/player_palette.docx"]); - - std::unique_ptr playercolors = std::make_unique(player_color_lines.size() * 4); - for (size_t i = 0; i < player_color_lines.size(); i++) { - auto line = &player_color_lines[i]; - playercolors[i * 4] = line->r / 255.0; - playercolors[i * 4 + 1] = line->g / 255.0; - playercolors[i * 4 + 2] = line->b / 255.0; - playercolors[i * 4 + 3] = line->a / 255.0; - } - - // shader initialisation - // read shader source codes and create shader objects for wrapping them. - const char *shader_header_code = "#version 120\n"; - std::string equals_epsilon_code = asset_dir["shaders/equalsEpsilon.glsl"].open().read(); - std::string texture_vert_code = asset_dir["shaders/maptexture.vert.glsl"].open().read(); - auto plaintexture_vert = std::make_unique( - GL_VERTEX_SHADER, - std::initializer_list{shader_header_code, texture_vert_code.c_str()}); - - std::string texture_frag_code = asset_dir["shaders/maptexture.frag.glsl"].open().read(); - auto plaintexture_frag = std::make_unique( - GL_FRAGMENT_SHADER, - std::initializer_list{shader_header_code, texture_frag_code.c_str()}); - - std::string teamcolor_frag_code = asset_dir["shaders/teamcolors.frag.glsl"].open().read(); - std::stringstream ss; - ss << player_color_lines.size(); - auto teamcolor_frag = std::make_unique( - GL_FRAGMENT_SHADER, - std::initializer_list{ - shader_header_code, - ("#define NUM_OF_PLAYER_COLORS " + ss.str() + "\n").c_str(), - equals_epsilon_code.c_str(), - teamcolor_frag_code.c_str()}); - - std::string alphamask_vert_code = asset_dir["shaders/alphamask.vert.glsl"].open().read(); - auto alphamask_vert = std::make_unique( - GL_VERTEX_SHADER, - std::initializer_list{shader_header_code, alphamask_vert_code.c_str()}); - - std::string alphamask_frag_code = asset_dir["shaders/alphamask.frag.glsl"].open().read(); - auto alphamask_frag = std::make_unique( - GL_FRAGMENT_SHADER, - std::initializer_list{shader_header_code, alphamask_frag_code.c_str()}); - - std::string texturefont_vert_code = asset_dir["shaders/texturefont.vert.glsl"].open().read(); - auto texturefont_vert = std::make_unique( - GL_VERTEX_SHADER, - std::initializer_list{shader_header_code, texturefont_vert_code.c_str()}); - - std::string texturefont_frag_code = asset_dir["shaders/texturefont.frag.glsl"].open().read(); - auto texturefont_frag = std::make_unique( - GL_FRAGMENT_SHADER, - std::initializer_list{shader_header_code, texturefont_frag_code.c_str()}); - - // create program for rendering simple textures - texture_shader::program = new shader::Program(plaintexture_vert.get(), plaintexture_frag.get()); - texture_shader::program->link(); - texture_shader::texture = texture_shader::program->get_uniform_id("texture"); - texture_shader::tex_coord = texture_shader::program->get_attribute_id("tex_coordinates"); - texture_shader::program->use(); - glUniform1i(texture_shader::texture, 0); - texture_shader::program->stopusing(); - - - // create program for tinting textures at alpha-marked pixels - // with team colors - teamcolor_shader::program = new shader::Program(plaintexture_vert.get(), teamcolor_frag.get()); - teamcolor_shader::program->link(); - teamcolor_shader::texture = teamcolor_shader::program->get_uniform_id("texture"); - teamcolor_shader::tex_coord = teamcolor_shader::program->get_attribute_id("tex_coordinates"); - teamcolor_shader::player_id_var = teamcolor_shader::program->get_uniform_id("player_number"); - teamcolor_shader::alpha_marker_var = teamcolor_shader::program->get_uniform_id("alpha_marker"); - teamcolor_shader::player_color_var = teamcolor_shader::program->get_uniform_id("player_color"); - teamcolor_shader::program->use(); - glUniform1i(teamcolor_shader::texture, 0); - glUniform1f(teamcolor_shader::alpha_marker_var, 254.0 / 255.0); - // fill the teamcolor shader's player color table: - glUniform4fv(teamcolor_shader::player_color_var, 64, playercolors.get()); - teamcolor_shader::program->stopusing(); - - - // create program for drawing textures that are alpha-masked before - alphamask_shader::program = new shader::Program(alphamask_vert.get(), alphamask_frag.get()); - alphamask_shader::program->link(); - alphamask_shader::base_coord = alphamask_shader::program->get_attribute_id("base_tex_coordinates"); - alphamask_shader::mask_coord = alphamask_shader::program->get_attribute_id("mask_tex_coordinates"); - alphamask_shader::show_mask = alphamask_shader::program->get_uniform_id("show_mask"); - alphamask_shader::base_texture = alphamask_shader::program->get_uniform_id("base_texture"); - alphamask_shader::mask_texture = alphamask_shader::program->get_uniform_id("mask_texture"); - alphamask_shader::program->use(); - glUniform1i(alphamask_shader::base_texture, 0); - glUniform1i(alphamask_shader::mask_texture, 1); - alphamask_shader::program->stopusing(); - - // Create program for texture based font rendering - texturefont_shader::program = new shader::Program(texturefont_vert.get(), texturefont_frag.get()); - texturefont_shader::program->link(); - texturefont_shader::texture = texturefont_shader::program->get_uniform_id("texture"); - texturefont_shader::color = texturefont_shader::program->get_uniform_id("color"); - texturefont_shader::tex_coord = texturefont_shader::program->get_attribute_id("tex_coordinates"); - texturefont_shader::program->use(); - glUniform1i(texturefont_shader::texture, 0); - texturefont_shader::program->stopusing(); - - // Renderer keybinds - // TODO: a renderer settings struct - // would allow these to be put somewhere better - input::ActionManager &action = this->engine->get_action_manager(); - auto &global_input_context = engine->get_input_manager().get_global_context(); - global_input_context.bind(action.get("TOGGLE_BLENDING"), [this](const input::action_arg_t &) { - this->settings.terrain_blending.value = !this->settings.terrain_blending.value; - }); - global_input_context.bind(action.get("TOGGLE_UNIT_DEBUG"), [this](const input::action_arg_t &) { - this->settings.draw_debug.value = !this->settings.draw_debug.value; - - log::log(MSG(dbg) << "Toggle debug grid"); - - // TODO remove this hack, use render settings instead - UnitAction::show_debug = !UnitAction::show_debug; - }); - - log::log(MSG(dbg) << "Loaded Renderer"); -} - -GameRenderer::~GameRenderer() { - // oh noes, release hl3 before that! - delete this->gaben; - - delete texture_shader::program; - delete teamcolor_shader::program; - delete alphamask_shader::program; - delete texturefont_shader::program; -} - - -bool GameRenderer::on_draw() { - // draw terrain - GameMain *game = this->engine->get_game(); - - if (game) { - // draw gaben, our great and holy protector, bringer of the half-life 3. - gaben->draw(this->engine->coord, coord::camgame{0, 0}); - - // TODO move render code out of terrain - if (game->terrain) { - game->terrain->draw(this->engine, &this->settings); - } - } - return true; -} - -GameMain *GameRenderer::game() const { - return this->engine->get_game(); -} - -GameSpec *GameRenderer::game_spec() const { - return this->game()->get_spec(); -} - -} // namespace openage diff --git a/libopenage/gamestate/old/game_spec.cpp b/libopenage/gamestate/old/game_spec.cpp index 048fbc4ce9..d696b748f5 100644 --- a/libopenage/gamestate/old/game_spec.cpp +++ b/libopenage/gamestate/old/game_spec.cpp @@ -82,31 +82,6 @@ index_t GameSpec::get_slp_graphic(index_t slp) { return this->slp_to_graphic[slp]; } -Texture *GameSpec::get_texture(index_t graphic_id) const { - if (graphic_id <= 0 || this->graphics.count(graphic_id) == 0) { - log::log(MSG(dbg) << " -> ignoring graphics_id: " << graphic_id); - return nullptr; - } - - auto g = this->graphics.at(graphic_id); - int slp_id = g->slp_id; - if (slp_id <= 0) { - log::log(MSG(dbg) << " -> ignoring negative slp_id: " << slp_id); - return nullptr; - } - - log::log(MSG(dbg) << " slp id/name: " << slp_id << " " << g->name); - std::string tex_fname = util::sformat("converted/graphics/%d.slp.png", slp_id); - - return this->get_texture(tex_fname, true); -} - -Texture *GameSpec::get_texture(const std::string &file_name, bool use_metafile) const { - // return nullptr if the texture wasn't found (3rd param) - // return this->assetmanager->get_texture(file_name, use_metafile, true); - return nullptr; -} - std::shared_ptr GameSpec::get_unit_texture(index_t unit_id) const { if (this->unit_textures.count(unit_id) == 0) { if (unit_id > 0) { diff --git a/libopenage/gamestate/old/game_spec.h b/libopenage/gamestate/old/game_spec.h index 0fc34dcd40..b1377fd2d9 100644 --- a/libopenage/gamestate/old/game_spec.h +++ b/libopenage/gamestate/old/game_spec.h @@ -90,16 +90,6 @@ class GameSpec { */ index_t get_slp_graphic(index_t slp); - /** - * lookup using a texture id, this specifically avoids returning the missing placeholder texture - */ - Texture *get_texture(index_t graphic_id) const; - - /** - * lookup using a texture file name - */ - Texture *get_texture(const std::string &file_name, bool use_metafile = true) const; - /** * get unit texture by graphic id -- this is an directional texture * which also includes graphic deltas diff --git a/libopenage/gui/gui.cpp b/libopenage/gui/gui.cpp index ebaf20b24a..d8ffe9e3e2 100644 --- a/libopenage/gui/gui.cpp +++ b/libopenage/gui/gui.cpp @@ -23,20 +23,9 @@ GUI::GUI(SDL_Window *window, render_updater{}, renderer{window}, game_logic_updater{}, - image_provider_by_filename{ - &render_updater, - GuiGameSpecImageProvider::Type::ByFilename}, - image_provider_by_graphic_id{ - &render_updater, - GuiGameSpecImageProvider::Type::ByGraphicId}, - image_provider_by_terrain_id{ - &render_updater, - GuiGameSpecImageProvider::Type::ByTerrainId}, engine{ &renderer, - {&image_provider_by_filename, - &image_provider_by_graphic_id, - &image_provider_by_terrain_id}, + {}, info}, subtree{ &renderer, diff --git a/libopenage/gui/gui.h b/libopenage/gui/gui.h index 0abb70ff5e..d5adadef70 100644 --- a/libopenage/gui/gui.h +++ b/libopenage/gui/gui.h @@ -12,7 +12,6 @@ #include "guisys/public/gui_renderer.h" #include "guisys/public/gui_subtree.h" #include "integration/public/gui_application_with_logger.h" -#include "integration/public/gui_game_spec_image_provider.h" namespace qtsdl { @@ -58,9 +57,6 @@ class GUI : public InputHandler qtsdl::GuiEventQueue render_updater; qtsdl::GuiRenderer renderer; qtsdl::GuiEventQueue game_logic_updater; - GuiGameSpecImageProvider image_provider_by_filename; - GuiGameSpecImageProvider image_provider_by_graphic_id; - GuiGameSpecImageProvider image_provider_by_terrain_id; qtsdl::GuiEngine engine; qtsdl::GuiSubtree subtree; qtsdl::GuiInput input; diff --git a/libopenage/gui/integration/private/CMakeLists.txt b/libopenage/gui/integration/private/CMakeLists.txt index 97f03fc4f4..f727980b6f 100644 --- a/libopenage/gui/integration/private/CMakeLists.txt +++ b/libopenage/gui/integration/private/CMakeLists.txt @@ -1,9 +1,5 @@ add_sources(libopenage gui_filled_texture_handles.cpp - gui_game_spec_image_provider_by_filename_impl.cpp - gui_game_spec_image_provider_by_graphic_id_impl.cpp - gui_game_spec_image_provider_by_id_impl.cpp - gui_game_spec_image_provider_by_terrain_id_impl.cpp gui_game_spec_image_provider_impl.cpp gui_image_provider_link.cpp gui_log.cpp diff --git a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_filename_impl.cpp b/libopenage/gui/integration/private/gui_game_spec_image_provider_by_filename_impl.cpp deleted file mode 100644 index 216422e5ba..0000000000 --- a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_filename_impl.cpp +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. - -#include "gui_game_spec_image_provider_by_filename_impl.h" - -#include "../../../error/error.h" - -#include "../../../gamestate/old/game_spec.h" - -namespace openage::gui { - -GuiGameSpecImageProviderByFilenameImpl::GuiGameSpecImageProviderByFilenameImpl(qtsdl::GuiEventQueue *render_updater) - : - GuiGameSpecImageProviderImpl{render_updater} { -} - -GuiGameSpecImageProviderByFilenameImpl::~GuiGameSpecImageProviderByFilenameImpl() = default; - -const char* GuiGameSpecImageProviderByFilenameImpl::id() { - return "by-filename"; -} - -const char* GuiGameSpecImageProviderByFilenameImpl::get_id() const { - return GuiGameSpecImageProviderByFilenameImpl::id(); -} - -TextureHandle GuiGameSpecImageProviderByFilenameImpl::get_texture_handle(const QString &id) { - ENSURE(this->loaded_game_spec, "trying to actually get a texture from a non-loaded spec"); - - auto filename = id.section(".", 0, -2); - auto subid_str = id.section(".", -1, -1); - - bool ok = false; - const int subid = subid_str.toInt(&ok); - - if (not filename.isEmpty() and ok) { - auto tex = this->loaded_game_spec->get_texture(filename.toStdString()); - - if (tex != nullptr) { - return TextureHandle{tex, subid}; - } - else { - return this->get_missing_texture(); - } - } else { - qWarning("Invalid texture id: 'image://%s/%s'. Example formatting: 'image://%s/myfile.png.18'.", this->get_id(), qUtf8Printable(id), this->get_id()); - return this->get_missing_texture(); - } -} - -} // namespace openage::gui diff --git a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_filename_impl.h b/libopenage/gui/integration/private/gui_game_spec_image_provider_by_filename_impl.h deleted file mode 100644 index 0def78ae82..0000000000 --- a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_filename_impl.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include "gui_game_spec_image_provider_impl.h" - -namespace openage { -namespace gui { - -/** - * Exposes game textures to the Qt by their file name. - * - * Id has a form of . where is an integer. - */ -class GuiGameSpecImageProviderByFilenameImpl : public GuiGameSpecImageProviderImpl { -public: - explicit GuiGameSpecImageProviderByFilenameImpl(qtsdl::GuiEventQueue *render_updater); - virtual ~GuiGameSpecImageProviderByFilenameImpl(); - - static const char* id(); - -private: - virtual const char* get_id() const override; - virtual TextureHandle get_texture_handle(const QString &id) override; -}; - -}} // namespace openage::gui diff --git a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_graphic_id_impl.cpp b/libopenage/gui/integration/private/gui_game_spec_image_provider_by_graphic_id_impl.cpp deleted file mode 100644 index ede6e5ef21..0000000000 --- a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_graphic_id_impl.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. - -#include "gui_game_spec_image_provider_by_graphic_id_impl.h" - -#include "../../../error/error.h" - -#include "../../../gamestate/old/game_spec.h" - -namespace openage::gui { - -GuiGameSpecImageProviderByGraphicIdImpl::GuiGameSpecImageProviderByGraphicIdImpl(qtsdl::GuiEventQueue *render_updater) - : - GuiGameSpecImageProviderByIdImpl{render_updater} { -} - -GuiGameSpecImageProviderByGraphicIdImpl::~GuiGameSpecImageProviderByGraphicIdImpl() = default; - -const char* GuiGameSpecImageProviderByGraphicIdImpl::id() { - return "by-graphic-id"; -} - -const char* GuiGameSpecImageProviderByGraphicIdImpl::get_id() const { - return GuiGameSpecImageProviderByGraphicIdImpl::id(); -} - -openage::Texture* GuiGameSpecImageProviderByGraphicIdImpl::get_texture(int texture_id) { - ENSURE(this->loaded_game_spec, "trying to actually get a texture from a non-loaded spec"); - return this->loaded_game_spec->get_texture(texture_id); -} - -} // namespace openage::gui diff --git a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_graphic_id_impl.h b/libopenage/gui/integration/private/gui_game_spec_image_provider_by_graphic_id_impl.h deleted file mode 100644 index 02e1c1be4b..0000000000 --- a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_graphic_id_impl.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include "gui_game_spec_image_provider_by_id_impl.h" - -namespace openage { -namespace gui { - -/** - * Exposes game textures to the Qt by their id. - * - * Numeric id has a form of .. - */ -class GuiGameSpecImageProviderByGraphicIdImpl : public GuiGameSpecImageProviderByIdImpl { -public: - explicit GuiGameSpecImageProviderByGraphicIdImpl(qtsdl::GuiEventQueue *render_updater); - virtual ~GuiGameSpecImageProviderByGraphicIdImpl(); - - static const char* id(); - -private: - virtual const char* get_id() const override; - virtual openage::Texture* get_texture(int texture_id) override; -}; - -}} // namespace openage::gui diff --git a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_id_impl.cpp b/libopenage/gui/integration/private/gui_game_spec_image_provider_by_id_impl.cpp deleted file mode 100644 index 82752e06de..0000000000 --- a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_id_impl.cpp +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "gui_game_spec_image_provider_by_id_impl.h" - -#include "../../../error/error.h" - -#include "../../../gamestate/old/game_spec.h" -#include "gui_texture_factory.h" - -namespace openage::gui { - -GuiGameSpecImageProviderByIdImpl::GuiGameSpecImageProviderByIdImpl(qtsdl::GuiEventQueue *render_updater) : - GuiGameSpecImageProviderImpl{render_updater} { -} - -GuiGameSpecImageProviderByIdImpl::~GuiGameSpecImageProviderByIdImpl() = default; - -TextureHandle GuiGameSpecImageProviderByIdImpl::get_texture_handle(const QString &id) { - ENSURE(this->loaded_game_spec, "trying to actually get a texture from a non-loaded spec"); - - auto ids = id.split("."); - - if (ids.size() == 2) { - bool id_ok = false, subid_ok = false; - const int texture_id = ids[0].toInt(&id_ok); - const int subid = ids[1].toInt(&subid_ok); - - auto tex = (id_ok and subid_ok) ? - this->get_texture(texture_id) : - nullptr; - - if (tex != nullptr and subid < static_cast(tex->get_subtexture_count())) { - return TextureHandle{tex, subid}; - } - else { - return this->get_missing_texture(); - } - } - else { - qWarning("Invalid texture id: 'image://%s/%s'. Example formatting: 'image://%s/7366.18'.", this->get_id(), qUtf8Printable(id), this->get_id()); - return this->get_missing_texture(); - } -} - -} // namespace openage::gui diff --git a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_id_impl.h b/libopenage/gui/integration/private/gui_game_spec_image_provider_by_id_impl.h deleted file mode 100644 index d52402a669..0000000000 --- a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_id_impl.h +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include "gui_game_spec_image_provider_impl.h" - -namespace openage { -namespace gui { - -/** - * Base for providers that expose textures to the Qt by their id. - * - * Numeric id has a form of .. - */ -class GuiGameSpecImageProviderByIdImpl : public GuiGameSpecImageProviderImpl { -public: - explicit GuiGameSpecImageProviderByIdImpl(qtsdl::GuiEventQueue *render_updater); - virtual ~GuiGameSpecImageProviderByIdImpl(); - -private: - virtual TextureHandle get_texture_handle(const QString &id) override; - virtual openage::Texture* get_texture(int texture_id) = 0; -}; - -}} // namespace openage::gui diff --git a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_terrain_id_impl.cpp b/libopenage/gui/integration/private/gui_game_spec_image_provider_by_terrain_id_impl.cpp deleted file mode 100644 index f875118636..0000000000 --- a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_terrain_id_impl.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. - -#include "gui_game_spec_image_provider_by_terrain_id_impl.h" - -#include "../../../error/error.h" - -#include "../../../gamestate/old/game_spec.h" - -namespace openage::gui { - -GuiGameSpecImageProviderByTerrainIdImpl::GuiGameSpecImageProviderByTerrainIdImpl(qtsdl::GuiEventQueue *render_updater) - : - GuiGameSpecImageProviderByIdImpl{render_updater} { -} - -GuiGameSpecImageProviderByTerrainIdImpl::~GuiGameSpecImageProviderByTerrainIdImpl() = default; - -const char* GuiGameSpecImageProviderByTerrainIdImpl::id() { - return "by-terrain-id"; -} - -const char* GuiGameSpecImageProviderByTerrainIdImpl::get_id() const { - return GuiGameSpecImageProviderByTerrainIdImpl::id(); -} - -openage::Texture* GuiGameSpecImageProviderByTerrainIdImpl::get_texture(int texture_id) { - ENSURE(this->loaded_game_spec, "trying to actually get a texture from a non-loaded spec"); - auto meta = this->loaded_game_spec->get_terrain_meta(); - return meta && texture_id >=0 && texture_id < std::distance(std::begin(meta->textures), std::end(meta->textures)) ? meta->textures[texture_id] : nullptr; -} - -} // namespace openage::gui diff --git a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_terrain_id_impl.h b/libopenage/gui/integration/private/gui_game_spec_image_provider_by_terrain_id_impl.h deleted file mode 100644 index d5aba95279..0000000000 --- a/libopenage/gui/integration/private/gui_game_spec_image_provider_by_terrain_id_impl.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include "gui_game_spec_image_provider_by_id_impl.h" - -namespace openage { -namespace gui { - -/** - * Exposes terrain textures to the Qt by their id. - * - * Numeric id has a form of . where texture-id is the position in the terrain_meta. - */ -class GuiGameSpecImageProviderByTerrainIdImpl : public GuiGameSpecImageProviderByIdImpl { -public: - explicit GuiGameSpecImageProviderByTerrainIdImpl(qtsdl::GuiEventQueue *render_updater); - virtual ~GuiGameSpecImageProviderByTerrainIdImpl(); - - static const char* id(); - -private: - virtual const char* get_id() const override; - virtual openage::Texture* get_texture(int texture_id) override; -}; - -}} // namespace openage::gui diff --git a/libopenage/gui/integration/private/gui_game_spec_image_provider_impl.cpp b/libopenage/gui/integration/private/gui_game_spec_image_provider_impl.cpp index 4be3693854..01579aa8fa 100644 --- a/libopenage/gui/integration/private/gui_game_spec_image_provider_impl.cpp +++ b/libopenage/gui/integration/private/gui_game_spec_image_provider_impl.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "gui_game_spec_image_provider_impl.h" @@ -11,18 +11,16 @@ #include "../../../gamestate/old/game_spec.h" #include "../../guisys/private/gui_event_queue_impl.h" -#include "gui_texture_factory.h" #include "gui_filled_texture_handles.h" +#include "gui_texture_factory.h" namespace openage::gui { -GuiGameSpecImageProviderImpl::GuiGameSpecImageProviderImpl(qtsdl::GuiEventQueue *render_updater) - : +GuiGameSpecImageProviderImpl::GuiGameSpecImageProviderImpl(qtsdl::GuiEventQueue *render_updater) : GuiImageProviderImpl{}, invalidated{}, filled_handles{std::make_shared()}, ended{} { - QThread *render_thread = qtsdl::GuiEventQueueImpl::impl(render_updater)->get_thread(); this->render_thread_callback.moveToThread(render_thread); QObject::connect(&this->render_thread_callback, &qtsdl::GuiCallback::process_blocking, &this->render_thread_callback, &qtsdl::GuiCallback::process, render_thread != QThread::currentThread() ? Qt::BlockingQueuedConnection : Qt::DirectConnection); @@ -30,7 +28,7 @@ GuiGameSpecImageProviderImpl::GuiGameSpecImageProviderImpl(qtsdl::GuiEventQueue GuiGameSpecImageProviderImpl::~GuiGameSpecImageProviderImpl() = default; -void GuiGameSpecImageProviderImpl::on_game_spec_loaded(const std::shared_ptr& loaded_game_spec) { +void GuiGameSpecImageProviderImpl::on_game_spec_loaded(const std::shared_ptr &loaded_game_spec) { ENSURE(loaded_game_spec, "spec hasn't been checked or was invalidated"); std::unique_lock lck{this->loaded_game_spec_mutex}; @@ -44,7 +42,7 @@ void GuiGameSpecImageProviderImpl::on_game_spec_loaded(const std::shared_ptr& loaded_game_spec) { +void GuiGameSpecImageProviderImpl::migrate_to_new_game_spec(const std::shared_ptr &loaded_game_spec) { ENSURE(loaded_game_spec, "spec hasn't been checked or was invalidated"); if (this->loaded_game_spec) { @@ -57,7 +55,8 @@ void GuiGameSpecImageProviderImpl::migrate_to_new_game_spec(const std::shared_pt using namespace std::placeholders; this->filled_handles->refresh_all_handles_with_texture(std::bind(&GuiGameSpecImageProviderImpl::overwrite_texture_handle, this, _1, _2, _3)); }); - } else { + } + else { this->loaded_game_spec = loaded_game_spec; } } @@ -71,12 +70,6 @@ void GuiGameSpecImageProviderImpl::on_game_spec_invalidated() { std::unique_lock lck{this->loaded_game_spec_mutex}; if (this->loaded_game_spec) { - const TextureHandle missing_texture = get_missing_texture(); - - emit this->render_thread_callback.process_blocking([this, &missing_texture] { - this->filled_handles->fill_all_handles_with_texture(missing_texture); - }); - invalidated = true; } } @@ -86,19 +79,16 @@ GuiFilledTextureHandleUser GuiGameSpecImageProviderImpl::fill_texture_handle(con return GuiFilledTextureHandleUser(this->filled_handles, id, requested_size, filled_handle); } -TextureHandle GuiGameSpecImageProviderImpl::get_missing_texture() { - return TextureHandle{this->loaded_game_spec->get_texture("missing.png", false), -1}; -} - -QQuickTextureFactory* GuiGameSpecImageProviderImpl::requestTexture(const QString &id, QSize *size, const QSize &requestedSize) { +QQuickTextureFactory *GuiGameSpecImageProviderImpl::requestTexture(const QString &id, QSize *size, const QSize &requestedSize) { std::unique_lock lck{this->loaded_game_spec_mutex}; - this->loaded_game_spec_cond.wait(lck, [this] {return this->ended || this->loaded_game_spec;}); + this->loaded_game_spec_cond.wait(lck, [this] { return this->ended || this->loaded_game_spec; }); if (this->ended) { qWarning("ImageProvider was stopped during the load, so it'll appear like the requestTexture() isn't implemented."); return this->GuiImageProviderImpl::requestTexture(id, size, requestedSize); - } else { + } + else { auto tex_factory = new GuiTextureFactory{this, id, requestedSize}; *size = tex_factory->textureSize(); return tex_factory; diff --git a/libopenage/gui/integration/private/gui_game_spec_image_provider_impl.h b/libopenage/gui/integration/private/gui_game_spec_image_provider_impl.h index 4882d67447..3fd199dbd0 100644 --- a/libopenage/gui/integration/private/gui_game_spec_image_provider_impl.h +++ b/libopenage/gui/integration/private/gui_game_spec_image_provider_impl.h @@ -1,17 +1,17 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once +#include #include #include -#include #include -#include +#include -#include "../../guisys/private/gui_image_provider_impl.h" #include "../../guisys/private/gui_callback.h" -#include "gui_texture_handle.h" +#include "../../guisys/private/gui_image_provider_impl.h" #include "gui_filled_texture_handles.h" +#include "gui_texture_handle.h" namespace qtsdl { @@ -44,7 +44,7 @@ class GuiGameSpecImageProviderImpl : public qtsdl::GuiImageProviderImpl { * * @param loaded_game_spec new source (can't be null) */ - void on_game_spec_loaded(const std::shared_ptr& loaded_game_spec); + void on_game_spec_loaded(const std::shared_ptr &loaded_game_spec); /** * Set to every sprite the 'missing texture' from current spec. @@ -64,8 +64,6 @@ class GuiGameSpecImageProviderImpl : public qtsdl::GuiImageProviderImpl { GuiFilledTextureHandleUser fill_texture_handle(const QString &id, const QSize &requested_size, SizedTextureHandle *filled_handle); protected: - TextureHandle get_missing_texture(); - std::shared_ptr loaded_game_spec; /** @@ -75,13 +73,13 @@ class GuiGameSpecImageProviderImpl : public qtsdl::GuiImageProviderImpl { private: virtual TextureHandle get_texture_handle(const QString &id) = 0; - virtual QQuickTextureFactory* requestTexture(const QString &id, QSize *size, const QSize &requestedSize) override; + virtual QQuickTextureFactory *requestTexture(const QString &id, QSize *size, const QSize &requestedSize) override; virtual void give_up() override; /** * Change the already produced texture handles to use new source. */ - void migrate_to_new_game_spec(const std::shared_ptr& loaded_game_spec); + void migrate_to_new_game_spec(const std::shared_ptr &loaded_game_spec); void overwrite_texture_handle(const QString &id, const QSize &requested_size, SizedTextureHandle *filled_handle); @@ -100,4 +98,5 @@ class GuiGameSpecImageProviderImpl : public qtsdl::GuiImageProviderImpl { qtsdl::GuiCallback render_thread_callback; }; -}} // namespace openage::gui +} // namespace gui +} // namespace openage diff --git a/libopenage/gui/integration/private/gui_image_provider_link.cpp b/libopenage/gui/integration/private/gui_image_provider_link.cpp index a0454449c7..f2b72eecb3 100644 --- a/libopenage/gui/integration/private/gui_image_provider_link.cpp +++ b/libopenage/gui/integration/private/gui_image_provider_link.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "gui_image_provider_link.h" @@ -14,31 +14,17 @@ #include "../../game_spec_link.h" #include "gui_game_spec_image_provider_impl.h" -#include "gui_game_spec_image_provider_by_filename_impl.h" -#include "gui_game_spec_image_provider_by_graphic_id_impl.h" -#include "gui_game_spec_image_provider_by_terrain_id_impl.h" - namespace openage::gui { -namespace { -const int registration_by_filename = qmlRegisterSingletonType("yay.sfttech.openage", 1, 0, "ImageProviderByFilename", &GuiImageProviderLink::provider_by_filename); -const int registration_by_id = qmlRegisterSingletonType("yay.sfttech.openage", 1, 0, "ImageProviderById", &GuiImageProviderLink::provider_by_graphic_id); -const int registration_by_terrain_id = qmlRegisterSingletonType("yay.sfttech.openage", 1, 0, "ImageProviderByTerrainId", &GuiImageProviderLink::provider_by_terrain_id); -} - -GuiImageProviderLink::GuiImageProviderLink(QObject *parent, GuiGameSpecImageProviderImpl &image_provider) - : +GuiImageProviderLink::GuiImageProviderLink(QObject *parent, GuiGameSpecImageProviderImpl &image_provider) : QObject{parent}, image_provider{image_provider}, game_spec{} { - Q_UNUSED(registration_by_filename); - Q_UNUSED(registration_by_id); - Q_UNUSED(registration_by_terrain_id); } GuiImageProviderLink::~GuiImageProviderLink() = default; -GameSpecLink* GuiImageProviderLink::get_game_spec() const { +GameSpecLink *GuiImageProviderLink::get_game_spec() const { return this->game_spec; } @@ -61,30 +47,18 @@ void GuiImageProviderLink::set_game_spec(GameSpecLink *game_spec) { } } -QObject* GuiImageProviderLink::provider(QQmlEngine *engine, const char *id) { - qtsdl::QmlEngineWithSingletonItemsInfo *engine_with_singleton_items_info = qtsdl::checked_static_cast(engine); +QObject *GuiImageProviderLink::provider(QQmlEngine *engine, const char *id) { + qtsdl::QmlEngineWithSingletonItemsInfo *engine_with_singleton_items_info = qtsdl::checked_static_cast(engine); auto image_providers = engine_with_singleton_items_info->get_image_providers(); - auto found_it = std::find_if(std::begin(image_providers), std::end(image_providers), [id] (qtsdl::GuiImageProviderImpl *image_provider) { + auto found_it = std::find_if(std::begin(image_providers), std::end(image_providers), [id](qtsdl::GuiImageProviderImpl *image_provider) { return image_provider->get_id() == id; }); ENSURE(found_it != std::end(image_providers), "The image provider '" << id << "' wasn't created or wasn't passed to the QML engine creation function."); // owned by the QML engine - return new GuiImageProviderLink{nullptr, *qtsdl::checked_static_cast(*found_it)}; -} - -QObject* GuiImageProviderLink::provider_by_filename(QQmlEngine *engine, QJSEngine*) { - return GuiImageProviderLink::provider(engine, GuiGameSpecImageProviderByFilenameImpl::id()); -} - -QObject* GuiImageProviderLink::provider_by_graphic_id(QQmlEngine *engine, QJSEngine*) { - return GuiImageProviderLink::provider(engine, GuiGameSpecImageProviderByGraphicIdImpl::id()); -} - -QObject* GuiImageProviderLink::provider_by_terrain_id(QQmlEngine *engine, QJSEngine*) { - return GuiImageProviderLink::provider(engine, GuiGameSpecImageProviderByTerrainIdImpl::id()); + return new GuiImageProviderLink{nullptr, *qtsdl::checked_static_cast(*found_it)}; } void GuiImageProviderLink::on_game_spec_loaded(GameSpecLink *game_spec, std::shared_ptr loaded_game_spec) { diff --git a/libopenage/gui/integration/private/gui_image_provider_link.h b/libopenage/gui/integration/private/gui_image_provider_link.h index d0f4c3e944..510214f652 100644 --- a/libopenage/gui/integration/private/gui_image_provider_link.h +++ b/libopenage/gui/integration/private/gui_image_provider_link.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once @@ -22,13 +22,13 @@ class GameSpecLink; class GuiImageProviderLink : public QObject { Q_OBJECT - Q_PROPERTY(openage::gui::GameSpecLink* gameSpec READ get_game_spec WRITE set_game_spec) + Q_PROPERTY(openage::gui::GameSpecLink *gameSpec READ get_game_spec WRITE set_game_spec) public: explicit GuiImageProviderLink(QObject *parent, GuiGameSpecImageProviderImpl &image_provider); virtual ~GuiImageProviderLink(); - GameSpecLink* get_game_spec() const; + GameSpecLink *get_game_spec() const; /** * Sets the game spec to load textures from. @@ -38,10 +38,7 @@ class GuiImageProviderLink : public QObject { */ void set_game_spec(GameSpecLink *game_spec); - static QObject* provider(QQmlEngine*, const char *id); - static QObject* provider_by_filename(QQmlEngine*, QJSEngine*); - static QObject* provider_by_graphic_id(QQmlEngine*, QJSEngine*); - static QObject* provider_by_terrain_id(QQmlEngine*, QJSEngine*); + static QObject *provider(QQmlEngine *, const char *id); private slots: /** @@ -57,4 +54,5 @@ private slots: QPointer game_spec; }; -}} // namespace openage::gui +} // namespace gui +} // namespace openage diff --git a/libopenage/gui/integration/private/gui_texture.cpp b/libopenage/gui/integration/private/gui_texture.cpp index 7890981918..8c6de0746a 100644 --- a/libopenage/gui/integration/private/gui_texture.cpp +++ b/libopenage/gui/integration/private/gui_texture.cpp @@ -4,7 +4,6 @@ #include -#include "../../../texture.h" #include "gui_make_standalone_subtexture.h" #include "gui_texture.h" @@ -68,32 +67,6 @@ GLuint create_compatible_texture(GLuint texture_id, GLsizei w, GLsizei h) { QSGTexture *GuiTexture::removedFromAtlas(QRhiResourceUpdateBatch *resourceUpdates /* = nullptr */) const { if (this->isAtlasTexture()) { - if (!this->standalone) { - auto tex = this->texture_handle.texture; - auto sub = tex->get_subtexture(this->texture_handle.subid); - - GLuint sub_texture_id = create_compatible_texture(tex->get_texture_id(), sub->w, sub->h); - - std::array fbo; - glGenFramebuffers(fbo.size(), &fbo.front()); - - glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo[0]); - glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, this->textureId(), 0); - glReadBuffer(GL_COLOR_ATTACHMENT0); - - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[1]); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, sub_texture_id, 0); - glDrawBuffer(GL_COLOR_ATTACHMENT0); - - glBlitFramebuffer(sub->x, sub->y, sub->x + sub->w, sub->y + sub->h, 0, 0, sub->w, sub->h, GL_COLOR_BUFFER_BIT, GL_NEAREST); - - glBindFramebuffer(GL_FRAMEBUFFER, 0); - - glDeleteFramebuffers(fbo.size(), &fbo.front()); - - this->standalone = make_standalone_subtexture(sub_texture_id, QSize(sub->w, sub->h)); - } - return this->standalone.get(); } @@ -101,18 +74,11 @@ QSGTexture *GuiTexture::removedFromAtlas(QRhiResourceUpdateBatch *resourceUpdate } QRectF GuiTexture::normalizedTextureSubRect() const { - if (this->isAtlasTexture()) { - auto tex = this->texture_handle.texture; - auto sub = tex->get_subtexture(this->texture_handle.subid); - return QTransform::fromScale(tex->w, tex->h).inverted().mapRect(QRectF(sub->x, sub->y, sub->w, sub->h)); - } - else { - return QSGTexture::normalizedTextureSubRect(); - } + return QSGTexture::normalizedTextureSubRect(); } int GuiTexture::textureId() const { - return this->texture_handle.texture->get_texture_id(); + return 0; } QSize GuiTexture::textureSize() const { diff --git a/libopenage/gui/integration/private/gui_texture_factory.cpp b/libopenage/gui/integration/private/gui_texture_factory.cpp index 06abcdada3..35bc04cea1 100644 --- a/libopenage/gui/integration/private/gui_texture_factory.cpp +++ b/libopenage/gui/integration/private/gui_texture_factory.cpp @@ -1,30 +1,27 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "gui_texture_factory.h" +#include "gui_filled_texture_handles.h" #include "gui_game_spec_image_provider_impl.h" -#include "../../../texture.h" #include "gui_texture.h" -#include "gui_filled_texture_handles.h" namespace openage::gui { -GuiTextureFactory::GuiTextureFactory(GuiGameSpecImageProviderImpl *provider, const QString &id, const QSize &requested_size) - : +GuiTextureFactory::GuiTextureFactory(GuiGameSpecImageProviderImpl *provider, const QString &id, const QSize &requested_size) : texture_handle(), texture_handle_user{provider->fill_texture_handle(id, requested_size, &this->texture_handle)} { } GuiTextureFactory::~GuiTextureFactory() = default; -QSGTexture* GuiTextureFactory::createTexture(QQuickWindow *window) const { +QSGTexture *GuiTextureFactory::createTexture(QQuickWindow *window) const { Q_UNUSED(window); return new GuiTexture{this->texture_handle}; } int GuiTextureFactory::textureByteCount() const { - // assume 32bit textures - return this->texture_handle.texture->w * this->texture_handle.texture->h * 4; + return 0; } QSize GuiTextureFactory::textureSize() const { diff --git a/libopenage/gui/integration/private/gui_texture_handle.cpp b/libopenage/gui/integration/private/gui_texture_handle.cpp index da9c974c80..efe9da7fb8 100644 --- a/libopenage/gui/integration/private/gui_texture_handle.cpp +++ b/libopenage/gui/integration/private/gui_texture_handle.cpp @@ -1,28 +1,25 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "gui_texture_handle.h" #include -#include "../../../texture.h" namespace openage { namespace gui { -SizedTextureHandle::SizedTextureHandle() - : - TextureHandle{nullptr, 0}, +SizedTextureHandle::SizedTextureHandle() : + TextureHandle{0}, size{} { } -SizedTextureHandle::SizedTextureHandle(const TextureHandle &handle, const QSize &size) - : +SizedTextureHandle::SizedTextureHandle(const TextureHandle &handle, const QSize &size) : TextureHandle(handle), size{size} { } bool isAtlasTexture(const TextureHandle &texture_handle) { - return texture_handle.subid >= 0 && texture_handle.texture->get_subtexture_count() > 1; + return texture_handle.subid >= 0; } QSize textureSize(const SizedTextureHandle &texture_handle) { @@ -30,14 +27,7 @@ QSize textureSize(const SizedTextureHandle &texture_handle) { } QSize native_size(const TextureHandle &texture_handle) { - auto tex = texture_handle.texture; - - if (isAtlasTexture(texture_handle)) { - auto sub = tex->get_subtexture(texture_handle.subid); - return QSize(sub->w, sub->h); - } else { - return QSize(tex->w, tex->h); - } + return QSize(0, 0); } QSize aspect_fit_size(const TextureHandle &texture_handle, const QSize &requested_size) { @@ -48,9 +38,11 @@ QSize aspect_fit_size(const TextureHandle &texture_handle, const QSize &requeste // If requested_size.isEmpty() then the caller don't care how big one or two dimensions can grow. return size.scaled(bounding_size, requested_size.isEmpty() && (requested_size.width() > size.width() || requested_size.height() > size.height()) ? Qt::KeepAspectRatioByExpanding : Qt::KeepAspectRatio); - } else { + } + else { return size; } } -}} // namespace openage::gui +} // namespace gui +} // namespace openage diff --git a/libopenage/gui/integration/private/gui_texture_handle.h b/libopenage/gui/integration/private/gui_texture_handle.h index 58b86ee1bb..5d943a1358 100644 --- a/libopenage/gui/integration/private/gui_texture_handle.h +++ b/libopenage/gui/integration/private/gui_texture_handle.h @@ -1,4 +1,4 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once @@ -6,13 +6,10 @@ namespace openage { -class Texture; - namespace gui { class TextureHandle { public: - openage::Texture *texture; int subid; }; @@ -36,4 +33,5 @@ QSize native_size(const TextureHandle &texture_handle); */ QSize aspect_fit_size(const TextureHandle &texture_handle, const QSize &requested_size); -}} // namespace openage::gui +} // namespace gui +} // namespace openage diff --git a/libopenage/gui/integration/public/CMakeLists.txt b/libopenage/gui/integration/public/CMakeLists.txt index c7a535bb1f..4acda98465 100644 --- a/libopenage/gui/integration/public/CMakeLists.txt +++ b/libopenage/gui/integration/public/CMakeLists.txt @@ -1,4 +1,3 @@ add_sources(libopenage gui_application_with_logger.cpp - gui_game_spec_image_provider.cpp ) diff --git a/libopenage/gui/integration/public/gui_game_spec_image_provider.cpp b/libopenage/gui/integration/public/gui_game_spec_image_provider.cpp deleted file mode 100644 index 0fb622807b..0000000000 --- a/libopenage/gui/integration/public/gui_game_spec_image_provider.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "gui_game_spec_image_provider.h" - -#include "../../../error/error.h" - -#include "../private/gui_game_spec_image_provider_by_filename_impl.h" -#include "../private/gui_game_spec_image_provider_by_graphic_id_impl.h" -#include "../private/gui_game_spec_image_provider_by_terrain_id_impl.h" - -namespace openage::gui { - -GuiGameSpecImageProvider::GuiGameSpecImageProvider(qtsdl::GuiEventQueue *render_updater, Type type) - : - GuiImageProvider{[render_updater, type] () -> std::unique_ptr { - switch(type) { - case Type::ByFilename: - return std::make_unique(render_updater); - - case Type::ByGraphicId: - return std::make_unique(render_updater); - - case Type::ByTerrainId: - return std::make_unique(render_updater); - - default: - break; - } - - ENSURE(false, "unhandled image provider type"); - return std::unique_ptr{}; - }()} { -} - -GuiGameSpecImageProvider::~GuiGameSpecImageProvider() = default; - -} // namespace openage::gui diff --git a/libopenage/gui/integration/public/gui_game_spec_image_provider.h b/libopenage/gui/integration/public/gui_game_spec_image_provider.h deleted file mode 100644 index 2727d90af6..0000000000 --- a/libopenage/gui/integration/public/gui_game_spec_image_provider.h +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include "../../guisys/public/gui_image_provider.h" - -namespace qtsdl { -class GuiEventQueue; -} // namespace qtsdl - -namespace openage { -namespace gui { - -class GuiGameSpecImageProvider : public qtsdl::GuiImageProvider { -public: - enum class Type { - ByFilename, - ByGraphicId, - ByTerrainId, - }; - - explicit GuiGameSpecImageProvider(qtsdl::GuiEventQueue *render_updater, Type type); - ~GuiGameSpecImageProvider(); -}; - -}} // namespace openage::gui diff --git a/libopenage/pathfinding/path.cpp b/libopenage/pathfinding/path.cpp index c04e5876c4..c3f5e91eff 100644 --- a/libopenage/pathfinding/path.cpp +++ b/libopenage/pathfinding/path.cpp @@ -1,21 +1,20 @@ -// Copyright 2014-2019 the openage authors. See copying.md for legal info. +// Copyright 2014-2023 the openage authors. See copying.md for legal info. #include -#include "path.h" #include "../terrain/terrain.h" +#include "path.h" namespace openage::path { -bool compare_node_cost::operator ()(const node_pt &lhs, const node_pt &rhs) const { +bool compare_node_cost::operator()(const node_pt &lhs, const node_pt &rhs) const { // TODO: use node operator < return lhs->future_cost < rhs->future_cost; } -Node::Node(const coord::phys3 &pos, node_pt prev) - : +Node::Node(const coord::phys3 &pos, node_pt prev) : position(pos), tile_position(pos.to_tile3().to_tile()), direction{}, @@ -24,22 +23,17 @@ Node::Node(const coord::phys3 &pos, node_pt prev) factor{1.0f}, path_predecessor{prev}, heap_node(nullptr) { - if (prev) { this->direction = (this->position - prev->position).normalize(); // TODO: add dot product to coord - cost_t similarity = ((this->direction.ne.to_float() * - prev->direction.ne.to_float()) + - (this->direction.se.to_float() * - prev->direction.se.to_float())); + cost_t similarity = ((this->direction.ne.to_float() * prev->direction.ne.to_float()) + (this->direction.se.to_float() * prev->direction.se.to_float())); this->factor += (1 - similarity); } } -Node::Node(const coord::phys3 &pos, node_pt prev, cost_t past, cost_t heuristic) - : +Node::Node(const coord::phys3 &pos, node_pt prev, cost_t past, cost_t heuristic) : Node{pos, prev} { this->past_cost = past; this->heuristic_cost = heuristic; @@ -47,12 +41,12 @@ Node::Node(const coord::phys3 &pos, node_pt prev, cost_t past, cost_t heuristic) } -bool Node::operator <(const Node &other) const { +bool Node::operator<(const Node &other) const { return this->future_cost < other.future_cost; } -bool Node::operator ==(const Node &other) const { +bool Node::operator==(const Node &other) const { return this->position == other.position; } @@ -72,7 +66,8 @@ Path Node::generate_backtrace() { Node other = *current; waypoints.push_back(*current); current = current->path_predecessor; - } while (current != nullptr); + } + while (current != nullptr); waypoints.pop_back(); // remove start return {waypoints}; @@ -86,10 +81,10 @@ std::vector Node::get_neighbors(const nodemap_t &nodes, float scale) { coord::phys3 n_pos = this->position + (neigh_phys[n] * scale); if (nodes.count(n_pos) > 0) { - neighbors.push_back( nodes.at(n_pos) ); + neighbors.push_back(nodes.at(n_pos)); } else { - neighbors.push_back( std::make_shared(n_pos, this->shared_from_this()) ); + neighbors.push_back(std::make_shared(n_pos, this->shared_from_this())); } } return neighbors; @@ -114,22 +109,8 @@ bool passable_line(node_pt start, node_pt end, std::function &nodes) - : +Path::Path(const std::vector &nodes) : waypoints{nodes} {} -void Path::draw_path(const coord::CoordManager &mgr) { - glLineWidth(1); - glColor3f(0.3, 1.0, 0.3); - glBegin(GL_LINES); { - for (Node &n : waypoints) { - coord::viewport draw_pos = n.position.to_viewport(mgr); - glVertex3f(draw_pos.x, draw_pos.y, 0); - } - } - glEnd(); -} - - -} // openage::path +} // namespace openage::path diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/path.h index 16784cbd90..033090ca6b 100644 --- a/libopenage/pathfinding/path.h +++ b/libopenage/pathfinding/path.h @@ -1,4 +1,4 @@ -// Copyright 2014-2019 the openage authors. See copying.md for legal info. +// Copyright 2014-2023 the openage authors. See copying.md for legal info. #pragma once @@ -10,9 +10,8 @@ #include "../coord/phys.h" #include "../coord/tile.h" #include "../datastructure/pairing_heap.h" -#include "../util/misc.h" #include "../util/hash.h" - +#include "../util/misc.h" namespace openage { @@ -48,7 +47,7 @@ using nodemap_t = std::unordered_map; * Calls operator < on Node. */ struct compare_node_cost { - bool operator ()(const node_pt &lhs, const node_pt &rhs) const; + bool operator()(const node_pt &lhs, const node_pt &rhs) const; }; /** @@ -59,31 +58,30 @@ using heap_t = datastructure::PairingHeap; /** * Size of phys-coord grid for path nodes. */ -constexpr coord::phys_t path_grid_size{1.f/8}; +constexpr coord::phys_t path_grid_size{1.f / 8}; /** * Phys3 delta coordinates to select for path neighbors. */ constexpr coord::phys3_delta const neigh_phys[] = { - {path_grid_size * 1, path_grid_size * -1, 0}, - {path_grid_size * 1, path_grid_size * 0, 0}, - {path_grid_size * 1, path_grid_size * 1, 0}, - {path_grid_size * 0, path_grid_size * 1, 0}, - {path_grid_size * -1, path_grid_size * 1, 0}, - {path_grid_size * -1, path_grid_size * 0, 0}, + {path_grid_size * 1, path_grid_size * -1, 0}, + {path_grid_size * 1, path_grid_size * 0, 0}, + {path_grid_size * 1, path_grid_size * 1, 0}, + {path_grid_size * 0, path_grid_size * 1, 0}, + {path_grid_size * -1, path_grid_size * 1, 0}, + {path_grid_size * -1, path_grid_size * 0, 0}, {path_grid_size * -1, path_grid_size * -1, 0}, - {path_grid_size * 0, path_grid_size * -1, 0} -}; + {path_grid_size * 0, path_grid_size * -1, 0}}; /** * */ -bool passable_line(node_pt start, node_pt end, std::functionpassable, float samples=5.0f); +bool passable_line(node_pt start, node_pt end, std::function passable, float samples = 5.0f); /** * One navigation waypoint in a path. */ -class Node: public std::enable_shared_from_this { +class Node : public std::enable_shared_from_this { public: Node(const coord::phys3 &pos, node_pt prev); Node(const coord::phys3 &pos, node_pt prev, cost_t past, cost_t heuristic); @@ -91,13 +89,13 @@ class Node: public std::enable_shared_from_this { /** * Orders nodes according to their future cost value. */ - bool operator <(const Node &other) const; + bool operator<(const Node &other) const; /** * Compare the node to another one. * They are the same if their position is. */ - bool operator ==(const Node &other) const; + bool operator==(const Node &other) const; /** * Calculates the actual movement cose to another node. @@ -112,7 +110,7 @@ class Node: public std::enable_shared_from_this { /** * Get all neighbors of this graph node. */ - std::vector get_neighbors(const nodemap_t &, float scale=1.0f); + std::vector get_neighbors(const nodemap_t &, float scale = 1.0f); /** * The tile position this node is associated to. @@ -184,8 +182,6 @@ class Path { Path() = default; Path(const std::vector &nodes); - void draw_path(const coord::CoordManager &mgr); - /** * These are the waypoints to navigate in order. * Includes the start and end node. @@ -203,9 +199,9 @@ namespace std { * Hash function for path nodes. * Just uses their position. */ -template<> +template <> struct hash { - size_t operator ()(const openage::path::Node &x) const { + size_t operator()(const openage::path::Node &x) const { openage::coord::phys3 node_pos = x.position; size_t hash = openage::util::type_hash(); hash = openage::util::hash_combine(hash, std::hash{}(node_pos.ne)); diff --git a/libopenage/renderer/gui/gui.h b/libopenage/renderer/gui/gui.h index bb83217c48..dc8b0fcfdc 100644 --- a/libopenage/renderer/gui/gui.h +++ b/libopenage/renderer/gui/gui.h @@ -7,7 +7,6 @@ #include #include "gui/guisys/public/gui_event_queue.h" -#include "gui/integration/public/gui_game_spec_image_provider.h" #include "renderer/gui/guisys/public/gui_subtree.h" namespace qtgui { @@ -130,8 +129,6 @@ class GUI { */ qtgui::GuiSubtree subtree; - // openage::gui::GuiGameSpecImageProvider image_provider_by_filename; - /** * Reference to the openage renderer. * Used to fetch texture objects for the GUI texture. diff --git a/libopenage/shader/program.h b/libopenage/shader/program.h index c6848d68be..d2244b5bf3 100644 --- a/libopenage/shader/program.h +++ b/libopenage/shader/program.h @@ -1,4 +1,4 @@ -// Copyright 2013-2016 the openage authors. See copying.md for legal info. +// Copyright 2013-2023 the openage authors. See copying.md for legal info. #pragma once @@ -9,7 +9,7 @@ namespace shader { class Shader; -class Program { +class [[deprecated]] Program { public: GLuint id; GLint pos_id, mvpm_id; @@ -43,4 +43,5 @@ class Program { }; -}} // openage::shader +} // namespace shader +} // namespace openage diff --git a/libopenage/shader/shader.h b/libopenage/shader/shader.h index a8a263e6b9..93e44b6123 100644 --- a/libopenage/shader/shader.h +++ b/libopenage/shader/shader.h @@ -1,4 +1,4 @@ -// Copyright 2013-2016 the openage authors. See copying.md for legal info. +// Copyright 2013-2023 the openage authors. See copying.md for legal info. #pragma once @@ -9,9 +9,9 @@ namespace openage { namespace shader { -const char *type_to_string(GLenum type); +[[deprecated]] const char *type_to_string(GLenum type); -class Shader { +class [[deprecated]] Shader { public: Shader(GLenum type, std::initializer_list sources); ~Shader(); @@ -20,4 +20,5 @@ class Shader { GLenum type; }; -}} // openage::shader +} // namespace shader +} // namespace openage diff --git a/libopenage/terrain/CMakeLists.txt b/libopenage/terrain/CMakeLists.txt index 2dbe9a39cc..c7ae65ad65 100644 --- a/libopenage/terrain/CMakeLists.txt +++ b/libopenage/terrain/CMakeLists.txt @@ -2,6 +2,5 @@ add_sources(libopenage terrain.cpp terrain_chunk.cpp terrain_object.cpp - terrain_outline.cpp terrain_search.cpp ) diff --git a/libopenage/terrain/terrain.cpp b/libopenage/terrain/terrain.cpp index 325db0a5d5..59f9239a9d 100644 --- a/libopenage/terrain/terrain.cpp +++ b/libopenage/terrain/terrain.cpp @@ -185,16 +185,6 @@ int Terrain::blendmode(terrain_t terrain_id) { return this->meta->terrain_id_blendmode_map[terrain_id]; } -Texture *Terrain::texture(terrain_t terrain_id) { - this->validate_terrain(terrain_id); - return this->meta->textures[terrain_id]; -} - -Texture *Terrain::blending_mask(ssize_t mask_id) { - this->validate_mask(mask_id); - return this->meta->blending_masks[mask_id]; -} - unsigned Terrain::get_subtexture_id(const coord::tile &pos, unsigned atlas_size) { unsigned result = 0; @@ -367,17 +357,11 @@ struct tile_draw_data Terrain::create_tile_advice(coord::tile position, bool ble this->validate_terrain(base_tile_data.terrain_id); - Texture *tex = this->texture(base_tile_data.terrain_id); - base_tile_data.state = tile_state::existing; base_tile_data.pos = position; base_tile_data.priority = this->priority(base_tile_data.terrain_id); - base_tile_data.tex = tex; - base_tile_data.subtexture_id = this->get_subtexture_id( - position, - std::sqrt(tex->get_subtexture_count())); + base_tile_data.subtexture_id = 0; base_tile_data.blend_mode = -1; - base_tile_data.mask_tex = nullptr; base_tile_data.mask_id = -1; tile.data[tile.count] = base_tile_data; @@ -635,11 +619,7 @@ void Terrain::calculate_masks(coord::tile position, overlay->mask_id = adjacent_mask_id; overlay->blend_mode = blend_mode; overlay->terrain_id = neighbor_terrain_id; - overlay->tex = this->texture(neighbor_terrain_id); - overlay->subtexture_id = this->get_subtexture_id( - position, - std::sqrt(overlay->tex->get_subtexture_count())); - overlay->mask_tex = this->blending_mask(blend_mode); + overlay->subtexture_id = 0; overlay->state = tile_state::existing; tile_data->count += 1; @@ -666,11 +646,7 @@ void Terrain::calculate_masks(coord::tile position, overlay->mask_id = diag_mask_id_map[l]; overlay->blend_mode = blend_mode; overlay->terrain_id = neighbor_terrain_id; - overlay->tex = this->texture(neighbor_terrain_id); - overlay->subtexture_id = this->get_subtexture_id( - position, - std::sqrt(overlay->tex->get_subtexture_count())); - overlay->mask_tex = this->blending_mask(blend_mode); + overlay->subtexture_id = 0; overlay->state = tile_state::existing; tile_data->count += 1; diff --git a/libopenage/terrain/terrain.h b/libopenage/terrain/terrain.h index fcde460ad7..bb43a04e73 100644 --- a/libopenage/terrain/terrain.h +++ b/libopenage/terrain/terrain.h @@ -13,7 +13,6 @@ #include "../coord/phys.h" #include "../coord/pixel.h" #include "../coord/tile.h" -#include "../texture.h" #include "../util/misc.h" namespace openage { @@ -120,11 +119,9 @@ struct tile_data { terrain_t terrain_id; coord::tile pos{0, 0}; int subtexture_id; - Texture *tex; int priority; int mask_id; int blend_mode; - Texture *mask_tex; tile_state state; }; @@ -155,9 +152,6 @@ struct terrain_meta { size_t terrain_id_count; size_t blendmode_count; - std::vector textures; - std::vector blending_masks; - std::unique_ptr terrain_id_priority_map; std::unique_ptr terrain_id_blendmode_map; @@ -307,16 +301,6 @@ class Terrain { */ int blendmode(terrain_t terrain_id); - /** - * get the terrain texture for a given terrain id. - */ - Texture *texture(terrain_t terrain_id); - - /** - * get the blendomatic mask with the given mask id. - */ - Texture *blending_mask(ssize_t mask_id); - /** * return the blending mode id for two given neighbor ids. */ diff --git a/libopenage/terrain/terrain_chunk.cpp b/libopenage/terrain/terrain_chunk.cpp index 46d877175e..acbd76d7a7 100644 --- a/libopenage/terrain/terrain_chunk.cpp +++ b/libopenage/terrain/terrain_chunk.cpp @@ -10,7 +10,6 @@ #include "../error/error.h" #include "../legacy_engine.h" #include "../log/log.h" -#include "../texture.h" #include "../util/misc.h" #include "terrain.h" diff --git a/libopenage/terrain/terrain_chunk.h b/libopenage/terrain/terrain_chunk.h index 0fd5cceecd..4d6d7e24f4 100644 --- a/libopenage/terrain/terrain_chunk.h +++ b/libopenage/terrain/terrain_chunk.h @@ -1,4 +1,4 @@ -// Copyright 2013-2018 the openage authors. See copying.md for legal info. +// Copyright 2013-2023 the openage authors. See copying.md for legal info. #pragma once @@ -7,7 +7,6 @@ #include "../coord/pixel.h" #include "../coord/tile.h" -#include "../texture.h" #include "../util/file.h" namespace openage { diff --git a/libopenage/terrain/terrain_object.cpp b/libopenage/terrain/terrain_object.cpp index fdcf611ae3..c4f4c6c9f9 100644 --- a/libopenage/terrain/terrain_object.cpp +++ b/libopenage/terrain/terrain_object.cpp @@ -10,12 +10,10 @@ #include "../coord/tile.h" #include "../error/error.h" #include "../legacy_engine.h" -#include "../texture.h" #include "../unit/unit.h" #include "terrain.h" #include "terrain_chunk.h" -#include "terrain_outline.h" namespace openage { @@ -59,10 +57,6 @@ bool TerrainObject::check_collisions() const { return this->state == object_state::placed; } -void TerrainObject::draw_outline(const coord::CoordManager &coord) const { - this->outline_texture->draw(coord, this->pos.draw); -} - bool TerrainObject::place(object_state init_state) { if (this->state == object_state::removed) { throw Error(MSG(err) << "Building cannot change state with no position"); @@ -279,13 +273,8 @@ void TerrainObject::place_unchecked(const std::shared_ptr &t, coord::ph } SquareObject::SquareObject(Unit &u, coord::tile_delta foundation_size) : - SquareObject(u, foundation_size, square_outline(foundation_size)) { -} - -SquareObject::SquareObject(Unit &u, coord::tile_delta foundation_size, std::shared_ptr out_tex) : TerrainObject(u), size(foundation_size) { - this->outline_texture = out_tex; } SquareObject::~SquareObject() = default; @@ -369,13 +358,8 @@ coord::phys_t SquareObject::min_axis() const { } RadialObject::RadialObject(Unit &u, float rad) : - RadialObject(u, rad, radial_outline(rad)) { -} - -RadialObject::RadialObject(Unit &u, float rad, std::shared_ptr out_tex) : TerrainObject(u), phys_radius(rad) { - this->outline_texture = out_tex; } RadialObject::~RadialObject() = default; diff --git a/libopenage/terrain/terrain_object.h b/libopenage/terrain/terrain_object.h index 4b6f76097b..2f9384b6ce 100644 --- a/libopenage/terrain/terrain_object.h +++ b/libopenage/terrain/terrain_object.h @@ -123,11 +123,6 @@ class TerrainObject : public std::enable_shared_from_this { */ std::function draw; - /** - * draws outline of this terrain space in current position - */ - void draw_outline(const coord::CoordManager &coord) const; - /** * changes the placement state of this object keeping the existing * position. this is useful for upgrading a floating building to a placed state @@ -254,11 +249,6 @@ class TerrainObject : public std::enable_shared_from_this { TerrainObject *parent; std::vector> children; - /** - * texture for drawing outline - */ - std::shared_ptr outline_texture; - /** * placement function which does not check passibility * used only when passibilty is already checked @@ -309,8 +299,6 @@ class SquareObject : public TerrainObject { private: SquareObject(Unit &u, coord::tile_delta foundation_size); - SquareObject(Unit &u, coord::tile_delta foundation_size, std::shared_ptr out_tex); - friend class TerrainObject; friend class Unit; @@ -342,7 +330,6 @@ class RadialObject : public TerrainObject { private: RadialObject(Unit &u, float rad); - RadialObject(Unit &u, float rad, std::shared_ptr out_tex); friend class TerrainObject; friend class Unit; diff --git a/libopenage/terrain/terrain_outline.cpp b/libopenage/terrain/terrain_outline.cpp deleted file mode 100644 index 6c20fd5983..0000000000 --- a/libopenage/terrain/terrain_outline.cpp +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2014-2019 the openage authors. See copying.md for legal info. - -#include -#include - -#include "../texture.h" -#include "terrain_outline.h" - -namespace openage { - -std::shared_ptr square_outline(coord::tile_delta foundation_size) { - int width = (foundation_size.ne + foundation_size.se) * 48; - int height = (foundation_size.ne + foundation_size.se) * 24; - - auto image_data = std::make_unique(width * height); - for (int i = 0; i < width; ++i) { - for (int j = 0; j < height; ++j) { - float w_percent = (float) abs(i - (width / 2)) / (float) (width / 2); - float h_percent = (float) abs(j - (height / 2)) / (float) (height / 2); - - // draw line where (w_percent + h_percent) == 1 - // line variable is in range 0.0 to 1.0 - float line = 1.0f - fabs(1.0f - fabs(h_percent + w_percent)); - unsigned char inten = 255 * pow(line, 16 * width/96); - image_data[i + j * width] = (inten << 24) | (inten << 16) | (inten << 8) | inten; - } - } - - return std::make_shared(width, height, std::move(image_data)); -} - -std::shared_ptr radial_outline(float radius) { - // additional pixels around the edge - int border = 4; - - // image size - int width = border + radius * 96 * 2; - int height = border + radius * 48 * 2; - int half_width = width / 2; - int half_height = height / 2; - - auto image_data = std::make_unique(width * height); - - for (int i = 0; i < width; ++i) { - for (int j = 0; j < height; ++j) { - float w_percent = (float) (border+i-half_width) / (float) (half_width-border); - float h_percent = (float) (border/2+j-half_height) / (float) (half_height-border/2); - - // line drawn where distance to image center == 1 - // line variable is in range 0.0 to 1.0 - float line = 1.0f - fabs(1.0f - std::hypot(w_percent, h_percent)); - unsigned char inten = 255 * pow(line, 32 * width/96); - image_data[i + j * width] = (inten << 24) | (inten << 16) | (inten << 8) | inten; - } - } - - return std::make_shared(width, height, std::move(image_data)); -} - -} // namespace openage diff --git a/libopenage/terrain/terrain_outline.h b/libopenage/terrain/terrain_outline.h deleted file mode 100644 index 8e66fadb6d..0000000000 --- a/libopenage/terrain/terrain_outline.h +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2014-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "../coord/tile.h" - -namespace openage { - -class Texture; - -/** - * Generate a isometric square outline texture - */ -std::shared_ptr square_outline(coord::tile_delta foundation_size); - -/** - * Generate a isometric circle outline texture - */ -std::shared_ptr radial_outline(float radius); - -} // namespace openage diff --git a/libopenage/texture.cpp b/libopenage/texture.cpp deleted file mode 100644 index ec5ebf2202..0000000000 --- a/libopenage/texture.cpp +++ /dev/null @@ -1,447 +0,0 @@ -// Copyright 2013-2019 the openage authors. See copying.md for legal info. - -#include "texture.h" - -#include -#include - -#include -#include - -#include "log/log.h" -#include "error/error.h" -#include "util/csv.h" -#include "coord/phys.h" - -namespace openage { - -// TODO: remove these global variables!!! -// definition of the shaders, -// they are "external" in the header. -namespace texture_shader { -shader::Program *program; -GLint texture, tex_coord; -} - -namespace teamcolor_shader { -shader::Program *program; -GLint texture, tex_coord; -GLint player_id_var, alpha_marker_var, player_color_var; -} - -namespace alphamask_shader { -shader::Program *program; -GLint base_texture, mask_texture, base_coord, mask_coord, show_mask; -} - -Texture::Texture(int width, int height, std::unique_ptr data) - : - use_metafile{false} { - ENSURE(glGenBuffers != nullptr, "gl not initialized properly"); - - this->w = width; - this->h = height; - this->buffer = std::make_unique(); - this->buffer->transferred = false; - this->buffer->texture_format_in = GL_RGBA8; - this->buffer->texture_format_out = GL_RGBA; - this->buffer->data = std::move(data); - this->subtextures.push_back({0, 0, this->w, this->h, this->w/2, this->h/2}); -} - -Texture::Texture(const util::Path &filename, bool use_metafile) - : - use_metafile{use_metafile}, - filename{filename} { - - // load the texture upon creation - this->load(); -} - -void Texture::load() { - // TODO: use libpng directly. - SDL_Surface *surface; - - // TODO: this will break if there is no native path. - // but then we need to load the image - // from the buffer provided by this->filename.open_r().read(). - - std::string native_path = this->filename.resolve_native_path(); - surface = IMG_Load(native_path.c_str()); - - if (!surface) { - throw Error( - MSG(err) << - "SDL_Image could not load texture from " - << this->filename << " (= " << native_path << "): " - << IMG_GetError() - ); - } else { - log::log(MSG(dbg) << "Texture has been loaded from " << native_path); - } - - this->buffer = std::make_unique(); - - // glTexImage2D format determination - switch (surface->format->BytesPerPixel) { - case 3: // RGB 24 bit - this->buffer->texture_format_in = GL_RGB8; - this->buffer->texture_format_out - = surface->format->Rmask == 0x000000ff - ? GL_RGB - : GL_BGR; - break; - case 4: // RGBA 32 bit - this->buffer->texture_format_in = GL_RGBA8; - this->buffer->texture_format_out - = surface->format->Rmask == 0x000000ff - ? GL_RGBA - : GL_BGRA; - break; - default: - throw Error(MSG(err) << - "Unknown texture bit depth for " << this->filename << ": " << - surface->format->BytesPerPixel << " bytes per pixel"); - - } - - this->w = surface->w; - this->h = surface->h; - - // temporary buffer for pixel data - this->buffer->transferred = false; - this->buffer->data = std::make_unique(this->w * this->h); - std::memcpy( - this->buffer->data.get(), - surface->pixels, - this->w * this->h * surface->format->BytesPerPixel - ); - SDL_FreeSurface(surface); - - if (use_metafile) { - // get subtexture information from the exported metainfo file - this->subtextures = util::read_csv_file( - filename.with_suffix(".docx") - ); - } - else { - // we don't have a subtexture description file. - // use the whole image as one texture then. - gamedata::subtexture s{0, 0, this->w, this->h, this->w/2, this->h/2}; - - this->subtextures.push_back(s); - } -} - -GLuint Texture::make_gl_texture(int iformat, int oformat, int w, int h, void *data) const { - // generate 1 texture handle - GLuint textureid; - glGenTextures(1, &textureid); - glBindTexture(GL_TEXTURE_2D, textureid); - - // sdl surface -> opengl texture - glTexImage2D( - GL_TEXTURE_2D, 0, - iformat, w, h, 0, - oformat, GL_UNSIGNED_BYTE, data - ); - - // settings for later drawing - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - - return textureid; -} - -void Texture::load_in_glthread() const { - if (not this->buffer->transferred) { - this->buffer->id = this->make_gl_texture( - this->buffer->texture_format_in, - this->buffer->texture_format_out, - this->w, - this->h, - this->buffer->data.get() - ); - this->buffer->data = nullptr; - glGenBuffers(1, &this->buffer->vertbuf); - this->buffer->transferred = true; - } -} - - -void Texture::unload() { - glDeleteTextures(1, &this->buffer->id); - glDeleteBuffers(1, &this->buffer->vertbuf); -} - - -void Texture::reload() { - this->unload(); - this->load(); -} - - -Texture::~Texture() { - this->unload(); -} - - -void Texture::fix_hotspots(unsigned x, unsigned y) { - for (auto &subtexture : this->subtextures) { - subtexture.cx = x; - subtexture.cy = y; - } -} - - -void Texture::draw(const coord::CoordManager &mgr, const coord::camhud pos, - unsigned int mode, bool mirrored, - int subid, unsigned player) const { - this->draw(pos.to_viewport(mgr), mode, mirrored, subid, player, nullptr, -1); -} - - -void Texture::draw(const coord::CoordManager &mgr, coord::camgame pos, - unsigned int mode, bool mirrored, - int subid, unsigned player) const { - this->draw(pos.to_viewport(mgr), mode, mirrored, subid, player, nullptr, -1); -} - - -void Texture::draw(const coord::CoordManager &mgr, coord::phys3 pos, - unsigned int mode, bool mirrored, - int subid, unsigned player) const { - this->draw(pos.to_viewport(mgr), mode, mirrored, subid, player, nullptr, -1); -} - - -void Texture::draw(const coord::CoordManager &mgr, const Terrain &terrain, - coord::tile pos, unsigned int mode, int subid, - Texture *alpha_texture, int alpha_subid) const { - - // currently used for drawing terrain tiles. - this->draw(pos.to_viewport(mgr, terrain), mode, false, - subid, 0, alpha_texture, alpha_subid); -} - - -void Texture::draw(coord::viewport pos, - unsigned int mode, bool mirrored, - int subid, unsigned player, - Texture *alpha_texture, int alpha_subid) const { - - this->load_in_glthread(); - - glColor4f(1, 1, 1, 1); - - bool use_playercolors = false; - bool use_alphashader = false; - const gamedata::subtexture *mtx; - - int *pos_id, *texcoord_id, *masktexcoord_id; - - // is this texture drawn with an alpha mask? - if ((mode & ALPHAMASKED) && alpha_subid >= 0 && alpha_texture != nullptr) { - alphamask_shader::program->use(); - - // bind the alpha mask texture to slot 1 - glActiveTexture(GL_TEXTURE1); - glEnable(GL_TEXTURE_2D); - glBindTexture(GL_TEXTURE_2D, alpha_texture->get_texture_id()); - - // get the alphamask subtexture (the blend mask!) - mtx = alpha_texture->get_subtexture(alpha_subid); - pos_id = &alphamask_shader::program->pos_id; - texcoord_id = &alphamask_shader::base_coord; - masktexcoord_id = &alphamask_shader::mask_coord; - use_alphashader = true; - } - // is this texure drawn with replaced pixels for team coloring? - else if (mode & PLAYERCOLORED) { - teamcolor_shader::program->use(); - - //set the desired player id in the shader - glUniform1i(teamcolor_shader::player_id_var, player); - pos_id = &teamcolor_shader::program->pos_id; - texcoord_id = &teamcolor_shader::tex_coord; - use_playercolors = true; - } - // mkay, we just draw the plain texture otherwise. - else { - texture_shader::program->use(); - pos_id = &texture_shader::program->pos_id; - texcoord_id = &texture_shader::tex_coord; - } - - glActiveTexture(GL_TEXTURE0); - glEnable(GL_TEXTURE_2D); - glBindTexture(GL_TEXTURE_2D, this->buffer->id); - - const gamedata::subtexture *tx = this->get_subtexture(subid); - - int left, right, top, bottom; - - // coordinates where the texture will be drawn on screen. - bottom = pos.y - (tx->h - tx->cy); - top = bottom + tx->h; - - if (not mirrored) { - left = pos.x - tx->cx; - right = left + tx->w; - } else { - left = pos.x + tx->cx; - right = left - tx->w; - } - - // convert the texture boundaries to float - // these will be the vertex coordinates. - float leftf, rightf, topf, bottomf; - leftf = (float) left; - rightf = (float) right; - topf = (float) top; - bottomf = (float) bottom; - - // subtexture coordinates - // left, right, top and bottom bounds as coordinates - // these pick the requested area out of the big texture. - float txl, txr, txt, txb; - this->get_subtexture_coordinates(tx, &txl, &txr, &txt, &txb); - - float mtxl=0, mtxr=0, mtxt=0, mtxb=0; - if (use_alphashader) { - alpha_texture->get_subtexture_coordinates(mtx, &mtxl, &mtxr, &mtxt, &mtxb); - } - - // this array will be uploaded to the GPU. - // it contains all dynamic vertex data (position, tex coordinates, mask coordinates) - float vdata[] { - leftf, topf, - leftf, bottomf, - rightf, bottomf, - rightf, topf, - txl, txt, - txl, txb, - txr, txb, - txr, txt, - mtxl, mtxt, - mtxl, mtxb, - mtxr, mtxb, - mtxr, mtxt - }; - - - // store vertex buffer data, TODO: prepare this sometime earlier. - glBindBuffer(GL_ARRAY_BUFFER, this->buffer->vertbuf); - glBufferData(GL_ARRAY_BUFFER, sizeof(vdata), vdata, GL_STREAM_DRAW); - - // enable vertex buffer and bind it to the vertex attribute - glEnableVertexAttribArray(*pos_id); - glEnableVertexAttribArray(*texcoord_id); - if (use_alphashader) { - glEnableVertexAttribArray(*masktexcoord_id); - } - - // set data types, offsets in the vdata array - glVertexAttribPointer(*pos_id, 2, GL_FLOAT, GL_FALSE, 0, (void *)(0)); - glVertexAttribPointer(*texcoord_id, 2, GL_FLOAT, GL_FALSE, 0, (void *)(sizeof(float) * 8)); - if (use_alphashader) { - glVertexAttribPointer(*masktexcoord_id, 2, GL_FLOAT, GL_FALSE, 0, (void *)(sizeof(float) * 8 * 2)); - } - - // draw the vertex array - glDrawArrays(GL_QUADS, 0, 4); - - - // unbind the current buffer - glBindBuffer(GL_ARRAY_BUFFER, 0); - - glDisableVertexAttribArray(*pos_id); - glDisableVertexAttribArray(*texcoord_id); - if (use_alphashader) { - glDisableVertexAttribArray(*masktexcoord_id); - } - - // disable the shaders. - if (use_playercolors) { - teamcolor_shader::program->stopusing(); - } else if (use_alphashader) { - alphamask_shader::program->stopusing(); - glActiveTexture(GL_TEXTURE1); - glDisable(GL_TEXTURE_2D); - } else { - texture_shader::program->stopusing(); - } - - glActiveTexture(GL_TEXTURE0); - glDisable(GL_TEXTURE_2D); - - //////////////////////////////////////// - /* - int size = 2; - float r = 1.0f, g = 1.0f, b = 0.0f; - glPushMatrix(); - glTranslatef(leftf, bottomf, 0); - glColor3f(r, g, b); - glBegin(GL_TRIANGLES); - glVertex3f(-size, -size, 0); - glVertex3f(-size, size, 0); - glVertex3f(size, size, 0); - glVertex3f(size, size, 0); - glVertex3f(-size, -size, 0); - glVertex3f(size, -size, 0); - glEnd(); - glPopMatrix(); - */ - //////////////////////////////////////// -} - - -const gamedata::subtexture *Texture::get_subtexture(uint64_t subid) const { - if (subid < this->subtextures.size()) { - return &this->subtextures[subid]; - } - else { - throw Error{ - ERR << "Unknown subtexture requested for texture " - << this->filename << ": " << subid - }; - } -} - - -void Texture::get_subtexture_coordinates(uint64_t subid, - float *txl, float *txr, - float *txt, float *txb) const { - const gamedata::subtexture *tx = this->get_subtexture(subid); - this->get_subtexture_coordinates(tx, txl, txr, txt, txb); -} - - -void Texture::get_subtexture_coordinates(const gamedata::subtexture *tx, - float *txl, float *txr, - float *txt, float *txb) const { - *txl = ((float)tx->x) /this->w; - *txr = ((float)(tx->x + tx->w)) /this->w; - *txt = ((float)tx->y) /this->h; - *txb = ((float)(tx->y + tx->h)) /this->h; -} - - -size_t Texture::get_subtexture_count() const { - return this->subtextures.size(); -} - - -void Texture::get_subtexture_size(uint64_t subid, int *w, int *h) const { - const gamedata::subtexture *subtex = this->get_subtexture(subid); - *w = subtex->w; - *h = subtex->h; -} - - -GLuint Texture::get_texture_id() const { - this->load_in_glthread(); - return this->buffer->id; -} - -} // openage diff --git a/libopenage/texture.h b/libopenage/texture.h deleted file mode 100644 index 64283e86cc..0000000000 --- a/libopenage/texture.h +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include - -#include "coord/pixel.h" -#include "coord/tile.h" -#include "gamedata/texture_dummy.h" -#include "shader/program.h" -#include "shader/shader.h" -#include "util/path.h" - - -namespace openage { -class Terrain; - -namespace util { -class Path; -} - -namespace coord { -class CoordManager; -} - -namespace texture_shader { -extern shader::Program *program; -extern GLint texture, tex_coord; -} // namespace texture_shader - -namespace teamcolor_shader { -extern shader::Program *program; -extern GLint texture, tex_coord; -extern GLint player_id_var, alpha_marker_var, player_color_var; -} // namespace teamcolor_shader - -namespace alphamask_shader { -extern shader::Program *program; -extern GLint base_texture, mask_texture, base_coord, mask_coord, show_mask; -} // namespace alphamask_shader - -// bitmasks for shader modes -constexpr int PLAYERCOLORED = 1 << 0; -constexpr int ALPHAMASKED = 1 << 1; - -/** - * enables transfer of data to opengl - */ -struct [[deprecated]] gl_texture_buffer { - GLuint id, vertbuf; - - // this requires loading on the main thread - bool transferred; - int texture_format_in; - int texture_format_out; - std::unique_ptr data; -}; - - -/** - * A texture for rendering graphically. - * - * You may believe it or not, but this class represents a single texture, - * which can be drawn on the screen. - * - * The class supports subtextures, so that one big texture can contain - * several small images. These are the ones actually to be rendered. - * - * TODO: Deprecated, replaced by new renderer - */ -class [[deprecated]] Texture { -public: - int w; - int h; - - /** - * Create a texture from a rgba8 array. - * It will have w * h * 32bit storage. - */ - Texture(int width, int height, std::unique_ptr data); - - /** - * Create a texture from a existing image file. - * For supported image file types, see the SDL_Image initialization in the engine. - */ - Texture(const util::Path &filename, bool use_metafile = false); - ~Texture(); - - /** - * Draws the texture at hud coordinates. - */ - void draw(const coord::CoordManager &mgr, coord::camhud pos, unsigned int mode = 0, bool mirrored = false, int subid = 0, unsigned player = 0) const; - - /** - * Draws the texture at game coordinates. - */ - void draw(const coord::CoordManager &mgr, coord::camgame pos, unsigned int mode = 0, bool mirrored = false, int subid = 0, unsigned player = 0) const; - - /** - * Draws the texture at phys coordinates. - */ - void draw(const coord::CoordManager &mgr, coord::phys3 pos, unsigned int mode = 0, bool mirrored = false, int subid = 0, unsigned player = 0) const; - - /** - * Draws the texture at tile coordinates. - */ - void draw(const coord::CoordManager &mgr, const Terrain &terrain, coord::tile pos, unsigned int mode, int subid, Texture *alpha_texture, int alpha_subid) const; - - /** - * Draws the texture at window coordinates. - */ - void draw(coord::viewport pos, unsigned int mode, bool mirrored, int subid, unsigned player, Texture *alpha_texture, int alpha_subid) const; - - /** - * Reload the image file. Used for inotify refreshing. - */ - void reload(); - - /** - * Get the subtexture coordinates by its id. - */ - const gamedata::subtexture *get_subtexture(uint64_t subid) const; - - /** - * @return the number of available subtextures - */ - size_t get_subtexture_count() const; - - /** - * Fetch the size of the given subtexture. - * @param subid: index of the requested subtexture - * @param w: the subtexture width - * @param h: the subtexture height - */ - void get_subtexture_size(uint64_t subid, int *w, int *h) const; - - /** - * get atlas subtexture coordinates. - * - * left, right, top and bottom bounds as coordinates - * these pick the requested area out of the big texture. - * returned as floats in range 0.0 to 1.0 - */ - void get_subtexture_coordinates(uint64_t subid, float *txl, float *txr, float *txt, float *txb) const; - void get_subtexture_coordinates(const gamedata::subtexture *subtex, float *txl, float *txr, float *txt, float *txb) const; - - /** - * fixes the hotspots of all subtextures to (x,y). - * this is a temporary workaround; such fixes should actually be done in the - * convert script. - */ - void fix_hotspots(unsigned x, unsigned y); - - /** - * activates the influence of a given alpha mask to this texture. - */ - void activate_alphamask(Texture *mask, uint64_t subid); - - /** - * disable a previously activated alpha mask. - */ - void disable_alphamask(); - - /** - * returns the opengl texture id of this texture. - */ - GLuint get_texture_id() const; - -private: - std::unique_ptr buffer; - std::vector subtextures; - bool use_metafile; - - util::Path filename; - - void load(); - - /** - * The texture loadin must occur on the thread that manages the gl context. - */ - void load_in_glthread() const; - GLuint make_gl_texture(int iformat, int oformat, int w, int h, void *) const; - void unload(); -}; - -} // namespace openage diff --git a/libopenage/unit/producer.cpp b/libopenage/unit/producer.cpp index 2e6ed907e6..d03192ee60 100644 --- a/libopenage/unit/producer.cpp +++ b/libopenage/unit/producer.cpp @@ -2,12 +2,11 @@ #include -#include "../legacy_engine.h" #include "../gamedata/unit_dummy.h" +#include "../legacy_engine.h" #include "../log/log.h" #include "../terrain/terrain.h" #include "../terrain/terrain_object.h" -#include "../terrain/terrain_outline.h" #include "../util/strings.h" #include "ability.h" #include "action.h" @@ -83,7 +82,6 @@ ObjectProducer::ObjectProducer(const Player &owner, const GameSpec &spec, const UnitType(owner), dataspec(spec), unit_data(*ud), - terrain_outline{nullptr}, default_tex{spec.get_unit_texture(ud->idle_graphic0)}, dead_unit_id{ud->dead_unit_id} { // copy the class type @@ -114,14 +112,6 @@ ObjectProducer::ObjectProducer(const Player &owner, const GameSpec &spec, const static_cast(this->unit_data.radius_y * 2), }; - // shape of the outline - if (this->unit_data.obstruction_class > 1) { - this->terrain_outline = radial_outline(this->unit_data.radius_x); - } - else { - this->terrain_outline = square_outline(this->foundation_size); - } - // graphic set auto standing = spec.get_unit_texture(this->unit_data.idle_graphic0); if (!standing) { @@ -277,14 +267,6 @@ void ObjectProducer::initialise(Unit *unit, Player &player) { } TerrainObject *ObjectProducer::place(Unit *u, std::shared_ptr terrain, coord::phys3 init_pos) const { - // create new object with correct base shape - if (this->unit_data.obstruction_class > 1) { - u->make_location(this->unit_data.radius_x, this->terrain_outline); - } - else { - u->make_location(this->foundation_size, this->terrain_outline); - } - // find set of allowed terrains std::unordered_set terrains = allowed_terrains(this->unit_data.terrain_restriction); @@ -532,8 +514,6 @@ BuildingProducer::BuildingProducer(const Player &owner, const GameSpec &spec, co this->graphics[graphic_type::dying] = dying_tex; } - this->terrain_outline = square_outline(this->foundation_size); - // TODO get cost, temp fixed cost of 100 wood this->cost.set(cost_type::constant, create_resource_cost(game_resource::wood, 100)); } @@ -646,9 +626,6 @@ std::vector BuildingProducer::get_accepted_resources() { } TerrainObject *BuildingProducer::place(Unit *u, std::shared_ptr terrain, coord::phys3 init_pos) const { - // buildings have a square base - u->make_location(this->foundation_size, this->terrain_outline); - /* * decide what terrain is passable using this lambda * currently unit avoids water and tiles with another unit @@ -762,9 +739,6 @@ ProjectileProducer::ProjectileProducer(const Player &owner, const GameSpec &spec if (destroyed) { this->graphics[graphic_type::dying] = destroyed; } - - // outline - terrain_outline = radial_outline(pd->radius_y); } ProjectileProducer::~ProjectileProducer() = default; @@ -803,11 +777,6 @@ void ProjectileProducer::initialise(Unit *unit, Player &player) { } TerrainObject *ProjectileProducer::place(Unit *u, std::shared_ptr terrain, coord::phys3 init_pos) const { - /* - * radial base shape without collision checking - */ - u->make_location(this->unit_data.radius_y, this->terrain_outline); - TerrainObject *obj_ptr = u->location.get(); std::weak_ptr terrain_ptr = terrain; u->location->passable = [obj_ptr, u, terrain_ptr](const coord::phys3 &pos) -> bool { diff --git a/libopenage/unit/producer.h b/libopenage/unit/producer.h index 804df598cf..53036078de 100644 --- a/libopenage/unit/producer.h +++ b/libopenage/unit/producer.h @@ -1,4 +1,4 @@ -// Copyright 2014-2021 the openage authors. See copying.md for legal info. +// Copyright 2014-2023 the openage authors. See copying.md for legal info. #pragma once @@ -19,7 +19,6 @@ class GameMain; class GameSpec; class Terrain; class TerrainObject; -class Texture; class Sound; class UnitAbility; @@ -32,7 +31,7 @@ std::unordered_set allowed_terrains(const gamedata::ground_type &rest /** * base game data unit type */ -class ObjectProducer: public UnitType { +class ObjectProducer : public UnitType { public: ObjectProducer(const Player &owner, const GameSpec &spec, const gamedata::unit_object *ud); virtual ~ObjectProducer(); @@ -57,7 +56,6 @@ class ObjectProducer: public UnitType { */ const Sound *on_create; const Sound *on_destroy; - std::shared_ptr terrain_outline; std::shared_ptr default_tex; int dead_unit_id; }; @@ -65,7 +63,7 @@ class ObjectProducer: public UnitType { /** * movable unit types */ -class MovableProducer: public ObjectProducer { +class MovableProducer : public ObjectProducer { public: MovableProducer(const Player &owner, const GameSpec &spec, const gamedata::projectile_unit *); virtual ~MovableProducer(); @@ -80,7 +78,6 @@ class MovableProducer: public ObjectProducer { const Sound *on_move; const Sound *on_attack; int projectile; - }; /** @@ -88,7 +85,7 @@ class MovableProducer: public ObjectProducer { * Stores graphics and attributes for a single unit type * in aoe living units are derived from objects */ -class LivingProducer: public MovableProducer { +class LivingProducer : public MovableProducer { public: LivingProducer(const Player &owner, const GameSpec &spec, const gamedata::living_unit *); virtual ~LivingProducer(); @@ -105,7 +102,7 @@ class LivingProducer: public MovableProducer { * Will be replaced with nyan system in future * in aoe buildings are derived from living units */ -class BuildingProducer: public UnitType { +class BuildingProducer : public UnitType { public: BuildingProducer(const Player &owner, const GameSpec &spec, @@ -126,7 +123,6 @@ class BuildingProducer: public UnitType { */ const Sound *on_create; const Sound *on_destroy; - std::shared_ptr terrain_outline; std::shared_ptr texture; std::shared_ptr destroyed; int projectile; @@ -146,7 +142,7 @@ class BuildingProducer: public UnitType { * creates projectiles * todo use MovableProducer as base class */ -class ProjectileProducer: public UnitType { +class ProjectileProducer : public UnitType { public: ProjectileProducer(const Player &owner, const GameSpec &spec, const gamedata::missile_unit *); virtual ~ProjectileProducer(); @@ -159,7 +155,6 @@ class ProjectileProducer: public UnitType { private: const gamedata::missile_unit unit_data; - std::shared_ptr terrain_outline; std::shared_ptr tex; std::shared_ptr sh; // shadow texture std::shared_ptr destroyed; diff --git a/libopenage/unit/unit_texture.cpp b/libopenage/unit/unit_texture.cpp index a146f471b2..0d0636f53a 100644 --- a/libopenage/unit/unit_texture.cpp +++ b/libopenage/unit/unit_texture.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "unit_texture.h" @@ -9,19 +9,16 @@ #include "../coord/pixel.h" #include "../gamestate/old/game_spec.h" #include "../log/log.h" -#include "../texture.h" #include "../util/math.h" #include "../util/math_constants.h" namespace openage { -UnitTexture::UnitTexture(GameSpec &spec, uint16_t graphic_id, bool delta) - : +UnitTexture::UnitTexture(GameSpec &spec, uint16_t graphic_id, bool delta) : UnitTexture{spec, spec.get_graphic_data(graphic_id), delta} {} -UnitTexture::UnitTexture(GameSpec &spec, const gamedata::graphic *graphic, bool delta) - : +UnitTexture::UnitTexture(GameSpec &spec, const gamedata::graphic *graphic, bool delta) : id{graphic->graphic_id}, sound_id{graphic->sound_id}, frame_count{graphic->frame_count}, @@ -30,7 +27,6 @@ UnitTexture::UnitTexture(GameSpec &spec, const gamedata::graphic *graphic, bool frame_rate{graphic->frame_rate}, use_up_angles{graphic->mirroring_mode == 24}, use_deltas{delta}, - texture{nullptr}, draw_this{true}, sound{nullptr}, delta_id{graphic->graphic_deltas.data} { @@ -38,125 +34,23 @@ UnitTexture::UnitTexture(GameSpec &spec, const gamedata::graphic *graphic, bool } bool UnitTexture::is_valid() const { - return texture; + return false; } coord::viewport UnitTexture::size() const { - return coord::viewport{this->texture->w, this->texture->h}; + return coord::viewport{0, 0}; } void UnitTexture::sample(const coord::CoordManager &coord, const coord::camhud &draw_pos, unsigned color) const { - - // draw delta list first - for (auto &d : this->deltas) { - coord::camhud_delta dlt = coord::camhud_delta{d.second.x, d.second.y}; - d.first->sample(coord, draw_pos + dlt, color); - } - - // draw texture - if (this->draw_this) { - this->texture->draw(coord, draw_pos, PLAYERCOLORED, false, 0, color); - } } void UnitTexture::draw(const coord::CoordManager &coord, const coord::camgame &draw_pos, unsigned int frame, unsigned color) const { - - // draw delta list first - for (auto &d : this->deltas) { - d.first->draw(coord, draw_pos + d.second, frame, color); - } - - // draw texture - if (this->draw_this) { - unsigned int to_draw = frame % this->texture->get_subtexture_count(); - this->texture->draw(coord, draw_pos, PLAYERCOLORED, false, to_draw, color); - } } void UnitTexture::draw(const coord::CoordManager &coord, const coord::camgame &draw_pos, coord::phys3_delta &dir, unsigned int frame, unsigned color) const { - unsigned int frame_to_use = frame; - if (this->use_up_angles) { - // up 1 => tilt 0 - // up -1 => tilt 1 - // up has a scale 5 times smaller - double len = math::hypot3(dir.ne.to_double(), dir.se.to_double(), dir.up.to_double()/5); - double up = dir.up.to_double()/(5.0 * len); - frame_to_use = (0.5 - (0.5 * up)) * this->frame_count; - } - else if (this->sound && frame == 0.0) { - this->sound->play(); - } - - // the index for the current direction - unsigned int angle = dir_group(dir, this->angle_count); - - // mirroring is used to make additional image sets - bool mirror = false; - if (this->angles_included <= angle) { - // this->angles_included <= angle < this->angle_count - angle = this->top_frame - angle; - mirror = true; - } - - // draw delta list first - for (auto &d : this->deltas) { - d.first->draw(coord, draw_pos + d.second, dir, frame, color); - } - - if (this->draw_this) { - unsigned int to_draw = this->subtexture(this->texture, angle, frame_to_use); - this->texture->draw(coord, draw_pos, PLAYERCOLORED, mirror, to_draw, color); - } } void UnitTexture::initialise(GameSpec &spec) { - this->texture = spec.get_texture(this->id); - this->sound = spec.get_sound(this->sound_id); - if (not is_valid()) { - this->draw_this = false; - } - - // find deltas - if (this->use_deltas) for (auto d : this->delta_id) { - if (spec.get_graphic_data(d.graphic_id)) { - auto ut = std::make_unique(spec, d.graphic_id, false); - if (ut->is_valid()) { - this->deltas.push_back({std::move(ut), coord::camgame_delta{d.offset_x, d.offset_y}}); - } - } - } - - if (this->draw_this) { - - // the graphic frame count includes deltas - unsigned int subtextures = this->texture->get_subtexture_count(); - if (subtextures >= this->frame_count) { - - // angles with graphic data - this->angles_included = subtextures / this->frame_count; - this->angles_mirrored = this->angle_count - this->angles_included; - this->safe_frame_count = this->frame_count; - } - else { - this->angles_included = 1; - this->angles_mirrored = 0; - this->safe_frame_count = subtextures; - } - - // find the top direction for mirroring over - this->top_frame = this->angle_count - (1 - (this->angles_included - this->angles_mirrored) / 2); - } -} - -unsigned int UnitTexture::subtexture(const Texture *t, unsigned int angle, unsigned int frame) const { - unsigned int tex_frames = t->get_subtexture_count(); - unsigned int count = tex_frames / this->angles_included; - unsigned int to_draw = angle * count + (frame % count); - if (tex_frames <= to_draw) { - log::log(MSG(err) << "Subtexture out of range (" << angle << ", " << frame << ")"); - return 0; - } - return to_draw; } unsigned int dir_group(coord::phys3_delta dir, unsigned int angles) { @@ -169,9 +63,9 @@ unsigned int dir_group(coord::phys3_delta dir, unsigned int angles) { // formula to find the correct angle return static_cast( - round(angles * atan2(dir_se, dir_ne) * (math::INV_PI / 2)) - + first_angle - ) % angles; + round(angles * atan2(dir_se, dir_ne) * (math::INV_PI / 2)) + + first_angle) + % angles; } diff --git a/libopenage/unit/unit_texture.h b/libopenage/unit/unit_texture.h index 20efe9eede..7e44899f8c 100644 --- a/libopenage/unit/unit_texture.h +++ b/libopenage/unit/unit_texture.h @@ -1,4 +1,4 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once @@ -23,7 +23,7 @@ class Sound; * * This type can also deal with playing position based game sounds. */ -class UnitTexture { +class [[deprecated]] UnitTexture { public: /** * Delta option specifies whether the delta graphics are included. @@ -31,29 +31,29 @@ class UnitTexture { * Note that the game data contains loops in delta links * which mean recursive loading should be avoided */ - UnitTexture(GameSpec &spec, uint16_t graphic_id, bool delta=true); - UnitTexture(GameSpec &spec, const gamedata::graphic *graphic, bool delta=true); + UnitTexture(GameSpec &spec, uint16_t graphic_id, bool delta = true); + UnitTexture(GameSpec &spec, const gamedata::graphic *graphic, bool delta = true); /** * const attributes of the graphic */ - const int16_t id; - const int16_t sound_id; + const int16_t id; + const int16_t sound_id; const unsigned int frame_count; const unsigned int angle_count; - const int16_t mirroring_mode; - const float frame_rate; + const int16_t mirroring_mode; + const float frame_rate; /** * draw object with vertical orientation (arrows) * adding an addtion degree of orientation */ - const bool use_up_angles; + const bool use_up_angles; /** * use delta information */ - const bool use_deltas; + const bool use_deltas; /** * invalid unit textures will cause errors if drawn @@ -68,7 +68,7 @@ class UnitTexture { /** * a sample drawing for hud */ - void sample(const coord::CoordManager &coord, const coord::camhud &draw_pos, unsigned color=1) const; + void sample(const coord::CoordManager &coord, const coord::camhud &draw_pos, unsigned color = 1) const; /** * draw object with no direction @@ -86,11 +86,6 @@ class UnitTexture { void initialise(GameSpec &spec); private: - /** - * use a regular texture for drawing - */ - const Texture *texture; - /** * the above frame count covers the entire graphic (with deltas) * the actual number in the base texture may be different @@ -109,11 +104,6 @@ class UnitTexture { // delta graphics std::vector, coord::camgame_delta>> deltas; - - /** - * find which subtexture should be used for drawing this texture - */ - unsigned int subtexture(const Texture *t, unsigned int angle, unsigned int frame) const; }; /** @@ -125,6 +115,6 @@ class UnitTexture { * @param first_angle offset added to angle, modulo number of angles * @return image set index */ -unsigned int dir_group(coord::phys3_delta dir, unsigned int angles=8); +unsigned int dir_group(coord::phys3_delta dir, unsigned int angles = 8); } // namespace openage From 59caca925ac137cb557d68168c95504aee3327ab Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Sep 2023 02:08:36 +0200 Subject: [PATCH 032/771] renderer: Remove most of the old draw code. --- libopenage/gamestate/old/game_spec.cpp | 10 -- libopenage/gamestate/old/game_spec.h | 12 --- libopenage/handlers.h | 13 --- libopenage/terrain/terrain_chunk.h | 7 -- libopenage/unit/CMakeLists.txt | 1 - libopenage/unit/ability.h | 90 ++++++++-------- libopenage/unit/action.cpp | 40 -------- libopenage/unit/action.h | 9 -- libopenage/unit/attribute.h | 9 +- libopenage/unit/producer.cpp | 136 +------------------------ libopenage/unit/producer.h | 9 -- libopenage/unit/unit.cpp | 67 +----------- libopenage/unit/unit.h | 18 ---- libopenage/unit/unit_texture.cpp | 72 ------------- libopenage/unit/unit_texture.h | 120 ---------------------- libopenage/unit/unit_type.cpp | 19 ++-- libopenage/unit/unit_type.h | 17 +--- 17 files changed, 55 insertions(+), 594 deletions(-) delete mode 100644 libopenage/unit/unit_texture.cpp delete mode 100644 libopenage/unit/unit_texture.h diff --git a/libopenage/gamestate/old/game_spec.cpp b/libopenage/gamestate/old/game_spec.cpp index d696b748f5..ca06c6b7e3 100644 --- a/libopenage/gamestate/old/game_spec.cpp +++ b/libopenage/gamestate/old/game_spec.cpp @@ -82,16 +82,6 @@ index_t GameSpec::get_slp_graphic(index_t slp) { return this->slp_to_graphic[slp]; } -std::shared_ptr GameSpec::get_unit_texture(index_t unit_id) const { - if (this->unit_textures.count(unit_id) == 0) { - if (unit_id > 0) { - log::log(MSG(dbg) << " -> ignoring unit_id: " << unit_id); - } - return nullptr; - } - return this->unit_textures.at(unit_id); -} - const Sound *GameSpec::get_sound(index_t sound_id) const { if (this->available_sounds.count(sound_id) == 0) { if (sound_id > 0) { diff --git a/libopenage/gamestate/old/game_spec.h b/libopenage/gamestate/old/game_spec.h index b1377fd2d9..0e03eb73a5 100644 --- a/libopenage/gamestate/old/game_spec.h +++ b/libopenage/gamestate/old/game_spec.h @@ -5,7 +5,6 @@ #include "../../gamedata/gamedata_dummy.h" #include "../../gamedata/graphic_dummy.h" #include "../../job/job.h" -#include "../../unit/unit_texture.h" #include "../../util/csv.h" #include "terrain/terrain.h" #include "types.h" @@ -90,12 +89,6 @@ class GameSpec { */ index_t get_slp_graphic(index_t slp); - /** - * get unit texture by graphic id -- this is an directional texture - * which also includes graphic deltas - */ - std::shared_ptr get_unit_texture(index_t graphic_id) const; - /** * get sound by sound id */ @@ -190,11 +183,6 @@ class GameSpec { */ std::unordered_map> commands; - /** - * graphic ids -> unit texture for that id - */ - std::unordered_map> unit_textures; - /** * sound ids mapped to playable sounds for all available sounds. */ diff --git a/libopenage/handlers.h b/libopenage/handlers.h index f695a2589e..9d8f00e536 100644 --- a/libopenage/handlers.h +++ b/libopenage/handlers.h @@ -11,19 +11,6 @@ namespace openage { class LegacyEngine; -/** - * superclass for all possible drawing operations in the game. - */ -class [[deprecated]] DrawHandler { -public: - virtual ~DrawHandler() = default; - - /** - * execute the drawing action. - */ - virtual bool on_draw() = 0; -}; - /** * superclass for all possible drawing operations in the game. */ diff --git a/libopenage/terrain/terrain_chunk.h b/libopenage/terrain/terrain_chunk.h index 4d6d7e24f4..676cfcc1e4 100644 --- a/libopenage/terrain/terrain_chunk.h +++ b/libopenage/terrain/terrain_chunk.h @@ -69,13 +69,6 @@ class TerrainChunk { */ chunk_neighbors neighbors; - /** - * draws the terrain chunk on screen. - * - * @param chunk_pos the chunk position where it will be drawn - */ - void draw(coord::chunk chunk_pos); - /** * get tile data by absolute coordinates. */ diff --git a/libopenage/unit/CMakeLists.txt b/libopenage/unit/CMakeLists.txt index 3c7a6e27cc..6550dc0234 100644 --- a/libopenage/unit/CMakeLists.txt +++ b/libopenage/unit/CMakeLists.txt @@ -9,6 +9,5 @@ add_sources(libopenage selection.cpp unit.cpp unit_container.cpp - unit_texture.cpp unit_type.cpp ) diff --git a/libopenage/unit/ability.h b/libopenage/unit/ability.h index 71985ebe8c..b2957c0585 100644 --- a/libopenage/unit/ability.h +++ b/libopenage/unit/ability.h @@ -1,4 +1,4 @@ -// Copyright 2014-2021 the openage authors. See copying.md for legal info. +// Copyright 2014-2023 the openage authors. See copying.md for legal info. #pragma once @@ -16,7 +16,6 @@ class Command; class Sound; class Unit; class UnitAction; -class UnitTexture; class UnitType; /** @@ -54,14 +53,14 @@ const ability_set ability_all = ability_set().set(); /** * the order abilities should be used when available */ -static std::vector ability_priority { - ability_type::gather, // targeting +static std::vector ability_priority{ + ability_type::gather, // targeting ability_type::convert, ability_type::repair, ability_type::heal, ability_type::attack, ability_type::build, - ability_type::move, // positional + ability_type::move, // positional ability_type::patrol, ability_type::garrison, ability_type::ungarrison, // inside buildings @@ -94,7 +93,7 @@ class UnitAbility { /** * applies command to a given unit */ - virtual void invoke(Unit &to_modify, const Command &cmd, bool play_sound=false) = 0; + virtual void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) = 0; /** * some common functions @@ -111,15 +110,14 @@ class UnitAbility { * using a brace enclosed list */ static ability_set set_from_list(const std::vector &items); - }; /** * initiates a move action when given a valid target */ -class MoveAbility: public UnitAbility { +class MoveAbility : public UnitAbility { public: - MoveAbility(const Sound *s=nullptr); + MoveAbility(const Sound *s = nullptr); ability_type type() override { return ability_type::move; @@ -127,7 +125,7 @@ class MoveAbility: public UnitAbility { bool can_invoke(Unit &to_modify, const Command &cmd) override; - void invoke(Unit &to_modify, const Command &cmd, bool play_sound=false) override; + void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; private: const Sound *sound; @@ -136,7 +134,7 @@ class MoveAbility: public UnitAbility { /** * sets the gather point on buildings */ -class SetPointAbility: public UnitAbility { +class SetPointAbility : public UnitAbility { public: SetPointAbility(); @@ -146,16 +144,16 @@ class SetPointAbility: public UnitAbility { bool can_invoke(Unit &to_modify, const Command &cmd) override; - void invoke(Unit &to_modify, const Command &cmd, bool play_sound=false) override; + void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; }; /** * ability to garrision inside a building */ -class GarrisonAbility: public UnitAbility { +class GarrisonAbility : public UnitAbility { public: - GarrisonAbility(const Sound *s=nullptr); + GarrisonAbility(const Sound *s = nullptr); ability_type type() override { return ability_type::garrison; @@ -163,7 +161,7 @@ class GarrisonAbility: public UnitAbility { bool can_invoke(Unit &to_modify, const Command &cmd) override; - void invoke(Unit &to_modify, const Command &cmd, bool play_sound=false) override; + void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; private: const Sound *sound; @@ -172,9 +170,9 @@ class GarrisonAbility: public UnitAbility { /** * ability to ungarrision a building */ -class UngarrisonAbility: public UnitAbility { +class UngarrisonAbility : public UnitAbility { public: - UngarrisonAbility(const Sound *s=nullptr); + UngarrisonAbility(const Sound *s = nullptr); ability_type type() override { return ability_type::ungarrison; @@ -182,7 +180,7 @@ class UngarrisonAbility: public UnitAbility { bool can_invoke(Unit &to_modify, const Command &cmd) override; - void invoke(Unit &to_modify, const Command &cmd, bool play_sound=false) override; + void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; private: const Sound *sound; @@ -191,9 +189,9 @@ class UngarrisonAbility: public UnitAbility { /** * buildings train new objects */ -class TrainAbility: public UnitAbility { +class TrainAbility : public UnitAbility { public: - TrainAbility(const Sound *s=nullptr); + TrainAbility(const Sound *s = nullptr); ability_type type() override { return ability_type::train; @@ -201,7 +199,7 @@ class TrainAbility: public UnitAbility { bool can_invoke(Unit &to_modify, const Command &cmd) override; - void invoke(Unit &to_modify, const Command &cmd, bool play_sound=false) override; + void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; private: const Sound *sound; @@ -210,9 +208,9 @@ class TrainAbility: public UnitAbility { /** * initiates a research */ -class ResearchAbility: public UnitAbility { +class ResearchAbility : public UnitAbility { public: - ResearchAbility(const Sound *s=nullptr); + ResearchAbility(const Sound *s = nullptr); ability_type type() override { return ability_type::research; @@ -220,7 +218,7 @@ class ResearchAbility: public UnitAbility { bool can_invoke(Unit &to_modify, const Command &cmd) override; - void invoke(Unit &to_modify, const Command &cmd, bool play_sound=false) override; + void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; private: const Sound *sound; @@ -229,9 +227,9 @@ class ResearchAbility: public UnitAbility { /** * villagers build new buildings */ -class BuildAbility: public UnitAbility { +class BuildAbility : public UnitAbility { public: - BuildAbility(const Sound *s=nullptr); + BuildAbility(const Sound *s = nullptr); ability_type type() override { return ability_type::build; @@ -239,7 +237,7 @@ class BuildAbility: public UnitAbility { bool can_invoke(Unit &to_modify, const Command &cmd) override; - void invoke(Unit &to_modify, const Command &cmd, bool play_sound=false) override; + void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; private: const Sound *sound; @@ -248,9 +246,9 @@ class BuildAbility: public UnitAbility { /** * initiates an gather resource action when given a valid target */ -class GatherAbility: public UnitAbility { +class GatherAbility : public UnitAbility { public: - GatherAbility(const Sound *s=nullptr); + GatherAbility(const Sound *s = nullptr); ability_type type() override { return ability_type::gather; @@ -258,7 +256,7 @@ class GatherAbility: public UnitAbility { bool can_invoke(Unit &to_modify, const Command &cmd) override; - void invoke(Unit &to_modify, const Command &cmd, bool play_sound=false) override; + void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; private: const Sound *sound; @@ -267,9 +265,9 @@ class GatherAbility: public UnitAbility { /** * initiates an attack action when given a valid target */ -class AttackAbility: public UnitAbility { +class AttackAbility : public UnitAbility { public: - AttackAbility(const Sound *s=nullptr); + AttackAbility(const Sound *s = nullptr); ability_type type() override { return ability_type::attack; @@ -277,7 +275,7 @@ class AttackAbility: public UnitAbility { bool can_invoke(Unit &to_modify, const Command &cmd) override; - void invoke(Unit &to_modify, const Command &cmd, bool play_sound=false) override; + void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; private: const Sound *sound; @@ -286,9 +284,9 @@ class AttackAbility: public UnitAbility { /** * initiates a repair action when given a valid target */ -class RepairAbility: public UnitAbility { +class RepairAbility : public UnitAbility { public: - RepairAbility(const Sound *s=nullptr); + RepairAbility(const Sound *s = nullptr); ability_type type() override { return ability_type::repair; @@ -296,7 +294,7 @@ class RepairAbility: public UnitAbility { bool can_invoke(Unit &to_modify, const Command &cmd) override; - void invoke(Unit &to_modify, const Command &cmd, bool play_sound=false) override; + void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; private: const Sound *sound; @@ -305,9 +303,9 @@ class RepairAbility: public UnitAbility { /** * initiates a heal action when given a valid target */ -class HealAbility: public UnitAbility { +class HealAbility : public UnitAbility { public: - HealAbility(const Sound *s=nullptr); + HealAbility(const Sound *s = nullptr); ability_type type() override { return ability_type::heal; @@ -315,7 +313,7 @@ class HealAbility: public UnitAbility { bool can_invoke(Unit &to_modify, const Command &cmd) override; - void invoke(Unit &to_modify, const Command &cmd, bool play_sound=false) override; + void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; private: const Sound *sound; @@ -325,9 +323,9 @@ class HealAbility: public UnitAbility { * initiates a patrol action when given a valid target * TODO implement */ -class PatrolAbility: public UnitAbility { +class PatrolAbility : public UnitAbility { public: - PatrolAbility(const Sound *s=nullptr); + PatrolAbility(const Sound *s = nullptr); ability_type type() override { return ability_type::patrol; @@ -335,7 +333,7 @@ class PatrolAbility: public UnitAbility { bool can_invoke(Unit &to_modify, const Command &cmd) override; - void invoke(Unit &to_modify, const Command &cmd, bool play_sound=false) override; + void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; private: const Sound *sound; @@ -344,9 +342,9 @@ class PatrolAbility: public UnitAbility { /** * initiates a convert action when given a valid target */ -class ConvertAbility: public UnitAbility { +class ConvertAbility : public UnitAbility { public: - ConvertAbility(const Sound *s=nullptr); + ConvertAbility(const Sound *s = nullptr); ability_type type() override { return ability_type::convert; @@ -354,7 +352,7 @@ class ConvertAbility: public UnitAbility { bool can_invoke(Unit &to_modify, const Command &cmd) override; - void invoke(Unit &to_modify, const Command &cmd, bool play_sound=false) override; + void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; private: const Sound *sound; @@ -369,7 +367,7 @@ std::string to_string(const openage::ability_type &at); /** * hasher for ability type enum */ -template<> +template <> struct hash { typedef underlying_type::type underlying_type; diff --git a/libopenage/unit/action.cpp b/libopenage/unit/action.cpp index 50a03e2155..9370609f2d 100644 --- a/libopenage/unit/action.cpp +++ b/libopenage/unit/action.cpp @@ -12,7 +12,6 @@ #include "command.h" #include "producer.h" #include "research.h" -#include "unit_texture.h" namespace openage { @@ -91,26 +90,6 @@ UnitAction::UnitAction(Unit *u, graphic_type initial_gt) : graphic{initial_gt}, frame{.0f}, frame_rate{.0f} { - auto &g_set = this->current_graphics(); - if (g_set.count(initial_gt) > 0) { - auto utex = g_set.at(initial_gt); - if (utex) { - this->frame_rate = utex->frame_rate; - } - else { - this->entity->log(MSG(dbg) << "Broken graphic (null)"); - } - } - else { - this->entity->log(MSG(dbg) << "Broken graphic (not available)"); - } - - if (this->frame_rate == 0) { - // a random starting point for static graphics - // this creates variations in trees / houses etc - // this value is also deterministic to match across clients - this->frame = (u->id * u->id * 19249) & 0xff; - } } graphic_type UnitAction::type() const { @@ -121,11 +100,6 @@ float UnitAction::current_frame() const { return this->frame; } -const graphic_set &UnitAction::current_graphics() const { - // return the default graphic - return this->entity->unit_type->graphics; -} - void UnitAction::draw_debug(const LegacyEngine &engine) { // draw debug content if available if (show_debug && this->debug_draw_action) { @@ -326,10 +300,6 @@ void TargetAction::set_target(UnitReference new_target) { DecayAction::DecayAction(Unit *e) : UnitAction(e, graphic_type::standing), end_frame{.0f} { - auto &g_set = this->current_graphics(); - if (g_set.count(this->graphic) > 0) { - this->end_frame = g_set.at(this->graphic)->frame_count - 1; - } } void DecayAction::update(unsigned int time) { @@ -346,10 +316,6 @@ DeadAction::DeadAction(Unit *e, std::function on_complete) : UnitAction(e, graphic_type::dying), end_frame{.0f}, on_complete_func{on_complete} { - auto &g_set = this->current_graphics(); - if (g_set.count(graphic) > 0) { - this->end_frame = g_set.at(graphic)->frame_count - 1; - } } void DeadAction::update(unsigned int time) { @@ -1127,9 +1093,6 @@ void AttackAction::update_in_range(unsigned int time, Unit *target_ptr) { if (this->timer.update(time)) { this->attack(*target_ptr); } - - // inc frame - this->frame += time * this->current_graphics().at(graphic)->frame_count * 1.0f / this->timer.get_interval(); } bool AttackAction::completed_in_range(Unit *target_ptr) const { @@ -1183,9 +1146,6 @@ void HealAction::update_in_range(unsigned int time, Unit *target_ptr) { if (this->timer.update(time)) { this->heal(*target_ptr); } - - // inc frame - this->frame += time * this->current_graphics().at(graphic)->frame_count * 1.0f / this->timer.get_interval(); } bool HealAction::completed_in_range(Unit *target_ptr) const { diff --git a/libopenage/unit/action.h b/libopenage/unit/action.h index 60299dc577..ca41229e87 100644 --- a/libopenage/unit/action.h +++ b/libopenage/unit/action.h @@ -146,15 +146,6 @@ class UnitAction { */ virtual std::string name() const = 0; - /** - * determines which graphic should be used for drawing this unit - * finds the default graphic using the units type, used by most actions - * - * this virtual function is overriden for special cases such as - * villager task graphics - */ - virtual const graphic_set ¤t_graphics() const; - void draw_debug(const LegacyEngine &engine); /** diff --git a/libopenage/unit/attribute.h b/libopenage/unit/attribute.h index 2390c1dacc..5a5ce05117 100644 --- a/libopenage/unit/attribute.h +++ b/libopenage/unit/attribute.h @@ -1,4 +1,4 @@ -// Copyright 2014-2021 the openage authors. See copying.md for legal info. +// Copyright 2014-2023 the openage authors. See copying.md for legal info. #pragma once @@ -46,13 +46,6 @@ enum class graphic_type { work }; -class UnitTexture; - -/** - * Collection of graphics attached to each unit. - */ -using graphic_set = std::map>; - /** * List of unit's attribute types. */ diff --git a/libopenage/unit/producer.cpp b/libopenage/unit/producer.cpp index d03192ee60..fae69a6f78 100644 --- a/libopenage/unit/producer.cpp +++ b/libopenage/unit/producer.cpp @@ -12,7 +12,6 @@ #include "action.h" #include "producer.h" #include "unit.h" -#include "unit_texture.h" /** @file * Many values in this file are hardcoded, due to limited understanding of how the original @@ -82,7 +81,6 @@ ObjectProducer::ObjectProducer(const Player &owner, const GameSpec &spec, const UnitType(owner), dataspec(spec), unit_data(*ud), - default_tex{spec.get_unit_texture(ud->idle_graphic0)}, dead_unit_id{ud->dead_unit_id} { // copy the class type this->unit_class = this->unit_data.unit_class; @@ -112,54 +110,6 @@ ObjectProducer::ObjectProducer(const Player &owner, const GameSpec &spec, const static_cast(this->unit_data.radius_y * 2), }; - // graphic set - auto standing = spec.get_unit_texture(this->unit_data.idle_graphic0); - if (!standing) { - // indicates problems with data converion - throw Error(MSG(err) << "Unit id " << this->unit_data.id0 - << " has invalid graphic data, try reconverting the data"); - } - this->graphics[graphic_type::standing] = standing; - auto dying_tex = spec.get_unit_texture(this->unit_data.dying_graphic); - if (dying_tex) { - this->graphics[graphic_type::dying] = dying_tex; - } - - // default extra graphics - this->graphics[graphic_type::attack] = this->graphics[graphic_type::standing]; - this->graphics[graphic_type::work] = this->graphics[graphic_type::standing]; - - // pull extra graphics from unit commands - auto cmds = spec.get_command_data(this->unit_data.id0); - for (auto cmd : cmds) { - // same attack / work graphic - if (cmd->work_sprite_id == -1 && cmd->proceed_sprite_id > 0) { - auto task = spec.get_unit_texture(cmd->proceed_sprite_id); - if (task) { - this->graphics[graphic_type::work] = task; - this->graphics[graphic_type::attack] = task; - } - } - - // separate work and attack graphics - if (cmd->work_sprite_id > 0 && cmd->proceed_sprite_id > 0) { - auto attack = spec.get_unit_texture(cmd->proceed_sprite_id); - auto work = spec.get_unit_texture(cmd->work_sprite_id); - if (attack) { - this->graphics[graphic_type::attack] = attack; - } - if (work) { - this->graphics[graphic_type::work] = work; - } - } - - // villager carrying resources graphics - if (cmd->carry_sprite_id > 0) { - auto carry = spec.get_unit_texture(cmd->carry_sprite_id); - this->graphics[graphic_type::carrying] = carry; - } - } - // TODO get cost, temp fixed cost of 50 food this->cost.set(cost_type::constant, create_resource_cost(game_resource::food, 50)); } @@ -238,24 +188,6 @@ void ObjectProducer::initialise(Unit *unit, Player &player) { unit->push_action(std::make_unique(unit), true); } else { - // if destruction graphic is available - if (this->dead_unit_id) { - unit->push_action( - std::make_unique( - unit, - [this, unit, &player]() { - // modify unit to have dead type - UnitType *t = player.get_type(this->dead_unit_id); - if (t) { - t->initialise(unit, player); - } - }), - true); - } - else if (this->graphics.count(graphic_type::dying) > 0) { - unit->push_action(std::make_unique(unit), true); - } - // the default action unit->push_action(std::make_unique(unit), true); } @@ -329,26 +261,6 @@ MovableProducer::MovableProducer(const Player &owner, const GameSpec &spec, cons on_move{spec.get_sound(this->unit_data.command_sound_id)}, on_attack{spec.get_sound(this->unit_data.command_sound_id)}, projectile{this->unit_data.attack_projectile_primary_unit_id} { - // extra graphics if available - // villagers have invalid attack and walk graphics - // it seems these come from the command data instead - auto walk = spec.get_unit_texture(this->unit_data.move_graphics); - if (!walk) { - // use standing instead - walk = this->graphics[graphic_type::standing]; - } - this->graphics[graphic_type::walking] = walk; - - // reuse as carry graphic if not already set - if (this->graphics.count(graphic_type::carrying) == 0) { - this->graphics[graphic_type::carrying] = walk; - } - - auto attack = spec.get_unit_texture(this->unit_data.attack_sprite_id); - if (attack && attack->is_valid()) { - this->graphics[graphic_type::attack] = attack; - } - // extra abilities this->type_abilities.emplace_back(std::make_shared(this->on_move)); this->type_abilities.emplace_back(std::make_shared(this->on_attack)); @@ -474,8 +386,6 @@ TerrainObject *LivingProducer::place(Unit *unit, std::shared_ptr terrai BuildingProducer::BuildingProducer(const Player &owner, const GameSpec &spec, const gamedata::building_unit *ud) : UnitType(owner), unit_data{*ud}, - texture{spec.get_unit_texture(ud->idle_graphic0)}, - destroyed{spec.get_unit_texture(ud->dying_graphic)}, projectile{this->unit_data.attack_projectile_primary_unit_id}, foundation_terrain{ud->foundation_terrain_id}, enable_collisions{this->unit_data.id0 != 109} { // 109 = town center @@ -505,15 +415,6 @@ BuildingProducer::BuildingProducer(const Player &owner, const GameSpec &spec, co static_cast(this->unit_data.radius_y * 2), }; - // graphic set - this->graphics[graphic_type::construct] = spec.get_unit_texture(ud->construction_graphic_id); - this->graphics[graphic_type::standing] = spec.get_unit_texture(ud->idle_graphic0); - this->graphics[graphic_type::attack] = spec.get_unit_texture(ud->idle_graphic0); - auto dying_tex = spec.get_unit_texture(ud->dying_graphic); - if (dying_tex) { - this->graphics[graphic_type::dying] = dying_tex; - } - // TODO get cost, temp fixed cost of 100 wood this->cost.set(cost_type::constant, create_resource_cost(game_resource::wood, 100)); } @@ -570,9 +471,6 @@ void BuildingProducer::initialise(Unit *unit, Player &player) { this->have_limit = 2; // TODO change to 1, 2 is for testing } - bool has_destruct_graphic = this->destroyed != nullptr; - unit->push_action(std::make_unique(unit, has_destruct_graphic), true); - UnitType *proj_type = this->owner.get_type(this->projectile); if (this->unit_data.attack_projectile_primary_unit_id > 0 && proj_type) { coord::phys_t range_phys = this->unit_data.weapon_range_max; @@ -708,37 +606,14 @@ TerrainObject *BuildingProducer::make_annex(Unit &u, std::shared_ptr t, object_state state = c ? object_state::placed : object_state::placed_no_collision; annex_loc->place(t, start_tile, state); - // create special drawing functions for annexes, - annex_loc->draw = [annex_loc, annex_type, &u, c](const LegacyEngine &e) { - // hack which draws the outline in the right order - // removable once rendering system is improved - if (c && u.selected) { - // annex_loc->get_parent()->draw_outline(e.coord); - } - - // only draw if building is completed - if (u.has_attribute(attr_type::building) && u.get_attribute().completed >= 1.0f) { - u.draw(annex_loc, annex_type->graphics, e); - } - }; return annex_loc; } ProjectileProducer::ProjectileProducer(const Player &owner, const GameSpec &spec, const gamedata::missile_unit *pd) : UnitType(owner), - unit_data{*pd}, - tex{spec.get_unit_texture(this->unit_data.idle_graphic0)}, - sh{spec.get_unit_texture(3379)}, // 3379 = general arrow shadow - destroyed{spec.get_unit_texture(this->unit_data.dying_graphic)} { + unit_data{*pd} { // copy the class type this->unit_class = this->unit_data.unit_class; - - // graphic set - this->graphics[graphic_type::standing] = this->tex; - this->graphics[graphic_type::shadow] = this->sh; - if (destroyed) { - this->graphics[graphic_type::dying] = destroyed; - } } ProjectileProducer::~ProjectileProducer() = default; @@ -769,11 +644,6 @@ void ProjectileProducer::initialise(Unit *unit, Player &player) { unit->add_attribute(std::make_shared>(sp)); unit->add_attribute(std::make_shared>(this->unit_data.projectile_arc)); unit->add_attribute(std::make_shared>(coord::phys3_delta{1, 0, 0})); - - // if destruction graphic is available - if (this->destroyed) { - unit->push_action(std::make_unique(unit), true); - } } TerrainObject *ProjectileProducer::place(Unit *u, std::shared_ptr terrain, coord::phys3 init_pos) const { @@ -816,10 +686,6 @@ TerrainObject *ProjectileProducer::place(Unit *u, std::shared_ptr terra return true; }; - u->location->draw = [u](const LegacyEngine &e) { - u->draw(e); - }; - // try to place the obj, it knows best whether it will fit. if (u->location->place(terrain, init_pos, object_state::placed_no_collision)) { return u->location.get(); diff --git a/libopenage/unit/producer.h b/libopenage/unit/producer.h index 53036078de..2ff837a147 100644 --- a/libopenage/unit/producer.h +++ b/libopenage/unit/producer.h @@ -23,7 +23,6 @@ class Sound; class UnitAbility; class UnitAction; -class UnitTexture; std::unordered_set allowed_terrains(const gamedata::ground_type &restriction); @@ -56,7 +55,6 @@ class ObjectProducer : public UnitType { */ const Sound *on_create; const Sound *on_destroy; - std::shared_ptr default_tex; int dead_unit_id; }; @@ -73,8 +71,6 @@ class MovableProducer : public ObjectProducer { protected: const gamedata::projectile_unit unit_data; - UnitTexture *moving; - UnitTexture *attacking; const Sound *on_move; const Sound *on_attack; int projectile; @@ -123,8 +119,6 @@ class BuildingProducer : public UnitType { */ const Sound *on_create; const Sound *on_destroy; - std::shared_ptr texture; - std::shared_ptr destroyed; int projectile; int foundation_terrain; std::vector get_accepted_resources(); @@ -155,9 +149,6 @@ class ProjectileProducer : public UnitType { private: const gamedata::missile_unit unit_data; - std::shared_ptr tex; - std::shared_ptr sh; // shadow texture - std::shared_ptr destroyed; }; } // namespace openage diff --git a/libopenage/unit/unit.cpp b/libopenage/unit/unit.cpp index 483d651bd1..234d47770e 100644 --- a/libopenage/unit/unit.cpp +++ b/libopenage/unit/unit.cpp @@ -12,7 +12,7 @@ #include "command.h" #include "producer.h" #include "unit.h" -#include "unit_texture.h" + namespace openage { @@ -149,71 +149,6 @@ void Unit::apply_cmd(std::shared_ptr ability, const Command &cmd) { } } - -void Unit::draw(const LegacyEngine &engine) { - // the top action decides the graphic type and action - this->draw(this->location.get(), this->top()->current_graphics(), engine); -} - - -void Unit::draw(TerrainObject *loc, const graphic_set &grpc, const LegacyEngine &engine) { - ENSURE(loc != nullptr, "there should always be a location for a placed unit"); - - auto top_action = this->top(); - auto draw_graphic = top_action->type(); - if (grpc.count(draw_graphic) == 0) { - this->log(MSG(warn) << "Graphic not available"); - return; - } - - // the texture to draw with - auto draw_texture = grpc.at(draw_graphic); - if (!draw_texture) { - this->log(MSG(warn) << "Graphic null"); - return; - } - - // frame specified by the current action - auto draw_frame = top_action->current_frame(); - this->draw(loc->pos.draw, draw_texture, draw_frame, engine); - - // draw a shadow if the graphic is available - if (grpc.count(graphic_type::shadow) > 0) { - auto draw_shadow = grpc.at(graphic_type::shadow); - if (draw_shadow) { - // position without height component - // TODO: terrain elevation - coord::phys3 shadow_pos = loc->pos.draw; - shadow_pos.up = 0; - this->draw(shadow_pos, draw_shadow, draw_frame, engine); - } - } - - // draw debug details - top_action->draw_debug(engine); -} - - -void Unit::draw(coord::phys3 draw_pos, std::shared_ptr graphic, unsigned int frame, const LegacyEngine &engine) { - // players color if available - unsigned color = 0; - if (this->has_attribute(attr_type::owner)) { - auto &own_attr = this->get_attribute(); - color = own_attr.player.color; - } - - // check if object has a direction - if (this->has_attribute(attr_type::direction)) { - // directional textures - auto &d_attr = this->get_attribute(); - coord::phys3_delta draw_dir = d_attr.unit_dir; - // graphic->draw(engine.coord, draw_pos.to_camgame(engine.coord), draw_dir, frame, color); - } - else { - // graphic->draw(engine.coord, draw_pos.to_camgame(engine.coord), frame, color); - } -} - void Unit::give_ability(std::shared_ptr ability) { this->ability_available.emplace(std::make_pair(ability->type(), ability)); } diff --git a/libopenage/unit/unit.h b/libopenage/unit/unit.h index 01416a6ac8..5165ed6a18 100644 --- a/libopenage/unit/unit.h +++ b/libopenage/unit/unit.h @@ -124,24 +124,6 @@ class Unit : public log::LogSource { */ bool update(time_nsec_t lastframe_duration); - /** - * draws this action by taking the graphic type of the top action - * the graphic is found from the current graphic set - * - * this function should be used for most draw purposes - */ - void draw(const LegacyEngine &engine); - - /** - * an generalized draw function which is useful for drawing annexes - */ - void draw(TerrainObject *loc, const graphic_set &graphics, const LegacyEngine &engine); - - /** - * draws with a specific graphic and frame - */ - void draw(coord::phys3 draw_pos, std::shared_ptr graphic, unsigned int frame, const LegacyEngine &engine); - /** * adds an available ability to this unit * this turns targeted objects into actions which are pushed diff --git a/libopenage/unit/unit_texture.cpp b/libopenage/unit/unit_texture.cpp deleted file mode 100644 index 0d0636f53a..0000000000 --- a/libopenage/unit/unit_texture.cpp +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "unit_texture.h" - -#include -#include - -#include "../coord/phys.h" -#include "../coord/pixel.h" -#include "../gamestate/old/game_spec.h" -#include "../log/log.h" -#include "../util/math.h" -#include "../util/math_constants.h" - - -namespace openage { - -UnitTexture::UnitTexture(GameSpec &spec, uint16_t graphic_id, bool delta) : - UnitTexture{spec, spec.get_graphic_data(graphic_id), delta} {} - -UnitTexture::UnitTexture(GameSpec &spec, const gamedata::graphic *graphic, bool delta) : - id{graphic->graphic_id}, - sound_id{graphic->sound_id}, - frame_count{graphic->frame_count}, - angle_count{graphic->angle_count}, - mirroring_mode{graphic->mirroring_mode}, - frame_rate{graphic->frame_rate}, - use_up_angles{graphic->mirroring_mode == 24}, - use_deltas{delta}, - draw_this{true}, - sound{nullptr}, - delta_id{graphic->graphic_deltas.data} { - this->initialise(spec); -} - -bool UnitTexture::is_valid() const { - return false; -} - -coord::viewport UnitTexture::size() const { - return coord::viewport{0, 0}; -} - -void UnitTexture::sample(const coord::CoordManager &coord, const coord::camhud &draw_pos, unsigned color) const { -} - -void UnitTexture::draw(const coord::CoordManager &coord, const coord::camgame &draw_pos, unsigned int frame, unsigned color) const { -} - -void UnitTexture::draw(const coord::CoordManager &coord, const coord::camgame &draw_pos, coord::phys3_delta &dir, unsigned int frame, unsigned color) const { -} - -void UnitTexture::initialise(GameSpec &spec) { -} - -unsigned int dir_group(coord::phys3_delta dir, unsigned int angles) { - unsigned int first_angle = 5 * angles / 8; - - // normalise direction vector - double len = std::hypot(dir.ne, dir.se); - double dir_ne = static_cast(dir.ne) / len; - double dir_se = static_cast(dir.se) / len; - - // formula to find the correct angle - return static_cast( - round(angles * atan2(dir_se, dir_ne) * (math::INV_PI / 2)) - + first_angle) - % angles; -} - - -} // namespace openage diff --git a/libopenage/unit/unit_texture.h b/libopenage/unit/unit_texture.h deleted file mode 100644 index 7e44899f8c..0000000000 --- a/libopenage/unit/unit_texture.h +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -#include "../coord/phys.h" -#include "../gamedata/graphic_dummy.h" - -namespace openage { - -class GameSpec; -class Texture; -class Sound; - -/** - * Handling animated and directional textures based on the game - * graphics data. - * - * These objects handle the drawing of regular textures to use a - * unit's direction and include delta graphics. - * - * This type can also deal with playing position based game sounds. - */ -class [[deprecated]] UnitTexture { -public: - /** - * Delta option specifies whether the delta graphics are included. - * - * Note that the game data contains loops in delta links - * which mean recursive loading should be avoided - */ - UnitTexture(GameSpec &spec, uint16_t graphic_id, bool delta = true); - UnitTexture(GameSpec &spec, const gamedata::graphic *graphic, bool delta = true); - - /** - * const attributes of the graphic - */ - const int16_t id; - const int16_t sound_id; - const unsigned int frame_count; - const unsigned int angle_count; - const int16_t mirroring_mode; - const float frame_rate; - - /** - * draw object with vertical orientation (arrows) - * adding an addtion degree of orientation - */ - const bool use_up_angles; - - /** - * use delta information - */ - const bool use_deltas; - - /** - * invalid unit textures will cause errors if drawn - */ - bool is_valid() const; - - /** - * pixel size of this texture - */ - coord::viewport size() const; - - /** - * a sample drawing for hud - */ - void sample(const coord::CoordManager &coord, const coord::camhud &draw_pos, unsigned color = 1) const; - - /** - * draw object with no direction - */ - void draw(const coord::CoordManager &coord, const coord::camgame &draw_pos, unsigned int frame, unsigned color) const; - - /** - * draw object with direction - */ - void draw(const coord::CoordManager &coord, const coord::camgame &draw_pos, coord::phys3_delta &dir, unsigned int frame, unsigned color) const; - - /** - * initialise graphic data - */ - void initialise(GameSpec &spec); - -private: - /** - * the above frame count covers the entire graphic (with deltas) - * the actual number in the base texture may be different - */ - unsigned int safe_frame_count; - unsigned int angles_included; - unsigned int angles_mirrored; - unsigned int top_frame; - - // avoid drawing missing graphics - bool draw_this; - const Sound *sound; - - // delta graphic ids - std::vector delta_id; - - // delta graphics - std::vector, coord::camgame_delta>> deltas; -}; - -/** - * the set of images to used based on unit direction, - * usually 8 directions to draw for each unit (3 are mirrored) - * - * @param dir a world space direction, - * @param angles number of angles, usually 8 - * @param first_angle offset added to angle, modulo number of angles - * @return image set index - */ -unsigned int dir_group(coord::phys3_delta dir, unsigned int angles = 8); - -} // namespace openage diff --git a/libopenage/unit/unit_type.cpp b/libopenage/unit/unit_type.cpp index bc760bd955..80677ebc75 100644 --- a/libopenage/unit/unit_type.cpp +++ b/libopenage/unit/unit_type.cpp @@ -1,10 +1,10 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. +#include "unit.h" #include "../gamestate/old/player.h" #include "../terrain/terrain_object.h" #include "../util/math_constants.h" #include "action.h" -#include "unit.h" #include "unit_container.h" #include "unit_type.h" @@ -12,8 +12,7 @@ namespace openage { -UnitTypeMeta::UnitTypeMeta(std::string name, int id, init_func f) - : +UnitTypeMeta::UnitTypeMeta(std::string name, int id, init_func f) : init{f}, type_name{std::move(name)}, type_id{id} { @@ -27,8 +26,7 @@ int UnitTypeMeta::id() const { return this->type_id; } -UnitType::UnitType(const Player &owner) - : +UnitType::UnitType(const Player &owner) : owner{owner}, have_limit{math::INT_INF}, had_limit{math::INT_INF} { @@ -54,10 +52,6 @@ bool UnitType::operator!=(const UnitType &other) const { return !(*this == other); } -UnitTexture *UnitType::default_texture() { - return this->graphics[graphic_type::standing].get(); -} - TerrainObject *UnitType::place_beside(Unit *u, TerrainObject const *other) const { if (!u || !other) { return nullptr; @@ -65,7 +59,7 @@ TerrainObject *UnitType::place_beside(Unit *u, TerrainObject const *other) const // find the range of possible tiles tile_range outline{other->pos.start - coord::tile_delta{1, 1}, - other->pos.end + coord::tile_delta{1, 1}, + other->pos.end + coord::tile_delta{1, 1}, other->pos.draw}; // find a free position adjacent to the object @@ -97,8 +91,7 @@ UnitType *UnitType::parent_type() const { return this->owner.get_type(this->parent_id()); } -NyanType::NyanType(const Player &owner) - : +NyanType::NyanType(const Player &owner) : UnitType(owner) { // TODO: the type should be given attributes and abilities } diff --git a/libopenage/unit/unit_type.h b/libopenage/unit/unit_type.h index 5e960915a3..57dc2c2b49 100644 --- a/libopenage/unit/unit_type.h +++ b/libopenage/unit/unit_type.h @@ -1,4 +1,4 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once @@ -19,7 +19,6 @@ class TerrainObject; class Unit; class UnitAbility; class UnitContainer; -class UnitTexture; /** @@ -43,7 +42,6 @@ class UnitTypeMeta { private: const std::string type_name; const int type_id; - }; /** @@ -112,11 +110,6 @@ class UnitType { bool operator==(const UnitType &other) const; bool operator!=(const UnitType &other) const; - /** - * Get a default texture for HUD drawing - */ - UnitTexture *default_texture(); - /** * similar to place but places adjacent to an existing object */ @@ -175,11 +168,6 @@ class UnitType { */ int had_limit; - /** - * The set of graphics used for this type - */ - graphic_set graphics; - /** * The index of the icon representing this unit */ @@ -199,7 +187,7 @@ class UnitType { /** * An example of how nyan can work with the type system */ -class NyanType: public UnitType { +class NyanType : public UnitType { public: /** * TODO: give the parsed nyan attributes @@ -213,7 +201,6 @@ class NyanType: public UnitType { std::string name() const override; void initialise(Unit *, Player &) override; TerrainObject *place(Unit *, std::shared_ptr, coord::phys3) const override; - }; } // namespace openage From 44cf90e7715e2bcabfcfa03b605643d0325474a0 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Sep 2023 02:37:34 +0200 Subject: [PATCH 033/771] renderer: Remove legacy signal handling for drawing/inputs. --- libopenage/CMakeLists.txt | 1 - libopenage/console/console.cpp | 72 ++++++------ libopenage/console/console.h | 14 +-- libopenage/gui/gui.cpp | 64 +++++------ libopenage/gui/gui.h | 11 +- libopenage/handlers.cpp | 7 -- libopenage/handlers.h | 66 ----------- libopenage/input/legacy/input_manager.cpp | 128 +++++++++++----------- libopenage/input/legacy/input_manager.h | 5 +- libopenage/legacy_engine.h | 1 - libopenage/unit/selection.cpp | 110 +++++++++---------- libopenage/unit/selection.h | 5 +- libopenage/unit/unit.h | 1 - libopenage/unit/unit_container.h | 5 +- 14 files changed, 201 insertions(+), 289 deletions(-) delete mode 100644 libopenage/handlers.cpp delete mode 100644 libopenage/handlers.h diff --git a/libopenage/CMakeLists.txt b/libopenage/CMakeLists.txt index 8c17eaa9d5..a87c120f50 100644 --- a/libopenage/CMakeLists.txt +++ b/libopenage/CMakeLists.txt @@ -322,7 +322,6 @@ get_codegen_scu_file() # are specified above the source file list. add_sources(libopenage - handlers.cpp legacy_engine.cpp main.cpp options.cpp diff --git a/libopenage/console/console.cpp b/libopenage/console/console.cpp index f2350a01ed..c28b6bab06 100644 --- a/libopenage/console/console.cpp +++ b/libopenage/console/console.cpp @@ -161,54 +161,54 @@ void Console::interpret(const std::string &command) { } } -bool Console::on_tick() { - if (!this->visible) { - return true; - } +// bool Console::on_tick() { +// if (!this->visible) { +// return true; +// } - // TODO: handle stuff such as cursor blinking, - // repeating held-down keys - return true; -} +// // TODO: handle stuff such as cursor blinking, +// // repeating held-down keys +// return true; +// } -bool Console::on_drawhud() { - if (!this->visible) { - return true; - } +// bool Console::on_drawhud() { +// if (!this->visible) { +// return true; +// } - // TODO: Use new renderer +// // TODO: Use new renderer - // draw::to_opengl(this->display, this); +// // draw::to_opengl(this->display, this); - return true; -} +// return true; +// } -bool Console::on_input(SDL_Event *e) { - // only handle inputs if the console is visible - if (!this->visible) { - return true; - } +// bool Console::on_input(SDL_Event *e) { +// // only handle inputs if the console is visible +// if (!this->visible) { +// return true; +// } - switch (e->type) { - case SDL_KEYDOWN: - //TODO handle key inputs +// switch (e->type) { +// case SDL_KEYDOWN: +// //TODO handle key inputs - //do not allow anyone else to handle this input - return false; - } +// //do not allow anyone else to handle this input +// return false; +// } - return true; -} +// return true; +// } -bool Console::on_resize(coord::viewport_delta new_size) { - coord::pixel_t w = this->buf.get_dims().x * this->charsize.x; - coord::pixel_t h = this->buf.get_dims().y * this->charsize.y; +// bool Console::on_resize(coord::viewport_delta new_size) { +// coord::pixel_t w = this->buf.get_dims().x * this->charsize.x; +// coord::pixel_t h = this->buf.get_dims().y * this->charsize.y; - this->bottomleft = {(new_size.x - w) / 2, (new_size.y - h) / 2}; - this->topright = {this->bottomleft.x + w, this->bottomleft.y - h}; +// this->bottomleft = {(new_size.x - w) / 2, (new_size.y - h) / 2}; +// this->topright = {this->bottomleft.x + w, this->bottomleft.y - h}; - return true; -} +// return true; +// } } // namespace console } // namespace openage diff --git a/libopenage/console/console.h b/libopenage/console/console.h index afe4095b51..dc9f00e9ef 100644 --- a/libopenage/console/console.h +++ b/libopenage/console/console.h @@ -7,7 +7,6 @@ #include "../coord/pixel.h" #include "../gamedata/color_dummy.h" -#include "../handlers.h" #include "../input/legacy/input_manager.h" #include "../renderer/font/font.h" #include "../util/color.h" @@ -22,10 +21,7 @@ class LegacyEngine; */ namespace console { -class Console : InputHandler - , TickHandler - , HudHandler - , ResizeHandler { +class Console { public: Console(/* presenter::LegacyDisplay *display */); ~Console(); @@ -53,10 +49,10 @@ class Console : InputHandler */ void interpret(const std::string &command); - bool on_drawhud() override; - bool on_tick() override; - bool on_input(SDL_Event *event) override; - bool on_resize(coord::viewport_delta new_size) override; + // bool on_drawhud() override; + // bool on_tick() override; + // bool on_input(SDL_Event *event) override; + // bool on_resize(coord::viewport_delta new_size) override; protected: // TODO: Replace with new renderer diff --git a/libopenage/gui/gui.cpp b/libopenage/gui/gui.cpp index d8ffe9e3e2..59d6e24c6b 100644 --- a/libopenage/gui/gui.cpp +++ b/libopenage/gui/gui.cpp @@ -93,14 +93,14 @@ void GUI::process_events() { this->application.processEvents(); } -bool GUI::on_resize(coord::viewport_delta new_size) { - this->renderer.resize(new_size.x, new_size.y); - return true; -} +// bool GUI::on_resize(coord::viewport_delta new_size) { +// this->renderer.resize(new_size.x, new_size.y); +// return true; +// } -bool GUI::on_input(SDL_Event *event) { - return not this->input.process(event); -} +// bool GUI::on_input(SDL_Event *event) { +// return not this->input.process(event); +// } namespace { /** @@ -138,43 +138,43 @@ class BlendPreserver { } // namespace -bool GUI::on_drawhud() { - this->render_updater.process_callbacks(); +// bool GUI::on_drawhud() { +// this->render_updater.process_callbacks(); - BlendPreserver preserve_blend; +// BlendPreserver preserve_blend; - auto tex = this->renderer.render(); +// auto tex = this->renderer.render(); - glEnable(GL_BLEND); - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); +// glEnable(GL_BLEND); +// glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - this->textured_screen_quad_shader->use(); +// this->textured_screen_quad_shader->use(); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, tex); +// glActiveTexture(GL_TEXTURE0); +// glBindTexture(GL_TEXTURE_2D, tex); - glEnableVertexAttribArray(this->textured_screen_quad_shader->pos_id); +// glEnableVertexAttribArray(this->textured_screen_quad_shader->pos_id); - glBindBuffer(GL_ARRAY_BUFFER, this->screen_quad_vbo); - glVertexAttribPointer( - this->textured_screen_quad_shader->pos_id, - 2, - GL_FLOAT, - GL_FALSE, - 2 * sizeof(float), - 0); +// glBindBuffer(GL_ARRAY_BUFFER, this->screen_quad_vbo); +// glVertexAttribPointer( +// this->textured_screen_quad_shader->pos_id, +// 2, +// GL_FLOAT, +// GL_FALSE, +// 2 * sizeof(float), +// 0); - glDrawArrays(GL_TRIANGLE_FAN, 0, 4); +// glDrawArrays(GL_TRIANGLE_FAN, 0, 4); - glDisableVertexAttribArray(this->textured_screen_quad_shader->pos_id); - glBindBuffer(GL_ARRAY_BUFFER, 0); +// glDisableVertexAttribArray(this->textured_screen_quad_shader->pos_id); +// glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindTexture(GL_TEXTURE_2D, 0); +// glBindTexture(GL_TEXTURE_2D, 0); - this->textured_screen_quad_shader->stopusing(); +// this->textured_screen_quad_shader->stopusing(); - return true; -} +// return true; +// } } // namespace gui } // namespace openage diff --git a/libopenage/gui/gui.h b/libopenage/gui/gui.h index d5adadef70..8f662bd2ef 100644 --- a/libopenage/gui/gui.h +++ b/libopenage/gui/gui.h @@ -5,7 +5,6 @@ #include #include -#include "../handlers.h" #include "guisys/public/gui_engine.h" #include "guisys/public/gui_event_queue.h" #include "guisys/public/gui_input.h" @@ -33,9 +32,7 @@ class EngineQMLInfo; * * Legacy variant for the "old" renderer. */ -class GUI : public InputHandler - , public ResizeHandler - , public HudHandler { +class GUI { public: explicit GUI(SDL_Window *window, const std::string &source, @@ -46,9 +43,9 @@ class GUI : public InputHandler void process_events(); private: - virtual bool on_resize(coord::viewport_delta new_size) override; - virtual bool on_input(SDL_Event *event) override; - virtual bool on_drawhud() override; + // virtual bool on_resize(coord::viewport_delta new_size) override; + // virtual bool on_input(SDL_Event *event) override; + // virtual bool on_drawhud() override; GLint tex_loc; GLuint screen_quad_vbo; diff --git a/libopenage/handlers.cpp b/libopenage/handlers.cpp deleted file mode 100644 index 8ced9fecfa..0000000000 --- a/libopenage/handlers.cpp +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2014-2015 the openage authors. See copying.md for legal info. - -#include "handlers.h" - -namespace openage { - -} // namespace openage diff --git a/libopenage/handlers.h b/libopenage/handlers.h deleted file mode 100644 index 9d8f00e536..0000000000 --- a/libopenage/handlers.h +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "coord/pixel.h" - - -namespace openage { - -class LegacyEngine; - -/** - * superclass for all possible drawing operations in the game. - */ -class [[deprecated]] HudHandler { -public: - virtual ~HudHandler() = default; - - /** - * execute the drawing action. - */ - virtual bool on_drawhud() = 0; -}; - -/** - * superclass for all calculations being done on engine tick. - */ -class [[deprecated]] TickHandler { -public: - virtual ~TickHandler() = default; - - /** - * execute the tick action. - */ - virtual bool on_tick() = 0; -}; - -/** - * superclass for handling any input event. - */ -class [[deprecated]] InputHandler { -public: - virtual ~InputHandler() = default; - - /** - * execute the input handler. - */ - virtual bool on_input(SDL_Event *event) = 0; -}; - -/** - * superclass for handling a window resize event. - */ -class [[deprecated]] ResizeHandler { -public: - virtual ~ResizeHandler() = default; - - /** - * execute the resize handler. - */ - virtual bool on_resize(coord::viewport_delta new_size) = 0; -}; - -} // namespace openage diff --git a/libopenage/input/legacy/input_manager.cpp b/libopenage/input/legacy/input_manager.cpp index f8ce7e2999..e8736b5fcd 100644 --- a/libopenage/input/legacy/input_manager.cpp +++ b/libopenage/input/legacy/input_manager.cpp @@ -305,70 +305,70 @@ modset_t InputManager::get_mod() const { } -bool InputManager::on_input(SDL_Event *e) { - // top level input handler - switch (e->type) { - case SDL_KEYUP: { - SDL_Keycode code = reinterpret_cast(e)->keysym.sym; - Event ev = sdl_key(code, SDL_GetModState()); - this->set_state(ev, false); - break; - } // case SDL_KEYUP - - case SDL_KEYDOWN: { - SDL_Keycode code = reinterpret_cast(e)->keysym.sym; - this->set_state(sdl_key(code, SDL_GetModState()), true); - break; - } // case SDL_KEYDOWN - - case SDL_TEXTINPUT: { - this->trigger(utf8(e->text.text)); - break; - } // case SDL_TEXTINPUT - - case SDL_MOUSEBUTTONUP: { - this->set_relative(false); - this->trigger(sdl_mouse_up_down(e->button.button, true, SDL_GetModState())); - Event ev = sdl_mouse(e->button.button, SDL_GetModState()); - this->set_state(ev, false); - break; - } // case SDL_MOUSEBUTTONUP - - case SDL_MOUSEBUTTONDOWN: { - // TODO: set which buttons - if (e->button.button == 2) { - this->set_relative(true); - } - this->trigger(sdl_mouse_up_down(e->button.button, false, SDL_GetModState())); - Event ev = sdl_mouse(e->button.button, SDL_GetModState()); - this->set_state(ev, true); - break; - } // case SDL_MOUSEBUTTONDOWN - - case SDL_MOUSEMOTION: { - if (this->relative_mode) { - this->set_motion(e->motion.xrel, e->motion.yrel); - } - else { - this->set_mouse(e->button.x, e->button.y); - } - - // must occur after setting mouse position - Event ev(event_class::MOUSE_MOTION, 0, this->get_mod()); - this->trigger(ev); - break; - } // case SDL_MOUSEMOTION - - case SDL_MOUSEWHEEL: { - Event ev = sdl_wheel(e->wheel.y, SDL_GetModState()); - this->trigger(ev); - break; - } // case SDL_MOUSEWHEEL - - } // switch (e->type) - - return true; -} +// bool InputManager::on_input(SDL_Event *e) { +// // top level input handler +// switch (e->type) { +// case SDL_KEYUP: { +// SDL_Keycode code = reinterpret_cast(e)->keysym.sym; +// Event ev = sdl_key(code, SDL_GetModState()); +// this->set_state(ev, false); +// break; +// } // case SDL_KEYUP + +// case SDL_KEYDOWN: { +// SDL_Keycode code = reinterpret_cast(e)->keysym.sym; +// this->set_state(sdl_key(code, SDL_GetModState()), true); +// break; +// } // case SDL_KEYDOWN + +// case SDL_TEXTINPUT: { +// this->trigger(utf8(e->text.text)); +// break; +// } // case SDL_TEXTINPUT + +// case SDL_MOUSEBUTTONUP: { +// this->set_relative(false); +// this->trigger(sdl_mouse_up_down(e->button.button, true, SDL_GetModState())); +// Event ev = sdl_mouse(e->button.button, SDL_GetModState()); +// this->set_state(ev, false); +// break; +// } // case SDL_MOUSEBUTTONUP + +// case SDL_MOUSEBUTTONDOWN: { +// // TODO: set which buttons +// if (e->button.button == 2) { +// this->set_relative(true); +// } +// this->trigger(sdl_mouse_up_down(e->button.button, false, SDL_GetModState())); +// Event ev = sdl_mouse(e->button.button, SDL_GetModState()); +// this->set_state(ev, true); +// break; +// } // case SDL_MOUSEBUTTONDOWN + +// case SDL_MOUSEMOTION: { +// if (this->relative_mode) { +// this->set_motion(e->motion.xrel, e->motion.yrel); +// } +// else { +// this->set_mouse(e->button.x, e->button.y); +// } + +// // must occur after setting mouse position +// Event ev(event_class::MOUSE_MOTION, 0, this->get_mod()); +// this->trigger(ev); +// break; +// } // case SDL_MOUSEMOTION + +// case SDL_MOUSEWHEEL: { +// Event ev = sdl_wheel(e->wheel.y, SDL_GetModState()); +// this->trigger(ev); +// break; +// } // case SDL_MOUSEWHEEL + +// } // switch (e->type) + +// return true; +// } std::vector InputManager::active_binds(const std::unordered_map &ctx_actions) const { diff --git a/libopenage/input/legacy/input_manager.h b/libopenage/input/legacy/input_manager.h index 92a0044524..289b95d3c5 100644 --- a/libopenage/input/legacy/input_manager.h +++ b/libopenage/input/legacy/input_manager.h @@ -8,7 +8,6 @@ #include #include -#include "handlers.h" #include "input/legacy/action.h" #include "input/legacy/event.h" #include "input/legacy/input_context.h" @@ -39,7 +38,7 @@ using binding_map_t = std::unordered_map; * bool set_bind(char* bind_char, string action) except + * string get_bind(string action) except + */ -class InputManager : public InputHandler { +class InputManager { public: /** * Screen edges used for edge scrolling. @@ -182,7 +181,7 @@ class InputManager : public InputHandler { /** * When a SDL event happens, this is called. */ - bool on_input(SDL_Event *e) override; + // bool on_input(SDL_Event *e) override; /** * Return a string representation of active key bindings diff --git a/libopenage/legacy_engine.h b/libopenage/legacy_engine.h index 532a5e17d1..396bb5b8d1 100644 --- a/libopenage/legacy_engine.h +++ b/libopenage/legacy_engine.h @@ -14,7 +14,6 @@ // pxd: from libopenage.cvar cimport CVarManager #include "cvar/cvar.h" #include "gui/engine_info.h" -#include "handlers.h" #include "input/legacy/action.h" #include "job/job_manager.h" #include "options.h" diff --git a/libopenage/unit/selection.cpp b/libopenage/unit/selection.cpp index 49ddd78440..d8c4fa44f4 100644 --- a/libopenage/unit/selection.cpp +++ b/libopenage/unit/selection.cpp @@ -24,61 +24,61 @@ UnitSelection::UnitSelection(LegacyEngine *engine) : engine{engine} { } -bool UnitSelection::on_drawhud() { - // // the drag selection box - // if (drag_active) { - // coord::viewport s = this->start.to_viewport(this->engine->coord); - // coord::viewport e = this->end.to_viewport(this->engine->coord); - // glLineWidth(1); - // glColor3f(1.0, 1.0, 1.0); - // glBegin(GL_LINE_LOOP); { - // glVertex3f(s.x, s.y, 0); - // glVertex3f(e.x, s.y, 0); - // glVertex3f(e.x, e.y, 0); - // glVertex3f(s.x, e.y, 0); - // } - // glEnd(); - // } - // - // // draw hp bars for each selected unit - // glLineWidth(3); - // for (auto u : this->units) { - // if (u.second.is_valid()) { - // Unit *unit_ptr = u.second.get(); - // if (unit_ptr->location && - // unit_ptr->has_attribute(attr_type::hitpoints) && - // unit_ptr->has_attribute(attr_type::damaged)) { - // - // auto &hp = unit_ptr->get_attribute(); - // auto &dm = unit_ptr->get_attribute(); - // float percent = static_cast(dm.hp) / static_cast(hp.hp); - // int mid = percent * 28.0f - 14.0f; - // - // coord::phys3 &pos_phys3 = unit_ptr->location->pos.draw; - // auto pos = pos_phys3.to_viewport(this->engine->coord); - // // green part - // glColor3f(0.0, 1.0, 0.0); - // glBegin(GL_LINES); { - // glVertex3f(pos.x - 14, pos.y + 60, 0); - // glVertex3f(pos.x + mid, pos.y + 60, 0); - // } - // glEnd(); - // - // // red part - // glColor3f(1.0, 0.0, 0.0); - // glBegin(GL_LINES); { - // glVertex3f(pos.x + mid, pos.y + 60, 0); - // glVertex3f(pos.x + 14, pos.y + 60, 0); - // } - // glEnd(); - // } - // } - // } - // glColor3f(1.0, 1.0, 1.0); // reset - - // ui graphics 3404 and 3405 - return true; -} +// bool UnitSelection::on_drawhud() { +// // the drag selection box +// if (drag_active) { +// coord::viewport s = this->start.to_viewport(this->engine->coord); +// coord::viewport e = this->end.to_viewport(this->engine->coord); +// glLineWidth(1); +// glColor3f(1.0, 1.0, 1.0); +// glBegin(GL_LINE_LOOP); { +// glVertex3f(s.x, s.y, 0); +// glVertex3f(e.x, s.y, 0); +// glVertex3f(e.x, e.y, 0); +// glVertex3f(s.x, e.y, 0); +// } +// glEnd(); +// } +// +// // draw hp bars for each selected unit +// glLineWidth(3); +// for (auto u : this->units) { +// if (u.second.is_valid()) { +// Unit *unit_ptr = u.second.get(); +// if (unit_ptr->location && +// unit_ptr->has_attribute(attr_type::hitpoints) && +// unit_ptr->has_attribute(attr_type::damaged)) { +// +// auto &hp = unit_ptr->get_attribute(); +// auto &dm = unit_ptr->get_attribute(); +// float percent = static_cast(dm.hp) / static_cast(hp.hp); +// int mid = percent * 28.0f - 14.0f; +// +// coord::phys3 &pos_phys3 = unit_ptr->location->pos.draw; +// auto pos = pos_phys3.to_viewport(this->engine->coord); +// // green part +// glColor3f(0.0, 1.0, 0.0); +// glBegin(GL_LINES); { +// glVertex3f(pos.x - 14, pos.y + 60, 0); +// glVertex3f(pos.x + mid, pos.y + 60, 0); +// } +// glEnd(); +// +// // red part +// glColor3f(1.0, 0.0, 0.0); +// glBegin(GL_LINES); { +// glVertex3f(pos.x + mid, pos.y + 60, 0); +// glVertex3f(pos.x + 14, pos.y + 60, 0); +// } +// glEnd(); +// } +// } +// } +// glColor3f(1.0, 1.0, 1.0); // reset + +// ui graphics 3404 and 3405 +// return true; +// } void UnitSelection::drag_begin(coord::camgame pos) { this->start = pos; diff --git a/libopenage/unit/selection.h b/libopenage/unit/selection.h index e946266009..fd623f91b9 100644 --- a/libopenage/unit/selection.h +++ b/libopenage/unit/selection.h @@ -5,7 +5,6 @@ #include #include "../coord/pixel.h" -#include "../handlers.h" #include "ability.h" #include "unit_container.h" @@ -35,11 +34,11 @@ enum class selection_type_t { /** * a user interface component allowing control of a selected group */ -class UnitSelection : public HudHandler { +class UnitSelection { public: UnitSelection(LegacyEngine *engine); - bool on_drawhud() override; + // bool on_drawhud() override; void drag_begin(coord::camgame pos); void drag_update(coord::camgame pos); void drag_release(const Player &player, Terrain *terrain, bool append = false); diff --git a/libopenage/unit/unit.h b/libopenage/unit/unit.h index 5165ed6a18..d832c26d19 100644 --- a/libopenage/unit/unit.h +++ b/libopenage/unit/unit.h @@ -9,7 +9,6 @@ #include #include "../coord/phys.h" -#include "../handlers.h" #include "../log/logsource.h" #include "../terrain/terrain_object.h" #include "../util/timing.h" diff --git a/libopenage/unit/unit_container.h b/libopenage/unit/unit_container.h index 89c6848990..8627c8582f 100644 --- a/libopenage/unit/unit_container.h +++ b/libopenage/unit/unit_container.h @@ -1,4 +1,4 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2023 the openage authors. See copying.md for legal info. #pragma once @@ -7,7 +7,6 @@ #include #include "../coord/tile.h" -#include "../handlers.h" #include "../util/timing.h" @@ -45,7 +44,6 @@ struct reference_data { */ class UnitReference { public: - /** * create an invalid reference */ @@ -60,7 +58,6 @@ class UnitReference { Unit *get() const; private: - /** * The default copy constructor and assignment * will just copy the shared pointer From bbf1b56699bed08c6bc62ac0278f8a4a70215d69 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Sep 2023 02:53:44 +0200 Subject: [PATCH 034/771] input: Remove legacy input system. --- libopenage/console/console.h | 5 +- libopenage/input/CMakeLists.txt | 1 - libopenage/input/legacy/CMakeLists.txt | 11 - libopenage/input/legacy/action.cpp | 176 ---------- libopenage/input/legacy/action.h | 99 ------ libopenage/input/legacy/event.cpp | 164 --------- libopenage/input/legacy/event.h | 162 --------- libopenage/input/legacy/input_context.cpp | 83 ----- libopenage/input/legacy/input_context.h | 117 ------- libopenage/input/legacy/input_manager.cpp | 406 ---------------------- libopenage/input/legacy/input_manager.h | 255 -------------- libopenage/input/legacy/text_to_event.cpp | 132 ------- libopenage/input/legacy/text_to_event.h | 18 - libopenage/legacy_engine.h | 1 - openage/testing/testlist.py | 1 - 15 files changed, 3 insertions(+), 1628 deletions(-) delete mode 100644 libopenage/input/legacy/CMakeLists.txt delete mode 100644 libopenage/input/legacy/action.cpp delete mode 100644 libopenage/input/legacy/action.h delete mode 100644 libopenage/input/legacy/event.cpp delete mode 100644 libopenage/input/legacy/event.h delete mode 100644 libopenage/input/legacy/input_context.cpp delete mode 100644 libopenage/input/legacy/input_context.h delete mode 100644 libopenage/input/legacy/input_manager.cpp delete mode 100644 libopenage/input/legacy/input_manager.h delete mode 100644 libopenage/input/legacy/text_to_event.cpp delete mode 100644 libopenage/input/legacy/text_to_event.h diff --git a/libopenage/console/console.h b/libopenage/console/console.h index dc9f00e9ef..a06b9e6760 100644 --- a/libopenage/console/console.h +++ b/libopenage/console/console.h @@ -7,7 +7,6 @@ #include "../coord/pixel.h" #include "../gamedata/color_dummy.h" -#include "../input/legacy/input_manager.h" #include "../renderer/font/font.h" #include "../util/color.h" #include "buf.h" @@ -18,6 +17,8 @@ class LegacyEngine; /** * In-game console subsystem. Featuring a full terminal emulator. + * + * TODO: Adapt to new engine subsystems. */ namespace console { @@ -70,7 +71,7 @@ class Console { Buf buf; renderer::Font font; - input::legacy::InputContext input_context; + // input::legacy::InputContext input_context; // the command state std::string command; diff --git a/libopenage/input/CMakeLists.txt b/libopenage/input/CMakeLists.txt index d27770d98a..f24e810608 100644 --- a/libopenage/input/CMakeLists.txt +++ b/libopenage/input/CMakeLists.txt @@ -7,5 +7,4 @@ add_sources(libopenage text_to_event.cpp ) -add_subdirectory("legacy") add_subdirectory("controller") diff --git a/libopenage/input/legacy/CMakeLists.txt b/libopenage/input/legacy/CMakeLists.txt deleted file mode 100644 index 799b91e1ee..0000000000 --- a/libopenage/input/legacy/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -add_sources(libopenage - action.cpp - event.cpp - input_context.cpp - input_manager.cpp - text_to_event.cpp -) - -pxdgen( - input_manager.h -) diff --git a/libopenage/input/legacy/action.cpp b/libopenage/input/legacy/action.cpp deleted file mode 100644 index 83d324f161..0000000000 --- a/libopenage/input/legacy/action.cpp +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "action.h" - -#include - -#include "cvar/cvar.h" -#include "input/legacy/input_manager.h" -#include "log/log.h" -#include "log/message.h" -#include "util/repr.h" - - -namespace openage { -namespace input::legacy { - -namespace { - - -// the list of action names that will be added during the constructor of ActionManager. -const std::vector DEFAULT_ACTIONS = { - "START_GAME", - "STOP_GAME", - "TOGGLE_HUD", - "SCREENSHOT", - "TOGGLE_DEBUG_OVERLAY", - "TOGGLE_DEBUG_GRID", - "QUICK_SAVE", - "QUICK_LOAD", - "TOGGLE_MODE", - "TOGGLE_MENU", - "TOGGLE_ITEM", - "TOGGLE_BLENDING", - "TOGGLE_PROFILER", - "TOGGLE_CONSTRUCT_MODE", - "TOGGLE_UNIT_DEBUG", - "TRAIN_OBJECT", - "ENABLE_BUILDING_PLACEMENT", - "DISABLE_SET_ABILITY", - "SET_ABILITY_MOVE", - "SET_ABILITY_GATHER", - "SET_ABILITY_GARRISON", - "TOGGLE_CONSOLE", - "SPAWN_VILLAGER", - "KILL_UNIT", - "BUILD_MENU", - "BUILD_MENU_MIL", - "CANCEL", - "BUILDING_HOUS", - "BUILDING_MILL", - "BUILDING_MINE", - "BUILDING_SMIL", - "BUILDING_DOCK", - "BUILDING_FARM", - "BUILDING_BLAC", - "BUILDING_MRKT", - "BUILDING_CRCH", - "BUILDING_UNIV", - "BUILDING_RTWC", - "BUILDING_WNDR", - "BUILDING_BRKS", - "BUILDING_ARRG", - "BUILDING_STBL", - "BUILDING_SIWS", - "BUILDING_WCTWX", - "BUILDING_WALL", - "BUILDING_WALL2", - "BUILDING_WCTW", - "BUILDING_WCTW4", - "BUILDING_GTCA2", - "BUILDING_CSTL", - "BUILDING_TOWN_CENTER", - "SWITCH_TO_PLAYER_1", - "SWITCH_TO_PLAYER_2", - "SWITCH_TO_PLAYER_3", - "SWITCH_TO_PLAYER_4", - "SWITCH_TO_PLAYER_5", - "SWITCH_TO_PLAYER_6", - "SWITCH_TO_PLAYER_7", - "SWITCH_TO_PLAYER_8", - "UP_ARROW", - "DOWN_ARROW", - "LEFT_ARROW", - "RIGHT_ARROW", - "SELECT", - "DESELECT", - "BEGIN_SELECTION", - "END_SELECTION", - "FORWARD", - "BACK", - "PAINT_TERRAIN", - "BUILDING_5", - "BUILDING_6", - "BUILDING_7", - "BUILDING_8", - "BUILD", - "KEEP_BUILDING", - "INCREASE_SELECTION", - "ORDER_SELECT"}; - -} // anonymous namespace - - -ActionManager::ActionManager(InputManager *input_manager, - const std::shared_ptr &cvar_manager) : - input_manager{input_manager}, - cvar_manager{cvar_manager} { - this->create("UNDEFINED"); - - for (auto &type : DEFAULT_ACTIONS) { - this->create(type); - } -} // anonymous namespace - - -bool ActionManager::create(const std::string &type) { - if (this->actions.find(type) != this->actions.end()) { - // that action is already in the list. fail. - // TODO: throw an exception instead? - // for now, just print a warning log message - log::log(WARN << "can not create action " - << util::repr(type) << ": already exists"); - return false; - } - - action_t action_id = this->next_action_id++; - - this->reverse_map[action_id] = type; - this->actions[type] = action_id; - - // and the corresponding cvar, which modifies the action bindings - // TODO: this has nothing to do with the actionmanager! - // remove the cvarmanager-access here! - this->cvar_manager->create(type, std::make_pair( - // the cvar's getter - [type, this]() { - return this->input_manager->get_bind(type); - }, - // the cvar's setter - [type, this](const std::string &value) { - this->input_manager->set_bind(value.c_str(), type); - })); - - return true; -} - - -action_t ActionManager::get(const std::string &type) { - auto it = this->actions.find(type); - if (it != this->actions.end()) { - return it->second; - } - else { - return this->actions.at("UNDEFINED"); - } -} - - -std::string ActionManager::get_name(const action_t action) { - auto it = this->reverse_map.find(action); - if (it != this->reverse_map.end()) { - return it->second; - } - else { - return "UNDEFINED"; - } -} - - -bool ActionManager::is(const std::string &type, const action_t action) { - return this->get(type) == action; -} - - -} // namespace input::legacy -} // namespace openage diff --git a/libopenage/input/legacy/action.h b/libopenage/input/legacy/action.h deleted file mode 100644 index dadd618ef9..0000000000 --- a/libopenage/input/legacy/action.h +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include - -#include "coord/pixel.h" -#include "input/legacy/event.h" - -namespace openage { - -class LegacyEngine; - -namespace cvar { -class CVarManager; -} // namespace cvar - -namespace input::legacy { - -class InputManager; - -/** - * Used to identify actions. - */ -using action_t = unsigned int; - - -// TODO this whole class seems rather obsolete... -// remove it and instead provide a proper class for action_t? -/** - * The action manager manages all the actions allow creation, access - * information and equality. - */ -class ActionManager { -public: - ActionManager(InputManager *input_manager, - const std::shared_ptr &cvar_manager); - -public: - action_t get(const std::string &type); - std::string get_name(const action_t action); - bool is(const std::string &type, const action_t action); - -private: - bool create(const std::string &type); - - // mapping from action name to numbers - std::unordered_map actions; - // for high-speed reverse lookups (action number -> name) - std::unordered_map reverse_map; - - InputManager *const input_manager; - const std::shared_ptr cvar_manager; - - // the id of the next action that is added via create(). - action_t next_action_id = 0; -}; - - -// TODO: use action_hint_t = std::pair -// for example a building command -// std::make_pair(action_t::BUILD, 123) -using action_id_t = action_t; - - -/** - * Contains information about a triggered event. - */ -struct action_arg_t { - // Triggering event - const Event e; - - // Mouse position - const coord::input mouse; - const coord::input_delta motion; - - // hints for arg receiver - // these get set globally in input manager - std::vector hints; -}; - - -/** - * Performs the effect of an action. - */ -using action_func_t = std::function; - - -/** - * For receivers of sets of events a bool is returned - * to indicate if the event was used. - */ -using action_check_t = std::function; - - -} // namespace input::legacy -} // namespace openage diff --git a/libopenage/input/legacy/event.cpp b/libopenage/input/legacy/event.cpp deleted file mode 100644 index a2c5d1c004..0000000000 --- a/libopenage/input/legacy/event.cpp +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "event.h" - -#include -#include - -namespace openage::input::legacy { - -ClassCode::ClassCode(event_class cl, code_t code) : - eclass(cl), - code(code) { -} - - -std::vector ClassCode::get_classes() const { - std::vector result; - - // use event_base to traverse up the class tree - event_class ec = this->eclass; - result.push_back(ec); - while (event_base.count(ec) > 0) { - ec = event_base.at(ec); - result.push_back(ec); - } - return result; -} - - -bool ClassCode::has_class(const event_class &ec) const { - for (auto c : this->get_classes()) { - if (c == ec) { - return true; - } - } - return false; -} - - -bool operator==(ClassCode a, ClassCode b) { - return a.eclass == b.eclass && a.code == b.code; -} - - -Event::Event(event_class cl, code_t code, modset_t mod) : - cc(cl, code), - mod(std::move(mod)) {} - - -Event::Event(event_class cl, std::string text, modset_t mod) : - cc(cl, 0), - mod(std::move(mod)), - utf8(std::move(text)) {} - - -char Event::as_char() const { - return (cc.code & 0xff); -} - - -std::string Event::as_utf8() const { - return utf8; -} - - -std::string Event::info() const { - std::string result; - result += "[Event: "; - result += std::to_string(static_cast(this->cc.eclass)); - result += ", "; - result += std::to_string(this->cc.code); - result += ", "; - result += this->as_char(); - result += " ("; - result += std::to_string(this->mod.size()); - result += ")]"; - return result; -} - - -bool Event::operator==(const Event &other) const { - return this->cc == other.cc && this->mod == other.mod && this->utf8 == other.utf8; -} - - -int event_hash::operator()(const Event &e) const { - return class_code_hash()(e.cc); // ^ std::hash()(e.mod) * 3664657; -} - - -int event_class_hash::operator()(const event_class &c) const { - return std::hash()(static_cast(c)); -} - - -int modifier_hash::operator()(const modifier &m) const { - return std::hash()(static_cast(m)); -} - - -int class_code_hash::operator()(const ClassCode &e) const { - return event_class_hash()(e.eclass) ^ std::hash()(e.code) * 3664657; -} - - -modset_t sdl_mod(SDL_Keymod mod) { - // Remove modifiers like num lock and caps lock - // mod = static_cast(mod & this->used_keymods); - modset_t result; - if (mod & KMOD_CTRL) { - result.emplace(modifier::CTRL); - } - if (mod & KMOD_SHIFT) { - result.emplace(modifier::SHIFT); - } - if (mod & KMOD_ALT) { - result.emplace(modifier::ALT); - } - return result; -} - - -Event sdl_key(SDL_Keycode code, SDL_Keymod mod) { - // sdl values for non printable keys - if (code & (1 << 30)) { - return Event(event_class::OTHER, code, sdl_mod(mod)); - } - else { - event_class ec; - char c = (code & 0xff); - if (isdigit(c)) { - ec = event_class::DIGIT; - } - else if (isalpha(c)) { - ec = event_class::ALPHA; - } - else if (isprint(c)) { - ec = event_class::PRINT; - } - else { - ec = event_class::NONPRINT; - } - return Event(ec, code, sdl_mod(mod)); - } -} - -Event utf8(const std::string &text) { - return Event(event_class::UTF8, text, modset_t()); -} - -Event sdl_mouse(int button, SDL_Keymod mod) { - return Event(event_class::MOUSE_BUTTON, button, sdl_mod(mod)); -} - -Event sdl_mouse_up_down(int button, bool up, SDL_Keymod mod) { - return Event(up ? event_class::MOUSE_BUTTON_UP : event_class::MOUSE_BUTTON_DOWN, button, sdl_mod(mod)); -} - -Event sdl_wheel(int direction, SDL_Keymod mod) { - return Event(event_class::MOUSE_WHEEL, direction, sdl_mod(mod)); -} - - -} // namespace openage::input::legacy diff --git a/libopenage/input/legacy/event.h b/libopenage/input/legacy/event.h deleted file mode 100644 index 82d8e9a62e..0000000000 --- a/libopenage/input/legacy/event.h +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include -#include - -#include - -namespace openage { -namespace input::legacy { - -/** - * highest level classes of input - */ -enum class event_class { - ANY, - KEYBOARD, - CHAR, // basic keycodes (lower-case, non-modified) - ALPHA, // abc - DIGIT, // 123 - PRINT, // remaining printable chars - NONPRINT, // tab, return, backspace, delete - OTHER, // arrows, home, end - UTF8, // events with utf8 encoded data - MOUSE, - MOUSE_BUTTON, - MOUSE_BUTTON_UP, - MOUSE_BUTTON_DOWN, - MOUSE_WHEEL, - MOUSE_MOTION -}; - - -struct event_class_hash { - int operator()(const event_class &s) const; -}; - - -/** - * each event type mapped to parent type - */ -static std::unordered_map event_base{ - {event_class::KEYBOARD, event_class::ANY}, - {event_class::CHAR, event_class::KEYBOARD}, - {event_class::ALPHA, event_class::CHAR}, - {event_class::DIGIT, event_class::CHAR}, - {event_class::PRINT, event_class::CHAR}, - {event_class::NONPRINT, event_class::KEYBOARD}, - {event_class::OTHER, event_class::KEYBOARD}, - {event_class::UTF8, event_class::KEYBOARD}, - {event_class::MOUSE, event_class::ANY}, - {event_class::MOUSE_BUTTON, event_class::MOUSE}, - {event_class::MOUSE_WHEEL, event_class::MOUSE}, - {event_class::MOUSE_MOTION, event_class::MOUSE}, -}; - -/** - * mods set on an event - */ -enum class modifier { - CTRL, - ALT, - SHIFT -}; - - -struct modifier_hash { - int operator()(const modifier &s) const; -}; - - -/** - * types used by events - */ -using code_t = int; -using modset_t = std::unordered_set; - - -/** - * base event type containing event handler and event code - */ -class ClassCode { -public: - ClassCode(event_class cl, code_t code); - - /** - * classes ordered with most specific first - */ - std::vector get_classes() const; - bool has_class(const event_class &c) const; - - const event_class eclass; - const code_t code; -}; - - -bool operator==(ClassCode a, ClassCode b); - - -struct class_code_hash { - int operator()(const ClassCode &k) const; -}; - - -/** - * Input event, as triggered by some input device like - * mouse, kezb, joystick, tablet, microwave or dildo. - * Some modifier keys may also be pressed during the event. - */ -class Event { -public: - Event(event_class cl, code_t code, modset_t mod); - Event(event_class cl, std::string, modset_t mod); - - /** - * Return keyboard text as char - * returns 0 for non-text events - */ - char as_char() const; - - /** - * Returns a utf encoded char - * or an empty string for non-utf8 events - */ - std::string as_utf8() const; - - /** - * logable debug info - */ - std::string info() const; - - bool operator==(const Event &other) const; - - const ClassCode cc; - const modset_t mod; - const std::string utf8; -}; - - -struct event_hash { - int operator()(const Event &e) const; -}; - - -using event_set_t = std::unordered_set; - - -// SDL mapping functions - -modset_t sdl_mod(SDL_Keymod mod); -Event sdl_key(SDL_Keycode code, SDL_Keymod mod = KMOD_NONE); -Event utf8(const std::string &text); -Event sdl_mouse(int button, SDL_Keymod mod = KMOD_NONE); -Event sdl_mouse_up_down(int button, bool up, SDL_Keymod mod = KMOD_NONE); -Event sdl_wheel(int direction, SDL_Keymod mod = KMOD_NONE); - - -} // namespace input::legacy -} // namespace openage diff --git a/libopenage/input/legacy/input_context.cpp b/libopenage/input/legacy/input_context.cpp deleted file mode 100644 index b77ae592fe..0000000000 --- a/libopenage/input/legacy/input_context.cpp +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "input_context.h" - -#include "input/legacy/input_manager.h" - - -namespace openage { -namespace input::legacy { - - -InputContext::InputContext() : - InputContext{nullptr} {} - - -InputContext::InputContext(InputManager *manager) : - utf8_mode{false} { - this->register_to(manager); -} - - -std::vector InputContext::active_binds() const { - if (this->input_manager == nullptr) { - return {}; - } - - // TODO: try to purge this backpointer to the input manager. - return this->input_manager->active_binds(this->by_type); -} - -void InputContext::bind(action_t type, const action_func_t act) { - this->by_type.emplace(std::make_pair(type, act)); -} - -void InputContext::bind(const Event &ev, const action_func_t act) { - this->by_event.emplace(std::make_pair(ev, act)); -} - -void InputContext::bind(event_class ec, const action_check_t act) { - this->by_class.emplace(std::make_pair(ec, act)); -} - -bool InputContext::execute_if_bound(const action_arg_t &arg) { - // arg type hints are highest priority - for (auto &h : arg.hints) { - auto action = this->by_type.find(h); - if (action != this->by_type.end()) { - action->second(arg); - return true; - } - } - - // specific event mappings - auto action = this->by_event.find(arg.e); - if (action != this->by_event.end()) { - action->second(arg); - return true; - } - - // check all possible class mappings - for (auto &c : arg.e.cc.get_classes()) { - auto action = this->by_class.find(c); - if (action != this->by_class.end() && action->second(arg)) { - return true; - } - } - - return false; -} - - -void InputContext::register_to(InputManager *manager) { - this->input_manager = manager; -} - - -void InputContext::unregister() { - this->input_manager = nullptr; -} - - -} // namespace input::legacy -} // namespace openage diff --git a/libopenage/input/legacy/input_context.h b/libopenage/input/legacy/input_context.h deleted file mode 100644 index ccd0f32cd1..0000000000 --- a/libopenage/input/legacy/input_context.h +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include -#include - -#include "input/legacy/action.h" -#include "input/legacy/event.h" - -namespace openage { -namespace input::legacy { - -class InputManager; - - -/** - * An input context contains all keybindings and actions - * active in e.g. the HUD only. - * For the console, there's a different input context. - * That way, each context can have the same keys - * assigned to different actions, the active context - * decides, which one to trigger. - */ -class InputContext { -public: - /** - * Create an unbound input context. - */ - InputContext(); - - /** - * Create a bound context, assigned to its manager. - */ - InputContext(InputManager *manager); - - virtual ~InputContext() = default; - - /** - * a list of all keys of this context - * which are bound currently in the active context. - * - * TODO: move this method to the input manager. - * as InputManager::active_binds(const InputContext &) const; - */ - std::vector active_binds() const; - - /** - * bind a specific action idetifier - * this is the highest matching priority - */ - void bind(action_t type, const action_func_t act); - - /** - * bind a specific event - * this is the second matching priority - */ - void bind(const Event &ev, const action_func_t act); - - /** - * bind all events of a specific class - * this is the lowest matching priority - */ - void bind(event_class ec, const action_check_t act); - - /** - * lookup an action. If it is bound, execute it. - * @return true when the action is executed, false else. - */ - bool execute_if_bound(const action_arg_t &e); - - /** - * Called by the InputManager where this context - * shall be registered to. - */ - void register_to(InputManager *manager); - - /** - * Remove the registration to an input manager. - */ - void unregister(); - - - /** - * Affects which keyboard events are received: - * true to accpet utf8 text events, - * false to receive regular char events - */ - bool utf8_mode; - -private: - /** - * Input manager this context is bound to. - */ - InputManager *input_manager; - - /** - * Maps an action id to a event execution function. - */ - std::unordered_map by_type; - - /** - * map specific overriding events - */ - std::unordered_map by_event; - - /** - * event to action map - * event_class as key, to ensure all events can be mapped - */ - std::unordered_map by_class; -}; - -} // namespace input::legacy -} // namespace openage diff --git a/libopenage/input/legacy/input_manager.cpp b/libopenage/input/legacy/input_manager.cpp deleted file mode 100644 index e8736b5fcd..0000000000 --- a/libopenage/input/legacy/input_manager.cpp +++ /dev/null @@ -1,406 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include -#include - -#include "input/legacy/action.h" -#include "input/legacy/input_manager.h" -#include "input/legacy/text_to_event.h" -#include "log/log.h" - - -namespace openage::input::legacy { - -InputManager::InputManager(ActionManager *action_manager) : - action_manager{action_manager}, - global_context{this}, - relative_mode{false} { - this->global_context.register_to(this); -} - - -namespace { - -std::string mod_set_string(modset_t mod) { - if (not mod.empty()) { - for (auto &it : mod) { - switch (it) { - case modifier::ALT: - return "ALT + "; - case modifier::CTRL: - return "CTRL + "; - case modifier::SHIFT: - return "SHIFT + "; - default: - break; - } - } - } - return ""; -} - -std::string event_as_string(const Event &event) { - if (not event.as_utf8().empty()) { - return mod_set_string(event.mod) + event.as_utf8(); - } - else { - if (event.cc.eclass == event_class::MOUSE_WHEEL) { - if (event.cc.code == -1) { - return mod_set_string(event.mod) + "Wheel down"; - } - else { - return mod_set_string(event.mod) + "Wheel up"; - } - } - return mod_set_string(event.mod) + SDL_GetKeyName(event.cc.code); - } -} - -} // namespace - - -std::string InputManager::get_bind(const std::string &action_str) { - action_t action = this->action_manager->get(action_str); - if (this->action_manager->is("UNDEFINED", action)) { - return ""; - } - - auto it = this->keys.find(action); - if (it == this->keys.end()) { - return " "; - } - - switch (it->second.cc.eclass) { - case event_class::MOUSE_BUTTON: - return this->mouse_bind_to_string(it->second); - case event_class::MOUSE_WHEEL: - return this->wheel_bind_to_string(it->second); - default: - return this->key_bind_to_string(it->second); - } -} - -bool InputManager::set_bind(const std::string &bind_str, const std::string &action_str) { - try { - action_t action = this->action_manager->get(action_str); - if (this->action_manager->is("UNDEFINED", action)) { - return false; - } - - Event ev = text_to_event(bind_str); - - auto it = this->keys.find(action); - if (it != this->keys.end()) { - this->keys.erase(it); - } - this->keys.emplace(std::make_pair(action, ev)); - - return true; - } - catch (int error) { - return false; - } -} - -std::string InputManager::key_bind_to_string(const Event &ev) { - std::string key_str = std::string(SDL_GetKeyName(ev.cc.code)); - - auto end = ev.mod.end(); - if (ev.mod.find(modifier::ALT) != end) { - key_str = "Alt " + key_str; - } - if (ev.mod.find(modifier::SHIFT) != end) { - key_str = "Shift " + key_str; - } - if (ev.mod.find(modifier::CTRL) != end) { - key_str = "Ctrl " + key_str; - } - return key_str; -} - -std::string InputManager::mouse_bind_to_string(const Event &ev) { - return "MOUSE " + std::to_string(ev.cc.code); -} - -std::string InputManager::wheel_bind_to_string(const Event &ev) { - std::string base = "WHEEL "; - switch (ev.cc.code) { - case 1: - return base + "UP"; - case -1: - return base + "DOWN"; - default: - return ""; - } -} - -InputContext &InputManager::get_global_context() { - return this->global_context; -} - -InputContext &InputManager::get_top_context() { - // return the global input context - // if no override is pushed - if (this->contexts.empty()) { - return this->global_context; - } - return *this->contexts.back(); -} - -void InputManager::push_context(InputContext *context) { - // push the context to the top - this->contexts.push_back(context); - - context->register_to(this); -} - - -void InputManager::remove_context(InputContext *context) { - if (this->contexts.empty()) { - return; - } - - for (auto it = this->contexts.begin(); it != this->contexts.end(); ++it) { - if ((*it) == context) { - this->contexts.erase(it); - context->unregister(); - return; - } - } -} - - -bool InputManager::ignored(const Event &e) { - // filter duplicate utf8 events - // these are ignored unless the top mode enables - // utf8 mode, in which case regular char codes are ignored - return ((e.cc.has_class(event_class::CHAR) || e.cc.has_class(event_class::UTF8)) && this->get_top_context().utf8_mode != e.cc.has_class(event_class::UTF8)); -} - - -bool InputManager::trigger(const Event &e) { - if (this->ignored(e)) { - return false; - } - - // arg passed to receivers - action_arg_t arg{e, this->mouse_position, this->mouse_motion, {}}; - - for (auto &it : this->keys) { - if (e == it.second) { - arg.hints.emplace_back(it.first); - } - } - - // Check context list on top of the stack (most recent bound first) - for (auto it = this->contexts.rbegin(); it != this->contexts.rend(); ++it) { - if ((*it)->execute_if_bound(arg)) { - return true; - } - } - - // If no local keybinds were bound, check the global keybinds - return this->global_context.execute_if_bound(arg); -} - - -void InputManager::set_state(const Event &ev, bool is_down) { - if (this->ignored(ev)) { - return; - } - - // update key states - this->keymod = ev.mod; - bool was_down = this->states[ev.cc]; - this->states[ev.cc] = is_down; - - // a key going from pressed to unpressed - // will automatically trigger event handling - if (was_down && !is_down) { - this->trigger(ev); - } -} - - -void InputManager::set_mouse(int x, int y) { - auto last_position = this->mouse_position; - this->mouse_position = coord::input{coord::pixel_t{x}, coord::pixel_t{y}}; - this->mouse_motion = this->mouse_position - last_position; -} - - -void InputManager::set_motion(int x, int y) { - this->mouse_motion.x = x; - this->mouse_motion.y = y; -} - - -void InputManager::set_relative(bool mode) { - if (this->relative_mode == mode) { - return; - } - - // change mode - this->relative_mode = mode; - if (this->relative_mode) { - SDL_SetRelativeMouseMode(SDL_TRUE); - } - else { - SDL_SetRelativeMouseMode(SDL_FALSE); - } -} - -bool InputManager::is_mouse_at_edge(Edge edge, int window_size) { - int x, y; - SDL_GetMouseState(&x, &y); - - // Border width to consider screen edges for scrolling. - // AoE II appears to be approx 10px. - // TODO: make configurable through cvar. - const int edge_offset = 10; - - if (edge == Edge::LEFT && x <= edge_offset) { - return true; - } - if (edge == Edge::RIGHT && x >= window_size - edge_offset - 1) { - return true; - } - if (edge == Edge::UP && y <= edge_offset) { - return true; - } - if (edge == Edge::DOWN && y >= window_size - edge_offset - 1) { - return true; - } - - return false; -} - -bool InputManager::is_down(const ClassCode &cc) const { - auto it = this->states.find(cc); - if (it != this->states.end()) { - return it->second; - } - return false; -} - - -bool InputManager::is_down(event_class ec, code_t code) const { - return is_down(ClassCode(ec, code)); -} - - -bool InputManager::is_down(SDL_Keycode k) const { - return is_down(sdl_key(k).cc); -} - - -bool InputManager::is_mod_down(modifier mod) const { - return (this->keymod.count(mod) > 0); -} - - -modset_t InputManager::get_mod() const { - SDL_Keymod mod = SDL_GetModState(); - return sdl_mod(mod); -} - - -// bool InputManager::on_input(SDL_Event *e) { -// // top level input handler -// switch (e->type) { -// case SDL_KEYUP: { -// SDL_Keycode code = reinterpret_cast(e)->keysym.sym; -// Event ev = sdl_key(code, SDL_GetModState()); -// this->set_state(ev, false); -// break; -// } // case SDL_KEYUP - -// case SDL_KEYDOWN: { -// SDL_Keycode code = reinterpret_cast(e)->keysym.sym; -// this->set_state(sdl_key(code, SDL_GetModState()), true); -// break; -// } // case SDL_KEYDOWN - -// case SDL_TEXTINPUT: { -// this->trigger(utf8(e->text.text)); -// break; -// } // case SDL_TEXTINPUT - -// case SDL_MOUSEBUTTONUP: { -// this->set_relative(false); -// this->trigger(sdl_mouse_up_down(e->button.button, true, SDL_GetModState())); -// Event ev = sdl_mouse(e->button.button, SDL_GetModState()); -// this->set_state(ev, false); -// break; -// } // case SDL_MOUSEBUTTONUP - -// case SDL_MOUSEBUTTONDOWN: { -// // TODO: set which buttons -// if (e->button.button == 2) { -// this->set_relative(true); -// } -// this->trigger(sdl_mouse_up_down(e->button.button, false, SDL_GetModState())); -// Event ev = sdl_mouse(e->button.button, SDL_GetModState()); -// this->set_state(ev, true); -// break; -// } // case SDL_MOUSEBUTTONDOWN - -// case SDL_MOUSEMOTION: { -// if (this->relative_mode) { -// this->set_motion(e->motion.xrel, e->motion.yrel); -// } -// else { -// this->set_mouse(e->button.x, e->button.y); -// } - -// // must occur after setting mouse position -// Event ev(event_class::MOUSE_MOTION, 0, this->get_mod()); -// this->trigger(ev); -// break; -// } // case SDL_MOUSEMOTION - -// case SDL_MOUSEWHEEL: { -// Event ev = sdl_wheel(e->wheel.y, SDL_GetModState()); -// this->trigger(ev); -// break; -// } // case SDL_MOUSEWHEEL - -// } // switch (e->type) - -// return true; -// } - - -std::vector InputManager::active_binds(const std::unordered_map &ctx_actions) const { - std::vector result; - - // TODO: this only checks the by_type mappings, the others are missing! - for (auto &action : ctx_actions) { - std::string keyboard_key; - - for (auto &key : this->keys) { - if (key.first == action.first) { - keyboard_key = event_as_string(key.second); - break; - } - } - - // this is only possible if the action is registered, - // then this->input_manager != nullptr. - // TODO: try to purge the action manager access here. - // TODO: get_name takes O(n) time - std::string action_type_str = this->get_action_manager()->get_name(action.first); - - result.push_back(keyboard_key + " : " + action_type_str); - } - - return result; -} - - -ActionManager *InputManager::get_action_manager() const { - return this->action_manager; -} - - -} // namespace openage::input::legacy diff --git a/libopenage/input/legacy/input_manager.h b/libopenage/input/legacy/input_manager.h deleted file mode 100644 index 289b95d3c5..0000000000 --- a/libopenage/input/legacy/input_manager.h +++ /dev/null @@ -1,255 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -// pxd: from libcpp cimport bool -#include -// pxd: from libcpp.string cimport string -#include -#include - -#include "input/legacy/action.h" -#include "input/legacy/event.h" -#include "input/legacy/input_context.h" - - -namespace openage { - - -/** - * The openage input layer. - * It gets all the events and processes them accordingly. - */ -namespace input::legacy { - -/** - * maps actions to events. - */ -using binding_map_t = std::unordered_map; - - -/** - * The input manager manages all input layers (hud, game, ...) - * and triggers the registered actions depending on the active layer. - * - * pxd: - * - * cppclass InputManager: - * bool set_bind(char* bind_char, string action) except + - * string get_bind(string action) except + - */ -class InputManager { -public: - /** - * Screen edges used for edge scrolling. - */ - enum class Edge { - LEFT, - RIGHT, - UP, - DOWN - }; - - InputManager(ActionManager *action_manager); - - /** - * Return the string representation of the bind assignated to an action. - */ - std::string get_bind(const std::string &action); - - /** - * Set the given action to be triggered by the given bind (key/mouse - * /wheel). Remove previous assignation. Do nothing if either they - * given bind or action is invalid/unknow. - */ - bool set_bind(const std::string &bind_str, const std::string &action); - - /** - * Return the string representation of the key event. - */ - std::string key_bind_to_string(const Event &ev); - - /** - * Return the string representation of the mouse event. - */ - std::string mouse_bind_to_string(const Event &ev); - - /** - * Return the key representation of the event. - */ - std::string wheel_bind_to_string(const Event &ev); - - /** - * returns the global keybind context. - * actions bound here will be retained even when override_context is called. - */ - InputContext &get_global_context(); - - /** - * Returns the context on top. - * Note there is always a top context - * since the global context will be - * considered on top when none are registered - */ - InputContext &get_top_context(); - - /** - * register a hotkey context by pushing it onto the stack. - * - * this adds the given pointer to the `contexts` list. - * that way the context lays on "top". - * - * if other contexts are registered afterwards, - * it wanders down the stack, i.e. looses priority. - */ - void push_context(InputContext *context); - - /** - * removes any matching registered context from the stack. - * - * the removal is done by finding the given pointer - * in the `contexts` lists, then deleting it in there. - */ - void remove_context(InputContext *context); - - /** - * true if the given event type is being ignored - */ - bool ignored(const Event &e); - - /** - * manages the pressing of an input event (key, mouse, ...). - * first checks whether an action is bound to it. - * if it is, look for an handler to execute that handler. - * returns true if the event was responded to - */ - bool trigger(const Event &e); - - /** - * sets the state of a specific key - */ - void set_state(const Event &ev, bool is_down); - - /** - * updates mouse position state and motion - */ - void set_mouse(int x, int y); - - /** - * updates mouse motion only - */ - void set_motion(int x, int y); - - /** - * enable relative mouse mode - */ - void set_relative(bool mode); - - /** - * Query whether cursor is at edgo of screen - * - * edge variable is enum Edges - * - * @return true when the mouse is at the queried screen edge, false else. - */ - bool is_mouse_at_edge(Edge edge, int window_size); - - /** - * Query stored pressing stat for a key. - * - * note that the function stores a unknown/new keycode - * as 'not pressed' if requested - * @return true when the key is pressed, false else. - */ - bool is_down(const ClassCode &cc) const; - bool is_down(event_class ec, code_t code) const; - - /** - * Most cases should use above is_down(class, code) - * instead to avoid relying on sdl types - * - * Query stored pressing stat for a key. - * @return true when the key is pressed, false else. - */ - bool is_down(SDL_Keycode k) const; - - /** - * Checks whether a key modifier is held down. - */ - bool is_mod_down(modifier mod) const; - - /** - * When a SDL event happens, this is called. - */ - // bool on_input(SDL_Event *e) override; - - /** - * Return a string representation of active key bindings - * from the given context. - */ - std::vector active_binds(const std::unordered_map &ctx_actions) const; - - /** - * Get the action manager attached to this input manager. - */ - ActionManager *get_action_manager() const; - -private: - modset_t get_mod() const; - - /** - * The action manager to used for keybind action lookups. - */ - ActionManager *action_manager; - - /** - * The global context. Used as fallback. - */ - InputContext global_context; - - /** - * Maps actions to events. - */ - binding_map_t keys; - - /** - * Stack of active input contexts. - * The most recent entry is pushed on top of the stack. - */ - std::vector contexts; - - /** - * key to is_down map. - * stores a mapping between keycodes and its pressing state. - * a true value means the key is currently pressed, - * false indicates the key is untouched. - */ - std::unordered_map states; - - /** - * Current key modifiers. - * Included ALL modifiers including num lock and caps lock. - */ - modset_t keymod; - - /** - * mode where mouse position is ignored - * used for map scrolling - */ - bool relative_mode; - - /** - * mouse position in the window - */ - coord::input mouse_position{0, 0}; - - /** - * mouse position relative to the last frame position. - */ - coord::input_delta mouse_motion{0, 0}; - - friend InputContext; -}; - -} // namespace input::legacy -} // namespace openage diff --git a/libopenage/input/legacy/text_to_event.cpp b/libopenage/input/legacy/text_to_event.cpp deleted file mode 100644 index c0d410456b..0000000000 --- a/libopenage/input/legacy/text_to_event.cpp +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. - -#include "text_to_event.h" - -#include -#include -#include - -#include "error/error.h" -#include "log/log.h" -#include "testing/testing.h" - - -namespace openage { -namespace input::legacy { - -namespace { -const std::vector modifiers{ - KMOD_LCTRL, KMOD_LSHIFT, KMOD_RCTRL, KMOD_RSHIFT, KMOD_LALT, KMOD_RALT, KMOD_LGUI, KMOD_RGUI, KMOD_CTRL, KMOD_SHIFT, KMOD_ALT, KMOD_GUI, KMOD_MODE, KMOD_CAPS, KMOD_NUM}; - -const std::regex event_pattern{ - "(?:(?:(LCtrl)|(LShift)|(RCtrl)|(RShift)|(LAlt)|(RAlt)|(LGui)|(RGUI)|(Ctrl)|(Shift)|(Alt)|(Gui)|(AltGr)|(Caps)|(NumLck))[[:space:]]+)?" // modifier (optional) - "([^[:space:]]+)" // key - "(?:[[:space:]]+([^[:space:]]+))?" // parameter, like direction (optional) - "[[:space:]]*"}; - -void check_modifiers_once() { - static bool checked = false; - - if (!checked) { - checked = true; - ENSURE(event_pattern.mark_count() == modifiers.size() + 2, "Groups in the input event regex pattern: one per key modifier, key itself and amount."); - } -} - -Event to_event(const std::string &event_type, const std::string ¶m, const int mod) { - try { - if (event_type == "MOUSE") - return sdl_mouse(std::stoi(param), static_cast(mod)); - else if (event_type == "MOUSE_UP") - return sdl_mouse_up_down(std::stoi(param), true, static_cast(mod)); - else if (event_type == "MOUSE_DOWN") - return sdl_mouse_up_down(std::stoi(param), false, static_cast(mod)); - } - catch (std::logic_error &) { - throw Error(MSG(err) << "could not parse mouse button '" << param << "'!"); - } - - if (event_type == "WHEEL") { - if (param == "1" || param == "UP") { - return sdl_wheel(1, static_cast(mod)); - } - else if (param == "-1" || param == "DOWN") { - return sdl_wheel(-1, static_cast(mod)); - } - - throw Error(MSG(err) << "could not parse mouse wheel amount '" << param << "'!"); - } - - SDL_Keycode key_code = SDL_GetKeyFromName(event_type.c_str()); - if (key_code == SDLK_UNKNOWN) { - throw Error(MSG(err) << "could not parse key '" << event_type << "'!"); - } - - if (!param.empty()) - log::log(MSG(warn) << "nothing expected after key name '" << event_type << "', but got '" << param << "'."); - - return sdl_key(key_code, static_cast(mod)); -} - -} // namespace - -/** - * Convert a string to an event, throw if the string is not a valid event. - */ -Event text_to_event(const std::string &event_str) { - check_modifiers_once(); - ENSURE(event_str.find('\n'), "Input event string representation must be one line, got '" << event_str << "'."); - - int mod = 0; - std::smatch event_elements; - - if (std::regex_match(event_str, event_elements, event_pattern)) { - /** - * First element is the entire match, so search from the second. - */ - auto groups_it = std::begin(event_elements) + 1; - - auto first_non_empty = std::find_if(groups_it, std::end(event_elements), [](const std::ssub_match &element) { - return element.length(); - }); - - ENSURE(first_non_empty != std::end(event_elements), "Nothing captured from string representation of event '" << event_str << "': regex pattern is broken."); - - auto index_first_non_empty = std::distance(groups_it, first_non_empty); - - if (index_first_non_empty < std::distance(std::begin(modifiers), std::end(modifiers))) - mod = modifiers[index_first_non_empty]; - - auto event_type = groups_it[modifiers.size()].str(); - ENSURE(!event_type.empty(), "Empty group where key was expected in string representation of event '" << event_str << "': regex pattern is broken."); - - auto param = groups_it[modifiers.size() + 1].str(); - - return to_event(event_type, param, mod); - } - else { - throw Error(MSG(err) << "could not parse keybinding '" << event_str << "'!"); - } -} - -namespace tests { -void parse_event_string() { - text_to_event("q") == Event{event_class::ALPHA, SDL_GetKeyFromName("q"), modset_t{}} || TESTFAIL; - text_to_event("Return") == Event{event_class::NONPRINT, SDL_GetKeyFromName("Return"), modset_t{}} || TESTFAIL; - text_to_event("Ctrl p") == Event{event_class::ALPHA, SDL_GetKeyFromName("p"), sdl_mod(static_cast(KMOD_CTRL))} || TESTFAIL; - text_to_event("Shift MOUSE 1") == Event{event_class::MOUSE_BUTTON, 1, sdl_mod(static_cast(KMOD_SHIFT))} || TESTFAIL; - text_to_event("MOUSE_UP 1") == Event{event_class::MOUSE_BUTTON_UP, 1, modset_t{}} || TESTFAIL; - text_to_event("WHEEL -1") == Event{event_class::MOUSE_WHEEL, -1, modset_t{}} || TESTFAIL; - TESTTHROWS(text_to_event("")); - TESTTHROWS(text_to_event("WHEEL")); - TESTTHROWS(text_to_event("MOUSE")); - TESTTHROWS(text_to_event("MOUSE_DOWN")); - TESTTHROWS(text_to_event("Blurb MOUSE 1")); - TESTTHROWS(text_to_event("Shift MICKEY_MOUSE 1")); - TESTTHROWS(text_to_event("WHEEL TEAR_OFF")); -} - -} // namespace tests - -} // namespace input::legacy -} // namespace openage diff --git a/libopenage/input/legacy/text_to_event.h b/libopenage/input/legacy/text_to_event.h deleted file mode 100644 index 745f33e04b..0000000000 --- a/libopenage/input/legacy/text_to_event.h +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "input/legacy/event.h" - -namespace openage { -namespace input::legacy { - -/** - * Convert a string to an event, throw if the string is not a valid event. - */ -Event text_to_event(const std::string &event_str); - -} // namespace input::legacy -} // namespace openage diff --git a/libopenage/legacy_engine.h b/libopenage/legacy_engine.h index 396bb5b8d1..cf01cb391a 100644 --- a/libopenage/legacy_engine.h +++ b/libopenage/legacy_engine.h @@ -14,7 +14,6 @@ // pxd: from libopenage.cvar cimport CVarManager #include "cvar/cvar.h" #include "gui/engine_info.h" -#include "input/legacy/action.h" #include "job/job_manager.h" #include "options.h" #include "unit/selection.h" diff --git a/openage/testing/testlist.py b/openage/testing/testlist.py index 939732bd0d..75a684e85e 100644 --- a/openage/testing/testlist.py +++ b/openage/testing/testlist.py @@ -99,7 +99,6 @@ def tests_cpp(): yield "openage::util::tests::vector" yield "openage::util::tests::siphash" yield "openage::util::tests::array_conversion" - yield "openage::input::legacy::tests::parse_event_string", "keybinds parsing" yield "openage::curve::tests::container" yield "openage::curve::tests::curve_types" yield "openage::event::tests::eventtrigger" From 17959b3c464b83c5c1f84daa45fff2665f703190 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Sep 2023 02:59:55 +0200 Subject: [PATCH 035/771] renderer: Remove old shader code. --- libopenage/CMakeLists.txt | 1 - libopenage/console/draw.cpp | 1 - libopenage/gui/gui.cpp | 50 ------ libopenage/gui/gui.h | 12 -- libopenage/renderer/CMakeLists.txt | 1 - libopenage/renderer/text.cpp | 236 ----------------------------- libopenage/renderer/text.h | 125 --------------- libopenage/shader/CMakeLists.txt | 4 - libopenage/shader/program.cpp | 178 ---------------------- libopenage/shader/program.h | 47 ------ libopenage/shader/shader.cpp | 66 -------- libopenage/shader/shader.h | 24 --- libopenage/unit/selection.cpp | 1 - 13 files changed, 746 deletions(-) delete mode 100644 libopenage/renderer/text.cpp delete mode 100644 libopenage/renderer/text.h delete mode 100644 libopenage/shader/CMakeLists.txt delete mode 100644 libopenage/shader/program.cpp delete mode 100644 libopenage/shader/program.h delete mode 100644 libopenage/shader/shader.cpp delete mode 100644 libopenage/shader/shader.h diff --git a/libopenage/CMakeLists.txt b/libopenage/CMakeLists.txt index a87c120f50..af67e9e4ee 100644 --- a/libopenage/CMakeLists.txt +++ b/libopenage/CMakeLists.txt @@ -359,7 +359,6 @@ add_subdirectory("presenter") add_subdirectory("pyinterface") add_subdirectory("renderer") add_subdirectory("rng") -add_subdirectory("shader") add_subdirectory("terrain") add_subdirectory("testing") add_subdirectory("time") diff --git a/libopenage/console/draw.cpp b/libopenage/console/draw.cpp index 187079f86f..c8835d5bcc 100644 --- a/libopenage/console/draw.cpp +++ b/libopenage/console/draw.cpp @@ -10,7 +10,6 @@ #include "../util/timing.h" #include -#include "../renderer/text.h" #include "buf.h" #include "console.h" diff --git a/libopenage/gui/gui.cpp b/libopenage/gui/gui.cpp index 59d6e24c6b..dfa44fdc8b 100644 --- a/libopenage/gui/gui.cpp +++ b/libopenage/gui/gui.cpp @@ -1,9 +1,5 @@ // Copyright 2015-2023 the openage authors. See copying.md for legal info. -// include first to make opengl and libepoxy happy. -#include "../shader/program.h" -#include "../shader/shader.h" - #include "gui.h" #include "../legacy_engine.h" @@ -37,55 +33,9 @@ GUI::GUI(SDL_Window *window, // info->display->register_resize_action(this); // info->display->register_input_action(this); // info->display->register_drawhud_action(this); - - util::Path shader_dir = info->asset_dir / "shaders"; - - const char *shader_header_code = "#version 120\n"; - - auto text_vert_file = (shader_dir / "identity.vert.glsl").open(); - std::string texture_vert_code = text_vert_file.read(); - auto plaintexture_vert = std::make_unique( - GL_VERTEX_SHADER, - std::initializer_list{shader_header_code, texture_vert_code.c_str()}); - text_vert_file.close(); - - auto text_frag_file = (shader_dir / "maptexture.frag.glsl").open(); - std::string texture_frag_code = text_frag_file.read(); - auto plaintexture_frag = std::make_unique( - GL_FRAGMENT_SHADER, - std::initializer_list{shader_header_code, texture_frag_code.c_str()}); - text_vert_file.close(); - - this->textured_screen_quad_shader = std::make_unique( - plaintexture_vert.get(), - plaintexture_frag.get()); - - this->textured_screen_quad_shader->link(); - this->tex_loc = this->textured_screen_quad_shader->get_uniform_id("texture"); - this->textured_screen_quad_shader->use(); - glUniform1i(this->tex_loc, 0); - this->textured_screen_quad_shader->stopusing(); - - const float screen_quad[] = { - -1.f, - -1.f, - 1.f, - -1.f, - 1.f, - 1.f, - -1.f, - 1.f, - }; - - glGenBuffers(1, &this->screen_quad_vbo); - - glBindBuffer(GL_ARRAY_BUFFER, this->screen_quad_vbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(screen_quad), screen_quad, GL_STATIC_DRAW); - glBindBuffer(GL_ARRAY_BUFFER, 0); } GUI::~GUI() { - glDeleteBuffers(1, &this->screen_quad_vbo); } void GUI::process_events() { diff --git a/libopenage/gui/gui.h b/libopenage/gui/gui.h index 8f662bd2ef..600e4be52a 100644 --- a/libopenage/gui/gui.h +++ b/libopenage/gui/gui.h @@ -18,10 +18,6 @@ class GuiSingletonItemsInfo; } // namespace qtsdl namespace openage { -namespace shader { -class Program; -} // namespace shader - namespace gui { class EngineQMLInfo; @@ -47,9 +43,6 @@ class GUI { // virtual bool on_input(SDL_Event *event) override; // virtual bool on_drawhud() override; - GLint tex_loc; - GLuint screen_quad_vbo; - GuiApplicationWithLogger application; qtsdl::GuiEventQueue render_updater; qtsdl::GuiRenderer renderer; @@ -57,11 +50,6 @@ class GUI { qtsdl::GuiEngine engine; qtsdl::GuiSubtree subtree; qtsdl::GuiInput input; - - // needs to be deallocated before the GuiRenderer - // it accesses opengl api functions which require a - // current context: - std::unique_ptr textured_screen_quad_shader; }; } // namespace gui diff --git a/libopenage/renderer/CMakeLists.txt b/libopenage/renderer/CMakeLists.txt index 8a889de978..b6578cd128 100644 --- a/libopenage/renderer/CMakeLists.txt +++ b/libopenage/renderer/CMakeLists.txt @@ -6,7 +6,6 @@ add_sources(libopenage render_factory.cpp renderer.cpp shader_program.cpp - text.cpp texture.cpp texture_array.cpp types.cpp diff --git a/libopenage/renderer/text.cpp b/libopenage/renderer/text.cpp deleted file mode 100644 index 9bb108cc33..0000000000 --- a/libopenage/renderer/text.cpp +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright 2015-2018 the openage authors. See copying.md for legal info. - -#include "text.h" - -#include - -#include - -#include "../util/strings.h" -#include "font/font.h" - -namespace openage { - -namespace texturefont_shader { -shader::Program *program; -GLint texture, color, tex_coord; -} // namespace texture_shader - -namespace renderer { - -struct text_render_vertex { - float x; - float y; - float u; - float v; - - text_render_vertex() - : - text_render_vertex{0.0f, 0.0f, 0.0f, 0.0f} { - } - - text_render_vertex(float x, float y, float u, float v) - : - x{x}, - y{y}, - u{u}, - v{v} { - } -}; - -struct text_render_task { - GLenum mode; - Color color; - unsigned int num_elements; - unsigned int offset; -}; - -TextRenderer::TextRenderer() - : - current_font{nullptr}, - current_color{255, 255, 255, 255}, - is_dirty{true}, - vbo{0}, - ibo{0} { - - glGenBuffers(1, &this->vbo); - glGenBuffers(1, &this->ibo); -} - -TextRenderer::~TextRenderer() { - if (this->vbo != 0u) { - glDeleteBuffers(1, &this->vbo); - } - if (this->ibo != 0u) { - glDeleteBuffers(1, &this->ibo); - } -} - -void TextRenderer::set_font(Font *font) { - if (this->current_font == font) { - return; - } - - this->current_font = font; - this->is_dirty = true; -} - -void TextRenderer::set_color(const Color &color) { - if (this->current_color == color) { - return; - } - - this->current_color = color; - this->is_dirty = true; -} - -void TextRenderer::draw(coord::viewport position, const char *format, ...) { - std::string text; - va_list vl; - va_start(vl, format); - util::vsformat(format, vl, text); - va_end(vl); - - this->draw(position.x, position.y, text); -} - -void TextRenderer::draw(coord::viewport position, const std::string &text) { - this->draw(position.x, position.y, text); -} - -void TextRenderer::draw(int x, int y, const std::string &text) { - if (this->is_dirty || this->render_batches.empty()) { - this->render_batches.emplace_back(this->current_font, this->current_color); - this->is_dirty = false; - } - - this->render_batches.back().passes.emplace_back(x, y, text); -} - -void TextRenderer::render() { - // Sort the batches by font - std::sort(std::begin(this->render_batches), std::end(this->render_batches), - [](const text_render_batch &a, const text_render_batch &b) -> bool { - return a.font < b.font; - }); - - // Merge consecutive batches if font and color values are same - for (auto current_batch = std::begin(this->render_batches); current_batch != std::end(this->render_batches); ) { - auto next_batch = current_batch; - next_batch++; - if (next_batch != std::end(this->render_batches) && - current_batch->font == next_batch->font && - current_batch->color == next_batch->color) { - // Merge the render passes of current and next batches and remove the next batch - std::move(std::begin(next_batch->passes), - std::end(next_batch->passes), - std::back_inserter(current_batch->passes)); - this->render_batches.erase(next_batch); - } else { - current_batch++; - } - } - - size_t index = 0; - std::vector vertices; - std::vector indices; - std::vector render_tasks; - render_tasks.reserve(this->render_batches.size()); - unsigned int offset = 0; - - // Compute vertices and indices - for (auto &batch : this->render_batches) { - Font *font = batch.font; - - unsigned int num_elements = 0; - - for (auto &pass : batch.passes) { - auto x = static_cast(pass.x); - auto y = static_cast(pass.y); - - std::vector glyphs = font->get_glyphs(pass.text); - - // We will create 4 vertices & 6 indices for each glyph (GL_TRIANGLES) - vertices.resize(vertices.size() + glyphs.size() * 4); - indices.resize(indices.size() + glyphs.size() * 6); - - codepoint_t previous_glyph = 0; - for (codepoint_t glyph : glyphs) { - GlyphAtlas::Entry entry = this->glyph_atlas.get(font, glyph); - - float x0 = x + entry.glyph.x_offset; - float y0 = y + entry.glyph.y_offset - entry.glyph.height; - float x1 = x0 + entry.glyph.width; - float y1 = y0 + entry.glyph.height; - - vertices[index*4 + 0] = {x0, y0, entry.u0, entry.v0}; - vertices[index*4 + 1] = {x0, y1, entry.u0, entry.v1}; - vertices[index*4 + 2] = {x1, y1, entry.u1, entry.v1}; - vertices[index*4 + 3] = {x1, y0, entry.u1, entry.v0}; - - indices[index*6 + 0] = index*4 + 0; - indices[index*6 + 1] = index*4 + 1; - indices[index*6 + 2] = index*4 + 2; - indices[index*6 + 3] = index*4 + 2; - indices[index*6 + 4] = index*4 + 3; - indices[index*6 + 5] = index*4 + 0; - - // Advance the pen position - x += entry.glyph.x_advance; - y += entry.glyph.y_advance; - - // Handle font kerning - if (previous_glyph != 0) { - x += font->get_horizontal_kerning(previous_glyph, glyph); - } - - index++; - num_elements += 6; - previous_glyph = glyph; - } - } - - text_render_task render_task{GL_TRIANGLES, batch.color, num_elements, offset}; - render_tasks.push_back(render_task); - - offset += num_elements; - } - - // Upload vertices and indices - glBindBuffer(GL_ARRAY_BUFFER, this->vbo); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->ibo); - - glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(text_render_vertex), &vertices[0], GL_STATIC_DRAW); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW); - - texturefont_shader::program->use(); - - this->glyph_atlas.bind(0); - - glEnableVertexAttribArray(texturefont_shader::program->pos_id); - glEnableVertexAttribArray(texturefont_shader::tex_coord); - - glVertexAttribPointer(texturefont_shader::program->pos_id, 2, GL_FLOAT, GL_FALSE, - sizeof(text_render_vertex), (GLvoid *) offsetof(text_render_vertex, x)); - glVertexAttribPointer(texturefont_shader::tex_coord, 2, GL_FLOAT, GL_FALSE, - sizeof(text_render_vertex), (GLvoid *) offsetof(text_render_vertex, u)); - - for (auto &task : render_tasks) { - glUniform4f(texturefont_shader::color, - task.color.r/255.f, task.color.g/255.f, task.color.b/255.f, task.color.a/255.f); - glDrawElements(task.mode, task.num_elements, GL_UNSIGNED_INT, (GLvoid *) (task.offset * sizeof(unsigned int))); - } - - glDisableVertexAttribArray(texturefont_shader::program->pos_id); - glDisableVertexAttribArray(texturefont_shader::tex_coord); - - texturefont_shader::program->stopusing(); - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - glBindBuffer(GL_ARRAY_BUFFER, 0); - - // Clear the render batches for next frame - this->render_batches.clear(); -} - -}} // openage::renderer diff --git a/libopenage/renderer/text.h b/libopenage/renderer/text.h deleted file mode 100644 index 116cbe647a..0000000000 --- a/libopenage/renderer/text.h +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include - -#include "../coord/pixel.h" -#include "../shader/program.h" -#include "color.h" -#include "font/glyph_atlas.h" - -namespace openage { - -namespace texturefont_shader { -extern shader::Program *program; -extern GLint texture, color, tex_coord; -} // openage::texturefont_shader - -namespace renderer { - -/** - * Can render text with OpenGL. - * - * TODO: move to the main renderer! - */ -class TextRenderer { - -public: - /** - * Requires a working OpenGL context to create buffer objects. - */ - TextRenderer(); - - virtual ~TextRenderer(); - - /** - * Set the font to be used for the future text draw calls. - * - * @param font: the font to be used. - */ - void set_font(Font *font); - - /** - * Set the color to be used for the future text draw calls. - * - * @param color: the color to be used. - */ - void set_color(const Color &color); - - /** - * Draw a formatted string at the specified position. - * - * @param position: where the text should be displayed. - * @param format: the text format - */ - void draw(coord::viewport position, const char *format, ...); - - /** - * Draw text at the specified position. - * - * @param position: where the text should be displayed. - * @param text: the text to be displayed. - */ - void draw(coord::viewport position, const std::string &text); - - /** - * Draw text at the specified position. - * - * @param x: the position in x-direction. - * @param y: the position in y-direction. - * @param text: the text to be displayed. - */ - void draw(int x, int y, const std::string &text); - - /** - * Render all the text draw requests made during the frame. - */ - void render(); - -private: - /** - * A single text draw request containing the text and position. - */ - struct text_render_batch_pass { - int x; - int y; - std::string text; - - text_render_batch_pass(int x, int y, const std::string &text) - : - x{x}, - y{y}, - text{text} { - } - }; - - /** - * The set of text draw requests with the same font and color. - */ - struct text_render_batch { - Font *font; - Color color; - std::vector passes; - - text_render_batch(Font *font, const Color &color) - : - font{font}, - color{color} { - } - }; - - Font *current_font; - Color current_color; - bool is_dirty; - std::vector render_batches; - - GlyphAtlas glyph_atlas; - - GLuint vbo; - GLuint ibo; -}; - -}} // openage::renderer diff --git a/libopenage/shader/CMakeLists.txt b/libopenage/shader/CMakeLists.txt deleted file mode 100644 index 6477fa9fc7..0000000000 --- a/libopenage/shader/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -add_sources(libopenage - program.cpp - shader.cpp -) diff --git a/libopenage/shader/program.cpp b/libopenage/shader/program.cpp deleted file mode 100644 index 479d91a6ae..0000000000 --- a/libopenage/shader/program.cpp +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. - -#include "program.h" - -#include -#include -#include - -#include "../error/error.h" -#include "../log/log.h" -#include "../util/compiler.h" -#include "../util/file.h" -#include "../util/strings.h" - -#include "shader.h" - -namespace openage::shader { - -Program::Program() : is_linked(false), vert(nullptr), frag(nullptr), geom(nullptr) { - this->id = glCreateProgram(); -} - -Program::Program(Shader *s0, Shader *s1) : Program{} { - this->attach_shader(s0); - this->attach_shader(s1); -} - -Program::~Program() { - glDeleteProgram(this->id); -} - -void Program::attach_shader(Shader *s) { - switch (s->type) { - case GL_VERTEX_SHADER: - this->vert = s; - break; - case GL_FRAGMENT_SHADER: - this->frag = s; - break; - case GL_GEOMETRY_SHADER: - this->geom = s; - break; - } - glAttachShader(this->id, s->id); -} - -void Program::link() { - glLinkProgram(this->id); - this->check(GL_LINK_STATUS); - glValidateProgram(this->id); - this->check(GL_VALIDATE_STATUS); - this->is_linked = true; - this->post_link_hook(); - - if (this->vert != nullptr) { - glDetachShader(this->id, this->vert->id); - } - if (this->frag != nullptr) { - glDetachShader(this->id, this->frag->id); - } - if (this->geom != nullptr) { - glDetachShader(this->id, this->geom->id); - } -} - -/** - * checks a given status for this program. - * - * @param what_to_check GL_LINK_STATUS GL_VALIDATE_STATUS GL_COMPILE_STATUS - */ -void Program::check(GLenum what_to_check) { - GLint status; - glGetProgramiv(this->id, what_to_check, &status); - - if (status != GL_TRUE) { - GLint loglen; - glGetProgramiv(this->id, GL_INFO_LOG_LENGTH, &loglen); - char *infolog = new char[loglen]; - glGetProgramInfoLog(this->id, loglen, nullptr, infolog); - - const char *what_str; - switch(what_to_check) { - case GL_LINK_STATUS: - what_str = "linking"; - break; - case GL_VALIDATE_STATUS: - what_str = "validation"; - break; - case GL_COMPILE_STATUS: - what_str = "compiliation"; - break; - default: - what_str = ""; - break; - } - - auto errormsg = MSG(err); - errormsg << "Program " << what_str << " failed\n" << infolog; - delete[] infolog; - - throw Error(errormsg); - } -} - -void Program::use() { - glUseProgram(this->id); -} - -void Program::stopusing() { - glUseProgram(static_cast(0)); -} - -GLint Program::get_uniform_id(const char *name) { - return glGetUniformLocation(this->id, name); -} - -GLint Program::get_attribute_id(const char *name) { - if (!this->is_linked) [[unlikely]] { - throw Error(MSG(err) << - "Attribute " << name << - " was queried before program was linked!"); - } - - GLint aid = glGetAttribLocation(this->id, name); - - if (aid == -1) [[unlikely]] { - this->dump_active_attributes(); - throw Error(MSG(err) << - "Attribute " << name << " queried but not found or active" - " (pwnt by the compiler)."); - } - - return aid; -} - -void Program::set_attribute_id(const char *name, GLuint id) { - if (!this->is_linked) { - glBindAttribLocation(this->id, id, name); - } - else { - //TODO: maybe enable overwriting, but after that relink the program - throw Error(MSG(err) << "assigned attribute " << name << " = " << id - << " after program was linked!"); - } -} - -void Program::dump_active_attributes() { - auto msg = MSG(warn); - msg << "Dumping shader program active attribute list:"; - - GLint num_attribs; - glGetProgramiv(this->id, GL_ACTIVE_ATTRIBUTES, &num_attribs); - - GLint attrib_max_length; - glGetProgramiv(this->id, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, &attrib_max_length); - - for (int i = 0; i < num_attribs; i++) { - GLsizei attrib_length; - GLint attrib_size; - GLenum attrib_type; - char *attrib_name = new char[attrib_max_length]; - glGetActiveAttrib(this->id, i, attrib_max_length, &attrib_length, - &attrib_size, &attrib_type, attrib_name); - - msg << "\n" << - "-> attribute " << attrib_name << ": " - " : type=" << attrib_type << ", size=" << attrib_size; - delete[] attrib_name; - } -} - - -void Program::post_link_hook() { - this->pos_id = this->get_attribute_id("vertex_position"); - this->mvpm_id = this->get_uniform_id("mvp_matrix"); -} - -} // openage::shader diff --git a/libopenage/shader/program.h b/libopenage/shader/program.h deleted file mode 100644 index d2244b5bf3..0000000000 --- a/libopenage/shader/program.h +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -namespace openage { -namespace shader { - -class Shader; - -class [[deprecated]] Program { -public: - GLuint id; - GLint pos_id, mvpm_id; - - Program(); - Program(Shader *s0, Shader *s1); - ~Program(); - - void attach_shader(Shader *s); - - void link(); - - void use(); - void stopusing(); - - GLint get_uniform_id(const char *name); - GLint get_attribute_id(const char *name); - - void set_attribute_id(const char *name, GLuint id); - - void dump_active_attributes(); - -private: - bool is_linked; - Shader *vert, *frag, *geom; - - void check(GLenum what_to_check); - GLint get_info(GLenum pname); - char *get_log(); - void post_link_hook(); -}; - - -} // namespace shader -} // namespace openage diff --git a/libopenage/shader/shader.cpp b/libopenage/shader/shader.cpp deleted file mode 100644 index f9a7c555db..0000000000 --- a/libopenage/shader/shader.cpp +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2013-2019 the openage authors. See copying.md for legal info. - -#include "shader.h" - -#include -#include - -#include "../error/error.h" -#include "../log/log.h" -#include "../util/file.h" -#include "../util/strings.h" - -namespace openage::shader { - -const char *type_to_string(GLenum type) { - switch (type) { - case GL_VERTEX_SHADER: - return "vertex"; - case GL_FRAGMENT_SHADER: - return "fragment"; - case GL_GEOMETRY_SHADER: - return "geometry"; - default: - return "unknown"; - } -} - -Shader::Shader(GLenum type, std::initializer_list sources) { - //create shader - this->id = glCreateShader(type); - - //store type - this->type = type; - - //load shader source - std::vector x = std::vector(sources); - glShaderSource(this->id, x.size(), x.data(), nullptr); - - //compile shader source - glCompileShader(this->id); - - //check compiliation result - GLint status; - glGetShaderiv(this->id, GL_COMPILE_STATUS, &status); - - if (status != GL_TRUE) { - GLint loglen; - glGetShaderiv(this->id, GL_INFO_LOG_LENGTH, &loglen); - - auto infolog = std::make_unique(loglen); - glGetShaderInfoLog(this->id, loglen, nullptr, infolog.get()); - - auto errmsg = MSG(err); - errmsg << "Failed to compile " << type_to_string(type) << " shader\n" << infolog; - - glDeleteShader(this->id); - - throw Error(errmsg); - } -} - -Shader::~Shader() { - glDeleteShader(this->id); -} - -} // openage::shader diff --git a/libopenage/shader/shader.h b/libopenage/shader/shader.h deleted file mode 100644 index 93e44b6123..0000000000 --- a/libopenage/shader/shader.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include - -namespace openage { -namespace shader { - -[[deprecated]] const char *type_to_string(GLenum type); - -class [[deprecated]] Shader { -public: - Shader(GLenum type, std::initializer_list sources); - ~Shader(); - - GLuint id; - GLenum type; -}; - -} // namespace shader -} // namespace openage diff --git a/libopenage/unit/selection.cpp b/libopenage/unit/selection.cpp index d8c4fa44f4..48d38ba457 100644 --- a/libopenage/unit/selection.cpp +++ b/libopenage/unit/selection.cpp @@ -8,7 +8,6 @@ #include "../coord/tile.h" #include "../legacy_engine.h" #include "../log/log.h" -#include "../renderer/text.h" #include "../terrain/terrain.h" #include "action.h" #include "command.h" From e5ed4a0a3f1cc20ad690e0b309ca886318e66808 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Sep 2023 19:05:08 +0200 Subject: [PATCH 036/771] refactor: Move OpenGL error checks into renderer. --- libopenage/error/CMakeLists.txt | 1 - libopenage/error/gl_debug.cpp | 70 ------------------- libopenage/error/gl_debug.h | 12 ---- libopenage/renderer/opengl/CMakeLists.txt | 1 + .../opengl.cpp => renderer/opengl/error.cpp} | 20 +++--- .../opengl.h => renderer/opengl/error.h} | 8 +-- libopenage/renderer/opengl/shader_program.cpp | 2 +- libopenage/util/CMakeLists.txt | 1 - 8 files changed, 14 insertions(+), 101 deletions(-) delete mode 100644 libopenage/error/gl_debug.cpp delete mode 100644 libopenage/error/gl_debug.h rename libopenage/{util/opengl.cpp => renderer/opengl/error.cpp} (78%) rename libopenage/{util/opengl.h => renderer/opengl/error.h} (51%) diff --git a/libopenage/error/CMakeLists.txt b/libopenage/error/CMakeLists.txt index 9fefd61a5d..b54fa5e524 100644 --- a/libopenage/error/CMakeLists.txt +++ b/libopenage/error/CMakeLists.txt @@ -2,7 +2,6 @@ add_sources(libopenage backtrace.cpp demo.cpp error.cpp - gl_debug.cpp handlers.cpp stackanalyzer.cpp ) diff --git a/libopenage/error/gl_debug.cpp b/libopenage/error/gl_debug.cpp deleted file mode 100644 index fc89d33d74..0000000000 --- a/libopenage/error/gl_debug.cpp +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. - -#include "gl_debug.h" - -#include - -#include "log/message.h" - -#include "error/error.h" - -namespace openage::error { - -namespace { -void APIENTRY callback(GLenum source, GLenum, GLuint, GLenum, GLsizei, const GLchar *message, const void *) { - const char *source_name; - - switch (source) { - case GL_DEBUG_SOURCE_API: - source_name = "API"; - break; - case GL_DEBUG_SOURCE_WINDOW_SYSTEM: - source_name = "window system"; - break; - case GL_DEBUG_SOURCE_SHADER_COMPILER: - source_name = "shader compiler"; - break; - case GL_DEBUG_SOURCE_THIRD_PARTY: - source_name = "third party"; - break; - case GL_DEBUG_SOURCE_APPLICATION: - source_name = "application"; - break; - case GL_DEBUG_SOURCE_OTHER: - source_name = "other"; - break; - default: - source_name = "unknown"; - break; - } - - throw Error(MSG(err) << "OpenGL error from " << source_name << ": '" << message << "'."); -} - -} // namespace - -SDL_GLContext create_debug_context(SDL_Window *window) { - SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG); - - auto ctx = SDL_GL_CreateContext(window); - - if (ctx != nullptr) { - GLint flags; - glGetIntegerv(GL_CONTEXT_FLAGS, &flags); - - if (!(flags & GL_CONTEXT_FLAG_DEBUG_BIT)) - throw Error(MSG(err) << "Failed creating a debug OpenGL context."); - - glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_FALSE); - - glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_ERROR, GL_DONT_CARE, 0, nullptr, GL_TRUE); - glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR, GL_DONT_CARE, 0, nullptr, GL_TRUE); - - glDebugMessageCallback(callback, nullptr); - glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); - } - - return ctx; -} - -} // namespace openage::error diff --git a/libopenage/error/gl_debug.h b/libopenage/error/gl_debug.h deleted file mode 100644 index a874c1086e..0000000000 --- a/libopenage/error/gl_debug.h +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2016-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -namespace openage { -namespace error { - -SDL_GLContext create_debug_context(SDL_Window *window); - -}} // openage::error diff --git a/libopenage/renderer/opengl/CMakeLists.txt b/libopenage/renderer/opengl/CMakeLists.txt index eb93f1df56..300dac811f 100644 --- a/libopenage/renderer/opengl/CMakeLists.txt +++ b/libopenage/renderer/opengl/CMakeLists.txt @@ -2,6 +2,7 @@ add_sources(libopenage buffer.cpp context.cpp debug.cpp + error.cpp framebuffer.cpp geometry.cpp render_pass.cpp diff --git a/libopenage/util/opengl.cpp b/libopenage/renderer/opengl/error.cpp similarity index 78% rename from libopenage/util/opengl.cpp rename to libopenage/renderer/opengl/error.cpp index 1cc8b31ee3..790847fccc 100644 --- a/libopenage/util/opengl.cpp +++ b/libopenage/renderer/opengl/error.cpp @@ -1,20 +1,18 @@ -// Copyright 2014-2016 the openage authors. See copying.md for legal info. +// Copyright 2014-2023 the openage authors. See copying.md for legal info. -#include "opengl.h" +#include "error.h" #include -#include "../error/error.h" +#include "error/error.h" -namespace openage { -namespace util { +namespace openage::renderer::opengl { void gl_check_error() { int glerrorstate = 0; glerrorstate = glGetError(); if (glerrorstate != GL_NO_ERROR) { - const char *errormsg; //generate error message @@ -63,11 +61,11 @@ void gl_check_error() { // unknown error state errormsg = "unknown error"; } - throw Error(MSG(err) << - "OpenGL error state after running draw method: " << glerrorstate << "\n" - "\t" << errormsg << "\n" - << "Run the game with --gl-debug to get more information: './run game --gl-debug'."); + throw Error(MSG(err) << "OpenGL error state after running draw method: " << glerrorstate << "\n" + "\t" + << errormsg << "\n" + << "Run the game with --gl-debug to get more information: './run game --gl-debug'."); } } -}} // openage::util +} // namespace openage::renderer::opengl diff --git a/libopenage/util/opengl.h b/libopenage/renderer/opengl/error.h similarity index 51% rename from libopenage/util/opengl.h rename to libopenage/renderer/opengl/error.h index d1c3a2e300..5fe692fd2d 100644 --- a/libopenage/util/opengl.h +++ b/libopenage/renderer/opengl/error.h @@ -1,9 +1,8 @@ -// Copyright 2014-2016 the openage authors. See copying.md for legal info. +// Copyright 2014-2023 the openage authors. See copying.md for legal info. #pragma once -namespace openage { -namespace util { +namespace openage::renderer::opengl { /** * query the current opengl context for any errors. @@ -12,5 +11,4 @@ namespace util { */ void gl_check_error(); -} -} +} // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/shader_program.cpp b/libopenage/renderer/opengl/shader_program.cpp index 67127ecc1f..3ccd49ce80 100644 --- a/libopenage/renderer/opengl/shader_program.cpp +++ b/libopenage/renderer/opengl/shader_program.cpp @@ -9,9 +9,9 @@ #include "datastructure/constexpr_map.h" #include "error/error.h" #include "log/log.h" -#include "util/opengl.h" #include "renderer/opengl/context.h" +#include "renderer/opengl/error.h" #include "renderer/opengl/geometry.h" #include "renderer/opengl/lookup.h" #include "renderer/opengl/shader.h" diff --git a/libopenage/util/CMakeLists.txt b/libopenage/util/CMakeLists.txt index ac5b585f06..841a00757f 100644 --- a/libopenage/util/CMakeLists.txt +++ b/libopenage/util/CMakeLists.txt @@ -20,7 +20,6 @@ add_sources(libopenage matrix_test.cpp misc.cpp misc_test.cpp - opengl.cpp os.cpp path.cpp profiler.cpp From 485cfde752449df899e7f690bf5bf583c1375b79 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Sep 2023 19:10:45 +0200 Subject: [PATCH 037/771] refactor: Move old screenshot manager into renderer. --- libopenage/CMakeLists.txt | 1 - .../renderer/stages/screen/CMakeLists.txt | 1 + .../stages/screen}/screenshot.cpp | 38 +++++++++---------- .../{ => renderer/stages/screen}/screenshot.h | 3 ++ 4 files changed, 22 insertions(+), 21 deletions(-) rename libopenage/{ => renderer/stages/screen}/screenshot.cpp (74%) rename libopenage/{ => renderer/stages/screen}/screenshot.h (95%) diff --git a/libopenage/CMakeLists.txt b/libopenage/CMakeLists.txt index af67e9e4ee..8b7d535b96 100644 --- a/libopenage/CMakeLists.txt +++ b/libopenage/CMakeLists.txt @@ -325,7 +325,6 @@ add_sources(libopenage legacy_engine.cpp main.cpp options.cpp - screenshot.cpp ${CMAKE_CURRENT_BINARY_DIR}/config.cpp ${CMAKE_CURRENT_BINARY_DIR}/version.cpp ${CODEGEN_SCU_FILE} diff --git a/libopenage/renderer/stages/screen/CMakeLists.txt b/libopenage/renderer/stages/screen/CMakeLists.txt index b964aae19b..59529a556b 100644 --- a/libopenage/renderer/stages/screen/CMakeLists.txt +++ b/libopenage/renderer/stages/screen/CMakeLists.txt @@ -1,3 +1,4 @@ add_sources(libopenage screen_renderer.cpp + screenshot.cpp ) diff --git a/libopenage/screenshot.cpp b/libopenage/renderer/stages/screen/screenshot.cpp similarity index 74% rename from libopenage/screenshot.cpp rename to libopenage/renderer/stages/screen/screenshot.cpp index dfb3d2d5e1..73b4230106 100644 --- a/libopenage/screenshot.cpp +++ b/libopenage/renderer/stages/screen/screenshot.cpp @@ -16,11 +16,10 @@ #include "log/log.h" #include "util/strings.h" -namespace openage { +namespace openage::renderer::screen { -ScreenshotManager::ScreenshotManager(job::JobManager *job_mgr) - : +ScreenshotManager::ScreenshotManager(job::JobManager *job_mgr) : count{0}, job_manager{job_mgr} { } @@ -30,12 +29,12 @@ ScreenshotManager::~ScreenshotManager() {} std::string ScreenshotManager::gen_next_filename() { - std::time_t t = std::time(NULL); if (t == this->last_time) { this->count++; - } else { + } + else { this->count = 0; this->last_time = t; } @@ -50,12 +49,12 @@ std::string ScreenshotManager::gen_next_filename() { void ScreenshotManager::save_screenshot(coord::viewport_delta size) { coord::pixel_t width = size.x, - height = size.y; + height = size.y; - std::shared_ptr pxdata(new uint8_t[4*width*height], std::default_delete()); + std::shared_ptr pxdata(new uint8_t[4 * width * height], std::default_delete()); glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pxdata.get()); - auto encode_function = [this, pxdata, size] () { + auto encode_function = [this, pxdata, size]() { return this->encode_png(pxdata, size); }; this->job_manager->enqueue(encode_function); @@ -66,24 +65,25 @@ bool ScreenshotManager::encode_png(std::shared_ptr pxdata, coord::viewport_delta size) { std::FILE *fout = NULL; coord::pixel_t width = size.x, - height = size.y; - auto warn_fn = [] (png_structp /*png_ptr*/, png_const_charp message) { + height = size.y; + auto warn_fn = [](png_structp /*png_ptr*/, png_const_charp message) { log::log(MSG(err) << "Creating screenshot failed: libpng error: " << message); }; - auto err_fn = [] (png_structp png_ptr, png_const_charp message) { + auto err_fn = [](png_structp png_ptr, png_const_charp message) { log::log(MSG(err) << "Creating screenshot failed: libpng error: " << message); longjmp(png_jmpbuf(png_ptr), 1); }; png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, - (png_voidp) NULL, - err_fn, warn_fn); + (png_voidp)NULL, + err_fn, + warn_fn); if (!png_ptr) return false; png_infop info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { - png_destroy_write_struct(&png_ptr, (png_infopp) NULL); + png_destroy_write_struct(&png_ptr, (png_infopp)NULL); return false; } @@ -97,16 +97,14 @@ bool ScreenshotManager::encode_png(std::shared_ptr pxdata, fout = std::fopen(filename.c_str(), "wb"); if (fout == NULL) { png_destroy_write_struct(&png_ptr, &info_ptr); - log::log(MSG(err) << "Could not open '"<< filename << "': " - << std::string(strerror(errno))); + log::log(MSG(err) << "Could not open '" << filename << "': " + << std::string(strerror(errno))); return false; } png_init_io(png_ptr, fout); - png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB, - PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, - PNG_FILTER_TYPE_DEFAULT); + png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); // Put image row pointer into info_ptr so that we can use the high-level // write interface. @@ -129,4 +127,4 @@ bool ScreenshotManager::encode_png(std::shared_ptr pxdata, return true; } -} // openage +} // namespace openage::renderer::screen diff --git a/libopenage/screenshot.h b/libopenage/renderer/stages/screen/screenshot.h similarity index 95% rename from libopenage/screenshot.h rename to libopenage/renderer/stages/screen/screenshot.h index 45e2660f5b..ce330c553f 100644 --- a/libopenage/screenshot.h +++ b/libopenage/renderer/stages/screen/screenshot.h @@ -14,6 +14,8 @@ namespace job { class JobManager; } +namespace renderer::screen { + /** * Takes screenshots, duh. * @@ -52,4 +54,5 @@ class [[deprecated]] ScreenshotManager { job::JobManager *job_manager; }; +} // namespace renderer::screen } // namespace openage From 986b3e001af1efaa88b5da6d6319efeac49b9e0e Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Sep 2023 22:15:41 +0200 Subject: [PATCH 038/771] renderer: Rewrite screenshot manager for new renderer. --- libopenage/renderer/opengl/render_target.cpp | 13 +++ libopenage/renderer/opengl/render_target.h | 7 ++ libopenage/renderer/renderer.h | 9 ++ .../renderer/stages/screen/screenshot.cpp | 103 ++++-------------- .../renderer/stages/screen/screenshot.h | 64 +++++++---- 5 files changed, 92 insertions(+), 104 deletions(-) diff --git a/libopenage/renderer/opengl/render_target.cpp b/libopenage/renderer/opengl/render_target.cpp index c0da3a22a8..ba0505adba 100644 --- a/libopenage/renderer/opengl/render_target.cpp +++ b/libopenage/renderer/opengl/render_target.cpp @@ -22,6 +22,19 @@ GlRenderTarget::GlRenderTarget(const std::shared_ptr &context, this->size = this->textures.value().at(0)->get_info().get_size(); } +resources::Texture2dData GlRenderTarget::into_data() { + // make sure the framebuffer is bound + this->bind_read(); + + std::vector pxdata(this->size.first * this->size.second * 4); + glReadPixels(0, 0, this->size.first, this->size.second, GL_RGBA, GL_UNSIGNED_BYTE, pxdata.data()); + + resources::Texture2dInfo info{this->size.first, + this->size.second, + resources::pixel_format::rgba8}; + return resources::Texture2dData{info, std::move(pxdata)}; +} + std::vector> GlRenderTarget::get_texture_targets() { std::vector> textures{}; if (this->framebuffer->get_type() == gl_framebuffer_t::display) { diff --git a/libopenage/renderer/opengl/render_target.h b/libopenage/renderer/opengl/render_target.h index 5246b03cb7..acf0c0af00 100644 --- a/libopenage/renderer/opengl/render_target.h +++ b/libopenage/renderer/opengl/render_target.h @@ -56,6 +56,13 @@ class GlRenderTarget final : public RenderTarget { GlRenderTarget(const std::shared_ptr &context, std::vector> const &textures); + /** + * Get the pixels stored in the render target's buffer. + * + * @return Texture data with the image contents of the buffer. + */ + resources::Texture2dData into_data() override; + /** * Get the targeted textures. * diff --git a/libopenage/renderer/renderer.h b/libopenage/renderer/renderer.h index 8dec3dec2f..8f6e4ed3b9 100644 --- a/libopenage/renderer/renderer.h +++ b/libopenage/renderer/renderer.h @@ -34,6 +34,15 @@ class RenderTarget : public std::enable_shared_from_this { public: virtual ~RenderTarget() = default; + /** + * Get an image from the pixels in the render target's framebuffer. + * + * This should only be called _after_ rendering to the framebuffer has finished. + * + * @return RGBA texture data. + */ + virtual resources::Texture2dData into_data() = 0; + virtual std::vector> get_texture_targets() = 0; }; diff --git a/libopenage/renderer/stages/screen/screenshot.cpp b/libopenage/renderer/stages/screen/screenshot.cpp index 73b4230106..8fad08cab9 100644 --- a/libopenage/renderer/stages/screen/screenshot.cpp +++ b/libopenage/renderer/stages/screen/screenshot.cpp @@ -11,23 +11,27 @@ #include #include -#include "coord/pixel.h" #include "job/job_manager.h" #include "log/log.h" +#include "renderer/renderer.h" +#include "renderer/resources/texture_data.h" +#include "renderer/stages/screen/screen_renderer.h" #include "util/strings.h" + namespace openage::renderer::screen { -ScreenshotManager::ScreenshotManager(job::JobManager *job_mgr) : +ScreenshotManager::ScreenshotManager(std::shared_ptr &renderer, + util::Path &outdir, + std::shared_ptr &job_mgr) : + outdir{outdir}, count{0}, + last_time{0}, + renderer{renderer}, job_manager{job_mgr} { } - -ScreenshotManager::~ScreenshotManager() {} - - std::string ScreenshotManager::gen_next_filename() { std::time_t t = std::time(NULL); @@ -43,88 +47,21 @@ std::string ScreenshotManager::gen_next_filename() { char timestamp[32]; std::strftime(timestamp, 32, "%Y-%m-%d_%H-%M-%S", std::localtime(&t)); - return util::sformat("/tmp/openage_%s_%02d.png", timestamp, this->count); + return util::sformat("openage_%s_%02d.png", timestamp, this->count); } -void ScreenshotManager::save_screenshot(coord::viewport_delta size) { - coord::pixel_t width = size.x, - height = size.y; +void ScreenshotManager::save_screenshot() { + // get screenshot image from scren renderer + auto pass = this->renderer->get_render_pass(); + auto target = pass->get_target(); + auto image = target->into_data(); - std::shared_ptr pxdata(new uint8_t[4 * width * height], std::default_delete()); - glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pxdata.get()); - - auto encode_function = [this, pxdata, size]() { - return this->encode_png(pxdata, size); + auto store_function = [this, image]() { + image.store(this->outdir / this->gen_next_filename()); + return true; }; - this->job_manager->enqueue(encode_function); -} - - -bool ScreenshotManager::encode_png(std::shared_ptr pxdata, - coord::viewport_delta size) { - std::FILE *fout = NULL; - coord::pixel_t width = size.x, - height = size.y; - auto warn_fn = [](png_structp /*png_ptr*/, png_const_charp message) { - log::log(MSG(err) << "Creating screenshot failed: libpng error: " << message); - }; - auto err_fn = [](png_structp png_ptr, png_const_charp message) { - log::log(MSG(err) << "Creating screenshot failed: libpng error: " << message); - longjmp(png_jmpbuf(png_ptr), 1); - }; - - png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, - (png_voidp)NULL, - err_fn, - warn_fn); - if (!png_ptr) - return false; - - png_infop info_ptr = png_create_info_struct(png_ptr); - if (!info_ptr) { - png_destroy_write_struct(&png_ptr, (png_infopp)NULL); - return false; - } - - if (setjmp(png_jmpbuf(png_ptr))) { - std::fclose(fout); - png_destroy_write_struct(&png_ptr, &info_ptr); - return false; - } - - std::string filename = this->gen_next_filename(); - fout = std::fopen(filename.c_str(), "wb"); - if (fout == NULL) { - png_destroy_write_struct(&png_ptr, &info_ptr); - log::log(MSG(err) << "Could not open '" << filename << "': " - << std::string(strerror(errno))); - return false; - } - - png_init_io(png_ptr, fout); - - png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); - - // Put image row pointer into info_ptr so that we can use the high-level - // write interface. - std::vector row_ptrs; - - // Invert rows. - row_ptrs.reserve(height); - for (int i = 1; i <= height; i++) { - row_ptrs.push_back(pxdata.get() + (height - i) * 4 * width); - } - png_set_rows(png_ptr, info_ptr, &row_ptrs[0]); - - //TODO: print ingame message. - log::log(MSG(info) << "Saving screenshot to '" << filename << "'."); - - png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_FILLER_AFTER, NULL); - - std::fclose(fout); - png_destroy_write_struct(&png_ptr, &info_ptr); - return true; + this->job_manager->enqueue(store_function); } } // namespace openage::renderer::screen diff --git a/libopenage/renderer/stages/screen/screenshot.h b/libopenage/renderer/stages/screen/screenshot.h index ce330c553f..df4ebcee48 100644 --- a/libopenage/renderer/stages/screen/screenshot.h +++ b/libopenage/renderer/stages/screen/screenshot.h @@ -6,7 +6,8 @@ #include #include -#include "coord/pixel.h" +#include "util/path.h" + namespace openage { @@ -15,43 +16,64 @@ class JobManager; } namespace renderer::screen { +class ScreenRenderer; /** * Takes screenshots, duh. - * - * TODO: move into renderer! */ -class [[deprecated]] ScreenshotManager { +class ScreenshotManager { public: /** - * Initializes the screenshot manager with the given job manager. + * Create a new screenshot manager. + * + * @param renderer Screen render stage to take the screenshot from. + * @param outdir Directory where the screenshots are saved. + * @param job_mgr Job manager to use for writing the screenshot to disk. */ - ScreenshotManager(job::JobManager *job_mgr); - - ~ScreenshotManager(); - - /** To be called to save a screenshot. */ - void save_screenshot(coord::viewport_delta size); + ScreenshotManager(std::shared_ptr &renderer, + util::Path &outdir, + std::shared_ptr &job_mgr); - /** To be called by the job manager. Returns true on success, false otherwise. */ - bool encode_png(std::shared_ptr pxdata, - coord::viewport_delta size); + ~ScreenshotManager() = default; - /** size of the game window, in coord_sdl */ - coord::viewport_delta window_size; + /** + * Generate and save a screenshot of the last frame. + */ + void save_screenshot(); private: - /** to be called to get the next screenshot filename into the array */ + /** + * Generates a filename for the screenshot. + * + * @return Filename for the screenshot. + */ std::string gen_next_filename(); - /** contains the number to be in the next screenshot filename */ + /** + * Directory where the screenshots are saved. + */ + util::Path outdir; + + /** + * Counter for the screenshot filename. Used if multiple screenshots + * are taken in the same second. + */ unsigned count; - /** contains the last time when a screenshot was taken */ + /** + * Last time when a screenshot was taken. + */ std::time_t last_time; - /** the job manager this screenshot manager uses */ - job::JobManager *job_manager; + /** + * Screen render stage to take the screenshot from. + */ + std::shared_ptr renderer; + + /** + * Job manager to use for writing the screenshot to disk. + */ + std::shared_ptr job_manager; }; } // namespace renderer::screen From 99078f3431433a52fa123dae58dec3a0d5d87480 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Sep 2023 23:00:05 +0200 Subject: [PATCH 039/771] engine: Remove legacy engine integration. --- libopenage/CMakeLists.txt | 2 - libopenage/audio/audio_manager.h | 2 - libopenage/audio/dynamic_resource.cpp | 1 - libopenage/audio/resource.cpp | 2 - libopenage/console/console.cpp | 1 - libopenage/console/console.h | 2 - libopenage/console/draw.h | 2 - libopenage/gamestate/old/game_main.cpp | 29 -- libopenage/gamestate/old/game_main.h | 8 - libopenage/gamestate/old/game_spec.cpp | 1 - libopenage/gui/CMakeLists.txt | 4 - libopenage/gui/engine_info.cpp | 14 - libopenage/gui/engine_info.h | 39 -- libopenage/gui/engine_link.cpp | 95 ----- libopenage/gui/engine_link.h | 76 ---- libopenage/gui/game_creator.cpp | 40 +- libopenage/gui/game_creator.h | 3 - libopenage/gui/game_main_link.cpp | 73 ---- libopenage/gui/game_main_link.h | 74 ---- libopenage/gui/game_saver.cpp | 47 +-- libopenage/gui/game_saver.h | 11 +- libopenage/gui/gui.cpp | 7 +- libopenage/gui/main_args_link.cpp | 38 -- libopenage/gui/main_args_link.h | 36 -- libopenage/legacy_engine.cpp | 77 ---- libopenage/legacy_engine.h | 209 ---------- .../renderer/stages/screen/screenshot.cpp | 2 +- libopenage/terrain/terrain.cpp | 1 - libopenage/terrain/terrain.h | 1 - libopenage/terrain/terrain_chunk.cpp | 1 - libopenage/terrain/terrain_object.cpp | 2 - libopenage/terrain/terrain_object.h | 6 - libopenage/unit/action.cpp | 8 - libopenage/unit/action.h | 7 - libopenage/unit/producer.cpp | 1 - libopenage/unit/selection.cpp | 6 +- libopenage/unit/selection.h | 6 +- libopenage/unit/unit.cpp | 1 - libopenage/util/profiler.cpp | 375 +++++++++--------- libopenage/util/profiler.h | 179 ++++----- 40 files changed, 301 insertions(+), 1188 deletions(-) delete mode 100644 libopenage/gui/engine_info.cpp delete mode 100644 libopenage/gui/engine_info.h delete mode 100644 libopenage/gui/engine_link.cpp delete mode 100644 libopenage/gui/engine_link.h delete mode 100644 libopenage/gui/game_main_link.cpp delete mode 100644 libopenage/gui/game_main_link.h delete mode 100644 libopenage/gui/main_args_link.cpp delete mode 100644 libopenage/gui/main_args_link.h delete mode 100644 libopenage/legacy_engine.cpp delete mode 100644 libopenage/legacy_engine.h diff --git a/libopenage/CMakeLists.txt b/libopenage/CMakeLists.txt index 8b7d535b96..9dc9db84d9 100644 --- a/libopenage/CMakeLists.txt +++ b/libopenage/CMakeLists.txt @@ -322,7 +322,6 @@ get_codegen_scu_file() # are specified above the source file list. add_sources(libopenage - legacy_engine.cpp main.cpp options.cpp ${CMAKE_CURRENT_BINARY_DIR}/config.cpp @@ -331,7 +330,6 @@ add_sources(libopenage ) pxdgen( - legacy_engine.h main.h ) diff --git a/libopenage/audio/audio_manager.h b/libopenage/audio/audio_manager.h index d97197b5b5..af14d5ebb2 100644 --- a/libopenage/audio/audio_manager.h +++ b/libopenage/audio/audio_manager.h @@ -17,8 +17,6 @@ namespace openage { -class LegacyEngine; - namespace job { class JobManager; } diff --git a/libopenage/audio/dynamic_resource.cpp b/libopenage/audio/dynamic_resource.cpp index bb2347c07b..97aac8df37 100644 --- a/libopenage/audio/dynamic_resource.cpp +++ b/libopenage/audio/dynamic_resource.cpp @@ -4,7 +4,6 @@ #include "../job/job_manager.h" -#include "../legacy_engine.h" #include "../log/log.h" #include "audio_manager.h" diff --git a/libopenage/audio/resource.cpp b/libopenage/audio/resource.cpp index b5b3a3af55..66efcf52c2 100644 --- a/libopenage/audio/resource.cpp +++ b/libopenage/audio/resource.cpp @@ -3,7 +3,6 @@ #include "resource.h" #include "../error/error.h" -#include "../legacy_engine.h" #include "dynamic_resource.h" #include "in_memory_resource.h" @@ -30,7 +29,6 @@ int Resource::get_id() const { std::shared_ptr Resource::create_resource(AudioManager *manager, const resource_def &def) { - if (not def.location.is_file()) [[unlikely]] { throw Error{ERR << "sound file does not exist: " << def.location}; } diff --git a/libopenage/console/console.cpp b/libopenage/console/console.cpp index c28b6bab06..20e414e13d 100644 --- a/libopenage/console/console.cpp +++ b/libopenage/console/console.cpp @@ -3,7 +3,6 @@ #include "console.h" #include "../error/error.h" -#include "../legacy_engine.h" #include "../log/log.h" #include "../util/strings.h" #include "../util/unicode.h" diff --git a/libopenage/console/console.h b/libopenage/console/console.h index a06b9e6760..6994c4cbe9 100644 --- a/libopenage/console/console.h +++ b/libopenage/console/console.h @@ -13,8 +13,6 @@ namespace openage { -class LegacyEngine; - /** * In-game console subsystem. Featuring a full terminal emulator. * diff --git a/libopenage/console/draw.h b/libopenage/console/draw.h index a359556fb4..13634ea5b4 100644 --- a/libopenage/console/draw.h +++ b/libopenage/console/draw.h @@ -5,8 +5,6 @@ namespace openage { -class LegacyEngine; - namespace util { class FD; } // namespace util diff --git a/libopenage/gamestate/old/game_main.cpp b/libopenage/gamestate/old/game_main.cpp index 0fd3624f47..5d928e548a 100644 --- a/libopenage/gamestate/old/game_main.cpp +++ b/libopenage/gamestate/old/game_main.cpp @@ -2,7 +2,6 @@ #include "game_main.h" -#include "../../legacy_engine.h" #include "../../log/log.h" #include "../../terrain/terrain.h" #include "../../unit/unit_type.h" @@ -69,37 +68,9 @@ Civilisation *GameMain::add_civ(int civ_id) { GameMainHandle::GameMainHandle(qtsdl::GuiItemLink *gui_link) : game{}, - engine{}, gui_link{gui_link} { } -void GameMainHandle::set_engine(LegacyEngine *engine) { - ENSURE(!this->engine || this->engine == engine, "relinking GameMain to another engine is not supported and not caught properly"); - this->engine = engine; -} - -void GameMainHandle::clear() { - if (this->engine) { - this->game = nullptr; - // this->display->end_game(); - announce_running(); - } -} - -void GameMainHandle::set_game(std::unique_ptr &&game) { - if (this->engine) { - ENSURE(game, "linking game to engine problem"); - - // remember the pointer - this->game = game.get(); - - // then pass on the game to the engine - // this->display->start_game(std::move(game)); - - announce_running(); - } -} - GameMain *GameMainHandle::get_game() const { return this->game; } diff --git a/libopenage/gamestate/old/game_main.h b/libopenage/gamestate/old/game_main.h index a7318f25dc..af3651aadc 100644 --- a/libopenage/gamestate/old/game_main.h +++ b/libopenage/gamestate/old/game_main.h @@ -18,7 +18,6 @@ namespace openage { -class LegacyEngine; class Generator; class Terrain; @@ -130,8 +129,6 @@ class GameMainHandle { public: explicit GameMainHandle(qtsdl::GuiItemLink *gui_link); - void set_engine(LegacyEngine *engine); - /** * End the game and delete the game handle. */ @@ -164,11 +161,6 @@ class GameMainHandle { */ GameMain *game; - /** - * The engine the main game handle is attached to. - */ - LegacyEngine *engine; - public: GameMainSignals gui_signals; qtsdl::GuiItemLink *gui_link; diff --git a/libopenage/gamestate/old/game_spec.cpp b/libopenage/gamestate/old/game_spec.cpp index ca06c6b7e3..c23ddcd0bd 100644 --- a/libopenage/gamestate/old/game_spec.cpp +++ b/libopenage/gamestate/old/game_spec.cpp @@ -9,7 +9,6 @@ #include "../../gamedata/blending_mode_dummy.h" #include "../../gamedata/string_resource_dummy.h" #include "../../gamedata/terrain_dummy.h" -#include "../../legacy_engine.h" #include "../../log/log.h" #include "../../rng/global_rng.h" #include "../../unit/producer.h" diff --git a/libopenage/gui/CMakeLists.txt b/libopenage/gui/CMakeLists.txt index a1dd56541b..749136617c 100644 --- a/libopenage/gui/CMakeLists.txt +++ b/libopenage/gui/CMakeLists.txt @@ -1,15 +1,11 @@ add_sources(libopenage actions_list_model.cpp category_contents_list_model.cpp - engine_info.cpp - engine_link.cpp game_creator.cpp - game_main_link.cpp game_saver.cpp game_spec_link.cpp generator_link.cpp gui.cpp - main_args_link.cpp registrations.cpp resources_list_model.cpp ) diff --git a/libopenage/gui/engine_info.cpp b/libopenage/gui/engine_info.cpp deleted file mode 100644 index fad4eef3d3..0000000000 --- a/libopenage/gui/engine_info.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. - -#include "engine_info.h" - -namespace openage { -namespace gui { - -EngineQMLInfo::EngineQMLInfo(LegacyEngine *engine, - const util::Path &asset_dir) : - engine{engine}, - asset_dir{asset_dir} {} - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/engine_info.h b/libopenage/gui/engine_info.h deleted file mode 100644 index 999360ac2a..0000000000 --- a/libopenage/gui/engine_info.h +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include "../util/path.h" -#include "guisys/public/gui_singleton_items_info.h" - -namespace openage { -class LegacyEngine; - -namespace gui { - -/** - * This container is attached to the QML engine. - * - * It allows that one can access the members in the qml engine context then. - * That also means the members accessible during creation of any singleton QML item. - * - * This struct is used to link the openage Engine with QML in engine_link.cpp. - */ -class EngineQMLInfo : public qtsdl::GuiSingletonItemsInfo { -public: - EngineQMLInfo(LegacyEngine *engine, const util::Path &asset_dir); - - /** - * The openage engine, so it can be "used" in QML as a "QML Singleton". - * With this pointer, all of QML can find back to the engine. - */ - LegacyEngine *engine; - - /** - * Search path for finding assets n stuff. - */ - util::Path asset_dir; -}; - - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/engine_link.cpp b/libopenage/gui/engine_link.cpp deleted file mode 100644 index f98ca82ea4..0000000000 --- a/libopenage/gui/engine_link.cpp +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "engine_link.h" - -#include - -#include "../error/error.h" - -#include "../legacy_engine.h" - -#include "guisys/link/qml_engine_with_singleton_items_info.h" -#include "guisys/link/qtsdl_checked_static_cast.h" - -namespace openage::gui { - -namespace { -// this pushes the EngineLink in the QML engine. -// a qml engine calls the static provider() to obtain a handle. -const int registration = qmlRegisterSingletonType("yay.sfttech.openage", 1, 0, "LegacyEngine", &EngineLink::provider); -} // namespace - - -EngineLink::EngineLink(QObject *parent, LegacyEngine *engine) : - GuiSingletonItem{parent}, - core{engine} { - Q_UNUSED(registration); - - ENSURE(!unwrap(this)->gui_link, "Sharing singletons between QML engines is not supported for now."); - - // when the engine announces that the global key bindings - // changed, update the display. - QObject::connect( - &unwrap(this)->gui_signals, - &EngineSignals::global_binds_changed, - this, - &EngineLink::on_global_binds_changed); - - // trigger the engine signal, - // which then triggers this->on_global_binds_changed. - // unwrap(this)->announce_global_binds(); -} - -EngineLink::~EngineLink() { - unwrap(this)->gui_link = nullptr; -} - -// a qml engine requests a handle to the engine link with that static -// method we do this by extracting the per-qmlengine singleton from the -// engine (the qmlenginewithsingletoninfo), then just return the new link -// instance -QObject *EngineLink::provider(QQmlEngine *engine, QJSEngine *) { - // cast the engine to our specialization - qtsdl::QmlEngineWithSingletonItemsInfo *engine_with_singleton_items_info = qtsdl::checked_static_cast(engine); - - // get the singleton container out of the custom qml engine - auto info = static_cast( - engine_with_singleton_items_info->get_singleton_items_info()); - ENSURE(info, "qml-globals were lost or not passed to the gui subsystem"); - - // owned by the QML engine - // this handle contains the pointer to the openage engine, - // obtained through the qmlengine - return new EngineLink{nullptr, info->engine}; -} - -QStringList EngineLink::get_global_binds() const { - return this->global_binds; -} - -void EngineLink::stop() { - this->core->stop(); -} - -void EngineLink::on_global_binds_changed(const std::vector &global_binds) { - QStringList new_global_binds; - - // create the qstring list from the std string list - // which is then displayed in the ui - std::transform( - std::begin(global_binds), - std::end(global_binds), - std::back_inserter(new_global_binds), - [](const std::string &s) { - return QString::fromStdString(s); - }); - - new_global_binds.sort(); - - if (this->global_binds != new_global_binds) { - this->global_binds = new_global_binds; - emit this->global_binds_changed(); - } -} - -} // namespace openage::gui diff --git a/libopenage/gui/engine_link.h b/libopenage/gui/engine_link.h deleted file mode 100644 index dcd587035a..0000000000 --- a/libopenage/gui/engine_link.h +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "../util/path.h" -#include "guisys/link/gui_singleton_item.h" - -QT_FORWARD_DECLARE_CLASS(QQmlEngine) -QT_FORWARD_DECLARE_CLASS(QJSEngine) - -namespace openage { -class LegacyEngine; - -namespace gui { -class EngineLink; -} // namespace gui -} // namespace openage - -namespace qtsdl { -template <> -struct Wrap { - using Type = openage::gui::EngineLink; -}; - -template <> -struct Unwrap { - using Type = openage::LegacyEngine; -}; - -} // namespace qtsdl - -namespace openage { -namespace gui { - -class EngineLink : public qtsdl::GuiSingletonItem { - Q_OBJECT - - /** - * The text list of global key bindings. - * displayed so one can see what keys are active. - */ - Q_PROPERTY(QStringList globalBinds - READ get_global_binds - NOTIFY global_binds_changed) - -public: - explicit EngineLink(QObject *parent, LegacyEngine *engine); - virtual ~EngineLink(); - - static QObject *provider(QQmlEngine *, QJSEngine *); - - template - U *get() const { - return core; - } - - QStringList get_global_binds() const; - - Q_INVOKABLE void stop(); - -signals: - void global_binds_changed(); - -private slots: - void on_global_binds_changed(const std::vector &global_binds); - -private: - LegacyEngine *core; - - QStringList global_binds; -}; - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/game_creator.cpp b/libopenage/gui/game_creator.cpp index f187ab5ef2..392efe6139 100644 --- a/libopenage/gui/game_creator.cpp +++ b/libopenage/gui/game_creator.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "game_creator.h" @@ -8,7 +8,6 @@ #include "../gamestate/old/game_spec.h" #include "../gamestate/old/generator.h" -#include "game_main_link.h" #include "game_spec_link.h" #include "generator_link.h" @@ -18,11 +17,8 @@ namespace { const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "GameCreator"); } -GameCreator::GameCreator(QObject *parent) - : +GameCreator::GameCreator(QObject *parent) : QObject{parent}, - game{}, - game_spec{}, generator_parameters{} { Q_UNUSED(registration); } @@ -34,11 +30,10 @@ QString GameCreator::get_error_string() const { } void GameCreator::activate() { - static auto f = [] (GameMainHandle *game, - GameSpecHandle *game_spec, - Generator *generator, - std::shared_ptr callback) { - + static auto f = [](GameMainHandle *game, + GameSpecHandle *game_spec, + Generator *generator, + std::shared_ptr callback) { QString error_msg; if (game->is_game_running()) { @@ -60,27 +55,6 @@ void GameCreator::activate() { emit callback->error_message(error_msg); }; - - if (this->game && this->game_spec && this->generator_parameters) { - std::shared_ptr callback = std::make_shared(); - - QObject::connect(callback.get(), &GameCreatorSignals::error_message, this, &GameCreator::on_processed); - - this->game->i(f, this->game_spec, this->generator_parameters, callback); - } else { - this->on_processed([this] { - if (!this->game) - return "provide 'game' before loading"; - if (!this->game_spec) - return "provide 'gameSpec' before loading"; - if (!this->generator_parameters) - return "provide 'generatorParameters' before loading"; - else - ENSURE(false, "unhandled case for refusal to create a game"); - - return "unknown error"; - }()); - } } void GameCreator::clearErrors() { @@ -93,4 +67,4 @@ void GameCreator::on_processed(const QString &error_string) { emit this->error_string_changed(); } -} // namespace openage::gui +} // namespace openage::gui diff --git a/libopenage/gui/game_creator.h b/libopenage/gui/game_creator.h index b3efb1ff4c..c1899971d0 100644 --- a/libopenage/gui/game_creator.h +++ b/libopenage/gui/game_creator.h @@ -16,10 +16,8 @@ class GameCreator : public QObject { Q_ENUMS(State) Q_PROPERTY(QString errorString READ get_error_string NOTIFY error_string_changed) - Q_MOC_INCLUDE("gui/game_main_link.h") Q_MOC_INCLUDE("gui/game_spec_link.h") Q_MOC_INCLUDE("gui/generator_link.h") - Q_PROPERTY(openage::gui::GameMainLink *game MEMBER game NOTIFY game_changed) Q_PROPERTY(openage::gui::GameSpecLink *gameSpec MEMBER game_spec NOTIFY game_spec_changed) Q_PROPERTY(openage::gui::GeneratorLink *generatorParameters MEMBER generator_parameters NOTIFY generator_parameters_changed) @@ -43,7 +41,6 @@ public slots: private: QString error_string; - GameMainLink *game; GameSpecLink *game_spec; GeneratorLink *generator_parameters; }; diff --git a/libopenage/gui/game_main_link.cpp b/libopenage/gui/game_main_link.cpp deleted file mode 100644 index 35b4af4c65..0000000000 --- a/libopenage/gui/game_main_link.cpp +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "game_main_link.h" - -#include - -#include "../legacy_engine.h" -#include "engine_link.h" - -namespace openage::gui { - -namespace { -const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "GameMain"); -} - -GameMainLink::GameMainLink(QObject *parent) : - GuiItemQObject{parent}, - QQmlParserStatus{}, - GuiItem{this}, - state{}, - active{}, - engine{} { - Q_UNUSED(registration); -} - -GameMainLink::~GameMainLink() = default; - -void GameMainLink::classBegin() { -} - -void GameMainLink::on_core_adopted() { - QObject::connect(&unwrap(this)->gui_signals, &GameMainSignals::game_running, this, &GameMainLink::on_game_running); -} - -void GameMainLink::componentComplete() { - static auto f = [](GameMainHandle *_this) { - _this->announce_running(); - }; - this->i(f); -} - -GameMainLink::State GameMainLink::get_state() const { - return this->state; -} - -EngineLink *GameMainLink::get_engine() const { - return this->engine; -} - -void GameMainLink::set_engine(EngineLink *engine) { - static auto f = [](GameMainHandle *_this, LegacyEngine *engine) { - _this->set_engine(engine); - }; - this->s(f, this->engine, engine); -} - -void GameMainLink::clear() { - static auto f = [](GameMainHandle *_this) { - _this->clear(); - }; - this->i(f); -} - -void GameMainLink::on_game_running(bool running) { - auto state = running ? State::Running : State::Null; - - if (this->state != state) { - this->state = state; - emit this->state_changed(); - } -} - -} // namespace openage::gui diff --git a/libopenage/gui/game_main_link.h b/libopenage/gui/game_main_link.h deleted file mode 100644 index 0277cab6b8..0000000000 --- a/libopenage/gui/game_main_link.h +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2015-2018 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "guisys/link/gui_item.h" - -#include "../gamestate/old/game_main.h" - -namespace openage { -namespace gui { - -class EngineLink; -class GameMainLink; - -}} // namespace openage::gui - -namespace qtsdl { -template<> -struct Wrap { - using Type = openage::gui::GameMainLink; -}; - -template<> -struct Unwrap { - using Type = openage::GameMainHandle; -}; - -} // namespace qtsdl - -namespace openage { -namespace gui { - -class GameMainLink : public qtsdl::GuiItemQObject, public QQmlParserStatus, public qtsdl::GuiItem { - Q_OBJECT - - Q_INTERFACES(QQmlParserStatus) - Q_ENUMS(State) - Q_PROPERTY(State state READ get_state NOTIFY state_changed) - Q_PROPERTY(openage::gui::EngineLink* engine READ get_engine WRITE set_engine) - -public: - explicit GameMainLink(QObject *parent=nullptr); - virtual ~GameMainLink(); - - enum class State { - Null, Running - }; - - State get_state() const; - - EngineLink* get_engine() const; - void set_engine(EngineLink *engine); - - Q_INVOKABLE void clear(); - -signals: - void state_changed(); - -private slots: - void on_game_running(bool running); - -private: - virtual void classBegin() override; - virtual void on_core_adopted() override; - virtual void componentComplete() override; - - State state; - bool active; - EngineLink *engine; -}; - -}} // namespace openage::gui diff --git a/libopenage/gui/game_saver.cpp b/libopenage/gui/game_saver.cpp index f6793207ca..a700dc704e 100644 --- a/libopenage/gui/game_saver.cpp +++ b/libopenage/gui/game_saver.cpp @@ -1,14 +1,13 @@ -// Copyright 2016-2021 the openage authors. See copying.md for legal info. +// Copyright 2016-2023 the openage authors. See copying.md for legal info. #include "game_saver.h" #include -#include "../gamestate/old/game_save.h" #include "../gamestate/old/game_main.h" +#include "../gamestate/old/game_save.h" #include "../gamestate/old/generator.h" -#include "game_main_link.h" #include "generator_link.h" namespace openage::gui { @@ -17,10 +16,8 @@ namespace { const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "GameSaver"); } -GameSaver::GameSaver(QObject *parent) - : +GameSaver::GameSaver(QObject *parent) : QObject{parent}, - game{}, generator_parameters{} { Q_UNUSED(registration); } @@ -33,47 +30,21 @@ QString GameSaver::get_error_string() const { // called when the save-game button is pressed: void GameSaver::activate() { - static auto f = [] (GameMainHandle *game, - Generator *generator, - std::shared_ptr callback) { - + static auto f = [](GameMainHandle *game, + Generator *generator, + std::shared_ptr callback) { QString error_msg; if (!game->is_game_running()) { error_msg = "no open game to save"; - } else { + } + else { auto filename = generator->getv("load_filename"); gameio::save(game->get_game(), filename); } emit callback->error_message(error_msg); }; - - if (this->game && this->generator_parameters) { - std::shared_ptr callback = std::make_shared(); - QObject::connect(callback.get(), - &GameSaverSignals::error_message, - this, - &GameSaver::on_processed); - - this->game->i(f, this->generator_parameters, callback); - } - else { - QString error_msg = "unknown error"; - - if (!this->game) { - error_msg = "provide 'game' before saving"; - } - - if (!this->generator_parameters) { - error_msg = "provide 'generatorParameters' before saving"; - } - else { - ENSURE(false, "unhandled case for refusal to create a game"); - } - - this->on_processed(error_msg); - } } void GameSaver::clearErrors() { @@ -86,4 +57,4 @@ void GameSaver::on_processed(const QString &error_string) { emit this->error_string_changed(); } -} // namespace openage::gui +} // namespace openage::gui diff --git a/libopenage/gui/game_saver.h b/libopenage/gui/game_saver.h index 13a4183de6..69cbbe75d8 100644 --- a/libopenage/gui/game_saver.h +++ b/libopenage/gui/game_saver.h @@ -1,4 +1,4 @@ -// Copyright 2016-2016 the openage authors. See copying.md for legal info. +// Copyright 2016-2023 the openage authors. See copying.md for legal info. #pragma once @@ -15,11 +15,10 @@ class GameSaver : public QObject { Q_ENUMS(State) Q_PROPERTY(QString errorString READ get_error_string NOTIFY error_string_changed) - Q_PROPERTY(openage::gui::GameMainLink* game MEMBER game NOTIFY game_changed) - Q_PROPERTY(openage::gui::GeneratorLink* generatorParameters MEMBER generator_parameters NOTIFY generator_parameters_changed) + Q_PROPERTY(openage::gui::GeneratorLink *generatorParameters MEMBER generator_parameters NOTIFY generator_parameters_changed) public: - explicit GameSaver(QObject *parent=nullptr); + explicit GameSaver(QObject *parent = nullptr); virtual ~GameSaver(); QString get_error_string() const; @@ -37,7 +36,6 @@ public slots: private: QString error_string; - GameMainLink *game; GeneratorLink *generator_parameters; }; @@ -49,4 +47,5 @@ class GameSaverSignals : public QObject { void error_message(const QString &error); }; -}} // namespace openage::gui +} // namespace gui +} // namespace openage diff --git a/libopenage/gui/gui.cpp b/libopenage/gui/gui.cpp index dfa44fdc8b..590e11124c 100644 --- a/libopenage/gui/gui.cpp +++ b/libopenage/gui/gui.cpp @@ -2,9 +2,7 @@ #include "gui.h" -#include "../legacy_engine.h" #include "../util/path.h" -#include "engine_info.h" namespace openage { @@ -19,10 +17,7 @@ GUI::GUI(SDL_Window *window, render_updater{}, renderer{window}, game_logic_updater{}, - engine{ - &renderer, - {}, - info}, + engine{&renderer}, subtree{ &renderer, &game_logic_updater, diff --git a/libopenage/gui/main_args_link.cpp b/libopenage/gui/main_args_link.cpp deleted file mode 100644 index 8d7b11daa4..0000000000 --- a/libopenage/gui/main_args_link.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "main_args_link.h" - -#include - -#include "../error/error.h" - -#include "engine_info.h" -#include "guisys/link/qml_engine_with_singleton_items_info.h" -#include "guisys/link/qtsdl_checked_static_cast.h" - -namespace openage::gui { - -namespace { -// register "MainArgs" in the qml engine to be used globally. -const int registration = qmlRegisterSingletonType("yay.sfttech.openage", 1, 0, "MainArgs", &MainArgsLink::provider); -} - - -MainArgsLink::MainArgsLink(QObject *parent, const util::Path &asset_dir) - : - QObject{parent}, - asset_dir{asset_dir} { - Q_UNUSED(registration); -} - - -QObject* MainArgsLink::provider(QQmlEngine *engine, QJSEngine*) { - auto *engine_with_singleton_items_info = qtsdl::checked_static_cast(engine); - auto info = static_cast(engine_with_singleton_items_info->get_singleton_items_info()); - ENSURE(info, "globals were lost or not passed to the gui subsystem"); - - // owned by the QML engine - return new MainArgsLink{nullptr, info->asset_dir}; -} - -} // namespace openage::gui diff --git a/libopenage/gui/main_args_link.h b/libopenage/gui/main_args_link.h deleted file mode 100644 index a615860d23..0000000000 --- a/libopenage/gui/main_args_link.h +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "../util/path.h" - -QT_FORWARD_DECLARE_CLASS(QQmlEngine) -QT_FORWARD_DECLARE_CLASS(QJSEngine) - -namespace openage { -namespace gui { - -/** - * Used to make arguments of the game available in QML. - */ -class MainArgsLink : public QObject { - Q_OBJECT - - Q_PROPERTY(openage::util::Path assetDir MEMBER asset_dir CONSTANT) - -public: - explicit MainArgsLink(QObject *parent, const util::Path &asset_dir); - virtual ~MainArgsLink() = default; - - /** - * Generates the MainArgsLink object which is then used within QML. - */ - static QObject* provider(QQmlEngine*, QJSEngine*); - -private: - util::Path asset_dir; -}; - -}} // namespace openage::gui diff --git a/libopenage/legacy_engine.cpp b/libopenage/legacy_engine.cpp deleted file mode 100644 index 8b972316d1..0000000000 --- a/libopenage/legacy_engine.cpp +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. - -#include "legacy_engine.h" - -#include -#include -#include -#include -#include -#include -#include - -#include "config.h" -#include "error/error.h" -#include "log/log.h" -#include "version.h" -#include "versions/compiletime.h" - -namespace openage { - -LegacyEngine::LegacyEngine(enum mode mode, - const util::Path &root_dir, - const std::shared_ptr &cvar_manager) : - running{false}, - run_mode{mode}, - root_dir{root_dir}, - job_manager{SDL_GetCPUCount()}, - qml_info{this, root_dir["assets"]}, - cvar_manager{cvar_manager}, - profiler{this}, - gui_link{} { - // TODO: implement FULL and HEADLESS mode :) -} - - -LegacyEngine::~LegacyEngine() {} - - -void LegacyEngine::run() { - try { - this->job_manager.start(); - this->running = true; - - this->running = false; - } - catch (...) { - this->job_manager.stop(); - throw; - } -} - - -void LegacyEngine::stop() { - this->job_manager.stop(); - this->running = false; -} - - -const util::Path &LegacyEngine::get_root_dir() { - return this->root_dir; -} - - -job::JobManager *LegacyEngine::get_job_manager() { - return &this->job_manager; -} - - -std::shared_ptr LegacyEngine::get_cvar_manager() { - return this->cvar_manager; -} - -gui::EngineQMLInfo LegacyEngine::get_qml_info() { - return this->qml_info; -} - -} // namespace openage diff --git a/libopenage/legacy_engine.h b/libopenage/legacy_engine.h deleted file mode 100644 index cf01cb391a..0000000000 --- a/libopenage/legacy_engine.h +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "log/file_logsink.h" -#include "log/log.h" -// pxd: from libopenage.cvar cimport CVarManager -#include "cvar/cvar.h" -#include "gui/engine_info.h" -#include "job/job_manager.h" -#include "options.h" -#include "unit/selection.h" -#include "util/externalprofiler.h" -#include "util/path.h" -#include "util/profiler.h" -#include "util/strings.h" - - -/** - * Main openage namespace to store all things that make the have to do with the game. - * - * Game entity management, graphics drawing, gui stuff, input handling etc. - * So basically everything that makes the game work lies in here... - */ -namespace openage { - -namespace gui { -class GuiItemLink; -} // namespace gui - -/** - * Qt signals for the engine. - */ -class EngineSignals : public QObject { - Q_OBJECT - -public: -signals: - void global_binds_changed(const std::vector &global_binds); -}; - - -/** - * main engine container. - * - * central foundation for everything the openage engine is capable of. - * - * pxd: - * - * cppclass LegacyEngine: - * - * InputManager &get_input_manager() except + - * CVarManager &get_cvar_manager() except + - */ -class LegacyEngine final { -public: - enum class mode { - LEGACY, - HEADLESS, - FULL, - }; - - LegacyEngine(); - - /** - * engine initialization method. - * starts the engine subsystems depending on the requested run mode. - */ - LegacyEngine(mode mode, - const util::Path &root_dir, - const std::shared_ptr &cvar_manager); - - /** - * engine copy constructor. - */ - LegacyEngine(const LegacyEngine ©) = delete; - - /** - * engine assignment operator. - */ - LegacyEngine &operator=(const LegacyEngine ©) = delete; - - /** - * engine move constructor. - */ - LegacyEngine(LegacyEngine &&other) = delete; - - /** - * engine move operator. - */ - LegacyEngine &operator=(LegacyEngine &&other) = delete; - -public: - /** - * engine destructor, cleans up memory etc. - * deletes opengl context, the SDL window, and engine variables. - */ - ~LegacyEngine(); - - /** - * starts the engine loop. - */ - void run(); - - /** - * enqueues the stop of the main loop. - */ - void stop(); - - /** - * return the data directory where the engine was started from. - */ - const util::Path &get_root_dir(); - - /** - * return this engine's job manager. - * - * TODO: remove ptr access - */ - job::JobManager *get_job_manager(); - - /** - * return this engine's cvar manager. - */ - std::shared_ptr get_cvar_manager(); - - /** - * return this engine's qml info. - */ - gui::EngineQMLInfo get_qml_info(); - - /** - * current engine state variable. - * to be set to false to stop the engine loop. - */ - bool running; - - /** - * profiler used by the engine - */ - util::ExternalProfiler external_profiler; - -private: - /** - * Run-mode of the engine, this determines the basic modules to be loaded. - */ - mode run_mode; - - /** - * The engine root directory. - * Uses the openage fslike path abstraction that can mount paths into one. - * - * This means that this path does simulataneously lead to global assets, - * home-folder-assets, settings, and basically the whole filesystem access. - * - * TODO: move this to a settings class, which then also hosts cvar and the options system. - */ - util::Path root_dir; - - /** - * the engine's job manager, for asynchronous background task queuing. - */ - job::JobManager job_manager; - - - /** - * This stores information to be accessible from the QML engine. - * - * Information in there (such as a pointer to the this engine) - * is then usable from within qml files, after some additional magic. - */ - gui::EngineQMLInfo qml_info; - - /** - * the engine's cvar manager. - */ - std::shared_ptr cvar_manager; - - - /** - * the engines profiler - */ - util::Profiler profiler; - - /** - * Logsink to store messages to the filesystem. - */ - std::unique_ptr logsink_file; - -public: - /** - * Signal emitting capability for the engine. - */ - EngineSignals gui_signals; - - /** - * Link to the Qt GUI. - */ - gui::GuiItemLink *gui_link; -}; - -} // namespace openage diff --git a/libopenage/renderer/stages/screen/screenshot.cpp b/libopenage/renderer/stages/screen/screenshot.cpp index 8fad08cab9..e73c875a11 100644 --- a/libopenage/renderer/stages/screen/screenshot.cpp +++ b/libopenage/renderer/stages/screen/screenshot.cpp @@ -1,4 +1,4 @@ -// Copyright 2014-2019 the openage authors. See copying.md for legal info. +// Copyright 2014-2023 the openage authors. See copying.md for legal info. #include "screenshot.h" diff --git a/libopenage/terrain/terrain.cpp b/libopenage/terrain/terrain.cpp index 59f9239a9d..62eeefc32b 100644 --- a/libopenage/terrain/terrain.cpp +++ b/libopenage/terrain/terrain.cpp @@ -11,7 +11,6 @@ #include "../coord/pixel.h" #include "../coord/tile.h" #include "../error/error.h" -#include "../legacy_engine.h" #include "../log/log.h" #include "../util/misc.h" #include "../util/strings.h" diff --git a/libopenage/terrain/terrain.h b/libopenage/terrain/terrain.h index bb43a04e73..4db8fdbcb1 100644 --- a/libopenage/terrain/terrain.h +++ b/libopenage/terrain/terrain.h @@ -17,7 +17,6 @@ namespace openage { -class LegacyEngine; class RenderOptions; class TerrainChunk; class TerrainObject; diff --git a/libopenage/terrain/terrain_chunk.cpp b/libopenage/terrain/terrain_chunk.cpp index acbd76d7a7..aec7d2c6ec 100644 --- a/libopenage/terrain/terrain_chunk.cpp +++ b/libopenage/terrain/terrain_chunk.cpp @@ -8,7 +8,6 @@ #include "../coord/pixel.h" #include "../coord/tile.h" #include "../error/error.h" -#include "../legacy_engine.h" #include "../log/log.h" #include "../util/misc.h" diff --git a/libopenage/terrain/terrain_object.cpp b/libopenage/terrain/terrain_object.cpp index c4f4c6c9f9..a81716491a 100644 --- a/libopenage/terrain/terrain_object.cpp +++ b/libopenage/terrain/terrain_object.cpp @@ -9,7 +9,6 @@ #include "../coord/pixel.h" #include "../coord/tile.h" #include "../error/error.h" -#include "../legacy_engine.h" #include "../unit/unit.h" #include "terrain.h" @@ -20,7 +19,6 @@ namespace openage { TerrainObject::TerrainObject(Unit &u) : unit(u), passable{[](const coord::phys3 &) -> bool { return true; }}, - draw{[](const LegacyEngine & /*e*/) {}}, state{object_state::removed}, occupied_chunk_count{0}, parent{nullptr} { diff --git a/libopenage/terrain/terrain_object.h b/libopenage/terrain/terrain_object.h index 2f9384b6ce..93657db11d 100644 --- a/libopenage/terrain/terrain_object.h +++ b/libopenage/terrain/terrain_object.h @@ -10,7 +10,6 @@ namespace openage { -class LegacyEngine; class Terrain; class TerrainChunk; class Texture; @@ -118,11 +117,6 @@ class TerrainObject : public std::enable_shared_from_this { */ std::function passable; - /** - * specifies content to be drawn - */ - std::function draw; - /** * changes the placement state of this object keeping the existing * position. this is useful for upgrading a floating building to a placed state diff --git a/libopenage/unit/action.cpp b/libopenage/unit/action.cpp index 9370609f2d..65bd1bed8b 100644 --- a/libopenage/unit/action.cpp +++ b/libopenage/unit/action.cpp @@ -3,7 +3,6 @@ #include #include -#include "../legacy_engine.h" #include "../pathfinding/a_star.h" #include "../pathfinding/heuristics.h" #include "../terrain/terrain.h" @@ -100,13 +99,6 @@ float UnitAction::current_frame() const { return this->frame; } -void UnitAction::draw_debug(const LegacyEngine &engine) { - // draw debug content if available - if (show_debug && this->debug_draw_action) { - this->debug_draw_action(engine); - } -} - void UnitAction::face_towards(const coord::phys3 pos) { if (this->entity->has_attribute(attr_type::direction)) { auto &d_attr = this->entity->get_attribute(); diff --git a/libopenage/unit/action.h b/libopenage/unit/action.h index ca41229e87..b0db755fe6 100644 --- a/libopenage/unit/action.h +++ b/libopenage/unit/action.h @@ -146,8 +146,6 @@ class UnitAction { */ virtual std::string name() const = 0; - void draw_debug(const LegacyEngine &engine); - /** * common functions for actions */ @@ -195,11 +193,6 @@ class UnitAction { graphic_type graphic; float frame; float frame_rate; - - /** - * additional drawing for debug purposes - */ - std::function debug_draw_action; }; /** diff --git a/libopenage/unit/producer.cpp b/libopenage/unit/producer.cpp index fae69a6f78..07f6f45463 100644 --- a/libopenage/unit/producer.cpp +++ b/libopenage/unit/producer.cpp @@ -3,7 +3,6 @@ #include #include "../gamedata/unit_dummy.h" -#include "../legacy_engine.h" #include "../log/log.h" #include "../terrain/terrain.h" #include "../terrain/terrain_object.h" diff --git a/libopenage/unit/selection.cpp b/libopenage/unit/selection.cpp index 48d38ba457..354bf8f5ba 100644 --- a/libopenage/unit/selection.cpp +++ b/libopenage/unit/selection.cpp @@ -6,7 +6,6 @@ #include #include "../coord/tile.h" -#include "../legacy_engine.h" #include "../log/log.h" #include "../terrain/terrain.h" #include "action.h" @@ -17,10 +16,9 @@ namespace openage { -UnitSelection::UnitSelection(LegacyEngine *engine) : +UnitSelection::UnitSelection() : selection_type{selection_type_t::nothing}, - drag_active{false}, - engine{engine} { + drag_active{false} { } // bool UnitSelection::on_drawhud() { diff --git a/libopenage/unit/selection.h b/libopenage/unit/selection.h index fd623f91b9..f82fc02bea 100644 --- a/libopenage/unit/selection.h +++ b/libopenage/unit/selection.h @@ -9,8 +9,6 @@ #include "unit_container.h" namespace openage { - -class LegacyEngine; class Terrain; std::vector tiles_in_range(coord::camgame p1, coord::camgame p2, const coord::CoordManager &coord); @@ -36,7 +34,7 @@ enum class selection_type_t { */ class UnitSelection { public: - UnitSelection(LegacyEngine *engine); + UnitSelection(/* LegacyEngine *engine */); // bool on_drawhud() override; void drag_begin(coord::camgame pos); @@ -111,7 +109,7 @@ class UnitSelection { /** * Engine where this selection is attached to. */ - LegacyEngine *engine; + // LegacyEngine *engine; }; } // namespace openage diff --git a/libopenage/unit/unit.cpp b/libopenage/unit/unit.cpp index 234d47770e..bccd729a8e 100644 --- a/libopenage/unit/unit.cpp +++ b/libopenage/unit/unit.cpp @@ -4,7 +4,6 @@ #include #include -#include "../legacy_engine.h" #include "../terrain/terrain.h" #include "ability.h" diff --git a/libopenage/util/profiler.cpp b/libopenage/util/profiler.cpp index 9b0917edd4..95ebd85af7 100644 --- a/libopenage/util/profiler.cpp +++ b/libopenage/util/profiler.cpp @@ -1,7 +1,6 @@ // Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "profiler.h" -#include "../legacy_engine.h" #include "../renderer/color.h" #include "misc.h" @@ -12,192 +11,192 @@ namespace openage::util { -Profiler::Profiler(LegacyEngine *engine) : - engine{engine} {} - - -Profiler::~Profiler() { - this->unregister_all(); -} - -void Profiler::register_component(const std::string &com, color component_color) { - if (this->registered(com)) { - return; - } - - component_time_data cdt; - cdt.display_name = com; - cdt.drawing_color = component_color; - - for (auto &val : cdt.history) { - val = 0; - } - - this->components[com] = cdt; -} - -void Profiler::unregister_component(const std::string &com) { - if (not this->registered(com)) { - return; - } - - this->components.erase(com); -} - -void Profiler::unregister_all() { - std::vector registered_components = this->registered_components(); - - for (auto com : registered_components) { - this->unregister_component(com); - } -} - -std::vector Profiler::registered_components() { - std::vector registered_components; - for (auto &pair : this->components) { - registered_components.push_back(pair.first); - } - - return registered_components; -} - -void Profiler::start_measure(const std::string &com, color component_color) { - if (not this->engine_in_debug_mode()) { - return; - } - - if (not this->registered(com)) { - this->register_component(com, component_color); - } - - this->components[com].start = std::chrono::high_resolution_clock::now(); -} - -void Profiler::end_measure(const std::string &com) { - if (not this->engine_in_debug_mode()) { - return; - } - - if (this->registered(com)) { - std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); - this->components[com].duration = end - this->components[com].start; - } -} - -void Profiler::draw_component_performance(const std::string &com) { - color rgb = this->components[com].drawing_color; - glColor4f(rgb.r, rgb.g, rgb.b, 1.0); - - glLineWidth(1.0); - glBegin(GL_LINE_STRIP); - float x_offset = 0.0; - float offset_factor = static_cast(PROFILER_CANVAS_WIDTH) / static_cast(MAX_DURATION_HISTORY); - float percentage_factor = static_cast(PROFILER_CANVAS_HEIGHT) / 100.0; - - for (auto i = this->insert_pos; mod(i, MAX_DURATION_HISTORY) != mod(this->insert_pos - 1, MAX_DURATION_HISTORY); ++i) { - i = mod(i, MAX_DURATION_HISTORY); - - auto percentage = this->components[com].history.at(i); - glVertex3f(PROFILER_CANVAS_POSITION_X + x_offset, PROFILER_CANVAS_POSITION_Y + percentage * percentage_factor, 0.0); - x_offset += offset_factor; - } - glEnd(); - - // reset color - glColor4f(1.0, 1.0, 1.0, 1.0); -} - -void Profiler::show(bool debug_mode) { - if (debug_mode) { - this->show(); - } -} - -void Profiler::show() { - this->draw_canvas(); - this->draw_legend(); - - for (auto com : this->components) { - this->draw_component_performance(com.first); - } -} - -bool Profiler::registered(const std::string &com) const { - return this->components.find(com) != this->components.end(); -} - -unsigned Profiler::size() const { - return this->components.size(); -} - -void Profiler::start_frame_measure() { - if (this->engine_in_debug_mode()) { - this->frame_start = std::chrono::high_resolution_clock::now(); - } -} - -void Profiler::end_frame_measure() { - if (not this->engine_in_debug_mode()) { - return; - } - - auto frame_end = std::chrono::high_resolution_clock::now(); - this->frame_duration = frame_end - this->frame_start; - - for (auto com : this->registered_components()) { - double percentage = this->duration_to_percentage(this->components[com].duration); - this->append_to_history(com, percentage); - } - - this->insert_pos++; -} - -void Profiler::draw_canvas() { - glColor4f(0.2, 0.2, 0.2, PROFILER_CANVAS_ALPHA); - glRecti(PROFILER_CANVAS_POSITION_X, - PROFILER_CANVAS_POSITION_Y, - PROFILER_CANVAS_POSITION_X + PROFILER_CANVAS_WIDTH, - PROFILER_CANVAS_POSITION_Y + PROFILER_CANVAS_HEIGHT); -} - -void Profiler::draw_legend() { - int offset = 0; - for (auto com : this->components) { - glColor4f(com.second.drawing_color.r, com.second.drawing_color.g, com.second.drawing_color.b, 1.0); - int box_x = PROFILER_CANVAS_POSITION_X + 2; - int box_y = PROFILER_CANVAS_POSITION_Y - PROFILER_COM_BOX_HEIGHT - 2 - offset; - glRecti(box_x, box_y, box_x + PROFILER_COM_BOX_WIDTH, box_y + PROFILER_COM_BOX_HEIGHT); - - glColor4f(0.2, 0.2, 0.2, 1); - coord::viewport position = coord::viewport{box_x + PROFILER_COM_BOX_WIDTH + 2, box_y + 2}; - // this->display->render_text(position, 12, renderer::Colors::WHITE, "%s", com.second.display_name.c_str()); - - offset += PROFILER_COM_BOX_HEIGHT + 2; - } -} - -double Profiler::duration_to_percentage(std::chrono::high_resolution_clock::duration duration) { - double dur = std::chrono::duration_cast(duration).count(); - double ref = std::chrono::duration_cast(this->frame_duration).count(); - double percentage = dur / ref * 100; - return percentage; -} - -void Profiler::append_to_history(const std::string &com, double percentage) { - if (this->insert_pos == MAX_DURATION_HISTORY) { - this->insert_pos = 0; - } - this->components[com].history[this->insert_pos] = percentage; -} - -bool Profiler::engine_in_debug_mode() { - // if (this->display->drawing_debug_overlay.value) { - // return true; - // } - // else { - // return false; - // } - return true; -} +// Profiler::Profiler() : +// {} + + +// Profiler::~Profiler() { +// this->unregister_all(); +// } + +// void Profiler::register_component(const std::string &com, color component_color) { +// if (this->registered(com)) { +// return; +// } + +// component_time_data cdt; +// cdt.display_name = com; +// cdt.drawing_color = component_color; + +// for (auto &val : cdt.history) { +// val = 0; +// } + +// this->components[com] = cdt; +// } + +// void Profiler::unregister_component(const std::string &com) { +// if (not this->registered(com)) { +// return; +// } + +// this->components.erase(com); +// } + +// void Profiler::unregister_all() { +// std::vector registered_components = this->registered_components(); + +// for (auto com : registered_components) { +// this->unregister_component(com); +// } +// } + +// std::vector Profiler::registered_components() { +// std::vector registered_components; +// for (auto &pair : this->components) { +// registered_components.push_back(pair.first); +// } + +// return registered_components; +// } + +// void Profiler::start_measure(const std::string &com, color component_color) { +// if (not this->engine_in_debug_mode()) { +// return; +// } + +// if (not this->registered(com)) { +// this->register_component(com, component_color); +// } + +// this->components[com].start = std::chrono::high_resolution_clock::now(); +// } + +// void Profiler::end_measure(const std::string &com) { +// if (not this->engine_in_debug_mode()) { +// return; +// } + +// if (this->registered(com)) { +// std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); +// this->components[com].duration = end - this->components[com].start; +// } +// } + +// void Profiler::draw_component_performance(const std::string &com) { +// color rgb = this->components[com].drawing_color; +// glColor4f(rgb.r, rgb.g, rgb.b, 1.0); + +// glLineWidth(1.0); +// glBegin(GL_LINE_STRIP); +// float x_offset = 0.0; +// float offset_factor = static_cast(PROFILER_CANVAS_WIDTH) / static_cast(MAX_DURATION_HISTORY); +// float percentage_factor = static_cast(PROFILER_CANVAS_HEIGHT) / 100.0; + +// for (auto i = this->insert_pos; mod(i, MAX_DURATION_HISTORY) != mod(this->insert_pos - 1, MAX_DURATION_HISTORY); ++i) { +// i = mod(i, MAX_DURATION_HISTORY); + +// auto percentage = this->components[com].history.at(i); +// glVertex3f(PROFILER_CANVAS_POSITION_X + x_offset, PROFILER_CANVAS_POSITION_Y + percentage * percentage_factor, 0.0); +// x_offset += offset_factor; +// } +// glEnd(); + +// // reset color +// glColor4f(1.0, 1.0, 1.0, 1.0); +// } + +// void Profiler::show(bool debug_mode) { +// if (debug_mode) { +// this->show(); +// } +// } + +// // void Profiler::show() { +// // this->draw_canvas(); +// // this->draw_legend(); + +// // for (auto com : this->components) { +// // this->draw_component_performance(com.first); +// // } +// // } + +// bool Profiler::registered(const std::string &com) const { +// return this->components.find(com) != this->components.end(); +// } + +// unsigned Profiler::size() const { +// return this->components.size(); +// } + +// void Profiler::start_frame_measure() { +// if (this->engine_in_debug_mode()) { +// this->frame_start = std::chrono::high_resolution_clock::now(); +// } +// } + +// void Profiler::end_frame_measure() { +// if (not this->engine_in_debug_mode()) { +// return; +// } + +// auto frame_end = std::chrono::high_resolution_clock::now(); +// this->frame_duration = frame_end - this->frame_start; + +// for (auto com : this->registered_components()) { +// double percentage = this->duration_to_percentage(this->components[com].duration); +// this->append_to_history(com, percentage); +// } + +// this->insert_pos++; +// } + +// // void Profiler::draw_canvas() { +// // glColor4f(0.2, 0.2, 0.2, PROFILER_CANVAS_ALPHA); +// // glRecti(PROFILER_CANVAS_POSITION_X, +// // PROFILER_CANVAS_POSITION_Y, +// // PROFILER_CANVAS_POSITION_X + PROFILER_CANVAS_WIDTH, +// // PROFILER_CANVAS_POSITION_Y + PROFILER_CANVAS_HEIGHT); +// // } + +// // void Profiler::draw_legend() { +// // int offset = 0; +// // for (auto com : this->components) { +// // glColor4f(com.second.drawing_color.r, com.second.drawing_color.g, com.second.drawing_color.b, 1.0); +// // int box_x = PROFILER_CANVAS_POSITION_X + 2; +// // int box_y = PROFILER_CANVAS_POSITION_Y - PROFILER_COM_BOX_HEIGHT - 2 - offset; +// // glRecti(box_x, box_y, box_x + PROFILER_COM_BOX_WIDTH, box_y + PROFILER_COM_BOX_HEIGHT); + +// // glColor4f(0.2, 0.2, 0.2, 1); +// // coord::viewport position = coord::viewport{box_x + PROFILER_COM_BOX_WIDTH + 2, box_y + 2}; +// // // this->display->render_text(position, 12, renderer::Colors::WHITE, "%s", com.second.display_name.c_str()); + +// // offset += PROFILER_COM_BOX_HEIGHT + 2; +// // } +// // } + +// double Profiler::duration_to_percentage(std::chrono::high_resolution_clock::duration duration) { +// double dur = std::chrono::duration_cast(duration).count(); +// double ref = std::chrono::duration_cast(this->frame_duration).count(); +// double percentage = dur / ref * 100; +// return percentage; +// } + +// void Profiler::append_to_history(const std::string &com, double percentage) { +// if (this->insert_pos == MAX_DURATION_HISTORY) { +// this->insert_pos = 0; +// } +// this->components[com].history[this->insert_pos] = percentage; +// } + +// bool Profiler::engine_in_debug_mode() { +// // if (this->display->drawing_debug_overlay.value) { +// // return true; +// // } +// // else { +// // return false; +// // } +// return true; +// } } // namespace openage::util diff --git a/libopenage/util/profiler.h b/libopenage/util/profiler.h index 24d3946394..1b3641d576 100644 --- a/libopenage/util/profiler.h +++ b/libopenage/util/profiler.h @@ -19,9 +19,6 @@ constexpr int PROFILER_COM_BOX_HEIGHT = 15; namespace openage { -class LegacyEngine; - - namespace util { struct color { @@ -36,95 +33,93 @@ struct component_time_data { std::array history; }; -class Profiler { -public: - Profiler(LegacyEngine *engine); - ~Profiler(); - - /** - * registers a component - * @param com the identifier to distinguish the components - * @param component_color color of the plotted line - */ - void register_component(const std::string &com, color component_color); - - /** - * unregisters an individual component - * @param com component name which should be unregistered - */ - void unregister_component(const std::string &com); - - /** - * unregisters all remaining components - */ - void unregister_all(); - - /** - *returns a vector of registered component names - */ - std::vector registered_components(); - - /* - * starts a measurement for the component com. If com is not yet - * registered, its getting registered and the profiler uses the color - * information given by component_color. The default value is white. - */ - void start_measure(const std::string &com, color component_color = {1.0, 1.0, 1.0}); - - /* - * stops the measurement for the component com. If com is not yet - * registered it does nothing. - */ - void end_measure(const std::string &com); - - /* - * draws the profiler gui if debug_mode is set - */ - void show(bool debug_mode); - - /* - * draws the profiler gui - */ - void show(); - - /* - * true if the component com is already registered, otherwise false - */ - bool registered(const std::string &com) const; - - /* - * returns the number of registered components - */ - unsigned size() const; - - /** - * sets the start point for the actual frame which is used as a reference - * value for the registered components - */ - void start_frame_measure(); - - /** - * sets the end point for the reference time used to compute the portions - * of the components. Each recorded measurement for the registered components - * get appended to their history complete the measurement. - */ - void end_frame_measure(); - -private: - void draw_canvas(); - void draw_legend(); - void draw_component_performance(const std::string &com); - double duration_to_percentage(std::chrono::high_resolution_clock::duration duration); - void append_to_history(const std::string &com, double percentage); - bool engine_in_debug_mode(); - - std::chrono::high_resolution_clock::time_point frame_start; - std::chrono::high_resolution_clock::duration frame_duration; - std::unordered_map components; - int insert_pos = 0; - - LegacyEngine *engine; -}; +// class Profiler { +// public: +// Profiler(); +// ~Profiler(); + +// /** +// * registers a component +// * @param com the identifier to distinguish the components +// * @param component_color color of the plotted line +// */ +// void register_component(const std::string &com, color component_color); + +// /** +// * unregisters an individual component +// * @param com component name which should be unregistered +// */ +// void unregister_component(const std::string &com); + +// /** +// * unregisters all remaining components +// */ +// void unregister_all(); + +// /** +// *returns a vector of registered component names +// */ +// std::vector registered_components(); + +// /* +// * starts a measurement for the component com. If com is not yet +// * registered, its getting registered and the profiler uses the color +// * information given by component_color. The default value is white. +// */ +// void start_measure(const std::string &com, color component_color = {1.0, 1.0, 1.0}); + +// /* +// * stops the measurement for the component com. If com is not yet +// * registered it does nothing. +// */ +// void end_measure(const std::string &com); + +// /* +// * draws the profiler gui if debug_mode is set +// */ +// void show(bool debug_mode); + +// /* +// * draws the profiler gui +// */ +// void show(); + +// /* +// * true if the component com is already registered, otherwise false +// */ +// bool registered(const std::string &com) const; + +// /* +// * returns the number of registered components +// */ +// unsigned size() const; + +// /** +// * sets the start point for the actual frame which is used as a reference +// * value for the registered components +// */ +// void start_frame_measure(); + +// /** +// * sets the end point for the reference time used to compute the portions +// * of the components. Each recorded measurement for the registered components +// * get appended to their history complete the measurement. +// */ +// void end_frame_measure(); + +// private: +// // void draw_canvas(); +// // void draw_legend(); +// void draw_component_performance(const std::string &com); +// double duration_to_percentage(std::chrono::high_resolution_clock::duration duration); +// void append_to_history(const std::string &com, double percentage); +// bool engine_in_debug_mode(); + +// std::chrono::high_resolution_clock::time_point frame_start; +// std::chrono::high_resolution_clock::duration frame_duration; +// std::unordered_map components; +// int insert_pos = 0; +// }; } // namespace util } // namespace openage From c1d4741439533c88c7222e3c14f8bd59edec8f67 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Sep 2023 23:54:57 +0200 Subject: [PATCH 040/771] gamestate: Remove legacy gamestate. --- libopenage/CMakeLists.txt | 1 - libopenage/gamestate/CMakeLists.txt | 3 - libopenage/gamestate/old/CMakeLists.txt | 15 - libopenage/gamestate/old/civilisation.cpp | 65 - libopenage/gamestate/old/civilisation.h | 85 -- libopenage/gamestate/old/cost.cpp | 48 - libopenage/gamestate/old/cost.h | 65 - libopenage/gamestate/old/game_main.cpp | 86 -- libopenage/gamestate/old/game_main.h | 169 --- libopenage/gamestate/old/game_save.cpp | 149 -- libopenage/gamestate/old/game_save.h | 27 - libopenage/gamestate/old/game_spec.cpp | 455 ------ libopenage/gamestate/old/game_spec.h | 290 ---- libopenage/gamestate/old/generator.cpp | 323 ----- libopenage/gamestate/old/generator.h | 179 --- libopenage/gamestate/old/market.cpp | 84 -- libopenage/gamestate/old/market.h | 71 - libopenage/gamestate/old/player.cpp | 291 ---- libopenage/gamestate/old/player.h | 244 ---- .../gamestate/old/population_tracker.cpp | 76 - libopenage/gamestate/old/population_tracker.h | 106 -- libopenage/gamestate/old/resource.cpp | 208 --- libopenage/gamestate/old/resource.h | 209 --- libopenage/gamestate/old/score.cpp | 109 -- libopenage/gamestate/old/score.h | 140 -- libopenage/gamestate/old/team.cpp | 77 - libopenage/gamestate/old/team.h | 75 - libopenage/gamestate/old/types.cpp | 8 - libopenage/gamestate/old/types.h | 16 - libopenage/gui/CMakeLists.txt | 6 - .../gui/category_contents_list_model.cpp | 70 - libopenage/gui/category_contents_list_model.h | 47 - libopenage/gui/game_creator.cpp | 70 - libopenage/gui/game_creator.h | 57 - libopenage/gui/game_saver.cpp | 60 - libopenage/gui/game_saver.h | 51 - libopenage/gui/game_spec_link.cpp | 100 -- libopenage/gui/game_spec_link.h | 104 -- libopenage/gui/generator_link.cpp | 25 - libopenage/gui/generator_link.h | 43 - .../gui/integration/private/CMakeLists.txt | 2 - .../gui_game_spec_image_provider_impl.cpp | 107 -- .../gui_game_spec_image_provider_impl.h | 102 -- .../private/gui_image_provider_link.cpp | 39 +- .../private/gui_image_provider_link.h | 32 +- .../private/gui_texture_factory.cpp | 31 - .../integration/private/gui_texture_factory.h | 33 - libopenage/gui/resources_list_model.cpp | 59 - libopenage/gui/resources_list_model.h | 36 - libopenage/pathfinding/a_star.cpp | 49 +- libopenage/pathfinding/a_star.h | 10 +- libopenage/terrain/CMakeLists.txt | 2 - libopenage/terrain/terrain.cpp | 26 +- libopenage/terrain/terrain.h | 8 - libopenage/terrain/terrain_chunk.cpp | 1 - libopenage/terrain/terrain_chunk.h | 1 - libopenage/terrain/terrain_object.cpp | 463 ------ libopenage/terrain/terrain_object.h | 332 ----- libopenage/terrain/terrain_search.cpp | 104 -- libopenage/terrain/terrain_search.h | 74 - libopenage/unit/CMakeLists.txt | 13 - libopenage/unit/ability.cpp | 446 ------ libopenage/unit/ability.h | 380 ----- libopenage/unit/action.cpp | 1249 ----------------- libopenage/unit/action.h | 720 ---------- libopenage/unit/attribute.cpp | 21 - libopenage/unit/attribute.h | 643 --------- libopenage/unit/attributes.cpp | 44 - libopenage/unit/attributes.h | 64 - libopenage/unit/command.cpp | 103 -- libopenage/unit/command.h | 153 -- libopenage/unit/producer.cpp | 695 --------- libopenage/unit/producer.h | 154 -- libopenage/unit/research.cpp | 63 - libopenage/unit/research.h | 162 --- libopenage/unit/selection.cpp | 306 ---- libopenage/unit/selection.h | 115 -- libopenage/unit/type_pair.cpp | 16 - libopenage/unit/type_pair.h | 37 - libopenage/unit/unit.cpp | 260 ---- libopenage/unit/unit.h | 304 ---- libopenage/unit/unit_container.cpp | 172 --- libopenage/unit/unit_container.h | 145 -- libopenage/unit/unit_type.cpp | 154 -- libopenage/unit/unit_type.h | 206 --- 85 files changed, 34 insertions(+), 12409 deletions(-) delete mode 100644 libopenage/gamestate/old/CMakeLists.txt delete mode 100644 libopenage/gamestate/old/civilisation.cpp delete mode 100644 libopenage/gamestate/old/civilisation.h delete mode 100644 libopenage/gamestate/old/cost.cpp delete mode 100644 libopenage/gamestate/old/cost.h delete mode 100644 libopenage/gamestate/old/game_main.cpp delete mode 100644 libopenage/gamestate/old/game_main.h delete mode 100644 libopenage/gamestate/old/game_save.cpp delete mode 100644 libopenage/gamestate/old/game_save.h delete mode 100644 libopenage/gamestate/old/game_spec.cpp delete mode 100644 libopenage/gamestate/old/game_spec.h delete mode 100644 libopenage/gamestate/old/generator.cpp delete mode 100644 libopenage/gamestate/old/generator.h delete mode 100644 libopenage/gamestate/old/market.cpp delete mode 100644 libopenage/gamestate/old/market.h delete mode 100644 libopenage/gamestate/old/player.cpp delete mode 100644 libopenage/gamestate/old/player.h delete mode 100644 libopenage/gamestate/old/population_tracker.cpp delete mode 100644 libopenage/gamestate/old/population_tracker.h delete mode 100644 libopenage/gamestate/old/resource.cpp delete mode 100644 libopenage/gamestate/old/resource.h delete mode 100644 libopenage/gamestate/old/score.cpp delete mode 100644 libopenage/gamestate/old/score.h delete mode 100644 libopenage/gamestate/old/team.cpp delete mode 100644 libopenage/gamestate/old/team.h delete mode 100644 libopenage/gamestate/old/types.cpp delete mode 100644 libopenage/gamestate/old/types.h delete mode 100644 libopenage/gui/category_contents_list_model.cpp delete mode 100644 libopenage/gui/category_contents_list_model.h delete mode 100644 libopenage/gui/game_creator.cpp delete mode 100644 libopenage/gui/game_creator.h delete mode 100644 libopenage/gui/game_saver.cpp delete mode 100644 libopenage/gui/game_saver.h delete mode 100644 libopenage/gui/game_spec_link.cpp delete mode 100644 libopenage/gui/game_spec_link.h delete mode 100644 libopenage/gui/generator_link.cpp delete mode 100644 libopenage/gui/generator_link.h delete mode 100644 libopenage/gui/integration/private/gui_game_spec_image_provider_impl.cpp delete mode 100644 libopenage/gui/integration/private/gui_game_spec_image_provider_impl.h delete mode 100644 libopenage/gui/integration/private/gui_texture_factory.cpp delete mode 100644 libopenage/gui/integration/private/gui_texture_factory.h delete mode 100644 libopenage/gui/resources_list_model.cpp delete mode 100644 libopenage/gui/resources_list_model.h delete mode 100644 libopenage/terrain/terrain_object.cpp delete mode 100644 libopenage/terrain/terrain_object.h delete mode 100644 libopenage/terrain/terrain_search.cpp delete mode 100644 libopenage/terrain/terrain_search.h delete mode 100644 libopenage/unit/CMakeLists.txt delete mode 100644 libopenage/unit/ability.cpp delete mode 100644 libopenage/unit/ability.h delete mode 100644 libopenage/unit/action.cpp delete mode 100644 libopenage/unit/action.h delete mode 100644 libopenage/unit/attribute.cpp delete mode 100644 libopenage/unit/attribute.h delete mode 100644 libopenage/unit/attributes.cpp delete mode 100644 libopenage/unit/attributes.h delete mode 100644 libopenage/unit/command.cpp delete mode 100644 libopenage/unit/command.h delete mode 100644 libopenage/unit/producer.cpp delete mode 100644 libopenage/unit/producer.h delete mode 100644 libopenage/unit/research.cpp delete mode 100644 libopenage/unit/research.h delete mode 100644 libopenage/unit/selection.cpp delete mode 100644 libopenage/unit/selection.h delete mode 100644 libopenage/unit/type_pair.cpp delete mode 100644 libopenage/unit/type_pair.h delete mode 100644 libopenage/unit/unit.cpp delete mode 100644 libopenage/unit/unit.h delete mode 100644 libopenage/unit/unit_container.cpp delete mode 100644 libopenage/unit/unit_container.h delete mode 100644 libopenage/unit/unit_type.cpp delete mode 100644 libopenage/unit/unit_type.h diff --git a/libopenage/CMakeLists.txt b/libopenage/CMakeLists.txt index 9dc9db84d9..bda4bc1611 100644 --- a/libopenage/CMakeLists.txt +++ b/libopenage/CMakeLists.txt @@ -359,6 +359,5 @@ add_subdirectory("rng") add_subdirectory("terrain") add_subdirectory("testing") add_subdirectory("time") -add_subdirectory("unit") add_subdirectory("util") add_subdirectory("versions") diff --git a/libopenage/gamestate/CMakeLists.txt b/libopenage/gamestate/CMakeLists.txt index c89d9cd52d..dddf843136 100644 --- a/libopenage/gamestate/CMakeLists.txt +++ b/libopenage/gamestate/CMakeLists.txt @@ -20,6 +20,3 @@ add_subdirectory(component/) add_subdirectory(demo/) add_subdirectory(event/) add_subdirectory(system/) - -# TODO: remove once migration is done. -add_subdirectory(old/) diff --git a/libopenage/gamestate/old/CMakeLists.txt b/libopenage/gamestate/old/CMakeLists.txt deleted file mode 100644 index d141c14607..0000000000 --- a/libopenage/gamestate/old/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -add_sources(libopenage - civilisation.cpp - cost.cpp - game_main.cpp - game_save.cpp - game_spec.cpp - generator.cpp - market.cpp - player.cpp - population_tracker.cpp - resource.cpp - score.cpp - team.cpp - types.cpp -) diff --git a/libopenage/gamestate/old/civilisation.cpp b/libopenage/gamestate/old/civilisation.cpp deleted file mode 100644 index 6139ed23fe..0000000000 --- a/libopenage/gamestate/old/civilisation.cpp +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2015-2018 the openage authors. See copying.md for legal info. - -#include "civilisation.h" - -#include "../../log/log.h" -#include "../../unit/unit_type.h" - - -namespace openage { - -Civilisation::Civilisation(const GameSpec &spec, int id) - : - civ_id{id}, - civ_name{spec.get_civ_name(id)} { - this->initialise_unit_types(spec); -} - - -std::vector> Civilisation::object_meta() const { - return civ_objects; -} - - -std::vector Civilisation::get_category(const std::string &c) const { - auto cat = this->categories.find(c); - if (cat == this->categories.end()) { - return std::vector(); - } - return cat->second; -} - - -std::vector Civilisation::get_type_categories() const { - return this->all_categories; -} - - -const gamedata::building_unit *Civilisation::get_building_data(index_t unit_id) const { - if (this->buildings.count(unit_id) == 0) { - log::log(MSG(info) << " -> ignoring unit_id: " << unit_id); - return nullptr; - } - return this->buildings.at(unit_id); -} - - -void Civilisation::initialise_unit_types(const GameSpec &spec) { - log::log(MSG(dbg) << "Init units of civilisation " << civ_name); - spec.create_unit_types(this->civ_objects, this->civ_id); - for (auto &type : this->civ_objects) { - this->add_to_category(type->name(), type->id()); - } -} - - -void Civilisation::add_to_category(const std::string &c, index_t type) { - if (this->categories.count(c) == 0) { - this->all_categories.push_back(c); - this->categories[c] = std::vector(); - } - this->categories[c].push_back(type); -} - - -} diff --git a/libopenage/gamestate/old/civilisation.h b/libopenage/gamestate/old/civilisation.h deleted file mode 100644 index 97045031ea..0000000000 --- a/libopenage/gamestate/old/civilisation.h +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include - -#include "game_spec.h" - -namespace openage { - -/** - * contains the initial tech structure for - * one civilisation - */ -class Civilisation { -public: - Civilisation(const GameSpec &spec, int id); - - /** - * civ index - */ - const int civ_id; - - /** - * civ name - */ - const std::string civ_name; - - /** - * return all the objects available to this civ - */ - std::vector> object_meta() const; - - /** - * return all types in a particular named category - */ - std::vector get_category(const std::string &c) const; - - /** - * return all used categories, such as living, building or projectile - */ - std::vector get_type_categories() const; - - /** - * gamedata for a building - */ - const gamedata::building_unit *get_building_data(index_t unit_id) const; - - /** - * initialise the unit meta data - */ - void initialise_unit_types(const GameSpec &spec); - -private: - - /** - * creates and adds items to categories - */ - void add_to_category(const std::string &c, index_t type); - - /** - * unit types which can be produced by this civilisation. - */ - std::vector> civ_objects; - - /** - * all available categories of units - */ - std::vector all_categories; - - /** - * category lists - */ - std::unordered_map> categories; - - /** - * used for annex creation - */ - std::unordered_map buildings; - -}; - -} diff --git a/libopenage/gamestate/old/cost.cpp b/libopenage/gamestate/old/cost.cpp deleted file mode 100644 index 089cb6da4b..0000000000 --- a/libopenage/gamestate/old/cost.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2017-2021 the openage authors. See copying.md for legal info. - -#include "cost.h" -#include "player.h" - - -namespace openage { - -ResourceCost::ResourceCost() - : - type{cost_type::constant}, - resources{} {} - -ResourceCost::ResourceCost(const ResourceBundle& resources) - : - type{cost_type::constant}, - resources{} { - this->resources.set(resources); -} - -ResourceCost::ResourceCost(cost_type type, const ResourceBundle& multiplier) - : - type{type}, - resources{} { - this->resources.set(multiplier); -} - -ResourceCost::~ResourceCost() = default; - -void ResourceCost::set(cost_type type, const ResourceBundle& resources) { - this->type = type; - this->resources.set(resources); -} - -const ResourceBundle ResourceCost::get(const Player& player) const { - if (type == cost_type::constant) { - return resources; - } - - // calculate dynamic cost - ResourceBundle resources = this->resources.clone(); - if (type == cost_type::workforce) { - resources *= player.get_workforce_count(); - } - return resources; -} - -} // openage diff --git a/libopenage/gamestate/old/cost.h b/libopenage/gamestate/old/cost.h deleted file mode 100644 index 749aa50986..0000000000 --- a/libopenage/gamestate/old/cost.h +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2017-2019 the openage authors. See copying.md for legal info. - -#pragma once - -#include "resource.h" - - -namespace openage { - -class Player; - -/** - * Types of dynamic cost calculation (and one constant). - * - * Used in ResourceCost - * TODO use in TimeCost - */ -enum class cost_type : int { - /** Constant resources. */ - constant, - /** Dynamic cost based on the workforce. */ - workforce -}; - -/** - * A container for a constant or dynamic ResourceBundle representing the cost. - */ -class ResourceCost { -public: - - /** - * Constant zero cost - */ - ResourceCost(); - - /** - * Constant cost - */ - ResourceCost(const ResourceBundle& resources); - - /** - * Dynamic cost - */ - ResourceCost(cost_type type, const ResourceBundle& multiplier); - - virtual ~ResourceCost(); - - void set(cost_type type, const ResourceBundle& multiplier); - - /** - * Returns the cost. - */ - const ResourceBundle get(const Player& player) const; - -private: - - cost_type type; - - ResourceBundle resources; - -}; - -// TODO implement TimeCost - -} // namespace openage diff --git a/libopenage/gamestate/old/game_main.cpp b/libopenage/gamestate/old/game_main.cpp deleted file mode 100644 index 5d928e548a..0000000000 --- a/libopenage/gamestate/old/game_main.cpp +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#include "game_main.h" - -#include "../../log/log.h" -#include "../../terrain/terrain.h" -#include "../../unit/unit_type.h" -#include "game_spec.h" -#include "generator.h" - - -namespace openage { - -GameMain::GameMain(const Generator &generator) : - OptionNode{"GameMain"}, - terrain{generator.terrain()}, - placed_units{}, - spec{generator.get_spec()} { - // players - this->players.reserve(generator.player_names().size()); - unsigned int i = 0; - for (auto &name : generator.player_names()) { - this->players.push_back(std::make_shared(this->add_civ(i), i, name)); - i++; - } - - // initialise types only after all players are added - for (auto &p : this->players) { - p->initialise_unit_types(); - } - - // initialise units - this->placed_units.set_terrain(this->terrain); - generator.add_units(*this); -} - -GameMain::~GameMain() = default; - -unsigned int GameMain::player_count() const { - return this->players.size(); -} - -Player *GameMain::get_player(unsigned int player_id) { - return this->players.at(player_id).get(); -} - -unsigned int GameMain::team_count() const { - return this->teams.size(); -} - -Team *GameMain::get_team(unsigned int team_id) { - return &this->teams.at(team_id); -} - -GameSpec *GameMain::get_spec() { - return this->spec.get(); -} - -void GameMain::update(time_nsec_t lastframe_duration) { - this->placed_units.update_all(lastframe_duration); -} - -Civilisation *GameMain::add_civ(int civ_id) { - auto new_civ = std::make_shared(*this->spec, civ_id); - this->civs.emplace_back(new_civ); - return new_civ.get(); -} - -GameMainHandle::GameMainHandle(qtsdl::GuiItemLink *gui_link) : - game{}, - gui_link{gui_link} { -} - -GameMain *GameMainHandle::get_game() const { - return this->game; -} - -bool GameMainHandle::is_game_running() const { - return this->game != nullptr; -} - -void GameMainHandle::announce_running() { - emit this->gui_signals.game_running(this->game); -} - -} // namespace openage diff --git a/libopenage/gamestate/old/game_main.h b/libopenage/gamestate/old/game_main.h deleted file mode 100644 index af3651aadc..0000000000 --- a/libopenage/gamestate/old/game_main.h +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include - -#include - -#include "../../options.h" -#include "../../terrain/terrain.h" -#include "../../unit/unit_container.h" -#include "../../util/timing.h" -#include "market.h" -#include "player.h" -#include "team.h" - -namespace openage { - -class Generator; -class Terrain; - - -/** - * Contains information for a single game - * This information must be synced across network clients - * - * TODO: include a list of actions to be saved - * as the game replay file - */ -class GameMain : public options::OptionNode { -public: - GameMain(const Generator &generator); - ~GameMain(); - - /** - * the number of players - */ - unsigned int player_count() const; - - /** - * player by index - */ - Player *get_player(unsigned int player_id); - - /** - * the number of teams - */ - unsigned int team_count() const; - - /** - * team by id - */ - Team *get_team(unsigned int team_id); - - /** - * the spec in this games settings - */ - GameSpec *get_spec(); - - /** - * updates the game by one frame - */ - void update(time_nsec_t lastframe_duration); - - /** - * map information - */ - std::shared_ptr terrain; - - /** - * all teams in the game - */ - std::vector teams; - - /** - * The global market (the global market prices). - */ - Market market; - - /** - * all the objects that have been placed. - */ - UnitContainer placed_units; - -private: - /** - * all players in the game - * no objects should be added of removed once populated - */ - std::vector> players; - - /** - * creates a random civ, owned and managed by this game - */ - Civilisation *add_civ(int civ_id); - - /** - * civs used in this game - */ - std::vector> civs; - - std::shared_ptr spec; -}; - -} // namespace openage - -namespace qtsdl { -class GuiItemLink; -} // namespace qtsdl - -namespace openage { - -class GameMainSignals : public QObject { - Q_OBJECT - -public: -signals: - void game_running(bool running); -}; - - -/** - * Class linked to the QML object "GameMain" via GameMainLink. - * Gets instanciated from QML. - */ -class GameMainHandle { -public: - explicit GameMainHandle(qtsdl::GuiItemLink *gui_link); - - /** - * End the game and delete the game handle. - */ - void clear(); - - /** - * Pass the given game to the engine and start it. - */ - void set_game(std::unique_ptr &&game); - - /** - * Return the game. - */ - GameMain *get_game() const; - - /** - * Test if there is a game running. - */ - bool is_game_running() const; - - /** - * Emit a qt signal to notify for changes in a running game. - */ - void announce_running(); - -private: - /** - * The game state as currently owned by the engine, - * just remembered here to access it quickly. - */ - GameMain *game; - -public: - GameMainSignals gui_signals; - qtsdl::GuiItemLink *gui_link; -}; - -} // namespace openage diff --git a/libopenage/gamestate/old/game_save.cpp b/libopenage/gamestate/old/game_save.cpp deleted file mode 100644 index 47d598530b..0000000000 --- a/libopenage/gamestate/old/game_save.cpp +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. - -#include "game_save.h" - -#include -#include - -#include "version.h" -#include "../../log/log.h" -#include "../../terrain/terrain_chunk.h" -#include "../../unit/producer.h" -#include "../../unit/unit.h" -#include "../../unit/unit_type.h" -#include "../../versions/compiletime.h" -#include "game_main.h" -#include "game_save.h" -#include "game_spec.h" - -namespace openage::gameio { - -void save_unit(std::ofstream &file, Unit *unit) { - file << unit->unit_type->id() << std::endl; - file << unit->get_attribute().player.player_number << std::endl; - coord::tile pos = unit->location->pos.start; - file << pos.ne << " " << pos.se << std::endl; - - bool has_building_attr = unit->has_attribute(attr_type::building); - file << has_building_attr << std::endl; - if (has_building_attr) { - file << unit->get_attribute().completed << std::endl; - } -} - -void load_unit(std::ifstream &file, GameMain *game) { - int pr_id; - int player_no; - coord::tile_t ne, se; - file >> pr_id; - file >> player_no; - file >> ne; - file >> se; - - UnitType &saved_type = *game->get_player(player_no)->get_type(pr_id); - auto ref = game->placed_units.new_unit(saved_type, *game->get_player(player_no), coord::tile{ne, se}.to_phys3(*game->terrain)); - - bool has_building_attr; - file >> has_building_attr; - if (has_building_attr) { - float completed; - file >> completed; - if (completed >= 1.0f && ref.is_valid()) { - complete_building(*ref.get()); - } - } -} - -void save_tile_content(std::ofstream &file, openage::TileContent *content) { - file << content->terrain_id << std::endl; - file << content->obj.size() << std::endl; -} - -TileContent load_tile_content(std::ifstream &file) { - openage::TileContent content; - file >> content.terrain_id; - - unsigned int o_size; - file >> o_size; - return content; -} - -void save(openage::GameMain *game, const std::string &fname) { - std::ofstream file(fname, std::ofstream::out); - log::log(MSG(dbg) << "saving " + fname); - - // metadata - file << save_label << std::endl; - file << save_version << std::endl; - file << versions::engine_version << std::endl; - - // how many chunks - std::vector used = game->terrain->used_chunks(); - file << used.size() << std::endl; - - // save each chunk - for (coord::chunk &position : used) { - file << position.ne << " " << position.se << std::endl; - openage::TerrainChunk *chunk = game->terrain->get_chunk(position); - - file << chunk->tile_count << std::endl; - for (size_t p = 0; p < chunk->tile_count; ++p) { - save_tile_content( file, chunk->get_data(p) ); - } - } - - // save units - std::vector units = game->placed_units.all_units(); - file << units.size() << std::endl; - for (Unit *u : units) { - save_unit(file, u); - } -} - -void load(openage::GameMain *game, const std::string &fname) { - std::ifstream file(fname, std::ifstream::in); - if (!file.good()) { - log::log(MSG(dbg) << "could not find " + fname); - return; - } - log::log(MSG(dbg) << "loading " + fname); - - // load metadata - std::string file_label; - file >> file_label; - if (file_label != save_label) { - log::log(MSG(warn) << fname << " is not a savefile"); - return; - } - std::string version; - file >> version; - if (version != save_version) { - log::log(MSG(warn) << "savefile has different version"); - } - std::string build; - file >> build; - - // read terrain chunks - unsigned int num_chunks; - file >> num_chunks; - for (unsigned int c = 0; c < num_chunks; ++c) { - coord::chunk_t ne, se; - size_t tile_count; - file >> ne; - file >> se; - file >> tile_count; - openage::TerrainChunk *chunk = game->terrain->get_create_chunk(coord::chunk{ne, se}); - for (size_t p = 0; p < tile_count; ++p) { - *chunk->get_data(p) = load_tile_content( file ); - } - } - - game->placed_units.reset(); - unsigned int num_units; - file >> num_units; - for (unsigned int u = 0; u < num_units; ++u) { - load_unit( file, game ); - } -} - -} // openage::gameio diff --git a/libopenage/gamestate/old/game_save.h b/libopenage/gamestate/old/game_save.h deleted file mode 100644 index 36ddb49b82..0000000000 --- a/libopenage/gamestate/old/game_save.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -namespace openage { - -class GameMain; -class Terrain; - -namespace gameio { - -const std::string save_label = "openage-save-file"; -const std::string save_version = "v0.1"; - -/** - * a game save function that sometimes works - */ -void save(openage::GameMain *, const std::string &fname); - -/** - * a game load function that sometimes works - */ -void load(openage::GameMain *, const std::string &fname); - -}} // openage::gameio diff --git a/libopenage/gamestate/old/game_spec.cpp b/libopenage/gamestate/old/game_spec.cpp deleted file mode 100644 index c23ddcd0bd..0000000000 --- a/libopenage/gamestate/old/game_spec.cpp +++ /dev/null @@ -1,455 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "game_spec.h" - -#include - -#include "../../audio/error.h" -#include "../../audio/resource_def.h" -#include "../../gamedata/blending_mode_dummy.h" -#include "../../gamedata/string_resource_dummy.h" -#include "../../gamedata/terrain_dummy.h" -#include "../../log/log.h" -#include "../../rng/global_rng.h" -#include "../../unit/producer.h" -#include "../../util/compiler.h" -#include "../../util/strings.h" -#include "../../util/timer.h" -#include "civilisation.h" - - -namespace openage { - -GameSpec::GameSpec() : - gamedata_loaded{false} { -} - -GameSpec::~GameSpec() = default; - - -bool GameSpec::initialize() { - util::Timer load_timer; - load_timer.start(); - - // const util::Path &asset_dir = this->assetmanager->get_asset_dir(); - - // log::log(MSG(info) << "Loading game specification files..."); - - // std::vector string_resources = util::read_csv_file( - // asset_dir["converted/string_resources.docx"]); - - // try { - // // read the packed csv file - // util::CSVCollection raw_gamedata{ - // asset_dir["converted/gamedata/gamedata.docx"]}; - - // // parse the original game description files - // this->gamedata = raw_gamedata.read( - // "gamedata-empiresdat.docx"); - - // this->load_terrain(this->gamedata[0]); - - // // process and load the game description files - // this->on_gamedata_loaded(this->gamedata[0]); - // this->gamedata_loaded = true; - // } - // catch (Error &exc) { - // // rethrow allmighty openage exceptions - // throw; - // } - // catch (std::exception &exc) { - // // unfortunately we have no idea of the std::exception backtrace - // throw Error{ERR << "gamedata could not be loaded: " - // << util::typestring(exc) - // << ": " << exc.what()}; - // } - - log::log(MSG(info).fmt("Loading time [data]: %5.3f s", - load_timer.getval() / 1e9)); - return true; -} - -bool GameSpec::load_complete() const { - return this->gamedata_loaded; -} - -terrain_meta *GameSpec::get_terrain_meta() { - return &this->terrain_data; -} - -index_t GameSpec::get_slp_graphic(index_t slp) { - return this->slp_to_graphic[slp]; -} - -const Sound *GameSpec::get_sound(index_t sound_id) const { - if (this->available_sounds.count(sound_id) == 0) { - if (sound_id > 0) { - log::log(MSG(dbg) << " -> ignoring sound_id: " << sound_id); - } - return nullptr; - } - return &this->available_sounds.at(sound_id); -} - - -const gamedata::graphic *GameSpec::get_graphic_data(index_t grp_id) const { - if (this->graphics.count(grp_id) == 0) { - log::log(MSG(dbg) << " -> ignoring grp_id: " << grp_id); - return nullptr; - } - return this->graphics.at(grp_id); -} - -std::vector GameSpec::get_command_data(index_t unit_id) const { - if (this->commands.count(unit_id) == 0) { - return std::vector(); // empty vector - } - return this->commands.at(unit_id); -} - -std::string GameSpec::get_civ_name(int civ_id) const { - return this->gamedata[0].civs.data[civ_id].name; -} - -void GameSpec::create_unit_types(unit_meta_list &objects, int civ_id) const { - if (!this->load_complete()) { - return; - } - - // create projectile types first - for (auto &obj : this->gamedata[0].civs.data[civ_id].units.missile.data) { - this->load_missile(obj, objects); - } - - // create object unit types - for (auto &obj : this->gamedata[0].civs.data[civ_id].units.object.data) { - this->load_object(obj, objects); - } - - // create dead unit types - for (auto &unit : this->gamedata[0].civs.data[civ_id].units.moving.data) { - this->load_object(unit, objects); - } - - // create living unit types - for (auto &unit : this->gamedata[0].civs.data[civ_id].units.living.data) { - this->load_living(unit, objects); - } - - // create building unit types - for (auto &building : this->gamedata[0].civs.data[civ_id].units.building.data) { - this->load_building(building, objects); - } -} - -void GameSpec::on_gamedata_loaded(const gamedata::empiresdat &gamedata) { - // const util::Path &asset_dir = this->assetmanager->get_asset_dir(); - // util::Path sound_dir = asset_dir["converted/sounds"]; - - // // create graphic id => graphic map - // for (auto &graphic : gamedata.graphics.data) { - // this->graphics[graphic.graphic_id] = &graphic; - // this->slp_to_graphic[graphic.slp_id] = graphic.graphic_id; - // } - - // log::log(INFO << "Loading textures..."); - - // // create complete set of unit textures - // for (auto &g : this->graphics) { - // this->unit_textures.insert({g.first, std::make_shared(*this, g.second)}); - // } - - // log::log(INFO << "Loading sounds..."); - - // // playable sound files for the audio manager - // std::vector load_sound_files; - - // // all sounds defined in the game specification - // for (const gamedata::sound &sound : gamedata.sounds.data) { - // std::vector sound_items; - - // // each sound may have multiple variation, - // // processed in this loop - // // these are the single sound files. - // for (const gamedata::sound_item &item : sound.sound_items.data) { - // if (item.resource_id < 0) { - // log::log(SPAM << " Invalid sound resource id < 0"); - // continue; - // } - - // std::string snd_filename = util::sformat("%d.opus", item.resource_id); - // util::Path snd_path = sound_dir[snd_filename]; - - // if (not snd_path.is_file()) { - // continue; - // } - - // // single items for a sound (so that we can ramdomize it) - // sound_items.push_back(item.resource_id); - - // // the single sound will be loaded in the audio system. - // audio::resource_def resource{ - // audio::category_t::GAME, - // item.resource_id, - // snd_path, - // audio::format_t::OPUS, - // audio::loader_policy_t::DYNAMIC}; - // load_sound_files.push_back(resource); - // } - - - // // create test sound objects that can be played later - // this->available_sounds.insert({sound.sound_id, - // Sound{ - // this, - // std::move(sound_items)}}); - // } - - // // TODO: move out the loading of the sound. - // // this class only provides the names and locations - - // // load the requested sounds. - // audio::AudioManager &am = this->assetmanager->get_display()->get_audio_manager(); - // am.load_resources(load_sound_files); - - // // this final step occurs after loading media - // // as producers require both the graphics and sounds - // this->create_abilities(gamedata); -} - -bool GameSpec::valid_graphic_id(index_t graphic_id) const { - if (graphic_id <= 0 || this->graphics.count(graphic_id) == 0) { - return false; - } - if (this->graphics.at(graphic_id)->slp_id <= 0) { - return false; - } - return true; -} - -void GameSpec::load_building(const gamedata::building_unit &building, unit_meta_list &list) const { - // check graphics - if (this->valid_graphic_id(building.idle_graphic0)) { - auto meta_type = std::make_shared("Building", building.id0, [this, &building](const Player &owner) { - return std::make_shared(owner, *this, &building); - }); - list.emplace_back(meta_type); - } -} - -void GameSpec::load_living(const gamedata::living_unit &unit, unit_meta_list &list) const { - // check graphics - if (this->valid_graphic_id(unit.dying_graphic) && this->valid_graphic_id(unit.idle_graphic0) && this->valid_graphic_id(unit.move_graphics)) { - auto meta_type = std::make_shared("Living", unit.id0, [this, &unit](const Player &owner) { - return std::make_shared(owner, *this, &unit); - }); - list.emplace_back(meta_type); - } -} - -void GameSpec::load_object(const gamedata::unit_object &object, unit_meta_list &list) const { - // check graphics - if (this->valid_graphic_id(object.idle_graphic0)) { - auto meta_type = std::make_shared("Object", object.id0, [this, &object](const Player &owner) { - return std::make_shared(owner, *this, &object); - }); - list.emplace_back(meta_type); - } -} - -void GameSpec::load_missile(const gamedata::missile_unit &proj, unit_meta_list &list) const { - // check graphics - if (this->valid_graphic_id(proj.idle_graphic0)) { - auto meta_type = std::make_shared("Projectile", proj.id0, [this, &proj](const Player &owner) { - return std::make_shared(owner, *this, &proj); - }); - list.emplace_back(meta_type); - } -} - - -void GameSpec::load_terrain(const gamedata::empiresdat &gamedata) { - // fetch blending modes - // util::Path convert_dir = this->assetmanager->get_asset_dir()["converted"]; - // std::vector blending_meta = util::read_csv_file( - // convert_dir["blending_modes.docx"]); - - // // copy the terrain metainformation - // std::vector terrain_meta = gamedata.terrains.data; - - // // remove any disabled textures - // terrain_meta.erase( - // std::remove_if( - // terrain_meta.begin(), - // terrain_meta.end(), - // [](const gamedata::terrain_type &t) { - // return not t.enabled; - // }), - // terrain_meta.end()); - - // // result attributes - // this->terrain_data.terrain_id_count = terrain_meta.size(); - // this->terrain_data.blendmode_count = blending_meta.size(); - // this->terrain_data.textures.resize(terrain_data.terrain_id_count); - // this->terrain_data.blending_masks.reserve(terrain_data.blendmode_count); - // this->terrain_data.terrain_id_priority_map = std::make_unique( - // this->terrain_data.terrain_id_count); - // this->terrain_data.terrain_id_blendmode_map = std::make_unique( - // this->terrain_data.terrain_id_count); - // this->terrain_data.influences_buf = std::make_unique( - // this->terrain_data.terrain_id_count); - - - // log::log(MSG(dbg) << "Terrain prefs: " - // << "tiletypes=" << terrain_data.terrain_id_count << ", " - // "blendmodes=" - // << terrain_data.blendmode_count); - - // // create tile textures (snow, ice, grass, whatever) - // for (size_t terrain_id = 0; - // terrain_id < terrain_data.terrain_id_count; - // terrain_id++) { - // auto line = &terrain_meta[terrain_id]; - - // // TODO: terrain double-define check? - // terrain_data.terrain_id_priority_map[terrain_id] = line->blend_priority; - // terrain_data.terrain_id_blendmode_map[terrain_id] = line->blend_mode; - - // // TODO: remove hardcoding and rely on nyan data - // auto terraintex_filename = util::sformat("converted/terrain/%d.slp.png", - // line->slp_id); - - // auto new_texture = this->assetmanager->get_texture(terraintex_filename, true); - - // terrain_data.textures[terrain_id] = new_texture; - // } - - // // create blending masks (see doc/media/blendomatic) - // for (size_t i = 0; i < terrain_data.blendmode_count; i++) { - // auto line = &blending_meta[i]; - - // // TODO: remove hardcodingn and use nyan data - // std::string mask_filename = util::sformat("converted/blendomatic/mode%02d.png", - // line->blend_mode); - // terrain_data.blending_masks[i] = this->assetmanager->get_texture(mask_filename); - // } -} - - -void GameSpec::create_abilities(const gamedata::empiresdat &gamedata) { - // use game data unit commands - int headers = gamedata.unit_headers.data.size(); - int total = 0; - - // it seems the index of the header indicates the unit - for (int i = 0; i < headers; ++i) { - // init unit command vector - std::vector list; - - // add each element - auto &head = gamedata.unit_headers.data[i]; - for (auto &cmd : head.unit_commands.data) { - total++; - - // commands either have a class id or a unit id - // log::dbg("unit command %d %d -> class %d, unit %d, resource %d", i, cmd.id, cmd.class_id, cmd.unit_id, cmd.resource_in); - list.push_back(&cmd); - } - - // insert to command map - this->commands[i] = list; - } -} - - -void Sound::play() const { - if (this->sound_items.size() <= 0) { - return; - } - - int rand = rng::random_range(0, this->sound_items.size()); - int sndid = this->sound_items.at(rand); - - // try { - // // TODO: buhuuuu gnargghh this has to be moved to the asset loading subsystem hnnnng - // audio::AudioManager &am = this->game_spec->get_asset_manager()->get_display()->get_audio_manager(); - - // if (not am.is_available()) { - // return; - // } - - // audio::Sound sound = am.get_sound(audio::category_t::GAME, sndid); - // sound.play(); - // } - // catch (audio::Error &e) { - // log::log(MSG(warn) << "cannot play: " << e); - // } -} - -GameSpecHandle::GameSpecHandle(qtsdl::GuiItemLink *gui_link) : - active{}, - gui_signals{std::make_shared()}, - gui_link{gui_link} { -} - -void GameSpecHandle::set_active(bool active) { - this->active = active; - - this->start_loading_if_needed(); -} - -bool GameSpecHandle::is_ready() const { - return this->spec && this->spec->load_complete(); -} - -void GameSpecHandle::invalidate() { - this->spec = nullptr; - - this->start_loading_if_needed(); -} - -void GameSpecHandle::announce_spec() { - if (this->spec && this->spec->load_complete()) - emit this->gui_signals->game_spec_loaded(this->spec); -} - -std::shared_ptr GameSpecHandle::get_spec() { - return this->spec; -} - -void GameSpecHandle::start_loading_if_needed() { -} - -void GameSpecHandle::start_load_job() { - // store the shared pointers in another sharedptr - // so we can pass them to the other thread - auto spec_and_job = std::make_tuple(this->spec, this->gui_signals, job::Job{}); - auto spec_and_job_ptr = std::make_shared(spec_and_job); - - // lambda to be executed to actually load the data files. - auto perform_load = [spec_and_job_ptr] { - return std::get>(*spec_and_job_ptr)->initialize(); - }; - - auto load_finished = [gui_signals_ptr = this->gui_signals.get()](job::result_function_t result) { - bool load_ok; - try { - load_ok = result(); - } - catch (Error &) { - // TODO: display that error in the ui. - throw; - } - catch (std::exception &) { - // TODO: same here. - throw Error{ERR << "gamespec loading failed!"}; - } - - if (load_ok) { - // send the signal that the load job was finished - emit gui_signals_ptr->load_job_finished(); - } - }; -} - -} // namespace openage diff --git a/libopenage/gamestate/old/game_spec.h b/libopenage/gamestate/old/game_spec.h deleted file mode 100644 index 0e03eb73a5..0000000000 --- a/libopenage/gamestate/old/game_spec.h +++ /dev/null @@ -1,290 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include "../../gamedata/gamedata_dummy.h" -#include "../../gamedata/graphic_dummy.h" -#include "../../job/job.h" -#include "../../util/csv.h" -#include "terrain/terrain.h" -#include "types.h" - -#include -#include -#include - - -namespace openage { - -class LegacyAssetManager; -class GameSpec; -class UnitType; -class UnitTypeMeta; -class Player; - - -/** - * could use unique ptr - */ -using unit_type_list = std::vector>; -using unit_meta_list = std::vector>; - - -/** - * simple sound object - * TODO: move to assetmanager - */ -class Sound { -public: - Sound(GameSpec *spec, std::vector &&sound_items) : - sound_items{sound_items}, - game_spec{spec} {} - - void play() const; - - std::vector sound_items; - - GameSpec *game_spec; -}; - - -/** - * GameSpec gives a collection of all game elements - * this currently includes unit types and terrain types - * This provides a system which can easily allow game modding - * - * uses the LegacyAssetManager to gather - * graphic data, composite textures and sounds. - * - * all types are sorted and stored by id values, - * each data object is referenced by a type and id pair - * - * dealing directly with files done by asset manager - * TODO: should the audio loading should be moved there? - */ -class GameSpec { -public: - GameSpec(); - virtual ~GameSpec(); - - /** - * perform the main loading job. - * this loads all the data into the storage. - */ - bool initialize(); - - /** - * Check if loading has been completed, - * a load percent would be nice - */ - bool load_complete() const; - - /** - * return data used for constructing terrain objects - */ - terrain_meta *get_terrain_meta(); - - /** - * reverse lookup of slp - */ - index_t get_slp_graphic(index_t slp); - - /** - * get sound by sound id - */ - const Sound *get_sound(index_t sound_id) const; - - /** - * gamedata for a graphic - * nyan will have to replace this somehow - */ - const gamedata::graphic *get_graphic_data(index_t grp_id) const; - - /** - * get available commands for a unit id - * nyan will have to replace this somehow - */ - std::vector get_command_data(index_t unit_id) const; - - /** - * returns the name of a civ by index - */ - std::string get_civ_name(int civ_id) const; - - /** - * makes initial unit types for a particular civ id - */ - void create_unit_types(unit_meta_list &objects, int civ_id) const; - -private: - /** - * check graphic id is valid - */ - bool valid_graphic_id(index_t) const; - - /** - * create unit abilities from game data - */ - void create_abilities(const gamedata::empiresdat &gamedata); - - /** - * loads required assets to construct a buildings. - * adds to the type list if the object can be created safely. - */ - void load_building(const gamedata::building_unit &, unit_meta_list &) const; - - /** - * loads assets for living things. - */ - void load_living(const gamedata::living_unit &, unit_meta_list &) const; - - /** - * load assets for other game objects (not building and living). - */ - void load_object(const gamedata::unit_object &, unit_meta_list &) const; - - /** - * load missile assets. - */ - void load_missile(const gamedata::missile_unit &, unit_meta_list &) const; - - /** - * fill in the terrain_data attribute of this - */ - void load_terrain(const gamedata::empiresdat &gamedata); - - /** - * Invoked when the gamedata has been loaded. - */ - void on_gamedata_loaded(const gamedata::empiresdat &gamedata); - - /** - * The full original gamedata tree. - */ - std::vector gamedata; - - /** - * data used for constructing terrain objects - */ - terrain_meta terrain_data; - - /** - * slp to graphic id reverse lookup - */ - std::unordered_map slp_to_graphic; - - /** - * map graphic id to gamedata graphic. - */ - std::unordered_map graphics; - - /** - * commands available for each unit id - */ - std::unordered_map> commands; - - /** - * sound ids mapped to playable sounds for all available sounds. - */ - std::unordered_map available_sounds; - - /** - * has game data been load yet - */ - bool gamedata_loaded; -}; - -} // namespace openage - -namespace qtsdl { -class GuiItemLink; -} // namespace qtsdl - -namespace openage { - -class GameSpecSignals; - -/** - * Game specification instanciated in QML. - * Linked to the "GameSpec" QML type. - * - * Wraps the "GameSpec" C++ class from above. - */ -class GameSpecHandle { -public: - explicit GameSpecHandle(qtsdl::GuiItemLink *gui_link); - - /** - * Control whether this specification can be loaded (=true) - * or will not be loaded (=false). - */ - void set_active(bool active); - - /** - * Return if the specification was fully loaded. - */ - bool is_ready() const; - - /** - * forget everything about the specification and - * reload it with `start_loading_if_needed`. - */ - void invalidate(); - - /** - * signal about a loaded spec if any - */ - void announce_spec(); - - /** - * Return the contained game specification. - */ - std::shared_ptr get_spec(); - -private: - /** - * load the game specification if not already present. - */ - void start_loading_if_needed(); - - /** - * Actually dispatch the loading job to the job manager. - */ - void start_load_job(); - - /** - * called from the job manager when the loading job finished. - */ - void on_loaded(job::result_function_t result); - - /** - * The real game specification. - */ - std::shared_ptr spec; - - /** - * enables the loading of the game specification. - */ - bool active; - -public: - std::shared_ptr gui_signals; - qtsdl::GuiItemLink *gui_link; -}; - -class GameSpecSignals : public QObject { - Q_OBJECT - -public: -signals: - /* - * Some load job has finished. - * - * To be sure that the latest result is used, do the verification at the point of use. - */ - void load_job_finished(); - - void game_spec_loaded(std::shared_ptr loaded_game_spec); -}; - -} // namespace openage diff --git a/libopenage/gamestate/old/generator.cpp b/libopenage/gamestate/old/generator.cpp deleted file mode 100644 index b60ce1ffd7..0000000000 --- a/libopenage/gamestate/old/generator.cpp +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. - -#include "generator.h" - -#include "../../log/log.h" -#include "../../rng/rng.h" -#include "../../terrain/terrain_chunk.h" -#include "../../unit/unit.h" -#include "../../util/math_constants.h" -#include "game_main.h" -#include "game_save.h" -#include "game_spec.h" - - -namespace openage { - -coord::tile random_tile(rng::RNG &rng, tileset_t tiles) { - if (tiles.empty()) { - log::log(MSG(err) << "random tile failed"); - return coord::tile{0, 0}; - } - uint64_t index = rng.random() % tiles.size(); - auto it = std::begin(tiles); - std::advance(it, index); - return *it; -} - - -Region::Region(int size) - : - owner{0}, - object_id{0}, - terrain_id{0}, - center{0, 0} { - for (int ne = -size; ne < size; ++ne) { - for (int se = -size; se < size; ++se) { - this->tiles.emplace(coord::tile{ne, se}); - } - } -} - -Region::Region(coord::tile center, tileset_t tiles) - : - owner{0}, - object_id{0}, - terrain_id{0}, - center(center), - tiles{tiles} { -} - -tileset_t Region::get_tiles() const { - return this->tiles; -} - -coord::tile Region::get_center() const { - return this->center; -} - -coord::tile Region::get_tile(rng::RNG &rng) const { - return random_tile(rng, this->tiles); -} - - -tileset_t Region::subset(rng::RNG &rng, coord::tile start_point, unsigned int number, double p) const { - if (p == 0.0) { - return tileset_t(); - } - - // the set of included tiles - std::unordered_set subtiles; - subtiles.emplace(start_point); - - // outside layer of tiles - std::unordered_set edge_set; - - while (subtiles.size() < number) { - if (edge_set.empty()) { - - // try fill the edge list - for (auto &t : subtiles) { - - // check adjacent tiles - for (int i = 0; i < 4; ++i) { - coord::tile adj = t + neigh_tiles[i]; - if (this->tiles.count(adj) && - !subtiles.count(adj)) { - edge_set.emplace(adj); - } - } - } - if (edge_set.empty()) { - - // unable to grow further - return subtiles; - } - } - - // transfer a random tile - coord::tile next_tile = random_tile(rng, edge_set); - edge_set.erase(next_tile); - if (rng.probability(p)) { - subtiles.emplace(next_tile); - } - } - return subtiles; -} - -Region Region::take_tiles(rng::RNG &rng, coord::tile start_point, unsigned int number, double p) { - - tileset_t new_set = this->subset(rng, start_point, number, p); - - // erase from current set - for (auto &t: new_set) { - this->tiles.erase(t); - } - - Region new_region(start_point, new_set); - new_region.terrain_id = this->terrain_id; - return new_region; -} - -Region Region::take_random(rng::RNG &rng, unsigned int number, double p) { - return this->take_tiles(rng, this->get_tile(rng), number, p); -} - -Generator::Generator(qtsdl::GuiItemLink *gui_link) - : - gui_link{gui_link} -{ - this->setv("generation_seed", 4321); - this->setv("terrain_size", 2); - this->setv("terrain_base_id", 0); - this->setv("player_area", 850); - this->setv("player_radius", 10); - this->setv("load_filename", "/tmp/default_save.oas"); - this->setv("from_file", false); - // TODO pick the users name - this->set_csv("player_names", std::vector{"Jonas", "Michael"}); -} - -std::shared_ptr Generator::get_spec() const { - return this->spec; -} - -std::vector Generator::player_names() const { - auto result = this->get_csv("player_names"); - - // gaia is player 0 - result.insert(result.begin(), "Gaia"); - - return result; -} - -void Generator::create_regions() { - - // get option settings - int seed = this->getv("generation_seed"); - int size = this->getv("terrain_size"); - int base_id = this->getv("terrain_base_id"); - int p_area = this->getv("player_area"); - int p_radius = this->getv("player_radius"); - - // enforce some lower limits - size = std::max(1, size); - base_id = std::max(0, base_id); - p_area = std::max(50, p_area); - p_radius = std::max(2, p_radius); - - rng::RNG rng(seed); - Region base(size * 16); - base.terrain_id = base_id; - std::vector player_regions; - - int player_count = this->player_names().size() - 1; - for (int i = 0; i < player_count; ++i) { - log::log(MSG(dbg) << "generate player " << i); - - // space players in a circular pattern - double angle = static_cast(i) / static_cast(player_count); - int ne = size * p_radius * sin(math::TAU * angle); - int se = size * p_radius * cos(math::TAU * angle); - coord::tile player_tile{ne, se}; - - Region player = base.take_tiles(rng, player_tile, p_area, 0.5); - player.terrain_id = 10; - - Region obj_space = player.take_tiles(rng, player.get_center(), p_area / 5, 0.5); - obj_space.owner = i + 1; - obj_space.terrain_id = 8; - - Region trees1 = player.take_random(rng, p_area / 10, 0.3); - trees1.terrain_id = 9; - trees1.object_id = 349; - - Region trees2 = player.take_random(rng, p_area / 10, 0.3); - trees2.terrain_id = 9; - trees2.object_id = 351; - - Region stone = player.take_random(rng, 5, 0.3); - stone.object_id = 102; - - Region gold = player.take_random(rng, 7, 0.3); - gold.object_id = 66; - - Region forage = player.take_random(rng, 6, 0.3); - forage.object_id = 59; - - Region sheep = player.take_random(rng, 4, 0.3); - sheep.owner = obj_space.owner; - sheep.object_id = 594; - - player_regions.push_back(player); - player_regions.push_back(obj_space); - player_regions.push_back(trees1); - player_regions.push_back(trees2); - player_regions.push_back(stone); - player_regions.push_back(gold); - player_regions.push_back(forage); - player_regions.push_back(sheep); - } - - for (int i = 0; i < 6; ++i) { - Region extra_trees = base.take_random(rng, 160, 0.3); - extra_trees.terrain_id = 9; - extra_trees.object_id = 349; - player_regions.push_back(extra_trees); - } - - // set regions - this->regions.clear(); - this->regions.push_back(base); - for (auto &r : player_regions) { - this->regions.push_back(r); - } -} - - -std::shared_ptr Generator::terrain() const { - auto terrain = std::make_shared(this->spec->get_terrain_meta(), true); - for (auto &r : this->regions) { - for (auto &tile : r.get_tiles()) { - TerrainChunk *chunk = terrain->get_create_chunk(tile); - chunk->get_data(tile.get_pos_on_chunk())->terrain_id = r.terrain_id; - } - } - - // mark the 0, 0 tile. - coord::tile debug_tile_pos{0, 0}; - terrain->get_data(debug_tile_pos)->terrain_id = 6; - return terrain; -} - -void Generator::add_units(GameMain &m) const { - for (auto &r : this->regions) { - - // Regions filled with resource objects - // trees / mines - if (r.object_id) { - Player* p = m.get_player(r.owner); - auto otype = p->get_type(r.object_id); - if (!otype) { - break; - } - for (auto &tile : r.get_tiles()) { - m.placed_units.new_unit(*otype, *p, tile.to_phys3(*m.terrain)); - } - } - - // A space for starting town center and villagers - else if (r.owner) { - Player* p = m.get_player(r.owner); - auto tctype = p->get_type(109); // town center - auto mvtype = p->get_type(83); // male villager - auto fvtype = p->get_type(293); // female villager - auto sctype = p->get_type(448); // scout cavarly - if (!tctype || !mvtype || !fvtype || !sctype) { - break; - } - - coord::tile tile = r.get_center(); - tile.ne -= 1; - tile.se -= 1; - - // Place a completed town center - auto ref = m.placed_units.new_unit(*tctype, *p, tile.to_phys3(*m.terrain)); - if (ref.is_valid()) { - complete_building(*ref.get()); - } - - // Place three villagers - tile.ne -= 1; - m.placed_units.new_unit(*fvtype, *p, tile.to_phys3(*m.terrain)); - tile.se += 1; - m.placed_units.new_unit(*mvtype, *p, tile.to_phys3(*m.terrain)); - tile.se += 1; - m.placed_units.new_unit(*fvtype, *p, tile.to_phys3(*m.terrain)); - // TODO uncomment when the scout looks better - //tile.se += 2; - //m.placed_units.new_unit(*sctype, *p, tile.to_tile3().to_phys3()); - } - } -} - -std::unique_ptr Generator::create(std::shared_ptr spec) { - ENSURE(spec->load_complete(), "spec hasn't been checked or was invalidated"); - this->spec = spec; - - if (this->getv("from_file")) { - // create an empty game - this->regions.clear(); - - auto game = std::make_unique(*this); - gameio::load(game.get(), this->getv("load_filename")); - - return game; - } else { - // generation - this->create_regions(); - return std::make_unique(*this); - } -} - -} // namespace openage diff --git a/libopenage/gamestate/old/generator.h b/libopenage/gamestate/old/generator.h deleted file mode 100644 index a2d4bf58b7..0000000000 --- a/libopenage/gamestate/old/generator.h +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "../../coord/tile.h" -#include "../../gui/guisys/public/gui_property_map.h" - -namespace qtsdl { -class GuiItemLink; -} // qtsdl - -namespace openage { - -class GameSpec; -class Terrain; -class GameMain; - -namespace rng { -class RNG; -} // openage::rng - -/** - * the type to store a set of tiles - */ -using tileset_t = std::unordered_set; - -/** - * picks a random tile from a set - */ -coord::tile random_tile(rng::RNG &rng, tileset_t tiles); - -/** - * the four directions available for 2d tiles - */ -constexpr coord::tile_delta const neigh_tiles[] = { - { 1, 0}, - {-1, 0}, - { 0, 1}, - { 0, -1} -}; - -/** - * A region is a set of tiles around a starting point, - * including functions to create child regions - */ -class Region { -public: - /** - * a square of tiles ranging from - * {-size, -size} to {size, size} - */ - Region(int size); - - /** - * a specified set of tiles - */ - Region(coord::tile center, tileset_t tiles); - - /** - * all tiles in this region - */ - tileset_t get_tiles() const; - - /** - * the center point of the region - */ - coord::tile get_center() const; - - /** - * picks a random tile from this subset - */ - coord::tile get_tile(rng::RNG &rng) const; - - /** - * find a group of tiles inside this region, number is the number of tiles to be contained - * in the subset, p is a probability between 0.0 and 1.0 which produces various shapes. a value - * of 1.0 produces circular shapes, where as a low value produces more scattered shapes. a value - * of 0.0 should not be used, and will always return no tiles - */ - tileset_t subset(rng::RNG &rng, coord::tile start_tile, unsigned int number, double p) const; - - /** - * removes the given set of tiles from this region, which get split of as a new child region - */ - Region take_tiles(rng::RNG &rng, coord::tile start_tile, unsigned int number, double p); - - /** - * similiar to take_tiles, but takes a random group of tiles - */ - Region take_random(rng::RNG &rng, unsigned int number, double p); - - /** - * player id of the owner, 0 for none - */ - int owner; - - /** - * the object to be placed on each tile of this region - * 0 for placing no object - */ - int object_id; - - /** - * the base terrain for this region - */ - int terrain_id; - -private: - - /** - * the center tile of this region - */ - coord::tile center; - - /** - * tiles in this region - */ - tileset_t tiles; -}; - - -/** - * Manages creation and setup of new games - * - * required values used to construct a game - * this includes game spec and players - * - * this will be identical for each networked - * player in a game - */ -class Generator : public qtsdl::GuiPropertyMap { -public: - explicit Generator(qtsdl::GuiItemLink *gui_link); - - /** - * game spec used by this generator - */ - std::shared_ptr get_spec() const; - - /** - * return the list of player names - */ - std::vector player_names() const; - - /** - * returns the generated terrain - */ - std::shared_ptr terrain() const; - - /** - * places all initial objects - */ - void add_units(GameMain &m) const; - - /** - * Create a game from a specification. - */ - std::unique_ptr create(std::shared_ptr spec); - -private: - void create_regions(); - - /** - * data version used to create a game - */ - std::shared_ptr spec; - - /** - * the generated data - */ - std::vector regions; - -public: - qtsdl::GuiItemLink *gui_link; -}; - -} // namespace openage diff --git a/libopenage/gamestate/old/market.cpp b/libopenage/gamestate/old/market.cpp deleted file mode 100644 index 4b1d50bc24..0000000000 --- a/libopenage/gamestate/old/market.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2017-2019 the openage authors. See copying.md for legal info. - -#include "player.h" -#include "market.h" - -namespace openage { - -Market::Market() { - // the default prices when a game starts - this->base_prices[game_resource::wood] = 100; - this->base_prices[game_resource::food] = 100; - this->base_prices[game_resource::stone] = 130; -} - -// Price calculation is documented at doc/reverse_engineering/market.md#prices - -bool Market::sell(Player &player, const game_resource res) { - // deduct the standard MARKET_TRANSACTION_AMOUNT of the selling res - if (player.deduct(res, MARKET_TRANSACTION_AMOUNT)) { - // if deduct was successful - // calc the gold received from selling MARKET_TRANSACTION_AMOUNT of res - double amount = this->get_sell_prices(player).get(res); - player.receive(game_resource::gold, amount); - - // decrease the price - this->base_prices[res] -= MARKET_PRICE_D; - if (this->base_prices.get(res) < MARKET_PRICE_MIN) { - this->base_prices[res] = MARKET_PRICE_MIN; - } - return true; - } - return false; -} - -bool Market::buy(Player &player, const game_resource res) { - // calc the gold needed to buy MARKET_TRANSACTION_AMOUNT of res - double price = this->get_buy_prices(player).get(res); - if (player.deduct(game_resource::gold, price)) { - // if deduct was successful - player.receive(res, MARKET_TRANSACTION_AMOUNT); - - // increase the price - this->base_prices[res] += MARKET_PRICE_D; - if (this->base_prices.get(res) > MARKET_PRICE_MAX) { - this->base_prices[res] = MARKET_PRICE_MAX; - } - return true; - } - return false; -} - -ResourceBundle Market::get_buy_prices(const Player &player) const { - return this->get_prices(player, true); -} - -ResourceBundle Market::get_sell_prices(const Player &player) const { - return this->get_prices(player, false); -} - -ResourceBundle Market::get_prices(const Player &player, const bool is_buy) const { - double mult = this->get_multiplier(player, is_buy); - - auto rb = ResourceBundle(this->base_prices); - rb *= mult; - rb.round(); // round to nearest integer - return rb; -} - -double Market::get_multiplier(const Player &/*player*/, const bool is_buy) const { - double base = 0.3; - // TODO change multiplier based on civ bonuses and player researched techs - double mult = base; - - if (is_buy) { - mult = 1 + mult; - } - else { - mult = 1 - mult; - } - - return mult; -} - -} // openage diff --git a/libopenage/gamestate/old/market.h b/libopenage/gamestate/old/market.h deleted file mode 100644 index 643668fe59..0000000000 --- a/libopenage/gamestate/old/market.h +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2017-2019 the openage authors. See copying.md for legal info. - -#pragma once - -#include "resource.h" - -namespace openage { - -class Player; - -constexpr double MARKET_PRICE_D = 3.0; -constexpr double MARKET_PRICE_MIN = 20.0; -constexpr double MARKET_PRICE_MAX = 9999.0; -constexpr double MARKET_TRANSACTION_AMOUNT = 100.0; - -/** - * The global market prices. - * - * Price calculation is documented at doc/reverse_engineering/market.md#prices - */ -class Market { -public: - Market(); - - /** - * The given player sells the given resource for gold. - * Returns true when the transaction is successful. - */ - bool sell(Player &player, const game_resource res); - - /** - * The given player buys the given resource with gold. - * Returns true when the transaction is successful. - */ - bool buy(Player &player, const game_resource res); - - /** - * Get the selling prices for a given player. - */ - ResourceBundle get_buy_prices(const Player &player) const; - - /** - * Get the buying prices for a given player. - */ - ResourceBundle get_sell_prices(const Player &player) const; - -protected: - - /** - * The getBuyPrices and getSellPrices are redirected here. - */ - ResourceBundle get_prices(const Player &player, const bool is_buy) const; - - /** - * Get the multiplier for the base prices - */ - double get_multiplier(const Player &player, const bool is_buy) const; - -private: - - /** - * Stores the base price values of each resource. - * - * The ResourceBundle is used to represent the prices instead of the amounts - * for each resource. - */ - ResourceBundle base_prices; - -}; - -} // openage diff --git a/libopenage/gamestate/old/player.cpp b/libopenage/gamestate/old/player.cpp deleted file mode 100644 index ed6bd9e8b9..0000000000 --- a/libopenage/gamestate/old/player.cpp +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright 2015-2018 the openage authors. See copying.md for legal info. - -#include "player.h" - -#include - -#include "../../log/log.h" -#include "../../unit/unit.h" -#include "../../unit/unit_type.h" -#include "../../util/math_constants.h" -#include "team.h" - - -namespace openage { - -Player::Player(Civilisation *civ, unsigned int number, std::string name) - : - player_number{number}, - color{number}, - civ{civ}, - name{std::move(name)}, - team{nullptr}, - population{0, 200}, // TODO change, get population cap max from game options - score{this}, - age{1} { // TODO change, get starting age from game options - // starting resources - // TODO change, get starting resources from game options - this->resources.set_all(1000); - // TODO change, get starting resources capacity from game options or nyan - this->resources_capacity.set_all(math::DOUBLE_INF / 2); // half to avoid overflows - this->on_resources_change(); -} - -bool Player::operator ==(const Player &other) const { - return this->player_number == other.player_number; -} - -bool Player::is_enemy(const Player &other) const { - - return !this->is_ally(other); -} - -bool Player::is_ally(const Player &other) const { - - if (this->player_number == other.player_number) { - return true; // same player - } - - if (this->team && this->team->is_member(other)) { - return true; // same team - } - - // everyone else is enemy - return false; -} - -bool Player::owns(Unit &unit) const { - if (unit.has_attribute(attr_type::owner)) { - return this == &unit.get_attribute().player; - } - return false; -} - -void Player::receive(const ResourceBundle& amount) { - this->resources += amount; - this->on_resources_change(); -} - -void Player::receive(const game_resource resource, double amount) { - this->resources[resource] += amount; - this->on_resources_change(); -} - -bool Player::can_receive(const ResourceBundle& amount) const { - return this->resources_capacity.has(this->resources, amount); -} - -bool Player::can_receive(const game_resource resource, double amount) const { - return this->resources_capacity.get(resource) >= this->resources.get(resource) + amount; -} - -bool Player::deduct(const ResourceBundle& amount) { - if (this->resources.deduct(amount)) { - this->on_resources_change(); - return true; - } - return false; -} - -bool Player::deduct(const game_resource resource, double amount) { - if (this->resources[resource] >= amount) { - this->resources[resource] -= amount; - this->on_resources_change(); - return true; - } - return false; -} - -bool Player::can_deduct(const ResourceBundle& amount) const { - return this->resources.has(amount); -} - -bool Player::can_deduct(const game_resource resource, double amount) const { - return this->resources.get(resource) >= amount; -} - -double Player::amount(const game_resource resource) const { - return this->resources.get(resource); -} - -bool Player::can_make(const UnitType &type) const { - return this->can_deduct(type.cost.get(*this)) && - this->get_units_have(type.id()) + this->get_units_pending(type.id()) < type.have_limit && - this->get_units_had(type.id()) + this->get_units_pending(type.id()) < type.had_limit; -} - -size_t Player::type_count() { - return this->available_ids.size(); -} - -UnitType *Player::get_type(index_t type_id) const { - if (this->available_ids.count(type_id) == 0) { - if (type_id > 0) { - log::log(MSG(info) << " -> ignoring type_id: " << type_id); - } - return nullptr; - } - return this->available_ids.at(type_id); -} - -UnitType *Player::get_type_index(size_t type_index) const { - if (type_index < available_objects.size()) { - return available_objects.at(type_index).get(); - } - log::log(MSG(info) << " -> ignoring type_index: " << type_index); - return nullptr; -} - - -void Player::initialise_unit_types() { - log::log(MSG(info) << name << " has civilisation " << this->civ->civ_name); - for (auto &type : this->civ->object_meta()) { - auto shared_type = type->init(*this); - index_t id = shared_type->id(); - this->available_objects.emplace_back(shared_type); - this->available_ids[id] = shared_type.get(); - } -} - -void Player::active_unit_added(Unit *unit, bool from_pending) { - // check if unit is actually active - if (this->is_unit_pending(unit)) { - this->units_pending[unit->unit_type->id()] += 1; - return; - } - - if (from_pending) { - this->units_pending[unit->unit_type->id()] -= 1; - } - - this->units_have[unit->unit_type->id()] += 1; - this->units_had[unit->unit_type->id()] += 1; - // TODO handle here building dependencies - - // population - if (unit->has_attribute(attr_type::population)) { - auto popul = unit->get_attribute(); - if (popul.demand > 0) { - this->population.demand_population(popul.demand); - } - if (popul.capacity > 0) { - this->population.add_capacity(popul.capacity); - } - } - - // resources capacity - if (unit->has_attribute(attr_type::storage)) { - auto storage = unit->get_attribute(); - this->resources_capacity += storage.capacity; - this->on_resources_change(); - } - - // score - // TODO improve selectors - if (unit->unit_type->id() == 82 || unit->unit_type->id() == 276) { // Castle, Wonder - this->score.add_score(score_category::society, unit->unit_type->cost.get(*this).sum() * 0.2); - } else if (unit->has_attribute(attr_type::building) || unit->has_attribute(attr_type::population)) { // building, living - this->score.add_score(score_category::economy, unit->unit_type->cost.get(*this).sum() * 0.2); - } - - // TODO handle here on create unit triggers - // TODO check for unit based win conditions -} - -void Player::active_unit_removed(Unit *unit) { - // check if unit is actually active - if (this->is_unit_pending(unit)) { - this->units_pending[unit->unit_type->id()] -= 1; - return; - } - - this->units_have[unit->unit_type->id()] -= 1; - // TODO handle here building dependencies - - // population - if (unit->has_attribute(attr_type::population)) { - auto popul = unit->get_attribute(); - if (popul.demand > 0) { - this->population.free_population(popul.demand); - } - if (popul.capacity > 0) { - this->population.remove_capacity(popul.capacity); - } - } - - // resources capacity - if (unit->has_attribute(attr_type::storage)) { - auto storage = unit->get_attribute(); - this->resources_capacity -= storage.capacity; - this->on_resources_change(); - } - - // score - // TODO improve selectors - if (unit->unit_type->id() == 82 || unit->unit_type->id() == 276) { // Castle, Wonder - // nothing - } else if (unit->has_attribute(attr_type::building) || unit->has_attribute(attr_type::population)) { // building, living - this->score.remove_score(score_category::economy, unit->unit_type->cost.get(*this).sum() * 0.2); - } - - // TODO handle here on death unit triggers - // TODO check for unit based win conditions -} - -void Player::killed_unit(const Unit & unit) { - // score - this->score.add_score(score_category::military, unit.unit_type->cost.get(*this).sum() * 0.2); -} - -void Player::advance_age() { - this->age += 1; -} - -void Player::on_resources_change() { - - // capacity overflow - if (! (this->resources_capacity >= this->resources_capacity)) { - this->resources.limit(this->resources_capacity); - } - - // score - this->score.update_resources(this->resources); - - // TODO check for resource based win conditions -} - -int Player::get_units_have(int type_id) const { - if (this->units_have.count(type_id)) { - return this->units_have.at(type_id); - } - return 0; -} - -int Player::get_units_had(int type_id) const { - if (this->units_had.count(type_id)) { - return this->units_had.at(type_id); - } - return 0; -} - -int Player::get_units_pending(int type_id) const { - if (this->units_pending.count(type_id)) { - return this->units_pending.at(type_id); - } - return 0; -} - -bool Player::is_unit_pending(Unit *unit) const { - // TODO check aslo if unit is training - return unit->has_attribute(attr_type::building) && unit->get_attribute().completed < 1.0f; -} - -int Player::get_workforce_count() const { - // TODO get all units tagged as work force - return this->units_have.at(83) + this->units_have.at(293) + // villagers - this->units_have.at(13) + // fishing ship - this->units_have.at(128) + // trade cart - this->units_have.at(545); // transport ship -} - -} // openage diff --git a/libopenage/gamestate/old/player.h b/libopenage/gamestate/old/player.h deleted file mode 100644 index 469a6c1f98..0000000000 --- a/libopenage/gamestate/old/player.h +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -#include "civilisation.h" -#include "population_tracker.h" -#include "resource.h" -#include "score.h" - - -namespace openage { - -class Unit; -class Team; - -class Player { -public: - Player(Civilisation *civ, unsigned int number, std::string name); - - /** - * values 0 .. player count - 1 - */ - const unsigned int player_number; - - /** - * values 1 .. player count - * would be better to have rgb color value - */ - const unsigned int color; - - /** - * civilisation and techs of this player - */ - const Civilisation *civ; - - /** - * visible name of this player - */ - const std::string name; - - /** - * the team of this player - * nullptr if member of no team - */ - Team *team; - - /** - * checks if two players are the same - */ - bool operator ==(const Player &other) const; - - /** - * the specified player is an enemy of this player - */ - bool is_enemy(const Player &) const; - - /** - * the specified player is an ally of this player - */ - bool is_ally(const Player &) const; - - /** - * this player owns the specified unit - */ - bool owns(Unit &) const; - - /** - * add to stockpile - */ - void receive(const ResourceBundle& amount); - void receive(const game_resource resource, double amount); - - /** - * Check if can add to stockpile - */ - bool can_receive(const ResourceBundle& amount) const; - bool can_receive(const game_resource resource, double amount) const; - - /** - * remove from stockpile if available - */ - bool deduct(const ResourceBundle& amount); - bool deduct(const game_resource resource, double amount); - - /** - * Check if the player has enough resources to deduct the given amount. - */ - bool can_deduct(const ResourceBundle& amount) const; - bool can_deduct(const game_resource resource, double amount) const; - - /** - * current stockpile amount - */ - double amount(const game_resource resource) const; - - /** - * Check if the player can make a new unit of the given type - */ - bool can_make(const UnitType &type) const; - - /** - * total number of unit types available - */ - size_t type_count(); - - /** - * unit types by aoe gamedata unit ids -- the unit type which corresponds to an aoe unit id - */ - UnitType *get_type(index_t type_id) const; - - /** - * unit types by list index -- a continuous array of all types - * probably not a useful function / can be removed - */ - UnitType *get_type_index(size_t type_index) const; - - /** - * initialise with the base tech level - */ - void initialise_unit_types(); - - /** - * Keeps track of the population information. - */ - PopulationTracker population; - - /** - * The score of the player. - */ - PlayerScore score; - - /** - * Called when a unit is created and active. - * - * If the unit was pending when create (constuction site, training) the method must be - * called again when the unit activates (with the from_penging param set to true) - */ - void active_unit_added(Unit *unit, bool from_pending=false); - - /** - * Called when a unit is destroyed. - */ - void active_unit_removed(Unit *unit); - - /** - * Called when a unit is killed by this player. - */ - void killed_unit(const Unit & unit); - - /** - * Advance to next age; - */ - void advance_age(); - - // Getters - - /** - * Get the number of units the player has for each unit type id. - */ - int get_units_have(int type_id) const; - - /** - * Get the number of units the player ever had for each unit type id. - */ - int get_units_had(int type_id) const; - - /** - * Get the number of units the player has being made for each unit type id. - */ - int get_units_pending(int type_id) const; - - /** - * Get the current age. - * The first age has the value 1. - */ - int get_age() const { return age; } - - /** - * The number of units considered part of the workforce. - */ - int get_workforce_count() const; - - -private: - - bool is_unit_pending(Unit *unit) const; - - /** - * The resources this player currently has - */ - ResourceBundle resources; - - /** - * The resources capacities this player currently has - */ - ResourceBundle resources_capacity; - - /** - * Called when the resources amounts change. - */ - void on_resources_change(); - - /** - * unit types which can be produced by this player. - * TODO revisit, can be simplified? - */ - unit_type_list available_objects; - - /** - * available objects mapped using type id - * unit ids -> unit type for that id - * TODO revisit, can be simplified? - */ - std::unordered_map available_ids; - - /** - * The number of units the player has for each unit type id. - * Used for and event triggers. - */ - std::unordered_map units_have; - - /** - * The number of units the player ever had for each unit type id. - * Used for unit dependencies (eg. Farm). - */ - std::unordered_map units_had; - - /* - * The number of units the player has being made for each unit type id. - * Used for unit limits (eg. Town Center). - */ - std::unordered_map units_pending; - - /** - * The current age. - */ - int age; - -}; - -} // openage diff --git a/libopenage/gamestate/old/population_tracker.cpp b/libopenage/gamestate/old/population_tracker.cpp deleted file mode 100644 index fdb79590f6..0000000000 --- a/libopenage/gamestate/old/population_tracker.cpp +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2017-2019 the openage authors. See copying.md for legal info. - -#include "population_tracker.h" - -namespace openage { - -PopulationTracker::PopulationTracker(int capacity_static, int capacity_max) { - this->demand = 0; - this->capacity_static = capacity_static; - this->capacity_real = 0; - this->capacity_max = capacity_max; - this->update_capacity(); -} - -void PopulationTracker::demand_population(int i) { - this->demand += i; - // TODO triger gui update -} - -void PopulationTracker::free_population(int i) { - this->demand -= i; - // TODO triger gui update -} - -void PopulationTracker::add_capacity_static(int i) { - this->capacity_static += i; - this->update_capacity(); -} - -void PopulationTracker::add_capacity(int i) { - this->capacity_real += i; - this->update_capacity(); -} - -void PopulationTracker::remove_capacity(int i) { - this->capacity_real -= i; - this->update_capacity(); -} - -void PopulationTracker::add_capacity_max(int i) { - this->capacity_max += i; - this->update_capacity(); -} - -void PopulationTracker::update_capacity() { - this->capacity_total = this->capacity_static + this->capacity_real; - // check the capacity limit - if (this->capacity_total > this->capacity_max) { - this->capacity = this->capacity_max; - } else { - this->capacity = this->capacity_total; - } - // TODO triger gui update -} - -int PopulationTracker::get_demand() const { - return this->demand; -} - -int PopulationTracker::get_capacity() const { - return this->capacity; -} - -int PopulationTracker::get_space() const { - return this->capacity - this->demand; -} - -int PopulationTracker::get_capacity_overflow() const { - return this->capacity_total - this->capacity; -} - -bool PopulationTracker::is_capacity_maxed() const { - return this->capacity >= this->capacity_max; -} - -} // openage diff --git a/libopenage/gamestate/old/population_tracker.h b/libopenage/gamestate/old/population_tracker.h deleted file mode 100644 index 431c0f28ea..0000000000 --- a/libopenage/gamestate/old/population_tracker.h +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2017-2019 the openage authors. See copying.md for legal info. - -#pragma once - -namespace openage { - -/** - * Keeps track of the population size and capacity. - */ -class PopulationTracker { -public: - - PopulationTracker(int capacity_static, int capacity_max); - - /** - * Add to the population demand - */ - void demand_population(int i); - - /** - * Remove from the population demand - */ - void free_population(int i); - - /** - * Changes the capacity given by civ bonuses - */ - void add_capacity_static(int i); - - /** - * Add to the capacity given by units - */ - void add_capacity(int i); - - /** - * Remove from the capacity given by units - */ - void remove_capacity(int i); - - /** - * Changes the max capacity given by civ bonuses - */ - void add_capacity_max(int i); - - int get_demand() const; - - int get_capacity() const; - - /** - * Returns the available population capacity for new units. - */ - int get_space() const; - - /** - * Returns the population capacity over the max limit. - */ - int get_capacity_overflow() const; - - /** - * Check if the population capacity has reached the max limit. - */ - bool is_capacity_maxed() const; - -private: - - /** - * Calculates the capacity values based on the limits. - * Must be called when a capacity variable changes. - */ - void update_capacity(); - - /** - * The population demand - */ - int demand; - - /** - * The population capacity given by civ bonuses - */ - int capacity_static; - - /** - * The population capacity given by units - */ - int capacity_real; - - /** - * The max population capacity - */ - int capacity_max; - - // generated values - - /** - * All the population capacity without the limitation - */ - int capacity_total; - - /** - * All the population capacity with the limitation - */ - int capacity; - -}; - -} // openage diff --git a/libopenage/gamestate/old/resource.cpp b/libopenage/gamestate/old/resource.cpp deleted file mode 100644 index 2c3acdda19..0000000000 --- a/libopenage/gamestate/old/resource.cpp +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. - -#include -#include -#include - -#include "resource.h" - -namespace openage { - -ResourceBundle Resources::create_bundle() const { - return ResourceBundle(*this); -} - -ResourceBundle::ResourceBundle() - : - ResourceBundle{4} { -} - -ResourceBundle::ResourceBundle(const Resources& resources) - : - ResourceBundle{static_cast(resources.get_count())} { -} - -ResourceBundle::ResourceBundle(const int count) - : - count{count}, - value{new double[count] {0}} { -} - -ResourceBundle::ResourceBundle(const ResourceBundle &resources) - : - ResourceBundle{resources.count} { - this->set(resources); -} - -ResourceBundle ResourceBundle::clone() const { - return ResourceBundle(*this); -} - -ResourceBundle::~ResourceBundle() { - delete[] value; -} - -void ResourceBundle::expand(const ResourceBundle& other) { - this->expand(other.count); -} - -void ResourceBundle::expand(const int count) { - if (this->count == count) { - return; - } - // create new array with old values - auto *new_value = new double[count]; - for (int i = 0; i < this->count; i++) { - new_value[i] = this->value[i]; - } - // replace the private variables - this->count = count; - delete[] value; - this->value = new_value; -} - -bool ResourceBundle::operator> (const ResourceBundle& other) const { - for (int i = 0; i < this->count; i++) { - if (!(this->get(i) > other.get(i))) { - return false; - } - } - // check also resources that are not in both bundles - for (int i = this->count; i < other.count; i++) { - if (other.get(i) > 0) { - return false; - } - } - return true; -} - -bool ResourceBundle::operator>= (const ResourceBundle& other) const { - for (int i = 0; i < this->count; i++) { - if (!(this->get(i) >= other.get(i))) { - return false; - } - } - // check also resources that are not in both bundles - for (int i = this->count; i < other.count; i++) { - if (other.get(i) > 0) { - return false; - } - } - return true; -} - -ResourceBundle& ResourceBundle::operator+= (const ResourceBundle& other) { - this->expand(other); - for (int i = 0; i < this->count; i++) { - (*this)[i] += other.get(i); - } - return *this; -} - -ResourceBundle& ResourceBundle::operator-= (const ResourceBundle& other) { - this->expand(other); - for (int i = 0; i < this->count; i++) { - (*this)[i] -= other.get(i); - } - return *this; -} - -ResourceBundle& ResourceBundle::operator*= (const double a) { - for (int i = 0; i < this->count; i++) { - (*this)[i] *= a; - } - return *this; -} - -ResourceBundle& ResourceBundle::round() { - for (int i = 0; i < this->count; i++) { - (*this)[i] = std::round(this->get(i)); - } - return *this; -} - -bool ResourceBundle::has(const ResourceBundle& amount) const { - return *this >= amount; -} - -bool ResourceBundle::has(const ResourceBundle& amount1, const ResourceBundle& amount2) const { - for (int i = 0; i < this->count; i++) { - if (!(this->get(i) >= amount1.get(i) + amount2.get(i))) { - return false; - } - } - // check also resources that are not in both bundles - for (int i = this->count; i < amount1.count; i++) { - if (amount1.get(i) > 0) { - return false; - } - } - for (int i = this->count; i < amount2.count; i++) { - if (amount2.get(i) > 0) { - return false; - } - } - return true; -} - -bool ResourceBundle::deduct(const ResourceBundle& amount) { - if (this->has(amount)) { - *this -= amount; - return true; - } - return false; -} - -void ResourceBundle::set(const ResourceBundle &amount) { - this->expand(amount); - for (int i = 0; i < this->count; i++) { - (*this)[i] = amount.get(i); - } -} - -void ResourceBundle::set_all(const double amount) { - for (int i = 0; i < this->count; i++) { - (*this)[i] = amount; - } -} - -void ResourceBundle::limit(const ResourceBundle &limits) { - for (int i = 0; i < this->min_count(limits); i++) { - if (this->get(i) > limits.get(i)) { - (*this)[i] = limits.get(i); - } - } -} - -double ResourceBundle::sum() const { - double sum = 0; - for (int i = 0; i < this->count; i++) { - sum += this->get(i); - } - return sum; -} - -int ResourceBundle::min_count(const ResourceBundle &other) { - return this->count <= other.count ? this->count : other.count; -} - -} // openage - -namespace std { - -string to_string(const openage::game_resource &res) { - switch (res) { - case openage::game_resource::wood: - return "wood"; - case openage::game_resource::food: - return "food"; - case openage::game_resource::gold: - return "gold"; - case openage::game_resource::stone: - return "stone"; - default: - return "unknown"; - } -} - -} // namespace std diff --git a/libopenage/gamestate/old/resource.h b/libopenage/gamestate/old/resource.h deleted file mode 100644 index 4a52b5c008..0000000000 --- a/libopenage/gamestate/old/resource.h +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright 2015-2018 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -namespace openage { - -class ResourceBundle; - -/** - * A resource - */ -class Resource { -public: - - Resource(); - - virtual int id() const = 0; - - virtual std::string name() const = 0; - - // TODO add images and icons - -}; - -class ResourceProducer : public Resource { -public: - - ResourceProducer(int id, std::string name) - : - _id{id}, - _name{name} { } - - int id() const override { return _id; } - - std::string name() const override { return _name; } - -private: - - int _id; - std::string _name; -}; - -/** - * All the resources. - * - * The the ids of the resources must be inside [0, count). - * - * The static variables wood, food, gold, stone are the ids of the representing resource. - * Any extension of Resources must use this ids as they are an engine dependency (at the moment). - */ -class Resources { -public: - - Resources(); - - virtual unsigned int get_count() const = 0; - - virtual const Resource& get_resource(int id) const = 0; - - ResourceBundle create_bundle() const; - - // TODO remove when the engine is fully decupled from the data - static const int wood = 0; - static const int food = 1; - static const int gold = 2; - static const int stone = 3; - -}; - -class ClassicResources : public Resources { -public: - - ClassicResources() - : - resources{{Resources::wood, "wood"}, - {Resources::food, "food"}, - {Resources::gold, "gold"}, - {Resources::stone, "stone"}} { - } - - unsigned int get_count() const override { return 4; } - - const Resource& get_resource(int id) const override { return this->resources[id]; }; - -private: - - const ResourceProducer resources[4]; -}; - -// TODO remove, here for backwards compatibility -enum class game_resource : int { - wood = 0, - food = 1, - gold = 2, - stone = 3, - RESOURCE_TYPE_COUNT = 4 -}; - - -/** - * A set of amounts of game resources. - * - * Can be also used to store other information about the resources. - * - * TODO change amounts from doubles to integers - */ -class ResourceBundle { -public: - - // TODO remove, here for backwards compatibility - ResourceBundle(); - - ResourceBundle(const Resources& resources); - - virtual ~ResourceBundle(); - - ResourceBundle(const ResourceBundle& other); - - ResourceBundle clone() const; - - bool operator> (const ResourceBundle& other) const; - bool operator>= (const ResourceBundle& other) const; - - ResourceBundle& operator+= (const ResourceBundle& other); - ResourceBundle& operator-= (const ResourceBundle& other); - - ResourceBundle& operator*= (const double a); - - /** - * Round each value to the nearest integer. - * Returns itself. - */ - ResourceBundle& round(); - - bool has(const ResourceBundle& amount) const; - - bool has(const ResourceBundle& amount1, const ResourceBundle& amount2) const; - - /** - * If amount can't be deducted return false, else deduct the given amount - * and return true. - */ - bool deduct(const ResourceBundle& amount); - - void set(const ResourceBundle& amount); - - void set_all(const double amount); - - void limit(const ResourceBundle& limits); - - double& operator[] (const game_resource res) { return value[static_cast(res)]; } - double& operator[] (const int id) { return value[id]; } - - // Getters - - double get(const game_resource res) const { return value[static_cast(res)]; } - double get(const int id) const { return value[id]; } - - /** - * Returns the sum of all the resources. - */ - double sum() const; - - /** - * The number of resources - */ - int get_count() const { return count; }; - -private: - - ResourceBundle(const int count); - - void expand(const ResourceBundle& other); - - void expand(const int count); - - /** - * The number of resources - */ - int count; - - double *value; - - int min_count(const ResourceBundle& other); - -}; - -} // namespace openage - -namespace std { - -std::string to_string(const openage::game_resource &res); - -/** - * hasher for game resource - */ -template<> struct hash { - typedef underlying_type::type underlying_type; - - size_t operator()(const openage::game_resource &arg) const { - hash hasher; - return hasher(static_cast(arg)); - } -}; - -} // namespace std diff --git a/libopenage/gamestate/old/score.cpp b/libopenage/gamestate/old/score.cpp deleted file mode 100644 index 9d26d9c9f0..0000000000 --- a/libopenage/gamestate/old/score.cpp +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2017-2021 the openage authors. See copying.md for legal info. - -#include - -#include "player.h" -#include "score.h" -#include "team.h" -#include "../../log/log.h" - -namespace openage { - -Score::Score() - : - score{0}, - score_total{0}, - score_exploration{0}, - score_resources{0} { -} - -void Score::add_score(const score_category cat, double value) { - this->add_score(cat, static_cast(std::lround(value))); -} - -void Score::add_score(const score_category cat, int value) { - this->score[static_cast(cat)] += value; - this->update_score(); -} - -void Score::remove_score(const score_category cat, double value) { - this->remove_score(cat, static_cast(std::lround(value))); -} - -void Score::remove_score(const score_category cat, int value) { - this->score[static_cast(cat)] -= value; - this->update_score(); -} - -void Score::update_map_explored(double progress) { - this->remove_score(score_category::technology, this->score_exploration); - this->score_exploration = progress * 1000; - this->add_score(score_category::technology, this->score_exploration); -} - -void Score::update_resources(const ResourceBundle & resources) { - this->remove_score(score_category::economy, this->score_resources); - this->score_resources = resources.sum() * 0.1; - this->add_score(score_category::economy, this->score_resources); -} - -void Score::update_score() { - this->score_total = 0; - for (int i = 0; i < static_cast(score_category::SCORE_CATEGORY_COUNT); i++) { - this->score_total += this->get_score(i); - } -} - -PlayerScore::PlayerScore(Player *player) - : - Score(), - player{player} { -} - -void PlayerScore::update_score() { - Score::update_score(); - // update team score - if (this->player->team) { - this->player->team->score.update_score(); - } -} - -TeamScore::TeamScore(Team *team) - : - Score(), - team{team} { -} - -void TeamScore::update_score() { - // scores are the corresponding sums of players score - for (int i = 0; i < static_cast(score_category::SCORE_CATEGORY_COUNT); i++) { - this->score[i] = 0; - } - for (auto player : this->team->get_players()) { - for (int i = 0; i < static_cast(score_category::SCORE_CATEGORY_COUNT); i++) { - this->score[i] += player->score.get_score(i); - } - } - Score::update_score(); -} - -} // openage - -namespace std { - -string to_string(const openage::score_category &cat) { - switch (cat) { - case openage::score_category::military: - return "military"; - case openage::score_category::economy: - return "economy"; - case openage::score_category::technology: - return "technology"; - case openage::score_category::society: - return "society"; - default: - return "unknown"; - } -} - -} // namespace std diff --git a/libopenage/gamestate/old/score.h b/libopenage/gamestate/old/score.h deleted file mode 100644 index 2a8c762290..0000000000 --- a/libopenage/gamestate/old/score.h +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "resource.h" - - -namespace openage { - -class Player; -class Team; - -/** - * The categories of sub-scores that sum to a player's score. - */ -enum class score_category : int { - /** 20% of units killed cost */ - military, - /** 20% of alive units cost and 10% of resources */ - economy, - /** 20% of researched technologies and 10 for each 1% of map explored*/ - technology, - /** 20% of Castles and Wonders cost */ - society, - SCORE_CATEGORY_COUNT -}; - -/** - * Keeps track of a score and all the sub-scores - */ -class Score { -public: - - Score(); - - void add_score(const score_category cat, double value); - void add_score(const score_category cat, int value); - - void remove_score(const score_category cat, double value); - void remove_score(const score_category cat, int value); - - /** - * Updates map exploration precentance based sub-scores - */ - void update_map_explored(double progress); - - /** - * Updates resource based sub-scores - */ - void update_resources(const ResourceBundle & resources); - - /** - * Calculates the total score from the sub-scores. - * TODO update gui here - */ - virtual void update_score(); - - // Getters - - int get_score(const score_category cat) const { return score[static_cast(cat)]; } - int get_score(const int index) const { return score[index]; } - - int get_score_total() const { return score_total; } - -protected: - - int score[static_cast(score_category::SCORE_CATEGORY_COUNT)]; - - // generated values - - int score_total; - -private: - - /** Used by update_map_explored. */ - int score_exploration; - - /** Used by update_resources. */ - int score_resources; -}; - - -/** - * The score of a player - */ -class PlayerScore : public Score { -public: - - PlayerScore(Player *player); - - virtual void update_score() override; - -protected: - -private: - - Player *player; -}; - - -/** - * The score of a team - */ -class TeamScore : public Score { -public: - - TeamScore(Team *team); - - virtual void update_score() override; - -protected: - -private: - - Team *team; -}; - -} // namespace openage - -namespace std { - -std::string to_string(const openage::score_category &cat); - -/** - * hasher for score_category - * TODO decide if needed, not used at the moment - */ -template<> -struct hash { - typedef underlying_type::type underlying_type; - - size_t operator()(const openage::score_category &arg) const { - hash hasher; - return hasher(static_cast(arg)); - } -}; - -} // namespace std diff --git a/libopenage/gamestate/old/team.cpp b/libopenage/gamestate/old/team.cpp deleted file mode 100644 index 92e556437f..0000000000 --- a/libopenage/gamestate/old/team.cpp +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2016-2021 the openage authors. See copying.md for legal info. - -#include "team.h" - -#include "player.h" -#include "score.h" -#include - - -namespace openage { - -Team::Team(unsigned int id) - : - Team{id, "Anonymous Team", nullptr} {} - -Team::Team(unsigned int id, std::string name) - : - Team{id, std::move(name), nullptr} {} - -Team::Team(unsigned int id, std::string name, Player *leader) - : - id{id}, - name{std::move(name)}, - score{this} { - - if (leader) { - this->add_member(*leader, member_type::leader); - } -} - -bool Team::operator ==(const Team &other) const { - return this->id == other.id; -} - -void Team::add_member(Player &player, const member_type type) { - // if already exists, replace member type - this->members[&player] = type; - // change player team pointer - player.team = this; -} - -void Team::change_member_type(Player &player, const member_type type) { - auto p = this->members.find(&player); - if (p != this->members.end()) { - this->members[&player] = type; - } -} - -bool Team::is_member(const Player &player) const { - auto p = this->members.find(&player); - return (p != this->members.end()); -} - -void Team::remove_member(Player &player) { - this->members.erase(&player); - // change player team pointer - player.team = nullptr; -} - -member_type Team::get_member_type(Player &player) { - auto p = this->members.find(&player); - if (p != this->members.end()) { - return this->members[&player]; - } - // return pseudo member type for completion - return member_type::none; -} - -std::vector Team::get_players() const { - std::vector players; - for (auto& i : members) { - players.push_back(i.first); - } - return players; -} - -} // openage diff --git a/libopenage/gamestate/old/team.h b/libopenage/gamestate/old/team.h deleted file mode 100644 index 657909df9e..0000000000 --- a/libopenage/gamestate/old/team.h +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2016-2019 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include - - -#include "score.h" - - -namespace openage { - -class Player; - -/** - * Types of membership - */ -enum class member_type { - leader, - member, - recruit, - none // pseudo type -}; - -/** - * A team of players - */ -class Team { -public: - Team(unsigned int id); - Team(unsigned int id, std::string name); - Team(unsigned int id, std::string name, Player *leader); - - /** - * unique id of the team - */ - const unsigned int id; - - /** - * visible name of this team - */ - const std::string name; - - bool operator ==(const Team &other) const; - - - void add_member(Player &player, const member_type type); - - void change_member_type(Player &player, const member_type type); - - bool is_member(const Player &player) const; - - void remove_member(Player &player); - - member_type get_member_type(Player &player); - - /** - * TODO find a better way to get all the players - */ - std::vector get_players() const; - - /** - * The score of the team, based on the team's players score. - */ - TeamScore score; - -private: - - std::unordered_map members; - -}; - -} // openage diff --git a/libopenage/gamestate/old/types.cpp b/libopenage/gamestate/old/types.cpp deleted file mode 100644 index ca80997a97..0000000000 --- a/libopenage/gamestate/old/types.cpp +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. - -#include "types.h" - - -namespace openage { - -} // openage diff --git a/libopenage/gamestate/old/types.h b/libopenage/gamestate/old/types.h deleted file mode 100644 index 805cd63627..0000000000 --- a/libopenage/gamestate/old/types.h +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. - -#pragma once - - -namespace openage { - -/** - * The key type mapped to data objects. - * Used for graphics indices, sounds, ... - * TODO: get rid of this. - */ -using index_t = int; - - -} // openage diff --git a/libopenage/gui/CMakeLists.txt b/libopenage/gui/CMakeLists.txt index 749136617c..a96c486f54 100644 --- a/libopenage/gui/CMakeLists.txt +++ b/libopenage/gui/CMakeLists.txt @@ -1,13 +1,7 @@ add_sources(libopenage actions_list_model.cpp - category_contents_list_model.cpp - game_creator.cpp - game_saver.cpp - game_spec_link.cpp - generator_link.cpp gui.cpp registrations.cpp - resources_list_model.cpp ) add_subdirectory("guisys") diff --git a/libopenage/gui/category_contents_list_model.cpp b/libopenage/gui/category_contents_list_model.cpp deleted file mode 100644 index 88408ee458..0000000000 --- a/libopenage/gui/category_contents_list_model.cpp +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "category_contents_list_model.h" - -#include - -namespace openage { -namespace gui { - -namespace { -const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "Category"); -} - -CategoryContentsListModel::CategoryContentsListModel(QObject *parent) : - QAbstractListModel{parent} { - Q_UNUSED(registration); -} - -CategoryContentsListModel::~CategoryContentsListModel() = default; - -QString CategoryContentsListModel::get_name() const { - return this->name; -} - -void CategoryContentsListModel::set_name(const QString &name) { - if (this->name != name) { - this->name = name; - - this->on_categories_content_changed(); - } -} - -void CategoryContentsListModel::on_category_content_changed(const std::string &category_name, std::vector> type_and_texture) { - if (this->name == QString::fromStdString(category_name)) { - this->beginResetModel(); - this->type_and_texture = type_and_texture; - this->endResetModel(); - } -} - -void CategoryContentsListModel::on_categories_content_changed() { -} - -QHash CategoryContentsListModel::roleNames() const { - auto names = this->QAbstractListModel::roleNames(); - names.insert(Qt::UserRole + 1, "typeId"); - return names; -} - -int CategoryContentsListModel::rowCount(const QModelIndex &) const { - return this->type_and_texture.size(); -} - -QVariant CategoryContentsListModel::data(const QModelIndex &index, int role) const { - switch (role) { - case Qt::DisplayRole: - return std::get<1>(this->type_and_texture[index.row()]); - - case Qt::UserRole + 1: - return std::get<0>(this->type_and_texture[index.row()]); - - default: - break; - } - - return QVariant{}; -} - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/category_contents_list_model.h b/libopenage/gui/category_contents_list_model.h deleted file mode 100644 index e3845a44db..0000000000 --- a/libopenage/gui/category_contents_list_model.h +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include -#include - -#include "../gamestate/old/types.h" - -#include - -namespace openage { -namespace gui { - -/** - * Adaptor for the contents of a category of the Civilisation. - */ -class CategoryContentsListModel : public QAbstractListModel { - Q_OBJECT - - Q_PROPERTY(QString name READ get_name WRITE set_name) - -public: - CategoryContentsListModel(QObject *parent = nullptr); - virtual ~CategoryContentsListModel(); - - QString get_name() const; - void set_name(const QString &name); - -private slots: - void on_category_content_changed(const std::string &category_name, std::vector> type_and_texture); - void on_categories_content_changed(); - -private: - virtual QHash roleNames() const override; - virtual int rowCount(const QModelIndex &) const override; - virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - std::vector> type_and_texture; - - QString name; -}; - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/game_creator.cpp b/libopenage/gui/game_creator.cpp deleted file mode 100644 index 392efe6139..0000000000 --- a/libopenage/gui/game_creator.cpp +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "game_creator.h" - -#include - -#include "../gamestate/old/game_main.h" -#include "../gamestate/old/game_spec.h" -#include "../gamestate/old/generator.h" - -#include "game_spec_link.h" -#include "generator_link.h" - -namespace openage::gui { - -namespace { -const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "GameCreator"); -} - -GameCreator::GameCreator(QObject *parent) : - QObject{parent}, - generator_parameters{} { - Q_UNUSED(registration); -} - -GameCreator::~GameCreator() = default; - -QString GameCreator::get_error_string() const { - return this->error_string; -} - -void GameCreator::activate() { - static auto f = [](GameMainHandle *game, - GameSpecHandle *game_spec, - Generator *generator, - std::shared_ptr callback) { - QString error_msg; - - if (game->is_game_running()) { - error_msg = "close existing game before loading"; - } - else if (not game_spec->is_ready()) { - error_msg = "game data has not finished loading"; - } - else { - auto game_main = generator->create(game_spec->get_spec()); - - if (game_main) { - game->set_game(std::move(game_main)); - } - else { - error_msg = "unknown error"; - } - } - - emit callback->error_message(error_msg); - }; -} - -void GameCreator::clearErrors() { - this->error_string.clear(); - emit this->error_string_changed(); -} - -void GameCreator::on_processed(const QString &error_string) { - this->error_string = error_string; - emit this->error_string_changed(); -} - -} // namespace openage::gui diff --git a/libopenage/gui/game_creator.h b/libopenage/gui/game_creator.h deleted file mode 100644 index c1899971d0..0000000000 --- a/libopenage/gui/game_creator.h +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -namespace openage { -namespace gui { - -class GameMainLink; -class GameSpecLink; -class GeneratorLink; - -class GameCreator : public QObject { - Q_OBJECT - - Q_ENUMS(State) - Q_PROPERTY(QString errorString READ get_error_string NOTIFY error_string_changed) - Q_MOC_INCLUDE("gui/game_spec_link.h") - Q_MOC_INCLUDE("gui/generator_link.h") - Q_PROPERTY(openage::gui::GameSpecLink *gameSpec MEMBER game_spec NOTIFY game_spec_changed) - Q_PROPERTY(openage::gui::GeneratorLink *generatorParameters MEMBER generator_parameters NOTIFY generator_parameters_changed) - -public: - explicit GameCreator(QObject *parent = nullptr); - virtual ~GameCreator(); - - QString get_error_string() const; - - Q_INVOKABLE void activate(); - Q_INVOKABLE void clearErrors(); - -public slots: - void on_processed(const QString &error_string); - -signals: - void error_string_changed(); - void game_changed(); - void game_spec_changed(); - void generator_parameters_changed(); - -private: - QString error_string; - GameSpecLink *game_spec; - GeneratorLink *generator_parameters; -}; - -class GameCreatorSignals : public QObject { - Q_OBJECT - -public: -signals: - void error_message(const QString &error); -}; - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/game_saver.cpp b/libopenage/gui/game_saver.cpp deleted file mode 100644 index a700dc704e..0000000000 --- a/libopenage/gui/game_saver.cpp +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. - -#include "game_saver.h" - -#include - -#include "../gamestate/old/game_main.h" -#include "../gamestate/old/game_save.h" -#include "../gamestate/old/generator.h" - -#include "generator_link.h" - -namespace openage::gui { - -namespace { -const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "GameSaver"); -} - -GameSaver::GameSaver(QObject *parent) : - QObject{parent}, - generator_parameters{} { - Q_UNUSED(registration); -} - -GameSaver::~GameSaver() = default; - -QString GameSaver::get_error_string() const { - return this->error_string; -} - -// called when the save-game button is pressed: -void GameSaver::activate() { - static auto f = [](GameMainHandle *game, - Generator *generator, - std::shared_ptr callback) { - QString error_msg; - - if (!game->is_game_running()) { - error_msg = "no open game to save"; - } - else { - auto filename = generator->getv("load_filename"); - gameio::save(game->get_game(), filename); - } - - emit callback->error_message(error_msg); - }; -} - -void GameSaver::clearErrors() { - this->error_string.clear(); - emit this->error_string_changed(); -} - -void GameSaver::on_processed(const QString &error_string) { - this->error_string = error_string; - emit this->error_string_changed(); -} - -} // namespace openage::gui diff --git a/libopenage/gui/game_saver.h b/libopenage/gui/game_saver.h deleted file mode 100644 index 69cbbe75d8..0000000000 --- a/libopenage/gui/game_saver.h +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -namespace openage { -namespace gui { - -class GameMainLink; -class GeneratorLink; - -class GameSaver : public QObject { - Q_OBJECT - - Q_ENUMS(State) - Q_PROPERTY(QString errorString READ get_error_string NOTIFY error_string_changed) - Q_PROPERTY(openage::gui::GeneratorLink *generatorParameters MEMBER generator_parameters NOTIFY generator_parameters_changed) - -public: - explicit GameSaver(QObject *parent = nullptr); - virtual ~GameSaver(); - - QString get_error_string() const; - - Q_INVOKABLE void activate(); - Q_INVOKABLE void clearErrors(); - -public slots: - void on_processed(const QString &error_string); - -signals: - void error_string_changed(); - void game_changed(); - void generator_parameters_changed(); - -private: - QString error_string; - GeneratorLink *generator_parameters; -}; - -class GameSaverSignals : public QObject { - Q_OBJECT - -public: -signals: - void error_message(const QString &error); -}; - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/game_spec_link.cpp b/libopenage/gui/game_spec_link.cpp deleted file mode 100644 index be8ebf6bb1..0000000000 --- a/libopenage/gui/game_spec_link.cpp +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "game_spec_link.h" - -#include - -namespace openage::gui { - -namespace { -const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "GameSpec"); -const int registration_of_ptr = qRegisterMetaType>("shared_ptr"); -} // namespace - -GameSpecLink::GameSpecLink(QObject *parent) : - GuiItemQObject{parent}, - QQmlParserStatus{}, - GuiItem{this}, - state{}, - active{}, - terrain_id_count{} { - Q_UNUSED(registration); - Q_UNUSED(registration_of_ptr); -} - -GameSpecLink::~GameSpecLink() = default; - -void GameSpecLink::classBegin() { -} - -void GameSpecLink::on_core_adopted() { - auto core = unwrap(this); - QObject::connect(core->gui_signals.get(), &GameSpecSignals::load_job_finished, this, &GameSpecLink::on_load_job_finished); - QObject::connect(core->gui_signals.get(), &GameSpecSignals::game_spec_loaded, this, &GameSpecLink::on_game_spec_loaded); -} - -void GameSpecLink::componentComplete() { - this->on_load_job_finished(); -} - -void GameSpecLink::on_load_job_finished() { - static auto f = [](GameSpecHandle *_this) { - _this->announce_spec(); - }; - this->i(f); -} - -void GameSpecLink::on_game_spec_loaded(std::shared_ptr loaded_game_spec) { - this->loaded_game_spec = loaded_game_spec; - - this->terrain_id_count = this->loaded_game_spec->get_terrain_meta()->terrain_id_count; - - this->state = State::Ready; - - emit this->state_changed(); - emit this->terrain_id_count_changed(); - emit this->game_spec_loaded(this, this->loaded_game_spec); -} - -std::shared_ptr GameSpecLink::get_loaded_spec() { - return this->loaded_game_spec; -} - -GameSpecLink::State GameSpecLink::get_state() const { - return this->state; -} - -void GameSpecLink::invalidate() { - static auto f = [](GameSpecHandle *_this) { - _this->invalidate(); - }; - this->i(f); - - this->set_state(this->active ? State::Loading : State::Null); -} - -bool GameSpecLink::get_active() const { - return this->active; -} - -void GameSpecLink::set_active(bool active) { - static auto f = [](GameSpecHandle *_this, bool active) { - _this->set_active(active); - }; - this->s(f, this->active, active); - - this->set_state(this->active && this->state == State::Null ? State::Loading : this->state); -} - -int GameSpecLink::get_terrain_id_count() const { - return this->terrain_id_count; -} - -void GameSpecLink::set_state(GameSpecLink::State state) { - if (state != this->state) { - this->state = state; - emit this->state_changed(); - } -} - -} // namespace openage::gui diff --git a/libopenage/gui/game_spec_link.h b/libopenage/gui/game_spec_link.h deleted file mode 100644 index c896227d23..0000000000 --- a/libopenage/gui/game_spec_link.h +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include -#include - -#include "guisys/link/gui_item.h" - -#include "../gamestate/old/game_spec.h" - -namespace openage { - -class GameSpec; - -namespace gui { - -class GameSpecLink; - -} // namespace gui -} // namespace openage - -namespace qtsdl { -template <> -struct Wrap { - using Type = openage::gui::GameSpecLink; -}; - -template <> -struct Unwrap { - using Type = openage::GameSpecHandle; -}; - -} // namespace qtsdl - -namespace openage { -namespace gui { - -class GameSpecLink : public qtsdl::GuiItemQObject - , public QQmlParserStatus - , public qtsdl::GuiItem { - Q_OBJECT - - Q_INTERFACES(QQmlParserStatus) - Q_ENUMS(State) - Q_PROPERTY(State state READ get_state NOTIFY state_changed) - Q_PROPERTY(bool active READ get_active WRITE set_active) - Q_PROPERTY(int terrainIdCount READ get_terrain_id_count NOTIFY terrain_id_count_changed) - -public: - explicit GameSpecLink(QObject *parent = nullptr); - virtual ~GameSpecLink(); - - enum class State { - Null, - Loading, - Ready - }; - - State get_state() const; - - bool get_active() const; - void set_active(bool active); - - int get_terrain_id_count() const; - - Q_INVOKABLE void invalidate(); - - std::shared_ptr get_loaded_spec(); - -signals: - /** - * Pass loaded assets to the image provider. - * - * Provider will check if it's still attached to that spec. - * Also it may be invalidated in the meantime, so share the ownership. - */ - void game_spec_loaded(GameSpecLink *game_spec, std::shared_ptr loaded_game_spec); - - void state_changed(); - void terrain_id_count_changed(); - -private slots: - void on_load_job_finished(); - void on_game_spec_loaded(std::shared_ptr loaded_game_spec); - -private: - virtual void classBegin() override; - virtual void on_core_adopted() override; - virtual void componentComplete() override; - - void set_state(State state); - - State state; - bool active; - int terrain_id_count; - - std::shared_ptr loaded_game_spec; -}; - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/generator_link.cpp b/libopenage/gui/generator_link.cpp deleted file mode 100644 index 1b0be636aa..0000000000 --- a/libopenage/gui/generator_link.cpp +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "generator_link.h" - -#include - -#include "guisys/link/gui_property_map_impl.h" - -namespace openage::gui { - -namespace -{ -const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "GeneratorParameters"); -} - -GeneratorLink::GeneratorLink(QObject *parent) - : - GuiListModel{parent}, - GuiItemListModel{this} { - Q_UNUSED(registration); -} - -GeneratorLink::~GeneratorLink() = default; - -} // namespace openage::gui diff --git a/libopenage/gui/generator_link.h b/libopenage/gui/generator_link.h deleted file mode 100644 index 7dd3f0567f..0000000000 --- a/libopenage/gui/generator_link.h +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include "gamestate/old/generator.h" - -#include "guisys/link/gui_item_list_model.h" -#include "guisys/link/gui_list_model.h" - -namespace openage { -class Generator; -namespace gui { -class GeneratorLink; -} -} // namespace openage - -namespace qtsdl { -template <> -struct Wrap { - using Type = openage::gui::GeneratorLink; -}; - -template <> -struct Unwrap { - using Type = openage::Generator; -}; - -} // namespace qtsdl - -namespace openage { -namespace gui { - -class GeneratorLink : public qtsdl::GuiListModel - , public qtsdl::GuiItemListModel { - Q_OBJECT - -public: - GeneratorLink(QObject *parent = nullptr); - virtual ~GeneratorLink(); -}; - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/integration/private/CMakeLists.txt b/libopenage/gui/integration/private/CMakeLists.txt index f727980b6f..88be5e9db6 100644 --- a/libopenage/gui/integration/private/CMakeLists.txt +++ b/libopenage/gui/integration/private/CMakeLists.txt @@ -1,11 +1,9 @@ add_sources(libopenage gui_filled_texture_handles.cpp - gui_game_spec_image_provider_impl.cpp gui_image_provider_link.cpp gui_log.cpp gui_make_standalone_subtexture.cpp gui_standalone_subtexture.cpp gui_texture.cpp - gui_texture_factory.cpp gui_texture_handle.cpp ) diff --git a/libopenage/gui/integration/private/gui_game_spec_image_provider_impl.cpp b/libopenage/gui/integration/private/gui_game_spec_image_provider_impl.cpp deleted file mode 100644 index 01579aa8fa..0000000000 --- a/libopenage/gui/integration/private/gui_game_spec_image_provider_impl.cpp +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "gui_game_spec_image_provider_impl.h" - -#include - -#include - -#include "../../../error/error.h" - -#include "../../../gamestate/old/game_spec.h" -#include "../../guisys/private/gui_event_queue_impl.h" - -#include "gui_filled_texture_handles.h" -#include "gui_texture_factory.h" - -namespace openage::gui { - -GuiGameSpecImageProviderImpl::GuiGameSpecImageProviderImpl(qtsdl::GuiEventQueue *render_updater) : - GuiImageProviderImpl{}, - invalidated{}, - filled_handles{std::make_shared()}, - ended{} { - QThread *render_thread = qtsdl::GuiEventQueueImpl::impl(render_updater)->get_thread(); - this->render_thread_callback.moveToThread(render_thread); - QObject::connect(&this->render_thread_callback, &qtsdl::GuiCallback::process_blocking, &this->render_thread_callback, &qtsdl::GuiCallback::process, render_thread != QThread::currentThread() ? Qt::BlockingQueuedConnection : Qt::DirectConnection); -} - -GuiGameSpecImageProviderImpl::~GuiGameSpecImageProviderImpl() = default; - -void GuiGameSpecImageProviderImpl::on_game_spec_loaded(const std::shared_ptr &loaded_game_spec) { - ENSURE(loaded_game_spec, "spec hasn't been checked or was invalidated"); - - std::unique_lock lck{this->loaded_game_spec_mutex}; - - if (invalidated || loaded_game_spec != this->loaded_game_spec) { - migrate_to_new_game_spec(loaded_game_spec); - invalidated = false; - - lck.unlock(); - this->loaded_game_spec_cond.notify_one(); - } -} - -void GuiGameSpecImageProviderImpl::migrate_to_new_game_spec(const std::shared_ptr &loaded_game_spec) { - ENSURE(loaded_game_spec, "spec hasn't been checked or was invalidated"); - - if (this->loaded_game_spec) { - auto keep_old_game_spec = this->loaded_game_spec; - - // Need to set up this because the load functions use this->loaded_game_spec internally. - this->loaded_game_spec = loaded_game_spec; - - emit this->render_thread_callback.process_blocking([this] { - using namespace std::placeholders; - this->filled_handles->refresh_all_handles_with_texture(std::bind(&GuiGameSpecImageProviderImpl::overwrite_texture_handle, this, _1, _2, _3)); - }); - } - else { - this->loaded_game_spec = loaded_game_spec; - } -} - -void GuiGameSpecImageProviderImpl::overwrite_texture_handle(const QString &id, const QSize &requested_size, SizedTextureHandle *filled_handle) { - const TextureHandle texture_handle = this->get_texture_handle(id); - *filled_handle = {texture_handle, aspect_fit_size(texture_handle, requested_size)}; -} - -void GuiGameSpecImageProviderImpl::on_game_spec_invalidated() { - std::unique_lock lck{this->loaded_game_spec_mutex}; - - if (this->loaded_game_spec) { - invalidated = true; - } -} - -GuiFilledTextureHandleUser GuiGameSpecImageProviderImpl::fill_texture_handle(const QString &id, const QSize &requested_size, SizedTextureHandle *filled_handle) { - this->overwrite_texture_handle(id, requested_size, filled_handle); - return GuiFilledTextureHandleUser(this->filled_handles, id, requested_size, filled_handle); -} - -QQuickTextureFactory *GuiGameSpecImageProviderImpl::requestTexture(const QString &id, QSize *size, const QSize &requestedSize) { - std::unique_lock lck{this->loaded_game_spec_mutex}; - - this->loaded_game_spec_cond.wait(lck, [this] { return this->ended || this->loaded_game_spec; }); - - if (this->ended) { - qWarning("ImageProvider was stopped during the load, so it'll appear like the requestTexture() isn't implemented."); - return this->GuiImageProviderImpl::requestTexture(id, size, requestedSize); - } - else { - auto tex_factory = new GuiTextureFactory{this, id, requestedSize}; - *size = tex_factory->textureSize(); - return tex_factory; - } -} - -void GuiGameSpecImageProviderImpl::give_up() { - { - std::unique_lock lck{this->loaded_game_spec_mutex}; - this->ended = true; - } - - this->loaded_game_spec_cond.notify_one(); -} - -} // namespace openage::gui diff --git a/libopenage/gui/integration/private/gui_game_spec_image_provider_impl.h b/libopenage/gui/integration/private/gui_game_spec_image_provider_impl.h deleted file mode 100644 index 3fd199dbd0..0000000000 --- a/libopenage/gui/integration/private/gui_game_spec_image_provider_impl.h +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include -#include -#include - -#include "../../guisys/private/gui_callback.h" -#include "../../guisys/private/gui_image_provider_impl.h" -#include "gui_filled_texture_handles.h" -#include "gui_texture_handle.h" - -namespace qtsdl { - -class GuiEventQueue; -class GuiEventQueueImpl; - -} // namespace qtsdl - -namespace openage { - -class GameSpec; -class Texture; - -namespace gui { - -class GuiTexture; - -/** - * Exposes game textures to the Qt. - */ -class GuiGameSpecImageProviderImpl : public qtsdl::GuiImageProviderImpl { -public: - explicit GuiGameSpecImageProviderImpl(qtsdl::GuiEventQueue *render_updater); - virtual ~GuiGameSpecImageProviderImpl(); - - /** - * Unblocks the provider. - * - * Refreshes already loaded assets (if this->loaded_game_spec wasn't null before the call). - * - * @param loaded_game_spec new source (can't be null) - */ - void on_game_spec_loaded(const std::shared_ptr &loaded_game_spec); - - /** - * Set to every sprite the 'missing texture' from current spec. - * - * Needed as a visible reaction to setting the property to null. - * We can't unload the textures without recreating the engine, so we keep the old source. - */ - void on_game_spec_invalidated(); - - /** - * Fills in the provided handle object. - * - * Memorizes pointer to it in order to update it if the source changes. - * - * @return pointer to all memorized handles, so the client can unsubscribe. - */ - GuiFilledTextureHandleUser fill_texture_handle(const QString &id, const QSize &requested_size, SizedTextureHandle *filled_handle); - -protected: - std::shared_ptr loaded_game_spec; - - /** - * When true, still uses the old source but shows the 'missing texture'. - */ - bool invalidated; - -private: - virtual TextureHandle get_texture_handle(const QString &id) = 0; - virtual QQuickTextureFactory *requestTexture(const QString &id, QSize *size, const QSize &requestedSize) override; - virtual void give_up() override; - - /** - * Change the already produced texture handles to use new source. - */ - void migrate_to_new_game_spec(const std::shared_ptr &loaded_game_spec); - - void overwrite_texture_handle(const QString &id, const QSize &requested_size, SizedTextureHandle *filled_handle); - - /** - * Changing the texture handles underneath the QSGTexture to reflect the reload of the GameSpec. - * - * It's not a proper Qt usage, so the live reload of the game assets for the gui may break in future Qt releases. - * When it breaks, this feature should be implemented via the recreation of the qml engine. - */ - std::shared_ptr filled_handles; - - std::mutex loaded_game_spec_mutex; - std::condition_variable loaded_game_spec_cond; - bool ended; - - qtsdl::GuiCallback render_thread_callback; -}; - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/integration/private/gui_image_provider_link.cpp b/libopenage/gui/integration/private/gui_image_provider_link.cpp index f2b72eecb3..a770c0f552 100644 --- a/libopenage/gui/integration/private/gui_image_provider_link.cpp +++ b/libopenage/gui/integration/private/gui_image_provider_link.cpp @@ -11,42 +11,14 @@ #include "../../guisys/link/qml_engine_with_singleton_items_info.h" #include "../../guisys/link/qtsdl_checked_static_cast.h" -#include "../../game_spec_link.h" -#include "gui_game_spec_image_provider_impl.h" - namespace openage::gui { -GuiImageProviderLink::GuiImageProviderLink(QObject *parent, GuiGameSpecImageProviderImpl &image_provider) : - QObject{parent}, - image_provider{image_provider}, - game_spec{} { +GuiImageProviderLink::GuiImageProviderLink(QObject *parent) : + QObject{parent} { } GuiImageProviderLink::~GuiImageProviderLink() = default; -GameSpecLink *GuiImageProviderLink::get_game_spec() const { - return this->game_spec; -} - -void GuiImageProviderLink::set_game_spec(GameSpecLink *game_spec) { - if (this->game_spec != game_spec) { - if (this->game_spec) - QObject::disconnect(this->game_spec.data(), &GameSpecLink::game_spec_loaded, this, &GuiImageProviderLink::on_game_spec_loaded); - - if (!game_spec) - this->image_provider.on_game_spec_invalidated(); - - this->game_spec = game_spec; - - if (this->game_spec) { - QObject::connect(this->game_spec.data(), &GameSpecLink::game_spec_loaded, this, &GuiImageProviderLink::on_game_spec_loaded); - - if (std::shared_ptr spec = this->game_spec->get_loaded_spec()) - this->image_provider.on_game_spec_loaded(spec); - } - } -} - QObject *GuiImageProviderLink::provider(QQmlEngine *engine, const char *id) { qtsdl::QmlEngineWithSingletonItemsInfo *engine_with_singleton_items_info = qtsdl::checked_static_cast(engine); auto image_providers = engine_with_singleton_items_info->get_image_providers(); @@ -58,12 +30,7 @@ QObject *GuiImageProviderLink::provider(QQmlEngine *engine, const char *id) { ENSURE(found_it != std::end(image_providers), "The image provider '" << id << "' wasn't created or wasn't passed to the QML engine creation function."); // owned by the QML engine - return new GuiImageProviderLink{nullptr, *qtsdl::checked_static_cast(*found_it)}; -} - -void GuiImageProviderLink::on_game_spec_loaded(GameSpecLink *game_spec, std::shared_ptr loaded_game_spec) { - if (this->game_spec == game_spec) - this->image_provider.on_game_spec_loaded(loaded_game_spec); + return nullptr; } } // namespace openage::gui diff --git a/libopenage/gui/integration/private/gui_image_provider_link.h b/libopenage/gui/integration/private/gui_image_provider_link.h index 510214f652..5090edcf2c 100644 --- a/libopenage/gui/integration/private/gui_image_provider_link.h +++ b/libopenage/gui/integration/private/gui_image_provider_link.h @@ -12,46 +12,16 @@ QT_FORWARD_DECLARE_CLASS(QJSEngine) namespace openage { -class GameSpec; - namespace gui { -class GuiGameSpecImageProviderImpl; -class GameSpecLink; - class GuiImageProviderLink : public QObject { Q_OBJECT - Q_PROPERTY(openage::gui::GameSpecLink *gameSpec READ get_game_spec WRITE set_game_spec) - public: - explicit GuiImageProviderLink(QObject *parent, GuiGameSpecImageProviderImpl &image_provider); + explicit GuiImageProviderLink(QObject *parent); virtual ~GuiImageProviderLink(); - GameSpecLink *get_game_spec() const; - - /** - * Sets the game spec to load textures from. - * - * Setting to null doesn't detach from the spec, but picks the - * 'missing texture' from that spec and sets it to every sprite. - */ - void set_game_spec(GameSpecLink *game_spec); - static QObject *provider(QQmlEngine *, const char *id); - -private slots: - /** - * Pass loaded assets to the image provider. - * - * Need to check if we still attached to that spec. - * Also it may be invalidated in the meantime, so share the ownership. - */ - void on_game_spec_loaded(GameSpecLink *game_spec, std::shared_ptr loaded_game_spec); - -private: - GuiGameSpecImageProviderImpl &image_provider; - QPointer game_spec; }; } // namespace gui diff --git a/libopenage/gui/integration/private/gui_texture_factory.cpp b/libopenage/gui/integration/private/gui_texture_factory.cpp deleted file mode 100644 index 35bc04cea1..0000000000 --- a/libopenage/gui/integration/private/gui_texture_factory.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "gui_texture_factory.h" - -#include "gui_filled_texture_handles.h" -#include "gui_game_spec_image_provider_impl.h" -#include "gui_texture.h" - -namespace openage::gui { - -GuiTextureFactory::GuiTextureFactory(GuiGameSpecImageProviderImpl *provider, const QString &id, const QSize &requested_size) : - texture_handle(), - texture_handle_user{provider->fill_texture_handle(id, requested_size, &this->texture_handle)} { -} - -GuiTextureFactory::~GuiTextureFactory() = default; - -QSGTexture *GuiTextureFactory::createTexture(QQuickWindow *window) const { - Q_UNUSED(window); - return new GuiTexture{this->texture_handle}; -} - -int GuiTextureFactory::textureByteCount() const { - return 0; -} - -QSize GuiTextureFactory::textureSize() const { - return openage::gui::textureSize(this->texture_handle); -} - -} // namespace openage::gui diff --git a/libopenage/gui/integration/private/gui_texture_factory.h b/libopenage/gui/integration/private/gui_texture_factory.h deleted file mode 100644 index bf4007c0a3..0000000000 --- a/libopenage/gui/integration/private/gui_texture_factory.h +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include - -#include "gui_texture_handle.h" -#include "gui_filled_texture_handles.h" - -namespace openage { -namespace gui { - -class GuiGameSpecImageProviderImpl; - -class GuiTextureFactory : public QQuickTextureFactory { - Q_OBJECT - -public: - explicit GuiTextureFactory(GuiGameSpecImageProviderImpl *provider, const QString &id, const QSize &requested_size); - virtual ~GuiTextureFactory(); - - virtual QSGTexture* createTexture(QQuickWindow *window) const override; - virtual int textureByteCount() const override; - virtual QSize textureSize() const override; - -private: - SizedTextureHandle texture_handle; - GuiFilledTextureHandleUser texture_handle_user; -}; - -}} // namespace openage::gui diff --git a/libopenage/gui/resources_list_model.cpp b/libopenage/gui/resources_list_model.cpp deleted file mode 100644 index d804d147b4..0000000000 --- a/libopenage/gui/resources_list_model.cpp +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. - -#include "resources_list_model.h" - -#include -#include - -#include - -#include "../error/error.h" - -namespace openage::gui { - -namespace { -const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "Resources"); - -const auto resource_type_count = static_cast::type>(game_resource::RESOURCE_TYPE_COUNT); - -} // namespace - -ResourcesListModel::ResourcesListModel(QObject *parent) : - QAbstractListModel(parent), - amounts(resource_type_count) { - Q_UNUSED(registration); -} - -ResourcesListModel::~ResourcesListModel() = default; - -void ResourcesListModel::on_resource_changed(game_resource resource, int amount) { - int i = static_cast(resource); - ENSURE(i >= 0 && i < std::distance(std::begin(this->amounts), std::end(this->amounts)), "Res type index is out of range: '" << i << "'."); - - if (this->amounts[i] != amount) { - auto model_index = this->index(i); - - this->amounts[i] = amount; - emit this->dataChanged(model_index, model_index, QVector{Qt::DisplayRole}); - } -} - -int ResourcesListModel::rowCount(const QModelIndex &) const { - ENSURE(resource_type_count == this->amounts.size(), "Res type count is compile-time const '" << resource_type_count << "', but got '" << this->amounts.size() << "'."); - return this->amounts.size(); -} - -QVariant ResourcesListModel::data(const QModelIndex &index, int role) const { - const int i = index.row(); - - switch (role) { - case Qt::DisplayRole: - ENSURE(i >= 0 && i < std::distance(std::begin(this->amounts), std::end(this->amounts)), "Res type index is out of range: '" << i << "'."); - return this->amounts[index.row()]; - - default: - return QVariant{}; - } -} - -} // namespace openage::gui diff --git a/libopenage/gui/resources_list_model.h b/libopenage/gui/resources_list_model.h deleted file mode 100644 index 51ef6425bb..0000000000 --- a/libopenage/gui/resources_list_model.h +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -#include - -#include "../gamestate/old/resource.h" - -namespace openage { -namespace gui { - -/** - * Resource table for the gui. - */ -class ResourcesListModel : public QAbstractListModel { - Q_OBJECT - -public: - ResourcesListModel(QObject *parent = nullptr); - virtual ~ResourcesListModel(); - -private slots: - void on_resource_changed(game_resource resource, int amount); - -private: - virtual int rowCount(const QModelIndex &) const override; - virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - std::vector amounts; -}; - -} // namespace gui -} // namespace openage diff --git a/libopenage/pathfinding/a_star.cpp b/libopenage/pathfinding/a_star.cpp index 03b85def97..7d9d9275cb 100644 --- a/libopenage/pathfinding/a_star.cpp +++ b/libopenage/pathfinding/a_star.cpp @@ -1,4 +1,4 @@ -// Copyright 2014-2019 the openage authors. See copying.md for legal info. +// Copyright 2014-2023 the openage authors. See copying.md for legal info. /** @file * @@ -17,10 +17,9 @@ #include "../datastructure/pairing_heap.h" #include "../log/log.h" #include "../terrain/terrain.h" -#include "../terrain/terrain_object.h" #include "../util/strings.h" -#include "path.h" #include "heuristics.h" +#include "path.h" namespace openage { @@ -30,7 +29,6 @@ namespace path { Path to_point(coord::phys3 start, coord::phys3 end, std::function passable) { - auto valid_end = [&](const coord::phys3 &point) -> bool { return euclidean_squared_cost(point, end) < path_grid_size.to_float(); }; @@ -41,18 +39,18 @@ Path to_point(coord::phys3 start, } -Path to_object(openage::TerrainObject *to_move, - openage::TerrainObject *end, - coord::phys_t rad) { - coord::phys3 start = to_move->pos.draw; - auto valid_end = [&](const coord::phys3 &pos) -> bool { - return end->from_edge(pos) < rad; - }; - auto heuristic = [&](const coord::phys3 &pos) -> cost_t { - return (end->from_edge(pos) - to_move->min_axis() / 2L).to_float(); - }; - return a_star(start, valid_end, heuristic, to_move->passable); -} +// Path to_object(openage::TerrainObject *to_move, +// openage::TerrainObject *end, +// coord::phys_t rad) { +// coord::phys3 start = to_move->pos.draw; +// auto valid_end = [&](const coord::phys3 &pos) -> bool { +// return end->from_edge(pos) < rad; +// }; +// auto heuristic = [&](const coord::phys3 &pos) -> cost_t { +// return (end->from_edge(pos) - to_move->min_axis() / 2L).to_float(); +// }; +// return a_star(start, valid_end, heuristic, to_move->passable); +// } Path find_nearest(coord::phys3 start, @@ -67,7 +65,6 @@ Path a_star(coord::phys3 start, std::function valid_end, std::function heuristic, std::function passable) { - // path node storage, always provides cheapest next node. heap_t node_candidates; @@ -93,9 +90,7 @@ Path a_star(coord::phys3 start, // node to terminate the search was found if (valid_end(best_candidate->position)) { - log::log(MSG(dbg) << - "path cost is " << - util::FloatFixed<3, 8>{closest_node->future_cost}); + log::log(MSG(dbg) << "path cost is " << util::FloatFixed<3, 8>{closest_node->future_cost}); return best_candidate->generate_backtrace(); } @@ -128,26 +123,26 @@ Path a_star(coord::phys3 start, } // update new cost knowledge - neighbor->past_cost = new_past_cost; - neighbor->future_cost = neighbor->past_cost + neighbor->heuristic_cost; + neighbor->past_cost = new_past_cost; + neighbor->future_cost = neighbor->past_cost + neighbor->heuristic_cost; neighbor->path_predecessor = best_candidate; if (not_visited) { neighbor->heap_node = node_candidates.push(neighbor); visited_tiles[neighbor->position] = neighbor; - } else { + } + else { node_candidates.decrease(neighbor->heap_node); } } } } - log::log(MSG(dbg) << - "incomplete path cost is " << - util::FloatFixed<3, 8>{closest_node->future_cost}); + log::log(MSG(dbg) << "incomplete path cost is " << util::FloatFixed<3, 8>{closest_node->future_cost}); return closest_node->generate_backtrace(); } -}} // namespace openage::path +} // namespace path +} // namespace openage diff --git a/libopenage/pathfinding/a_star.h b/libopenage/pathfinding/a_star.h index 15d95066e1..a7cc5aa75e 100644 --- a/libopenage/pathfinding/a_star.h +++ b/libopenage/pathfinding/a_star.h @@ -1,4 +1,4 @@ -// Copyright 2014-2016 the openage authors. See copying.md for legal info. +// Copyright 2014-2023 the openage authors. See copying.md for legal info. #pragma once @@ -9,8 +9,6 @@ namespace openage { -class TerrainObject; - namespace path { /** @@ -23,9 +21,9 @@ Path to_point(coord::phys3 start, /** * path between 2 objects, with how close to come to end point */ -Path to_object(TerrainObject *to_move, - TerrainObject *end, - coord::phys_t rad); +// Path to_object(TerrainObject *to_move, +// TerrainObject *end, +// coord::phys_t rad); /** * path to nearest object with lambda diff --git a/libopenage/terrain/CMakeLists.txt b/libopenage/terrain/CMakeLists.txt index c7ae65ad65..753cf4249a 100644 --- a/libopenage/terrain/CMakeLists.txt +++ b/libopenage/terrain/CMakeLists.txt @@ -1,6 +1,4 @@ add_sources(libopenage terrain.cpp terrain_chunk.cpp - terrain_object.cpp - terrain_search.cpp ) diff --git a/libopenage/terrain/terrain.cpp b/libopenage/terrain/terrain.cpp index 62eeefc32b..7ad26a0899 100644 --- a/libopenage/terrain/terrain.cpp +++ b/libopenage/terrain/terrain.cpp @@ -16,7 +16,6 @@ #include "../util/strings.h" #include "terrain_chunk.h" -#include "terrain_object.h" namespace openage { @@ -139,23 +138,6 @@ TileContent *Terrain::get_data(const coord::tile &position) { } } -TerrainObject *Terrain::obj_at_point(const coord::phys3 &point) { - coord::tile t = point.to_tile(); - TileContent *tc = this->get_data(t); - if (!tc) { - return nullptr; - } - - // prioritise selecting the smallest object - TerrainObject *smallest = nullptr; - for (auto obj_ptr : tc->obj) { - if (obj_ptr->contains(point) && (!smallest || obj_ptr->min_axis() < smallest->min_axis())) { - smallest = obj_ptr; - } - } - return smallest; -} - bool Terrain::validate_terrain(terrain_t terrain_id) { if (terrain_id >= static_cast(this->meta->terrain_id_count)) { throw Error(MSG(err) << "Requested terrain_id is out of range: " << terrain_id); @@ -301,7 +283,7 @@ struct terrain_render_data Terrain::create_draw_advice(const coord::tile &ab, // ordered set of objects on the terrain (buildings.) // it's ordered by the visibility layers. - auto objects = &data.objects; + // auto objects = &data.objects; coord::tile gb = {gh.ne, ab.se}; coord::tile cf = {cd.ne, ef.se}; @@ -321,9 +303,9 @@ struct terrain_render_data Terrain::create_draw_advice(const coord::tile &ab, // TODO: make the terrain independent of objects standing on it. TileContent *tile_content = this->get_data(tilepos); if (tile_content != nullptr) { - for (auto obj_item : tile_content->obj) { - objects->insert(obj_item); - } + // for (auto obj_item : tile_content->obj) { + // objects->insert(obj_item); + // } } } } diff --git a/libopenage/terrain/terrain.h b/libopenage/terrain/terrain.h index 4db8fdbcb1..02ceba1cae 100644 --- a/libopenage/terrain/terrain.h +++ b/libopenage/terrain/terrain.h @@ -19,7 +19,6 @@ namespace openage { class RenderOptions; class TerrainChunk; -class TerrainObject; /** * type that for terrain ids. @@ -53,7 +52,6 @@ class TileContent { TileContent(); ~TileContent(); terrain_t terrain_id; - std::vector obj; }; @@ -140,7 +138,6 @@ struct tile_draw_data { */ struct terrain_render_data { std::vector tiles; - std::set> objects; }; /** @@ -229,11 +226,6 @@ class Terrain { */ TileContent *get_data(const coord::tile &position); - /** - * an object which contains the given point, null otherwise - */ - TerrainObject *obj_at_point(const coord::phys3 &point); - /** * get the neighbor chunks of a given chunk. * diff --git a/libopenage/terrain/terrain_chunk.cpp b/libopenage/terrain/terrain_chunk.cpp index aec7d2c6ec..0fd77923dc 100644 --- a/libopenage/terrain/terrain_chunk.cpp +++ b/libopenage/terrain/terrain_chunk.cpp @@ -12,7 +12,6 @@ #include "../util/misc.h" #include "terrain.h" -#include "terrain_object.h" namespace openage { diff --git a/libopenage/terrain/terrain_chunk.h b/libopenage/terrain/terrain_chunk.h index 676cfcc1e4..683d46c0fd 100644 --- a/libopenage/terrain/terrain_chunk.h +++ b/libopenage/terrain/terrain_chunk.h @@ -14,7 +14,6 @@ namespace openage { class Terrain; class TerrainChunk; class TileContent; -class TerrainObject; /** diff --git a/libopenage/terrain/terrain_object.cpp b/libopenage/terrain/terrain_object.cpp deleted file mode 100644 index a81716491a..0000000000 --- a/libopenage/terrain/terrain_object.cpp +++ /dev/null @@ -1,463 +0,0 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. - -#include "terrain_object.h" - -#include -#include - -#include "../coord/phys.h" -#include "../coord/pixel.h" -#include "../coord/tile.h" -#include "../error/error.h" -#include "../unit/unit.h" - -#include "terrain.h" -#include "terrain_chunk.h" - -namespace openage { - -TerrainObject::TerrainObject(Unit &u) : - unit(u), - passable{[](const coord::phys3 &) -> bool { return true; }}, - state{object_state::removed}, - occupied_chunk_count{0}, - parent{nullptr} { -} - -TerrainObject::~TerrainObject() { - // remove all connections from terrain - this->unit.log(MSG(dbg) << "Cleanup terrain object"); - this->remove(); -} - -bool TerrainObject::is_floating() const { - // if parent is floating then all children also are - if (this->parent && this->parent->is_floating()) { - return true; - } - return this->state == object_state::floating; -} - -bool TerrainObject::is_placed() const { - // if object has a parent it must be placed - if (this->parent && !this->parent->is_placed()) { - return false; - } - return this->state == object_state::placed || this->state == object_state::placed_no_collision; -} - - -bool TerrainObject::check_collisions() const { - // if object has a parent it must be placed - if (this->parent && !this->parent->is_placed()) { - return false; - } - return this->state == object_state::placed; -} - -bool TerrainObject::place(object_state init_state) { - if (this->state == object_state::removed) { - throw Error(MSG(err) << "Building cannot change state with no position"); - } - - // remove any other floating objects - // which intersect with the new placement - // if non-floating objects are on the foundation - // then this placement will fail - for (coord::tile temp_pos : tile_list(this->pos)) { - std::vector to_remove; - TerrainChunk *chunk = this->get_terrain()->get_chunk(temp_pos); - - if (chunk == nullptr) { - continue; - } - - for (auto obj : chunk->get_data(temp_pos)->obj) { - // ignore self and annexes of self - if (obj != this && obj->get_parent() != this) { - if (obj->is_floating()) { - // floating objects get removed - to_remove.push_back(obj); - } - else if (obj->check_collisions()) { - // solid objects obstruct placement - return false; - } - } - } - - // all obstructing objects get deleted - for (auto remove_obj : to_remove) { - remove_obj->unit.location = nullptr; - } - } - - // set new state - this->state = init_state; - return true; -} - -bool TerrainObject::place(const std::shared_ptr &t, coord::phys3 &position, object_state init_state) { - if (this->state != object_state::removed) { - throw Error(MSG(err) << "This object has already been placed."); - } - else if (init_state == object_state::removed) { - throw Error(MSG(err) << "Cannot place an object with removed state."); - } - - // use passiblity test - if (not this->passable(position)) { - return false; - } - - // place on terrain - this->place_unchecked(t, position); - - // set state - this->state = init_state; - return true; -} - -bool TerrainObject::move(coord::phys3 &position) { - if (this->state == object_state::removed) { - return false; - } - auto old_state = this->state; - - // TODO should do outside of this function - bool can_move = this->passable(position); - if (can_move) { - this->remove(); - this->place_unchecked(this->get_terrain(), position); - this->state = old_state; - } - return can_move; -} - -void TerrainObject::remove() { - // remove all children first - for (auto &c : this->children) { - c->remove(); - } - this->children.clear(); - - if (this->occupied_chunk_count == 0 || this->state == object_state::removed) { - return; - } - - for (coord::tile temp_pos : tile_list(this->pos)) { - TerrainChunk *chunk = this->get_terrain()->get_chunk(temp_pos); - - if (chunk == nullptr) { - continue; - } - - auto &v = chunk->get_data(temp_pos.get_pos_on_chunk())->obj; - auto position_it = std::remove_if( - std::begin(v), - std::end(v), - [this](TerrainObject *obj) { - return this == obj; - }); - v.erase(position_it, std::end(v)); - } - - this->occupied_chunk_count = 0; - this->state = object_state::removed; -} - -void TerrainObject::set_ground(int id, int additional) { - if (not this->is_placed()) { - throw Error(MSG(err) << "Setting ground for object that is not placed yet."); - } - - coord::tile temp_pos = this->pos.start; - temp_pos.ne -= additional; - temp_pos.se -= additional; - while (temp_pos.ne < this->pos.end.ne + additional) { - while (temp_pos.se < this->pos.end.se + additional) { - TerrainChunk *chunk = this->get_terrain()->get_chunk(temp_pos); - - if (chunk == nullptr) { - continue; - } - - chunk->get_data(temp_pos.get_pos_on_chunk())->terrain_id = id; - temp_pos.se++; - } - temp_pos.se = this->pos.start.se - additional; - temp_pos.ne++; - } -} - -const TerrainObject *TerrainObject::get_parent() const { - return this->parent; -} - -std::vector TerrainObject::get_children() const { - // TODO: a better performing way of doing this - // for example accept a lambda to use for each element - // or maintain a duplicate class field for raw pointers - - std::vector result; - for (auto &obj : this->children) { - result.push_back(obj.get()); - } - return result; -} - -bool TerrainObject::operator<(const TerrainObject &other) { - if (this == &other) { - return false; - } - - auto this_ne = this->pos.draw.ne; - auto this_se = this->pos.draw.se; - auto other_ne = other.pos.draw.ne; - auto other_se = other.pos.draw.se; - - auto this_ypos = this_ne - this_se; - auto other_ypos = other_ne - other_se; - - if (this_ypos < other_ypos) { - return false; - } - else if (this_ypos == other_ypos) { - if (this_ne > other_ne) { - return false; - } - else if (this_ne == other_ne) { - return this_se > other_se; - } - } - - return true; -} - -void TerrainObject::place_unchecked(const std::shared_ptr &t, coord::phys3 &position) { - // storing the position: - this->pos = this->get_range(position, *t); - this->terrain = t; - this->occupied_chunk_count = 0; - - bool chunk_known = false; - - - // set pointers to this object on each terrain tile - // where the building will stand and block the ground - for (coord::tile temp_pos : tile_list(this->pos)) { - TerrainChunk *chunk = this->get_terrain()->get_chunk(temp_pos); - - if (chunk == nullptr) { - continue; - } - - for (int c = 0; c < this->occupied_chunk_count; c++) { - if (this->occupied_chunk[c] == chunk) { - chunk_known = true; - } - } - - if (not chunk_known) { - this->occupied_chunk[this->occupied_chunk_count] = chunk; - this->occupied_chunk_count += 1; - } - else { - chunk_known = false; - } - - chunk->get_data(temp_pos.get_pos_on_chunk())->obj.push_back(this); - } -} - -SquareObject::SquareObject(Unit &u, coord::tile_delta foundation_size) : - TerrainObject(u), - size(foundation_size) { -} - -SquareObject::~SquareObject() = default; - -tile_range SquareObject::get_range(const coord::phys3 &pos, const Terrain &terrain) const { - return building_center(pos, this->size, terrain); -} - -coord::phys_t SquareObject::from_edge(const coord::phys3 &point) const { - // clamp between start and end - coord::phys2 start_phys = this->pos.start.to_phys2(); - coord::phys2 end_phys = this->pos.end.to_phys2(); - - coord::phys_t cx = std::max(start_phys.ne, std::min(end_phys.ne, point.ne)); - coord::phys_t cy = std::max(start_phys.se, std::min(end_phys.se, point.se)); - - // distance to clamped point - coord::phys_t dx = point.ne - cx; - coord::phys_t dy = point.se - cy; - return std::hypot(dx, dy); -} - -coord::phys3 SquareObject::on_edge(const coord::phys3 &angle, coord::phys_t /* extra */) const { - // clamp between start and end - // TODO extra is unused - coord::phys2 start_phys = this->pos.start.to_phys2(); - coord::phys2 end_phys = this->pos.end.to_phys2(); - coord::phys_t cx = std::max(start_phys.ne, std::min(end_phys.ne, angle.ne)); - coord::phys_t cy = std::max(start_phys.se, std::min(end_phys.se, angle.se)); - - // todo use extra distance - return coord::phys3{cx, cy, 0}; -} - -bool SquareObject::contains(const coord::phys3 &other) const { - coord::tile other_tile = other.to_tile3().to_tile(); - - for (coord::tile check_pos : tile_list(this->pos)) { - if (check_pos == other_tile) { - return true; - } - } - return false; -} - -bool SquareObject::intersects(const TerrainObject &other, const coord::phys3 &position) const { - if (const auto *sq = dynamic_cast(&other)) { - auto terrain_ptr = this->terrain.lock(); - if (not terrain_ptr) { - throw Error{ERR << "object is not associated to a valid terrain"}; - } - - tile_range rng = this->get_range(position, *terrain_ptr); - return this->pos.end.ne < rng.start.ne - || rng.end.ne < sq->pos.start.ne - || rng.end.se < sq->pos.start.se - || rng.end.se < sq->pos.start.se; - } - else if (const auto *rad = dynamic_cast(&other)) { - auto terrain_ptr = this->terrain.lock(); - if (not terrain_ptr) { - throw Error{ERR << "object is not associated to a valid terrain"}; - } - // clamp between start and end - tile_range rng = this->get_range(position, *terrain_ptr); - coord::phys2 start_phys = rng.start.to_phys2(); - coord::phys2 end_phys = rng.end.to_phys2(); - coord::phys_t cx = std::max(start_phys.ne, std::min(end_phys.ne, rad->pos.draw.ne)); - coord::phys_t cy = std::max(start_phys.se, std::min(end_phys.se, rad->pos.draw.se)); - - // distance to square object base - coord::phys_t dx = rad->pos.draw.ne - cx; - coord::phys_t dy = rad->pos.draw.se - cy; - return std::hypot(dx, dy) < rad->phys_radius.to_double(); - } - return false; -} - -coord::phys_t SquareObject::min_axis() const { - return std::min(this->size.ne, this->size.se); -} - -RadialObject::RadialObject(Unit &u, float rad) : - TerrainObject(u), - phys_radius(rad) { -} - -RadialObject::~RadialObject() = default; - -tile_range RadialObject::get_range(const coord::phys3 &pos, const Terrain & /*terrain*/) const { - tile_range result; - - // create bounds - coord::phys3 p_start = pos, p_end = pos; - p_start.ne -= this->phys_radius; - p_start.se -= this->phys_radius; - p_end.ne += this->phys_radius; - p_end.se += this->phys_radius; - - // set result - result.start = p_start.to_tile3().to_tile(); - result.end = p_end.to_tile3().to_tile() + coord::tile_delta{1, 1}; - result.draw = pos; - return result; -} - -coord::phys_t RadialObject::from_edge(const coord::phys3 &point) const { - return std::max( - coord::phys_t(point.to_phys2().distance(this->pos.draw.to_phys2())) - this->phys_radius, - static_cast(0)); -} - -coord::phys3 RadialObject::on_edge(const coord::phys3 &angle, coord::phys_t extra) const { - return this->pos.draw + (angle - this->pos.draw).to_phys2().normalize((this->phys_radius + extra).to_double()).to_phys3(); -} - -bool RadialObject::contains(const coord::phys3 &other) const { - return this->pos.draw.to_phys2().distance(other.to_phys2()) < this->phys_radius.to_double(); -} - -bool RadialObject::intersects(const TerrainObject &other, const coord::phys3 &position) const { - if (const auto *sq = dynamic_cast(&other)) { - return sq->from_edge(position) < this->phys_radius; - } - else if (const auto *rad = dynamic_cast(&other)) { - return position.to_phys2().distance(rad->pos.draw.to_phys2()) < (this->phys_radius + rad->phys_radius).to_double(); - } - return false; -} - -coord::phys_t RadialObject::min_axis() const { - return this->phys_radius * 2; -} - -std::vector tile_list(const tile_range &rng) { - std::vector tiles; - - coord::tile check_pos = rng.start; - while (check_pos.ne < rng.end.ne) { - while (check_pos.se < rng.end.se) { - tiles.push_back(check_pos); - check_pos.se += 1; - } - check_pos.se = rng.start.se; - check_pos.ne += 1; - } - - // a case when the objects radius is zero - if (tiles.empty()) { - tiles.push_back(rng.start); - } - return tiles; -} - -tile_range building_center(coord::phys3 west, coord::tile_delta size, const Terrain &terrain) { - tile_range result; - - // TODO it should be possible that the building is placed on any position, - // not just tile positions. - result.start = west.to_tile(); - result.end = result.start + size; - - coord::phys2 draw_pos = result.start.to_phys2(); - - draw_pos.ne += coord::phys_t(size.ne / 2.0f); - draw_pos.se += coord::phys_t(size.se / 2.0f); - - result.draw = draw_pos.to_phys3(terrain); - return result; -} - -bool complete_building(Unit &u) { - if (u.has_attribute(attr_type::building)) { - auto &build = u.get_attribute(); - build.completed = 1.0f; - - // set ground under a completed building - auto target_location = u.location.get(); - bool placed_ok = target_location->place(build.completion_state); - if (placed_ok) { - target_location->set_ground(build.foundation_terrain, 0); - } - return placed_ok; - } - return false; -} - -} // namespace openage diff --git a/libopenage/terrain/terrain_object.h b/libopenage/terrain/terrain_object.h deleted file mode 100644 index 93657db11d..0000000000 --- a/libopenage/terrain/terrain_object.h +++ /dev/null @@ -1,332 +0,0 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -#include "../coord/phys.h" -#include "../coord/tile.h" - -namespace openage { - -class Terrain; -class TerrainChunk; -class Texture; -class Unit; - -/** - * only placed will enable collision checks - */ -enum class object_state { - removed, - floating, - placed, - placed_no_collision -}; - -/** - * A rectangle or square of tiles which is the minimim - * space to fit the units foundation or radius - * the end tile will have ne and se values greater or equal to - * the start tile - */ -struct tile_range { - coord::tile start{0, 0}; - coord::tile end{0, 0}; // start <= end - coord::phys3 draw{0, 0, 0}; // gets used as center point of radial objects -}; - -/** - * get all tiles in the tile range -- useful for iterating - * returns a flat list of tiles between the rectangle enclosed - * by the tile_range start and end tiles - */ -std::vector tile_list(const tile_range &rng); - -/** - * given the west most point of a building foundation and the tile_delta - * size of the foundation, this will return the tile range covered by the base, - * which includes start and end tiles, and phys3 center point (used for drawing) - */ -tile_range building_center(coord::phys3 west, coord::tile_delta size, const Terrain &terrain); - -/** - * sets a building to a fully completed state - */ -bool complete_building(Unit &); - -/** - * Base class for map location types which include square tile aligned - * positions and radial positions This enables two inheriting classes - * SquareObject and RadialObject to specify different areas of the map - * - * All TerrainObjects are owned by a Unit, to construct TerrainObjects, - * use the make_location function on any Unit - * - * This class allows intersection testing between two TerrainObjects to - * cover all cases of intersection (water, land or flying objects) the units - * lambda function is used which takes a tile and returns a bool value of - * whether that tile can be passed by this - * - * The name of this class is likely to change to TerrainBase or TerrainSpace - */ -class TerrainObject : public std::enable_shared_from_this { -public: - TerrainObject(Unit &u); - TerrainObject(const TerrainObject &) = delete; // disable copy constructor - TerrainObject(TerrainObject &&) = delete; // disable move constructor - virtual ~TerrainObject(); - - /** - * the range of tiles which are covered by this object - */ - tile_range pos; - - /* - * unit which is inside this base - * used to find the unit from user actions - * - * every terrain object should contain a single unit - */ - Unit &unit; - - /** - * is the object a floating outline -- it is only an indicator - * of where a building will be built, but not yet started building - * and does not affect any collisions - */ - bool is_floating() const; - - /** - * returns true if this object has been placed. this indicates that the object has a position and exists - * on the map, floating buildings are not considered placed as they are only an indicator - * for where something can begin construction - */ - bool is_placed() const; - - /** - * should this object be tested for collisions, which decides whether another object is allowed - * to overlap the location of this object. arrows and decaying objects will return false - */ - bool check_collisions() const; - - /** - * decide which terrains this object can be on - * this function should be true if given a valid position for the object - */ - std::function passable; - - /** - * changes the placement state of this object keeping the existing - * position. this is useful for upgrading a floating building to a placed state - */ - bool place(object_state init_state); - - /** - * binds the TerrainObject to a certain TerrainChunk. - * - * @param terrain: the terrain where the object will be placed onto. - * @param pos: (tile) position of the (nw,sw) corner - * @param init_state should be floating, placed or placed_no_collision - * @returns true when the object was placed, false when it did not fit at pos. - */ - bool place(const std::shared_ptr &t, coord::phys3 &pos, object_state init_state); - - /** - * moves the object -- returns false if object cannot be moved here - */ - bool move(coord::phys3 &pos); - - /** - * remove this TerrainObject from the terrain chunks. - */ - void remove(); - - /** - * sets all the ground below the object to a terrain id. - * - * @param id: the terrain id to which the ground is set - * @param additional: amount of additional space arround the building - */ - void set_ground(int id, int additional = 0); - - - /** - * appends new annex location for this object - * - * this does not replace any existing annex - */ - template - TerrainObject *make_annex(Arg... args) { - this->children.emplace_back(std::unique_ptr(new T(this->unit, args...))); - auto &annex_ptr = this->children.back(); - annex_ptr->parent = this; - return annex_ptr.get(); - } - - /** - * Returns the parent terrain object, - * if null the object has no parent which is - * the case for most objects - * - * objects with a parent are owned by that object - * and to be placed on the map the parent must also be placed - */ - const TerrainObject *get_parent() const; - - /** - * Returns a list of child objects, this is the inverse of the - * get_parent() function - * - * TODO: this does not perform optimally and is likely to change - */ - std::vector get_children() const; - - /* - * terrain this object was placed on - */ - std::shared_ptr get_terrain() const { - return terrain.lock(); - } - - /** - * comparison for TerrainObjects. - * - * sorting for vertical placement. - * by using this order algorithm, the overlapping order - * is optimal so the objects can be drawn in correct order. - */ - bool operator<(const TerrainObject &other); - - /** - * returns the range of tiles covered if the object was in the given pos - * @param pos the position to find a range for - */ - virtual tile_range get_range(const coord::phys3 &pos, const Terrain &terrain) const = 0; - - /** - * how far is a point from the edge of this object - */ - virtual coord::phys_t from_edge(const coord::phys3 &point) const = 0; - - /** - * get a position on the edge of this object - */ - virtual coord::phys3 on_edge(const coord::phys3 &angle, coord::phys_t extra = 0) const = 0; - - /** - * does this space contain a given point - */ - virtual bool contains(const coord::phys3 &other) const = 0; - - /** - * would this intersect with another object if it were positioned at the given point - */ - virtual bool intersects(const TerrainObject &other, const coord::phys3 &position) const = 0; - - /** - * the shortest line that can be placed across the objects center - */ - virtual coord::phys_t min_axis() const = 0; - -protected: - object_state state; - - std::weak_ptr terrain; - int occupied_chunk_count; - TerrainChunk *occupied_chunk[4]; - - /** - * annexes and grouped units - */ - TerrainObject *parent; - std::vector> children; - - /** - * placement function which does not check passibility - * used only when passibilty is already checked - * otherwise the place function should be used - * this does not modify the units placement state - */ - void place_unchecked(const std::shared_ptr &t, coord::phys3 &position); -}; - -/** - * terrain object class represents one immobile object on the map (building, trees, fish, ...). - * can only be constructed by unit->make_location(...) - */ -class SquareObject : public TerrainObject { -public: - virtual ~SquareObject(); - - - /** - * tile size of this objects base - */ - const coord::tile_delta size; - - /** - * calculate object start and end positions. - * - * @param pos: the center position of the building - * - * set the center position to "middle", - * start_pos is % and end_pos = & - * - * for a building, the # tile will be "the clicked one": - * @ @ @ - * @ @ @ @ %# & - * @ @ @ % # & @ - * % # @ & @ @ - * @ @ @ @ - * @ @ - * @ - */ - tile_range get_range(const coord::phys3 &pos, const Terrain &terrain) const override; - - coord::phys_t from_edge(const coord::phys3 &point) const override; - coord::phys3 on_edge(const coord::phys3 &angle, coord::phys_t extra = 0) const override; - bool contains(const coord::phys3 &other) const override; - bool intersects(const TerrainObject &other, const coord::phys3 &position) const override; - coord::phys_t min_axis() const override; - -private: - SquareObject(Unit &u, coord::tile_delta foundation_size); - - friend class TerrainObject; - friend class Unit; -}; - -/** - * Represents circular shaped objects (movable game units) - * can only be constructed by unit->make_location(...) - */ -class RadialObject : public TerrainObject { -public: - virtual ~RadialObject(); - - /** - * radius of this cirular space - */ - const coord::phys_t phys_radius; - - /** - * finds the range covered if the object was in a position - */ - tile_range get_range(const coord::phys3 &pos, const Terrain &terrain) const override; - - coord::phys_t from_edge(const coord::phys3 &point) const override; - coord::phys3 on_edge(const coord::phys3 &angle, coord::phys_t extra = 0) const override; - bool contains(const coord::phys3 &other) const override; - bool intersects(const TerrainObject &other, const coord::phys3 &position) const override; - coord::phys_t min_axis() const override; - -private: - RadialObject(Unit &u, float rad); - - friend class TerrainObject; - friend class Unit; -}; - -} // namespace openage diff --git a/libopenage/terrain/terrain_search.cpp b/libopenage/terrain/terrain_search.cpp deleted file mode 100644 index 7e3d0db22f..0000000000 --- a/libopenage/terrain/terrain_search.cpp +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#include - -#include "terrain.h" -#include "terrain_object.h" -#include "terrain_search.h" - -namespace openage { - -TerrainObject *find_near(const TerrainObject &start, - std::function found, - unsigned int search_limit) { - - auto terrain = start.get_terrain(); - auto tile = start.pos.draw.to_tile3().to_tile(); - TerrainSearch search(terrain, tile); - - for (unsigned int i = 0; i < search_limit; ++i) { - for (auto o : terrain->get_data(tile)->obj) { - - // invalid pointers are removed when the object is deleted - if (found(*o)) { - return o; - } - } - tile = search.next_tile(); - } - - return nullptr; -} - -TerrainObject *find_in_radius(const TerrainObject &start, - std::function found, - float radius) { - auto terrain = start.get_terrain(); - coord::tile start_tile = start.pos.draw.to_tile3().to_tile(); - TerrainSearch search(terrain, start_tile, radius); - - // next_tile will first return the starting tile, so we need to run it once. We also - // shouldn't discard this tile - if it isn't useful, ignore it in found - coord::tile tile = search.next_tile(); - do { - for (auto o : terrain->get_data(tile)->obj) { - if (found(*o)) { - return o; - } - } - tile = search.next_tile(); - // coord::tile doesn't have a != operator, so we need to use !(a==b) - } while (!(tile == start_tile)); - - return nullptr; -} - -TerrainSearch::TerrainSearch(std::shared_ptr t, coord::tile s) - : - TerrainSearch(t, s, .0f) { -} - -TerrainSearch::TerrainSearch(std::shared_ptr t, coord::tile s, float radius) - : - terrain{t}, - start(s), - previous_radius{.0f}, - max_radius{radius} { -} - -coord::tile TerrainSearch::start_tile() const { - return this->start; -} - -void TerrainSearch::reset() { - std::queue empty; - std::swap(this->tiles, empty); - this->visited.clear(); - this->tiles.push(this->start); - this->visited.insert(this->start); -} - -coord::tile TerrainSearch::next_tile() { - // conditions for returning to the initial tile - if (this->tiles.empty() || - (this->max_radius && this->previous_radius > this->max_radius)) { - this->reset(); - } - coord::tile result = this->tiles.front(); - this->tiles.pop(); - - for (auto i = 0; i < 4; ++i) { - auto to_add = result + neigh_tile[i]; - - // check not visited and tile is within map - if (this->visited.count(to_add) == 0 && terrain->get_data(to_add)) { - this->visited.insert(to_add); - this->tiles.push(to_add); - } - } - - this->previous_radius = std::hypot(result.ne - start.ne, result.se - start.se); - return result; -} - -} // namespace openage diff --git a/libopenage/terrain/terrain_search.h b/libopenage/terrain/terrain_search.h deleted file mode 100644 index 3221db31d2..0000000000 --- a/libopenage/terrain/terrain_search.h +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include -#include -#include - -#include "../coord/tile.h" - -namespace openage { - -class Terrain; -class TerrainObject; - -TerrainObject *find_near(const TerrainObject &start, - std::function found, - unsigned int search_limit=500); -TerrainObject *find_in_radius(const TerrainObject &start, - std::function found, - float radius); - -constexpr coord::tile_delta const neigh_tile[] = { - {0, 1}, - {0, -1}, - {1, 0}, - {-1, 0} -}; - -/** - * searches outward from a point and returns nearby objects - * The state of the search is kept within the class, which allows - * a user to look at a limited number of tiles per update cycle - */ -class TerrainSearch { -public: - /** - * next_tile will cover all tiles on the map - */ - TerrainSearch(std::shared_ptr t, coord::tile s); - - /** - * next_tile will iterate over a range of tiles within a radius - */ - TerrainSearch(std::shared_ptr t, coord::tile s, float radius); - ~TerrainSearch() = default; - - /** - * the tile the search began on - */ - coord::tile start_tile() const; - - /** - * restarts the search from the start tile - */ - void reset(); - - /** - * returns all objects on the next tile - */ - coord::tile next_tile(); - -private: - const std::shared_ptr terrain; - const coord::tile start; - std::queue tiles; - std::unordered_set visited; - float previous_radius, max_radius; - -}; - -} // namespace openage diff --git a/libopenage/unit/CMakeLists.txt b/libopenage/unit/CMakeLists.txt deleted file mode 100644 index 6550dc0234..0000000000 --- a/libopenage/unit/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -add_sources(libopenage - ability.cpp - action.cpp - attribute.cpp - attributes.cpp - command.cpp - producer.cpp - research.cpp - selection.cpp - unit.cpp - unit_container.cpp - unit_type.cpp -) diff --git a/libopenage/unit/ability.cpp b/libopenage/unit/ability.cpp deleted file mode 100644 index 0ab1eef175..0000000000 --- a/libopenage/unit/ability.cpp +++ /dev/null @@ -1,446 +0,0 @@ -// Copyright 2014-2021 the openage authors. See copying.md for legal info. - -#include - -#include "../terrain/terrain_object.h" -#include "../gamestate/old/cost.h" -#include "../gamestate/old/player.h" -#include "ability.h" -#include "action.h" -#include "command.h" -#include "research.h" -#include "unit.h" - -namespace openage { - -bool UnitAbility::has_hitpoints(Unit &target) { - return target.has_attribute(attr_type::damaged) && - target.get_attribute().hp > 0; -} - -bool UnitAbility::is_damaged(Unit &target) { - return target.has_attribute(attr_type::damaged) && target.has_attribute(attr_type::hitpoints) && - target.get_attribute().hp < target.get_attribute().hp; -} - -bool UnitAbility::has_resource(Unit &target) { - return target.has_attribute(attr_type::resource) && !target.has_attribute(attr_type::worker) && - target.get_attribute().amount > 0; -} - -bool UnitAbility::is_same_player(Unit &to_modify, Unit &target) { - if (to_modify.has_attribute(attr_type::owner) && - target.has_attribute(attr_type::owner)) { - auto &mod_player = to_modify.get_attribute().player; - auto &tar_player = target.get_attribute().player; - return mod_player.color == tar_player.color; - } - return false; - -} - -bool UnitAbility::is_ally(Unit &to_modify, Unit &target) { - if (to_modify.has_attribute(attr_type::owner) && - target.has_attribute(attr_type::owner)) { - auto &mod_player = to_modify.get_attribute().player; - auto &tar_player = target.get_attribute().player; - return mod_player.is_ally(tar_player); - } - return false; -} - -bool UnitAbility::is_enemy(Unit &to_modify, Unit &target) { - if (to_modify.has_attribute(attr_type::owner) && - target.has_attribute(attr_type::owner)) { - auto &mod_player = to_modify.get_attribute().player; - auto &tar_player = target.get_attribute().player; - return mod_player.is_enemy(tar_player); - } - return false; -} - -MoveAbility::MoveAbility(const Sound *s) - : - sound{s} { -} - -bool MoveAbility::can_invoke(Unit &to_modify, const Command &cmd) { - if (cmd.has_position()) { - return bool(to_modify.location); - } - else if (cmd.has_unit()) { - return to_modify.location && - cmd.unit()->location && - &to_modify != cmd.unit(); // cannot target self - } - return false; -} - -void MoveAbility::invoke(Unit &to_modify, const Command &cmd, bool play_sound) { - to_modify.log(MSG(dbg) << "invoke move action"); - if (play_sound && this->sound) { - this->sound->play(); - } - - if (cmd.has_position()) { - auto target = cmd.position(); - to_modify.push_action(std::make_unique(&to_modify, target)); - } - else if (cmd.has_unit()) { - auto target = cmd.unit(); - - // distance from the targets edge that is required to stop moving - coord::phys_t radius = path::path_grid_size + (to_modify.location->min_axis() / 2L); - - // add the range of the unit if cmd indicator is set - if (cmd.has_flag(command_flag::use_range) && to_modify.has_attribute(attr_type::attack)) { - auto &att = to_modify.get_attribute(); - radius += att.max_range; - } - to_modify.push_action(std::make_unique(&to_modify, target->get_ref(), radius)); - } -} - -SetPointAbility::SetPointAbility() = default; - -bool SetPointAbility::can_invoke(Unit &to_modify, const Command &cmd) { - return cmd.has_position() && - to_modify.has_attribute(attr_type::building); -} - -void SetPointAbility::invoke(Unit &to_modify, const Command &cmd, bool) { - auto &build_attr = to_modify.get_attribute(); - build_attr.gather_point = cmd.position(); -} - - -GarrisonAbility::GarrisonAbility(const Sound *s) - : - sound{s} { -} - -bool GarrisonAbility::can_invoke(Unit &to_modify, const Command &cmd) { - if (!cmd.has_unit()) { - return false; - } - Unit &target = *cmd.unit(); - - // make sure buildings are completed - if (target.has_attribute(attr_type::building)) { - auto &build_attr = target.get_attribute(); - if (build_attr.completed < 1.0f) { - return false; - } - } - return to_modify.location && - target.has_attribute(attr_type::garrison) && - is_ally(to_modify, target); -} - -void GarrisonAbility::invoke(Unit &to_modify, const Command &cmd, bool play_sound) { - to_modify.log(MSG(dbg) << "invoke garrison action"); - if (play_sound && this->sound) { - this->sound->play(); - } - to_modify.push_action(std::make_unique(&to_modify, cmd.unit()->get_ref())); -} - -UngarrisonAbility::UngarrisonAbility(const Sound *s) - : - sound{s} { -} - -bool UngarrisonAbility::can_invoke(Unit &to_modify, const Command &cmd) { - if (to_modify.has_attribute(attr_type::garrison)) { - auto &garrison_attr = to_modify.get_attribute(); - return cmd.has_position() && !garrison_attr.content.empty(); - } - return false; -} - -void UngarrisonAbility::invoke(Unit &to_modify, const Command &cmd, bool play_sound) { - to_modify.log(MSG(dbg) << "invoke ungarrison action"); - if (play_sound && this->sound) { - this->sound->play(); - } - - // add as secondary, so primary action is not disrupted - to_modify.secondary_action(std::make_unique(&to_modify, cmd.position())); -} - -TrainAbility::TrainAbility(const Sound *s) - : - sound{s} { -} - -bool TrainAbility::can_invoke(Unit &to_modify, const Command &cmd) { - if (to_modify.has_attribute(attr_type::building)) { - auto &build_attr = to_modify.get_attribute(); - return cmd.has_type() && 1.0f <= build_attr.completed; - } - return false; -} - -void TrainAbility::invoke(Unit &to_modify, const Command &cmd, bool play_sound) { - to_modify.log(MSG(dbg) << "invoke train action"); - if (play_sound && this->sound) { - this->sound->play(); - } - to_modify.push_action(std::make_unique(&to_modify, cmd.type())); -} - -ResearchAbility::ResearchAbility(const Sound *s) - : - sound{s} { -} - -bool ResearchAbility::can_invoke(Unit &to_modify, const Command &cmd) { - if (to_modify.has_attribute(attr_type::owner) && cmd.has_research()) { - auto &player = to_modify.get_attribute().player; - auto research = cmd.research(); - return research->can_start() && - player.can_deduct(research->type->get_research_cost().get(player)); - } - return false; -} - -void ResearchAbility::invoke(Unit &to_modify, const Command &cmd, bool play_sound) { - if (play_sound && this->sound) { - this->sound->play(); - } - to_modify.push_action(std::make_unique(&to_modify, cmd.research())); -} - -BuildAbility::BuildAbility(const Sound *s) - : - sound{s} { -} - -bool BuildAbility::can_invoke(Unit &to_modify, const Command &cmd) { - if (cmd.has_unit()) { - Unit *target = cmd.unit(); - return to_modify.location && - is_same_player(to_modify, *target) && - target->has_attribute(attr_type::building) && - target->get_attribute().completed < 1.0f; - } - return false; -} - -void BuildAbility::invoke(Unit &to_modify, const Command &cmd, bool play_sound) { - to_modify.log(MSG(dbg) << "invoke build action"); - if (play_sound && this->sound) { - this->sound->play(); - } - - if (cmd.has_unit()) { - to_modify.push_action(std::make_unique(&to_modify, cmd.unit()->get_ref())); - } -} - -GatherAbility::GatherAbility(const Sound *s) - : - sound{s} { -} - -bool GatherAbility::can_invoke(Unit &to_modify, const Command &cmd) { - if (cmd.has_unit()) { - Unit &target = *cmd.unit(); - return &to_modify != &target && - to_modify.location && - to_modify.has_attribute(attr_type::worker) && - has_resource(target); - } - return false; -} - -void GatherAbility::invoke(Unit &to_modify, const Command &cmd, bool play_sound) { - to_modify.log(MSG(dbg) << "invoke gather action"); - if (play_sound && this->sound) { - this->sound->play(); - } - - Unit *target = cmd.unit(); - try { - to_modify.push_action(std::make_unique(&to_modify, target->get_ref())); - } catch (const std::invalid_argument &e) { - to_modify.log(MSG(dbg) << "invoke gather action cancelled due to an exception. Reason: " << e.what()); - } -} - -AttackAbility::AttackAbility(const Sound *s) - : - sound{s} { -} - -bool AttackAbility::can_invoke(Unit &to_modify, const Command &cmd) { - if (cmd.has_unit()) { - Unit &target = *cmd.unit(); - bool target_is_resource = has_resource(target); - return &to_modify != &target && - to_modify.location && - target.location && - target.location->is_placed() && - to_modify.has_attribute(attr_type::attack) && - has_hitpoints(target) && - (is_enemy(to_modify, target) || target_is_resource) && - (cmd.has_flag(command_flag::attack_res) == target_is_resource); - } - return false; -} - -void AttackAbility::invoke(Unit &to_modify, const Command &cmd, bool play_sound) { - to_modify.log(MSG(dbg) << "invoke attack action"); - if (play_sound && this->sound) { - this->sound->play(); - } - - Unit *target = cmd.unit(); - to_modify.push_action(std::make_unique(&to_modify, target->get_ref())); -} - -RepairAbility::RepairAbility(const Sound *s) - : - sound{s} { -} - -bool RepairAbility::can_invoke(Unit &to_modify, const Command &cmd) { - if (cmd.has_unit()) { - Unit &target = *cmd.unit(); - return &to_modify != &target && - to_modify.location && - target.location && - target.location->is_placed() && - is_damaged(target) && - is_ally(to_modify, target); - } - return false; -} - -void RepairAbility::invoke(Unit &to_modify, const Command &cmd, bool play_sound) { - to_modify.log(MSG(dbg) << "invoke repair action"); - if (play_sound && this->sound) { - this->sound->play(); - } - - Unit *target = cmd.unit(); - to_modify.push_action(std::make_unique(&to_modify, target->get_ref())); -} - -HealAbility::HealAbility(const Sound *s) - : - sound{s} { -} - -bool HealAbility::can_invoke(Unit &to_modify, const Command &cmd) { - if (cmd.has_unit()) { - Unit &target = *cmd.unit(); - return &to_modify != &target && - to_modify.location && - target.location && - target.location->is_placed() && - is_damaged(target) && - is_ally(to_modify, target); - } - return false; -} - -void HealAbility::invoke(Unit &to_modify, const Command &cmd, bool play_sound) { - to_modify.log(MSG(dbg) << "invoke heal action"); - if (play_sound && this->sound) { - this->sound->play(); - } - - Unit *target = cmd.unit(); - to_modify.push_action(std::make_unique(&to_modify, target->get_ref())); -} - -PatrolAbility::PatrolAbility(const Sound *s) - : - sound{s} { -} - -bool PatrolAbility::can_invoke(Unit &/*to_modify*/, const Command &/*cmd*/) { - // TODO implement - return false; -} - -void PatrolAbility::invoke(Unit &to_modify, const Command &/*cmd*/, bool play_sound) { - to_modify.log(MSG(dbg) << "not implemented"); - if (play_sound && this->sound) { - this->sound->play(); - } - // TODO implement -} - -ConvertAbility::ConvertAbility(const Sound *s) - : - sound{s} { -} - -bool ConvertAbility::can_invoke(Unit &to_modify, const Command &cmd) { - if (cmd.has_unit()) { - Unit &target = *cmd.unit(); - return &to_modify != &target && - to_modify.location && - target.location && - target.location->is_placed() && - is_enemy(to_modify, target); - } - return false; -} - -void ConvertAbility::invoke(Unit &to_modify, const Command &cmd, bool play_sound) { - to_modify.log(MSG(dbg) << "invoke convert action"); - if (play_sound && this->sound) { - this->sound->play(); - } - - Unit *target = cmd.unit(); - to_modify.push_action(std::make_unique(&to_modify, target->get_ref())); -} - -ability_set UnitAbility::set_from_list(const std::vector &items) { - ability_set result; - for (auto i : items) { - result[static_cast(i)] = 1; - } - return result; -} - -} // namespace openage - -namespace std { - -string to_string(const openage::ability_type &at) { - switch (at) { - case openage::ability_type::move: - return "move"; - case openage::ability_type::garrison: - return "garrison"; - case openage::ability_type::ungarrison: - return "ungarrison"; - case openage::ability_type::patrol: - return "patrol"; - case openage::ability_type::train: - return "train"; - case openage::ability_type::build: - return "build"; - case openage::ability_type::research: - return "research"; - case openage::ability_type::gather: - return "gather"; - case openage::ability_type::attack: - return "attack"; - case openage::ability_type::convert: - return "convert"; - case openage::ability_type::repair: - return "repair"; - case openage::ability_type::heal: - return "heal"; - default: - return "unknown"; - } -} - -} // namespace std diff --git a/libopenage/unit/ability.h b/libopenage/unit/ability.h deleted file mode 100644 index b2957c0585..0000000000 --- a/libopenage/unit/ability.h +++ /dev/null @@ -1,380 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include -#include -#include - -#include "../coord/phys.h" - -namespace openage { - -class Command; -class Sound; -class Unit; -class UnitAction; -class UnitType; - -/** - * roughly the same as command_ability in game data - */ -enum class ability_type { - move, - patrol, - set_point, - garrison, - ungarrison, - train, - build, - research, - gather, - attack, - convert, - repair, - heal, - MAX -}; - -/** - * a container where each ability uses 1 bit - */ -constexpr int ability_type_size = static_cast(ability_type::MAX); -using ability_set = std::bitset; -using ability_id_t = unsigned int; - -/** - * all bits set to 1 - */ -const ability_set ability_all = ability_set().set(); - -/** - * the order abilities should be used when available - */ -static std::vector ability_priority{ - ability_type::gather, // targeting - ability_type::convert, - ability_type::repair, - ability_type::heal, - ability_type::attack, - ability_type::build, - ability_type::move, // positional - ability_type::patrol, - ability_type::garrison, - ability_type::ungarrison, // inside buildings - ability_type::train, - ability_type::research, - ability_type::set_point, -}; - - -/** - * Abilities create an action when given a target - * some abilities target positions such as moving or patrolling - * others target other game objects, such as attacking or - * collecting relics - * - * Abilities are constructed with a default unit texture, but allow the texture - * to be specified with the invoke function - */ -class UnitAbility { -public: - virtual ~UnitAbility() {} - - virtual ability_type type() = 0; - - /** - * true if the paramaters allow an action to be performed - */ - virtual bool can_invoke(Unit &to_modify, const Command &cmd) = 0; - - /** - * applies command to a given unit - */ - virtual void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) = 0; - - /** - * some common functions - */ - bool has_hitpoints(Unit &target); - bool is_damaged(Unit &target); - bool has_resource(Unit &target); - bool is_same_player(Unit &to_modify, Unit &target); - bool is_ally(Unit &to_modify, Unit &target); - bool is_enemy(Unit &to_modify, Unit &target); - - /** - * set bits corresponding to abilities, useful for initialising an ability_set - * using a brace enclosed list - */ - static ability_set set_from_list(const std::vector &items); -}; - -/** - * initiates a move action when given a valid target - */ -class MoveAbility : public UnitAbility { -public: - MoveAbility(const Sound *s = nullptr); - - ability_type type() override { - return ability_type::move; - } - - bool can_invoke(Unit &to_modify, const Command &cmd) override; - - void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; - -private: - const Sound *sound; -}; - -/** - * sets the gather point on buildings - */ -class SetPointAbility : public UnitAbility { -public: - SetPointAbility(); - - ability_type type() override { - return ability_type::set_point; - } - - bool can_invoke(Unit &to_modify, const Command &cmd) override; - - void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; -}; - - -/** - * ability to garrision inside a building - */ -class GarrisonAbility : public UnitAbility { -public: - GarrisonAbility(const Sound *s = nullptr); - - ability_type type() override { - return ability_type::garrison; - } - - bool can_invoke(Unit &to_modify, const Command &cmd) override; - - void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; - -private: - const Sound *sound; -}; - -/** - * ability to ungarrision a building - */ -class UngarrisonAbility : public UnitAbility { -public: - UngarrisonAbility(const Sound *s = nullptr); - - ability_type type() override { - return ability_type::ungarrison; - } - - bool can_invoke(Unit &to_modify, const Command &cmd) override; - - void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; - -private: - const Sound *sound; -}; - -/** - * buildings train new objects - */ -class TrainAbility : public UnitAbility { -public: - TrainAbility(const Sound *s = nullptr); - - ability_type type() override { - return ability_type::train; - } - - bool can_invoke(Unit &to_modify, const Command &cmd) override; - - void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; - -private: - const Sound *sound; -}; - -/** - * initiates a research - */ -class ResearchAbility : public UnitAbility { -public: - ResearchAbility(const Sound *s = nullptr); - - ability_type type() override { - return ability_type::research; - } - - bool can_invoke(Unit &to_modify, const Command &cmd) override; - - void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; - -private: - const Sound *sound; -}; - -/** - * villagers build new buildings - */ -class BuildAbility : public UnitAbility { -public: - BuildAbility(const Sound *s = nullptr); - - ability_type type() override { - return ability_type::build; - } - - bool can_invoke(Unit &to_modify, const Command &cmd) override; - - void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; - -private: - const Sound *sound; -}; - -/** - * initiates an gather resource action when given a valid target - */ -class GatherAbility : public UnitAbility { -public: - GatherAbility(const Sound *s = nullptr); - - ability_type type() override { - return ability_type::gather; - } - - bool can_invoke(Unit &to_modify, const Command &cmd) override; - - void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; - -private: - const Sound *sound; -}; - -/** - * initiates an attack action when given a valid target - */ -class AttackAbility : public UnitAbility { -public: - AttackAbility(const Sound *s = nullptr); - - ability_type type() override { - return ability_type::attack; - } - - bool can_invoke(Unit &to_modify, const Command &cmd) override; - - void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; - -private: - const Sound *sound; -}; - -/** - * initiates a repair action when given a valid target - */ -class RepairAbility : public UnitAbility { -public: - RepairAbility(const Sound *s = nullptr); - - ability_type type() override { - return ability_type::repair; - } - - bool can_invoke(Unit &to_modify, const Command &cmd) override; - - void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; - -private: - const Sound *sound; -}; - -/** - * initiates a heal action when given a valid target - */ -class HealAbility : public UnitAbility { -public: - HealAbility(const Sound *s = nullptr); - - ability_type type() override { - return ability_type::heal; - } - - bool can_invoke(Unit &to_modify, const Command &cmd) override; - - void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; - -private: - const Sound *sound; -}; - -/** - * initiates a patrol action when given a valid target - * TODO implement - */ -class PatrolAbility : public UnitAbility { -public: - PatrolAbility(const Sound *s = nullptr); - - ability_type type() override { - return ability_type::patrol; - } - - bool can_invoke(Unit &to_modify, const Command &cmd) override; - - void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; - -private: - const Sound *sound; -}; - -/** - * initiates a convert action when given a valid target - */ -class ConvertAbility : public UnitAbility { -public: - ConvertAbility(const Sound *s = nullptr); - - ability_type type() override { - return ability_type::convert; - } - - bool can_invoke(Unit &to_modify, const Command &cmd) override; - - void invoke(Unit &to_modify, const Command &cmd, bool play_sound = false) override; - -private: - const Sound *sound; -}; - -} // namespace openage - -namespace std { - -std::string to_string(const openage::ability_type &at); - -/** - * hasher for ability type enum - */ -template <> -struct hash { - typedef underlying_type::type underlying_type; - - size_t operator()(const openage::ability_type &arg) const { - hash hasher; - return hasher(static_cast(arg)); - } -}; - -} // namespace std diff --git a/libopenage/unit/action.cpp b/libopenage/unit/action.cpp deleted file mode 100644 index 65bd1bed8b..0000000000 --- a/libopenage/unit/action.cpp +++ /dev/null @@ -1,1249 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#include -#include - -#include "../pathfinding/a_star.h" -#include "../pathfinding/heuristics.h" -#include "../terrain/terrain.h" -#include "../terrain/terrain_search.h" -#include "action.h" -#include "command.h" -#include "producer.h" -#include "research.h" - -namespace openage { - -IntervalTimer::IntervalTimer(unsigned int interval) : - IntervalTimer{interval, -1} { -} - -IntervalTimer::IntervalTimer(unsigned int interval, int max_triggers) : - interval{interval}, - max_triggers{max_triggers}, - time_left{interval}, - triggers{0} { -} - -void IntervalTimer::skip_to_trigger() { - this->time_left = 0; -} - -bool IntervalTimer::update(unsigned int time) { - if (this->triggers == this->max_triggers) { - return false; - } - else if (this->time_left > time) { - this->time_left -= time; - return false; - } - else { - this->time_left += this->interval - time; - this->triggers += 1; - return true; - } -} - -unsigned int IntervalTimer::get_time_left() const { - return this->time_left; -} - -float IntervalTimer::get_progress() const { - return 1.0f - (this->time_left * 1.0f / this->interval); -} - -bool IntervalTimer::has_triggers() const { - return this->triggers > 0; -} - -bool IntervalTimer::finished() const { - return this->triggers == this->max_triggers; -} - -bool UnitAction::show_debug = false; - -coord::phys_t UnitAction::adjacent_range(Unit *u) { - return path::path_grid_size * 3 + (u->location->min_axis() / 2L); -} - -coord::phys_t UnitAction::get_attack_range(Unit *u) { - coord::phys_t range = adjacent_range(u); - if (u->has_attribute(attr_type::attack)) { - auto &attack = u->get_attribute(); - range += attack.max_range; - } - return range; -} - -coord::phys_t UnitAction::get_heal_range(Unit *u) { - coord::phys_t range = adjacent_range(u); - if (u->has_attribute(attr_type::heal)) { - auto &heal = u->get_attribute(); - range += heal.range; - } - return range; -} - -UnitAction::UnitAction(Unit *u, graphic_type initial_gt) : - entity{u}, - graphic{initial_gt}, - frame{.0f}, - frame_rate{.0f} { -} - -graphic_type UnitAction::type() const { - return this->graphic; -} - -float UnitAction::current_frame() const { - return this->frame; -} - -void UnitAction::face_towards(const coord::phys3 pos) { - if (this->entity->has_attribute(attr_type::direction)) { - auto &d_attr = this->entity->get_attribute(); - d_attr.unit_dir = pos - this->entity->location->pos.draw; - } -} - -bool UnitAction::damage_unit(Unit &target) { - bool killed = false; - - if (target.has_attribute(attr_type::damaged)) { - auto &dm = target.get_attribute(); - - // this is the damage calculation system - - if (dm.hp == 0) { - // already killed, do nothing - } - else if (target.has_attribute(attr_type::armor) && this->entity->has_attribute(attr_type::attack)) { - auto &armor = target.get_attribute().armor; - auto &damage = this->entity->get_attribute().damage; - - unsigned int actual_damage = 0; - for (const auto &pair : armor) { - auto search = damage.find(pair.first); - if (search != damage.end()) { - if (pair.second < search->second) { - actual_damage += search->second - pair.second; - } - } - } - // TODO add elevation modifier here - if (actual_damage < 1) { - actual_damage = 1; - } - - if (dm.hp > actual_damage) { - dm.hp -= actual_damage; - } - else { - dm.hp = 0; - killed = true; - } - } - else { - // TODO remove (keep for testing) - unsigned int dmg = 1; - if (dm.hp > dmg) { - dm.hp -= dmg; - } - else { - dm.hp = 0; - killed = true; - } - } - } - - if (killed) { - // if killed, give credit to a player - if (this->entity->has_attribute(attr_type::owner)) { - auto &owner = this->entity->get_attribute().player; - owner.killed_unit(target); - } - } - - return killed; -} - -void UnitAction::move_to(Unit &target, bool use_range) { - auto &player = this->entity->get_attribute().player; - Command cmd(player, &target); - cmd.set_ability(ability_type::move); - if (use_range) { - cmd.add_flag(command_flag::use_range); - } - this->entity->queue_cmd(cmd); -} - -TargetAction::TargetAction(Unit *u, graphic_type gt, UnitReference r, coord::phys_t rad) : - UnitAction(u, gt), - target{r}, - target_type_id{0}, - repath_attempts{10}, - end_action{false}, - radius{rad} { - // update type - if (this->target.is_valid()) { - auto target_ptr = this->target.get(); - this->target_type_id = target_ptr->unit_type->id(); - } - - // initial value for distance - this->update_distance(); -} - -TargetAction::TargetAction(Unit *u, graphic_type gt, UnitReference r) : - TargetAction(u, gt, r, adjacent_range(u)) { -} - -void TargetAction::update(unsigned int time) { - auto target_ptr = this->update_distance(); - if (!target_ptr) { - return; // target has become invalid - } - - // this update moves a unit within radius of the target - // once within the radius the update gets passed to the class - // derived from TargetAction - - // set direction unit should face - this->face_towards(target_ptr->location->pos.draw); - - // move to within the set radius - if (this->dist_to_target <= this->radius) { - // the derived class controls what to - // do when in range of the target - this->update_in_range(time, target_ptr); - this->repath_attempts = 10; - } - else if (this->repath_attempts) { - // out of range so try move towards - // if this unit has a move ability - this->move_to(*target_ptr); - this->repath_attempts -= 1; - } - else { - // unit is stuck - this->end_action = true; - } -} - -void TargetAction::on_completion() { - // do not retask if action is forced to end - if (this->end_action || !this->entity->location) { - return; - } - - // retask units on nearby objects - // such as gathers targeting a new resource - // when the current target expires - this->on_completion_in_range(this->target_type_id); -} - -bool TargetAction::completed() const { - if (this->end_action || !this->target.is_valid() || !this->target.get()->location) { - return true; - } - return this->completed_in_range(this->target.get()); -} - -coord::phys_t TargetAction::distance_to_target() { - return this->dist_to_target; -} - -Unit *TargetAction::update_distance() { - if (!this->target.is_valid()) { - return nullptr; - } - - // make sure object is not garrisoned - auto target_ptr = this->target.get(); - if (!target_ptr->location) { - return nullptr; - } - - // update distance - this->dist_to_target = target_ptr->location->from_edge(this->entity->location->pos.draw); - - // return the targeted unit - return target_ptr; -} - -UnitReference TargetAction::get_target() const { - return this->target; -} - -int TargetAction::get_target_type_id() const { - return this->target_type_id; -} - -void TargetAction::set_target(UnitReference new_target) { - if (new_target.is_valid()) { - this->target = new_target; - this->update_distance(); - } - else { - this->end_action = true; - } -} - -DecayAction::DecayAction(Unit *e) : - UnitAction(e, graphic_type::standing), - end_frame{.0f} { -} - -void DecayAction::update(unsigned int time) { - this->frame += time * this->frame_rate / 10000.0f; -} - -void DecayAction::on_completion() {} - -bool DecayAction::completed() const { - return this->frame > this->end_frame; -} - -DeadAction::DeadAction(Unit *e, std::function on_complete) : - UnitAction(e, graphic_type::dying), - end_frame{.0f}, - on_complete_func{on_complete} { -} - -void DeadAction::update(unsigned int time) { - if (this->entity->has_attribute(attr_type::damaged)) { - auto &dm = this->entity->get_attribute(); - dm.hp = 0; - } - - // decay resources - if (this->entity->has_attribute(attr_type::resource)) { - auto &resource = this->entity->get_attribute(); - if (resource.decay > 0) { - resource.amount -= resource.decay; - } - } - - // inc frame but do not pass the end frame - // the end frame will remain if the object carries resources - if (this->frame < this->end_frame) { - this->frame += 0.001 + time * this->frame_rate / 3.0f; - } - else { - this->frame = this->end_frame; - } -} - -void DeadAction::on_completion() { - if (this->entity->has_attribute(attr_type::owner)) { - auto &owner = this->entity->get_attribute().player; - owner.active_unit_removed(this->entity); // TODO move before the start of dead action? - } - - this->on_complete_func(); -} - -bool DeadAction::completed() const { - // check resource, trees/huntables with resource are not removed but not workers - if (this->entity->has_attribute(attr_type::resource) && !this->entity->has_attribute(attr_type::worker)) { - auto &res_attr = this->entity->get_attribute(); - return res_attr.amount <= 0; // cannot complete when resource remains - } - return this->frame > this->end_frame; -} - -FoundationAction::FoundationAction(Unit *e, bool add_destruction) : - UnitAction(e, graphic_type::construct), - add_destruct_effect{add_destruction}, - cancel{false} { -} - -void FoundationAction::update(unsigned int) { - if (!this->entity->location) { - this->cancel = true; - } -} - -void FoundationAction::on_completion() { - // do nothing if construction is cancelled - if (this->cancel) { - return; - } - - if (this->entity->has_attribute(attr_type::owner)) { - auto &owner = this->entity->get_attribute().player; - owner.active_unit_added(this->entity, true); - } - - // add destruction effect when available - if (this->add_destruct_effect) { - this->entity->push_action(std::make_unique(this->entity), true); - } - this->entity->push_action(std::make_unique(this->entity), true); -} - -bool FoundationAction::completed() const { - return this->cancel || (this->entity->has_attribute(attr_type::building) && (this->entity->get_attribute().completed >= 1.0f)); -} - -IdleAction::IdleAction(Unit *e) : - UnitAction(e, graphic_type::standing) { - auto terrain = this->entity->location->get_terrain(); - auto current_tile = this->entity->location->pos.draw.to_tile3().to_tile(); - this->search = std::make_shared(terrain, current_tile, 5.0f); - - // currently allow attack and heal automatically - this->auto_abilities = UnitAbility::set_from_list({ability_type::attack, ability_type::heal}); -} - -void IdleAction::update(unsigned int time) { - // auto task searching - if (this->entity->location && this->entity->has_attribute(attr_type::owner) && this->entity->has_attribute(attr_type::attack) && this->entity->has_attribute(attr_type::formation) && this->entity->get_attribute().stance != attack_stance::do_nothing) { - // restart search from new tile when moved - auto terrain = this->entity->location->get_terrain(); - auto current_tile = this->entity->location->pos.draw.to_tile3().to_tile(); - if (!(current_tile == this->search->start_tile())) { - this->search = std::make_shared(terrain, current_tile, 5.0f); - } - - // search one tile per update - // next tile will always be valid - coord::tile tile = this->search->next_tile(); - auto tile_data = terrain->get_data(tile); - auto &player = this->entity->get_attribute().player; - - // find and actions which can be invoked - for (auto object_location : tile_data->obj) { - Command to_object(player, &object_location->unit); - - // only allow abilities in the set of auto ability types - to_object.set_ability_set(auto_abilities); - if (this->entity->queue_cmd(to_object)) { - break; - } - } - } - - // generate resources - // TODO move elsewhere - if (this->entity->has_attribute(attr_type::resource_generator) && this->entity->has_attribute(attr_type::owner)) { - auto &player = this->entity->get_attribute().player; - auto &resource_generator = this->entity->get_attribute(); - - ResourceBundle resources = resource_generator.resources.clone(); - if (resource_generator.rate == 0) { - resources *= time; - } - else { - // TODO add in intervals and not continuously - resources *= time * resource_generator.rate; - } - - player.receive(resources); - } - - // unit carrying resources take the carrying sprite when idle - // we're not updating frames because the carrying sprite is walking - if (entity->has_attribute(attr_type::worker)) { - auto &worker_resource = this->entity->get_attribute(); - if (worker_resource.amount > 0) { - this->graphic = graphic_type::carrying; - } - else { - this->graphic = graphic_type::standing; - this->frame += time * this->frame_rate / 20.0f; - } - } - else { - // inc frame - this->frame += time * this->frame_rate / 20.0f; - } -} - -void IdleAction::on_completion() {} - -bool IdleAction::completed() const { - if (this->entity->has_attribute(attr_type::damaged)) { - auto &dm = this->entity->get_attribute(); - return dm.hp == 0; - } - else if (this->entity->has_attribute(attr_type::resource)) { - auto &res_attr = this->entity->get_attribute(); - return res_attr.amount <= 0.0; - } - return false; -} - -MoveAction::MoveAction(Unit *e, coord::phys3 tar, bool repath) : - UnitAction{e, graphic_type::walking}, - unit_target{}, - target(tar), - radius{path::path_grid_size}, - allow_repath{repath}, - end_action{false} { - this->initialise(); -} - -MoveAction::MoveAction(Unit *e, UnitReference tar, coord::phys_t within_range) : - UnitAction{e, graphic_type::walking}, - unit_target{tar}, - target(tar.get()->location->pos.draw), - radius{within_range}, - allow_repath{false}, - end_action{false} { - this->initialise(); -} - -void MoveAction::initialise() { - // switch workers to the carrying graphic - if (this->entity->has_attribute(attr_type::worker)) { - auto &worker_resource = this->entity->get_attribute(); - if (worker_resource.amount > 0) { - this->graphic = graphic_type::carrying; - } - } - - // set initial distance - this->set_distance(); - - // set an initial path - this->set_path(); - // this->debug_draw_action = [&](const Engine &engine) { - // this->path.draw_path(engine.coord); - // }; -} - -MoveAction::~MoveAction() = default; - -void MoveAction::update(unsigned int time) { - if (this->unit_target.is_valid()) { - // a unit is targeted, which may move - auto &target_object = this->unit_target.get()->location; - - // check for garrisoning objects - if (!target_object) { - this->end_action = true; - return; - } - coord::phys3 &target_pos = target_object->pos.draw; - coord::phys3 &unit_pos = this->entity->location->pos.draw; - - // repath if target changes tiles by a threshold - // this repathing is more frequent when the unit is - // close to its target - coord::phys_t tdx = target_pos.ne - this->target.ne; - coord::phys_t tdy = target_pos.se - this->target.se; - coord::phys_t udx = unit_pos.ne - this->target.ne; - coord::phys_t udy = unit_pos.se - this->target.se; - if (this->path.waypoints.empty() || std::hypot(tdx, tdy) > std::hypot(udx, udy)) { - this->target = target_pos; - this->set_path(); - } - } - - // path not found - if (this->path.waypoints.empty()) { - if (!this->allow_repath) { - this->entity->log(MSG(dbg) << "Path not found -- drop action"); - this->end_action = true; - } - return; - } - - // find distance to move in this update - auto &sp_attr = this->entity->get_attribute(); - double distance_to_move = sp_attr.unit_speed.to_double() * time; - - // current position and direction - coord::phys3 new_position = this->entity->location->pos.draw; - auto &d_attr = this->entity->get_attribute(); - coord::phys3_delta new_direction = d_attr.unit_dir; - - while (distance_to_move > 0) { - if (this->path.waypoints.empty()) { - break; - } - - // find a point to move directly towards - coord::phys3 waypoint = this->next_waypoint(); - coord::phys3_delta move_dir = waypoint - new_position; - - // normalise dir - double distance_to_waypoint = std::hypot(move_dir.ne, move_dir.se); - - if (distance_to_waypoint <= distance_to_move) { - distance_to_move -= distance_to_waypoint; - - // change entity position and direction - new_position = waypoint; - new_direction = move_dir; - - // remove the waypoint - this->path.waypoints.pop_back(); - } - else { - // change entity position and direction - new_position += move_dir * (distance_to_move / distance_to_waypoint); - new_direction = move_dir; - break; - } - } - - // check move collisions - bool move_completed = this->entity->location->move(new_position); - if (move_completed) { - d_attr.unit_dir = new_direction; - this->set_distance(); - } - else { - // cases for modifying path when blocked - if (this->allow_repath) { - this->entity->log(MSG(dbg) << "Path blocked -- finding new path"); - this->set_path(); - } - else { - this->entity->log(MSG(dbg) << "Path blocked -- drop action"); - this->end_action = true; - } - } - - // inc frame - this->frame += time * this->frame_rate / 5.0f; -} - -void MoveAction::on_completion() {} - -bool MoveAction::completed() const { - // no more waypoints to a static location - if (this->end_action || (!this->unit_target.is_valid() && this->path.waypoints.empty())) { - return true; - } - - //close enough to end action - if (this->distance_to_target < this->radius) { - return true; - } - return false; -} - - -coord::phys3 MoveAction::next_waypoint() const { - if (this->path.waypoints.size() > 0) { - return this->path.waypoints.back().position; - } - else { - throw Error{MSG(err) << "No next waypoint available!"}; - } -} - - -void MoveAction::set_path() { - if (this->unit_target.is_valid()) { - this->path = path::to_object(this->entity->location.get(), this->unit_target.get()->location.get(), this->radius); - } - else { - coord::phys3 start = this->entity->location->pos.draw; - coord::phys3 end = this->target; - this->path = path::to_point(start, end, this->entity->location->passable); - } -} - -void MoveAction::set_distance() { - if (this->unit_target.is_valid()) { - auto &target_object = this->unit_target.get()->location; - coord::phys3 &unit_pos = this->entity->location->pos.draw; - this->distance_to_target = target_object->from_edge(unit_pos); - } - else { - coord::phys3_delta move_dir = this->target - this->entity->location->pos.draw; - this->distance_to_target = static_cast(std::hypot(move_dir.ne, move_dir.se)); - } -} - -GarrisonAction::GarrisonAction(Unit *e, UnitReference build) : - TargetAction{e, graphic_type::standing, build}, - complete{false} { -} - -void GarrisonAction::update_in_range(unsigned int, Unit *target_unit) { - auto &garrison_attr = target_unit->get_attribute(); - garrison_attr.content.push_back(this->entity->get_ref()); - - if (this->entity->location) { - this->entity->location->remove(); - this->entity->location = nullptr; - } - this->complete = true; -} - -UngarrisonAction::UngarrisonAction(Unit *e, const coord::phys3 &pos) : - UnitAction{e, graphic_type::standing}, - position(pos), - complete{false} { -} - -void UngarrisonAction::update(unsigned int) { - auto &garrison_attr = this->entity->get_attribute(); - - // try unload all objects currently garrisoned - auto position_it = std::remove_if( - std::begin(garrison_attr.content), - std::end(garrison_attr.content), - [this](UnitReference &u) { - if (u.is_valid()) { - // ptr to unit being ungarrisoned - Unit *unit_ptr = u.get(); - - // make sure it was placed outside - if (unit_ptr->unit_type->place_beside(unit_ptr, this->entity->location.get())) { - // task unit to move to position - auto &player = this->entity->get_attribute().player; - Command cmd(player, this->position); - cmd.set_ability(ability_type::move); - unit_ptr->queue_cmd(cmd); - return true; - } - } - return false; - }); - - // remove elements which were ungarrisoned - garrison_attr.content.erase(position_it, std::end(garrison_attr.content)); - - // completed when no units are remaining - this->complete = garrison_attr.content.empty(); -} - -void UngarrisonAction::on_completion() {} - -TrainAction::TrainAction(Unit *e, UnitType *pp) : - UnitAction{e, graphic_type::standing}, - trained{pp}, - timer{10000, 1}, // TODO get the training time from unit type - started{false}, - complete{false} { - // TODO deduct resources -} - -void TrainAction::update(unsigned int time) { - if (!this->started) { - // check if there is enough population capacity - if (!this->trained->default_attributes.has(attr_type::population)) { - this->started = true; - } - else { - auto &player = this->entity->get_attribute().player; - auto &population_demand = this->trained->default_attributes.get().demand; - bool can_start = population_demand == 0 || population_demand <= player.population.get_space(); - // TODO trigger not enough population capacity message - this->started = can_start; - } - } - - if (this->started) { - // place unit when ready - if (this->timer.finished() || this->timer.update(time)) { - // create using the producer - UnitContainer *container = this->entity->get_container(); - auto &player = this->entity->get_attribute().player; - auto uref = container->new_unit(*this->trained, player, this->entity->location.get()); - - // make sure unit got placed - // try again next update if cannot place - if (uref.is_valid()) { - if (this->entity->has_attribute(attr_type::building)) { - // use a move command to the gather point - auto &build_attr = this->entity->get_attribute(); - Command cmd(player, build_attr.gather_point); - cmd.set_ability(ability_type::move); - uref.get()->queue_cmd(cmd); - } - this->complete = true; - } - } - } -} - -void TrainAction::on_completion() { - if (!this->complete) { - // TODO give back the resources - } -} - -ResearchAction::ResearchAction(Unit *e, Research *research) : - UnitAction{e, graphic_type::standing}, - research{research}, - timer{research->type->get_research_time(), 1}, - complete{false} { - this->research->started(); -} - -void ResearchAction::update(unsigned int time) { - if (timer.update(time)) { - this->complete = true; - this->research->apply(); - this->research->completed(); - } -} - -void ResearchAction::on_completion() { - if (!this->complete) { - this->research->stopped(); - } -} - -BuildAction::BuildAction(Unit *e, UnitReference foundation) : - TargetAction{e, graphic_type::work, foundation}, - complete{.0f}, - build_rate{.0001f} { - // update the units type - if (this->entity->has_attribute(attr_type::multitype)) { - this->entity->get_attribute().switchType(gamedata::unit_classes::BUILDING, this->entity); - } -} - -void BuildAction::update_in_range(unsigned int time, Unit *target_unit) { - if (target_unit->has_attribute(attr_type::building)) { - auto &build = target_unit->get_attribute(); - - // upgrade floating outlines - auto target_location = target_unit->location.get(); - if (target_location->is_floating()) { - // try to place the object - if (target_location->place(object_state::placed)) { - // modify ground terrain - if (build.foundation_terrain > 0) { - target_location->set_ground(build.foundation_terrain, 0); - } - } - else { - // failed to start construction - this->complete = 1.0f; - return; - } - } - - // increment building completion - build.completed += build_rate * time; - this->complete = build.completed; - - if (this->complete >= 1.0f) { - this->complete = build.completed = 1.0f; - target_location->place(build.completion_state); - } - } - else { - this->complete = 1.0f; - } - - // inc frame - this->frame += time * this->frame_rate / 2.5f; -} - -void BuildAction::on_completion() { - if (this->get_target().is_valid() && this->get_target().get()->get_attribute().completed < 1.0f) { - // The BuildAction was just aborted and we shouldn't look for new buildings - return; - } - this->entity->log(MSG(dbg) << "Done building, searching for new building"); - auto valid = [this](const TerrainObject &obj) { - if (!this->entity->get_attribute().player.owns(obj.unit) || !obj.unit.has_attribute(attr_type::building) || obj.unit.get_attribute().completed >= 1.0f) { - return false; - } - this->entity->log(MSG(dbg) << "Found unit " << obj.unit.logsource_name()); - return true; - }; - - TerrainObject *new_target = find_in_radius(*this->entity->location, valid, BuildAction::search_tile_distance); - if (new_target != nullptr) { - this->entity->log(MSG(dbg) << "Found new building, queueing command"); - Command cmd(this->entity->get_attribute().player, &new_target->unit); - this->entity->queue_cmd(cmd); - } - else { - this->entity->log(MSG(dbg) << "Didn't find new building"); - } -} - -RepairAction::RepairAction(Unit *e, UnitReference tar) : - TargetAction{e, graphic_type::work, tar}, - timer{80}, - complete{false} { - if (!tar.is_valid()) { - // the target no longer exists - complete = true; - } - else { - Unit *target = tar.get(); - - if (!target->has_attribute(attr_type::building)) { - this->timer.set_interval(this->timer.get_interval() * 4); - } - - // cost formula: 0.5 * (target cost) / (target max hp) - auto &hp = target->get_attribute(); - auto &owner = this->entity->get_attribute(); - - // get the target unit's cost - this->cost += target->unit_type->cost.get(owner.player); - this->cost *= 0.5 / hp.hp; - - if (!owner.player.deduct(this->cost)) { - // no resources to start - this->complete = true; - } - } -} - -void RepairAction::update_in_range(unsigned int time, Unit *target_unit) { - auto &hp = target_unit->get_attribute(); - auto &dm = target_unit->get_attribute(); - - if (dm.hp >= hp.hp) { - // repaired by something else - this->complete = true; - } - else if (this->timer.update(time)) { - dm.hp += 1; - - if (dm.hp >= hp.hp) { - this->complete = true; - } - - if (!this->complete) { - auto &owner = this->entity->get_attribute(); - if (!owner.player.deduct(this->cost)) { - // no resources to continue - this->complete = true; - } - } - } - - // inc frame - this->frame += time * this->frame_rate / 2.5f; -} - -GatherAction::GatherAction(Unit *e, UnitReference tar) : - TargetAction{e, graphic_type::work, tar}, - complete{false}, - target_resource{true}, - target{tar} { - Unit *target = this->target.get(); - this->resource_class = target->unit_type->unit_class; - - // handle unit type changes based on resource class - if (this->entity->has_attribute(attr_type::multitype)) { - this->entity->get_attribute().switchType(this->resource_class, this->entity); - } - - // set the type of gatherer - auto &worker_resource = this->entity->get_attribute(); - if (target->has_attribute(attr_type::resource)) { - auto &resource_attr = target->get_attribute(); - if (worker_resource.resource_type != resource_attr.resource_type) { - worker_resource.amount = 0; - } - worker_resource.resource_type = resource_attr.resource_type; - } - else { - throw std::invalid_argument("Unit reference has no resource attribute"); - } -} - -GatherAction::~GatherAction() = default; - -void GatherAction::update_in_range(unsigned int time, Unit *targeted_resource) { - auto &worker = this->entity->get_attribute(); - auto &worker_resource = this->entity->get_attribute(); - if (this->target_resource) { - // the targets attributes - if (!targeted_resource->has_attribute(attr_type::resource)) { - complete = true; - return; - } - - // attack objects which have hitpoints (trees, hunt, sheep) - if (this->entity->has_attribute(attr_type::owner) && targeted_resource->has_attribute(attr_type::damaged)) { - auto &pl = this->entity->get_attribute(); - auto &dm = targeted_resource->get_attribute(); - - // only attack if hitpoints remain - if (dm.hp > 0) { - Command cmd(pl.player, targeted_resource); - cmd.set_ability(ability_type::attack); - cmd.add_flag(command_flag::attack_res); - this->entity->queue_cmd(cmd); - return; - } - } - - // need to return to dropsite - if (worker_resource.amount > worker.capacity) { - // move to dropsite location - this->target_resource = false; - this->set_target(this->nearest_dropsite(worker_resource.resource_type)); - } - else { - auto &resource_attr = targeted_resource->get_attribute(); - if (resource_attr.amount <= 0.0) { - // when the resource runs out - if (worker_resource.amount > 0.0) { - this->target_resource = false; - this->set_target(this->nearest_dropsite(worker_resource.resource_type)); - } - else { - this->complete = true; - } - } - else { - // transfer using gather rate - double amount = worker.gather_rate[worker_resource.resource_type] - * resource_attr.gather_rate * time; - worker_resource.amount += amount; - resource_attr.amount -= amount; - } - } - } - else { - // dropsite has been reached - // add value to player stockpile - auto &player = this->entity->get_attribute().player; - player.receive(worker_resource.resource_type, worker_resource.amount); - worker_resource.amount = 0.0; - - // make sure the resource still exists - if (this->target.is_valid() && this->target.get()->get_attribute().amount > 0.0) { - // return to resource collection - this->target_resource = true; - this->set_target(this->target); - } - else { - // resource depleted - this->complete = true; - } - } - - // inc frame - this->frame += time * this->frame_rate / 3.0f; -} - -void GatherAction::on_completion_in_range(int target_type) { - // find a different target with same type - TerrainObject *new_target = nullptr; - new_target = find_near(*this->entity->location, - [target_type](const TerrainObject &obj) { - return obj.unit.unit_type->id() == target_type && !obj.unit.has_attribute(attr_type::worker) && obj.unit.has_attribute(attr_type::resource) && obj.unit.get_attribute().amount > 0.0; - }); - - if (new_target) { - this->entity->log(MSG(dbg) << "auto retasking"); - auto &pl_attr = this->entity->get_attribute(); - Command cmd(pl_attr.player, &new_target->unit); - this->entity->queue_cmd(cmd); - } -} - -UnitReference GatherAction::nearest_dropsite(game_resource res_type) { - // find nearest dropsite from the targeted resource - auto ds = find_near(*this->target.get()->location, - [=, this](const TerrainObject &obj) { - if (not obj.unit.has_attribute(attr_type::building) or &obj.unit == this->entity or &obj.unit == this->target.get()) { - return false; - } - - return obj.unit.get_attribute().completed >= 1.0f && obj.unit.has_attribute(attr_type::owner) && obj.unit.get_attribute().player.owns(*this->entity) && obj.unit.has_attribute(attr_type::dropsite) && obj.unit.get_attribute().accepting_resource(res_type); - }); - - if (ds) { - return ds->unit.get_ref(); - } - else { - this->entity->log(MSG(dbg) << "no dropsite found"); - return UnitReference(); - } -} - -AttackAction::AttackAction(Unit *e, UnitReference tar) : - TargetAction{e, graphic_type::attack, tar, get_attack_range(e)}, - timer{500} { // TODO get fire rate from unit type - - // check if attacking a non resource unit - if (this->entity->has_attribute(attr_type::worker) && (!tar.get()->has_attribute(attr_type::resource) || tar.get()->has_attribute(attr_type::worker))) { - // switch to default villager graphics - if (this->entity->has_attribute(attr_type::multitype)) { - this->entity->get_attribute().switchType(gamedata::unit_classes::CIVILIAN, this->entity); - } - } - - // TODO rivit logic, a start inside the animation should be provided - this->timer.skip_to_trigger(); -} - -AttackAction::~AttackAction() = default; - -void AttackAction::update_in_range(unsigned int time, Unit *target_ptr) { - if (this->timer.update(time)) { - this->attack(*target_ptr); - } -} - -bool AttackAction::completed_in_range(Unit *target_ptr) const { - auto &dm = target_ptr->get_attribute(); - return dm.hp < 1; // is unit still alive? -} - -void AttackAction::attack(Unit &target) { - auto &attack = this->entity->get_attribute(); - if (attack.ptype) { - // add projectile to the game - this->fire_projectile(attack, target.location->pos.draw); - } - else { - this->damage_unit(target); - } -} - -void AttackAction::fire_projectile(const Attribute &att, const coord::phys3 &target) { - // container terrain and initial position - UnitContainer *container = this->entity->get_container(); - coord::phys3 current_pos = this->entity->location->pos.draw; - current_pos.up = att.init_height; - - // create using the producer - auto &player = this->entity->get_attribute().player; - auto projectile_ref = container->new_unit(*att.ptype, player, current_pos); - - // send towards target using a projectile ability (creates projectile motion action) - if (projectile_ref.is_valid()) { - auto projectile = projectile_ref.get(); - auto &projectile_attr = projectile->get_attribute(); - projectile_attr.launcher = this->entity->get_ref(); - projectile_attr.launched = true; - projectile->push_action(std::make_unique(projectile, target), true); - } - else { - this->entity->log(MSG(dbg) << "projectile launch failed"); - } -} - - -HealAction::HealAction(Unit *e, UnitReference tar) : - TargetAction{e, graphic_type::heal, tar, get_attack_range(e)}, - timer{this->entity->get_attribute().rate} { -} - -HealAction::~HealAction() = default; - -void HealAction::update_in_range(unsigned int time, Unit *target_ptr) { - if (this->timer.update(time)) { - this->heal(*target_ptr); - } -} - -bool HealAction::completed_in_range(Unit *target_ptr) const { - auto &hp = target_ptr->get_attribute(); - auto &dm = target_ptr->get_attribute(); - return dm.hp >= hp.hp; // is unit at full hitpoints? -} - -void HealAction::heal(Unit &target) { - auto &heal = this->entity->get_attribute(); - - // TODO move to separate function heal_unit (like damage_unit)? - // heal object - if (target.has_attribute(attr_type::hitpoints) && target.has_attribute(attr_type::damaged)) { - auto &hp = target.get_attribute(); - auto &dm = target.get_attribute(); - if ((dm.hp + heal.life) < hp.hp) { - dm.hp += heal.life; - } - else { - dm.hp = hp.hp; - } - } -} - - -ConvertAction::ConvertAction(Unit *e, UnitReference tar) : - TargetAction{e, graphic_type::attack, tar}, - complete{.0f} { -} - -void ConvertAction::update_in_range(unsigned int, Unit *) {} - -ProjectileAction::ProjectileAction(Unit *e, coord::phys3 target) : - UnitAction{e, graphic_type::standing}, - has_hit{false} { - // find speed to move - auto &sp_attr = this->entity->get_attribute(); - double projectile_speed = sp_attr.unit_speed.to_double(); - - // arc of projectile - auto &pr_attr = this->entity->get_attribute(); - float projectile_arc = pr_attr.projectile_arc; - - // distance and time to target - coord::phys3_delta d = target - this->entity->location->pos.draw; - double distance_to_target = d.length(); - double flight_time = distance_to_target / projectile_speed; - - - if (projectile_arc < 0) { - // TODO negative values probably indicate something - projectile_arc += 0.2; - } - - // now figure gravity from arc parameter - // TODO projectile arc is the ratio between horizontal and - // vertical components of the initial direction - this->grav = 0.01f * (exp(pow(projectile_arc, 0.5f)) - 1) * projectile_speed; - - // inital launch direction - auto &d_attr = this->entity->get_attribute(); - d_attr.unit_dir = d * (projectile_speed / distance_to_target); - - // account for initial height - coord::phys_t initial_height = this->entity->location->pos.draw.up; - d_attr.unit_dir.up = coord::phys_t((grav * flight_time) / 2) - (initial_height * (1 / flight_time)); -} - -ProjectileAction::~ProjectileAction() = default; - -void ProjectileAction::update(unsigned int time) { - auto &d_attr = this->entity->get_attribute(); - - // apply gravity - d_attr.unit_dir.up -= this->grav * time; - - coord::phys3 new_position = this->entity->location->pos.draw + d_attr.unit_dir * time; - if (!this->entity->location->move(new_position)) { - // TODO implement friendly_fire (now friendly_fire is always on), attack_attribute.friendly_fire - - // find object which was hit - auto terrain = this->entity->location->get_terrain(); - TileContent *tc = terrain->get_data(new_position.to_tile3().to_tile()); - if (tc && !tc->obj.empty()) { - for (auto obj_location : tc->obj) { - if (this->entity->location.get() != obj_location && obj_location->check_collisions()) { - this->damage_unit(obj_location->unit); - break; - } - } - } - - // TODO implement area of effect, attack_attribute.area_of_effect - - has_hit = true; - } - - // inc frame - this->frame += time * this->frame_rate; -} - -void ProjectileAction::on_completion() {} - -bool ProjectileAction::completed() const { - return (has_hit || this->entity->location->pos.draw.up <= 0); -} - -} // namespace openage diff --git a/libopenage/unit/action.h b/libopenage/unit/action.h deleted file mode 100644 index b0db755fe6..0000000000 --- a/libopenage/unit/action.h +++ /dev/null @@ -1,720 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -#include "../gamestate/old/resource.h" -#include "../pathfinding/path.h" -#include "attribute.h" -#include "research.h" -#include "unit.h" -#include "unit_container.h" - -namespace openage { - -class TerrainSearch; - -// TODO use a type instead of unsigned int for time - -/** - * A interval triggering timer used in actions. - * TODO find a better name for triggers - */ -class IntervalTimer { -public: - /** - * Constructs a timer with a given interval - */ - IntervalTimer(unsigned int interval); - - /** - * Constructs a timer with a given interval which will - * stop after a given number of triggers. - */ - IntervalTimer(unsigned int interval, int max_triggers); - - void skip_to_trigger(); - - bool update(unsigned int time); - - /** - * Returns the time until the next trigger - */ - unsigned int get_time_left() const; - - float get_progress() const; - - /** - * Returns true if at least one interval has passed. - */ - bool has_triggers() const; - - /** - * Returns true if the interval passed have reached the max. - */ - bool finished() const; - - /** - * Returns the number of intervals passed. - */ - int get_triggers() const { - return this->triggers; - } - - unsigned int get_interval() const { - return this->interval; - } - - void set_interval(unsigned int interval) { - this->interval = interval; - } - -private: - unsigned int interval; - - int max_triggers; - - unsigned int time_left; - - int triggers; -}; - - -/** - * Actions can be pushed onto any units action stack - * - * Each update cycle will perform the update function of the - * action on top of this stack - */ -class UnitAction { -public: - /** - * Require unit to be updated and an initial graphic type - */ - UnitAction(Unit *u, graphic_type initial_gt); - - virtual ~UnitAction() {} - - /** - * type of graphic this action should use - */ - graphic_type type() const; - - /** - * frame number to use on the current graphic - */ - float current_frame() const; - - /** - * each action has its own update functionality which gets called when this - * is the active action - */ - virtual void update(unsigned int time) = 0; - - /** - * action to perform when popped from a units action stack - */ - virtual void on_completion() = 0; - - /** - * gets called for all actions on stack each update cycle - * @return true when action is completed so it and everything above it can be popped - */ - virtual bool completed() const = 0; - - /** - * checks if the action can be interrupted, allowing it to be popped if the user - * specifies a new action, if false the action must reach a completed state - * before removal - * eg dead action must be completed and cannot be discarded - */ - virtual bool allow_interrupt() const = 0; - - /** - * control whether stack can discard the action automatically and - * should the stack be modifiable when this action is on top - * - * if true this action must complete and will not allow new actions - * to be pushed while it is active and also does not update the secondary actions - */ - virtual bool allow_control() const = 0; - - /** - * debug string to identify action types - */ - virtual std::string name() const = 0; - - /** - * common functions for actions - */ - void face_towards(const coord::phys3 pos); - - /** - * Damage a unit, returns true if the unit was killed in the process - */ - bool damage_unit(Unit &target); - - void move_to(Unit &target, bool use_range = true); - - /** - * produce debug info such as visualising paths - */ - static bool show_debug; - - /** - * a small distance to which units are considered touching - * when within this distance - */ - static coord::phys_t adjacent_range(Unit *u); - - /** - * looks at an ranged attributes on the unit - * otherwise returns same as adjacent_range() - */ - static coord::phys_t get_attack_range(Unit *u); - - /** - * looks at heal attribute on the unit - * otherwise returns same as adjacent_range() - */ - static coord::phys_t get_heal_range(Unit *u); - -protected: - /** - * the entity being updated - */ - Unit *entity; - - /** - * common graphic controls - */ - graphic_type graphic; - float frame; - float frame_rate; -}; - -/** - * Base class for actions which target another unit such as - * gather, attack, heal and convert - * TODO implement min range - */ -class TargetAction : public UnitAction { -public: - /** - * action_rad is how close a unit must come to another - * unit to be considered to touch the other, for example in - * gathering resource and melee attack - */ - TargetAction(Unit *e, graphic_type gt, UnitReference r, coord::phys_t action_rad); - - /** - * this constructor uses the default action radius formula which will - * bring the object as near to the target as the pathing grid will allow. - */ - TargetAction(Unit *e, graphic_type gt, UnitReference r); - virtual ~TargetAction() {} - - void update(unsigned int time) override; - void on_completion() override; - bool completed() const override; - bool allow_interrupt() const override { - return true; - } - bool allow_control() const override { - return true; - } - virtual std::string name() const override = 0; - - /** - * Control units action when in range of the target - */ - virtual void update_in_range(unsigned int, Unit *) = 0; - virtual void on_completion_in_range(int target_type) = 0; - virtual bool completed_in_range(Unit *) const = 0; - - coord::phys_t distance_to_target(); - Unit *update_distance(); - - UnitReference get_target() const; - int get_target_type_id() const; - - /** - * changes target, ending action when new target is invalid - */ - void set_target(UnitReference new_target); - -private: - UnitReference target; - int target_type_id; - int repath_attempts; - bool end_action; - - /** - * tracks distance to target from last update - */ - coord::phys_t dist_to_target, radius; -}; - -/** - * plays a fixed number of frames for the units dying animation - */ -class DecayAction : public UnitAction { -public: - DecayAction(Unit *e); - virtual ~DecayAction() {} - - void update(unsigned int time) override; - void on_completion() override; - bool completed() const override; - bool allow_interrupt() const override { - return false; - } - bool allow_control() const override { - return false; - } - std::string name() const override { - return "decay"; - } - -private: - float end_frame; -}; - -/** - * plays a fixed number of frames for the units dying animation - */ -class DeadAction : public UnitAction { -public: - DeadAction( - Unit *e, - std::function on_complete = []() {}); - virtual ~DeadAction() {} - - void update(unsigned int time) override; - void on_completion() override; - bool completed() const override; - bool allow_interrupt() const override { - return false; - } - bool allow_control() const override { - return false; - } - std::string name() const override { - return "dead"; - } - -private: - float end_frame; - std::function on_complete_func; -}; - -/** - * places an idle action on the stack once building is complete - */ -class FoundationAction : public UnitAction { -public: - FoundationAction(Unit *e, bool add_destuction = false); - virtual ~FoundationAction() {} - - void update(unsigned int time) override; - void on_completion() override; - bool completed() const override; - bool allow_interrupt() const override { - return true; - } - bool allow_control() const override { - return false; - } - std::string name() const override { - return "foundation"; - } - -private: - bool add_destruct_effect, cancel; -}; - -/** - * keeps an entity in a fixed position - */ -class IdleAction : public UnitAction { -public: - IdleAction(Unit *e); - virtual ~IdleAction() {} - - void update(unsigned int time) override; - void on_completion() override; - bool completed() const override; - bool allow_interrupt() const override { - return false; - } - bool allow_control() const override { - return true; - } - std::string name() const override { - return "idle"; - } - -private: - // look for auto task actions - std::shared_ptr search; - ability_set auto_abilities; -}; - -/** - * moves an entity to another location - */ -class MoveAction : public UnitAction { -public: - /** - * moves unit to a given fixed location - */ - MoveAction(Unit *e, coord::phys3 tar, bool repath = true); - - /** - * moves a unit to within a distance to another unit - */ - MoveAction(Unit *e, UnitReference tar, coord::phys_t within_range); - virtual ~MoveAction(); - - void update(unsigned int time) override; - void on_completion() override; - bool completed() const override; - bool allow_interrupt() const override { - return true; - } - bool allow_control() const override { - return true; - } - std::string name() const override { - return "move"; - } - - coord::phys3 next_waypoint() const; - -private: - UnitReference unit_target; - coord::phys3 target; - - // how near the units should come to target - coord::phys_t distance_to_target, radius; - - path::Path path; - - // should a new path be found if unit gets blocked - bool allow_repath, end_action; - - void initialise(); - - /** - * use a star to find a path to target - */ - void set_path(); - - /** - * updates the distance_to_target value - */ - void set_distance(); -}; - -/** - * garrison inside a building - */ -class GarrisonAction : public TargetAction { -public: - GarrisonAction(Unit *e, UnitReference build); - virtual ~GarrisonAction() {} - - void update_in_range(unsigned int time, Unit *target_unit) override; - void on_completion_in_range(int) override {} - bool completed_in_range(Unit *) const override { - return this->complete; - } - std::string name() const override { - return "garrison"; - } - -private: - bool complete; -}; - -/** - * garrison inside a building - */ -class UngarrisonAction : public UnitAction { -public: - UngarrisonAction(Unit *e, const coord::phys3 &pos); - virtual ~UngarrisonAction() {} - - void update(unsigned int time) override; - void on_completion() override; - bool completed() const override { - return this->complete; - } - bool allow_interrupt() const override { - return true; - } - bool allow_control() const override { - return true; - } - std::string name() const override { - return "ungarrison"; - } - -private: - coord::phys3 position; - bool complete; -}; - -/** - * trains a new unit - */ -class TrainAction : public UnitAction { -public: - TrainAction(Unit *e, UnitType *pp); - virtual ~TrainAction() {} - - void update(unsigned int time) override; - void on_completion() override; - bool completed() const override { - return this->complete; - } - bool allow_interrupt() const override { - return false; - } - bool allow_control() const override { - return true; - } - std::string name() const override { - return "train"; - } - - float get_progress() const { - return this->timer.get_progress(); - } - -private: - UnitType *trained; - - IntervalTimer timer; - bool started; - bool complete; -}; - -/** - * trains a new unit - */ -class ResearchAction : public UnitAction { -public: - ResearchAction(Unit *e, Research *research); - virtual ~ResearchAction() {} - - void update(unsigned int time) override; - void on_completion() override; - bool completed() const override { - return this->complete; - } - bool allow_interrupt() const override { - return false; - } - bool allow_control() const override { - return true; - } - std::string name() const override { - return "train"; - } - - float get_progress() const { - return this->timer.get_progress(); - } - const ResearchType *get_research_type() const { - return this->research->type; - } - -private: - Research *research; - - IntervalTimer timer; - bool complete; -}; - -/** - * builds a building - */ -class BuildAction : public TargetAction { -public: - BuildAction(Unit *e, UnitReference foundation); - virtual ~BuildAction() {} - - void update_in_range(unsigned int time, Unit *target_unit) override; - void on_completion_in_range(int) override {} - bool completed_in_range(Unit *) const override { - return this->complete >= 1.0f; - } - void on_completion() override; - std::string name() const override { - return "build"; - } - - float get_progress() const { - return this->complete; - } - -private: - float complete, build_rate; - static constexpr float search_tile_distance = 9.0f; -}; - -/** - * repairs a unit - */ -class RepairAction : public TargetAction { -public: - RepairAction(Unit *e, UnitReference tar); - virtual ~RepairAction() {} - - void update_in_range(unsigned int time, Unit *target_unit) override; - void on_completion_in_range(int) override {} - bool completed_in_range(Unit *) const override { - return this->complete; - } - std::string name() const override { - return "repair"; - } - -private: - /** - * stores the cost of the repair for 1hp - */ - ResourceBundle cost; - - IntervalTimer timer; - bool complete; -}; - -/** - * gathers resource from another object - */ -class GatherAction : public TargetAction { -public: - GatherAction(Unit *e, UnitReference tar); - virtual ~GatherAction(); - - void update_in_range(unsigned int time, Unit *target_unit) override; - void on_completion_in_range(int target_type) override; - bool completed_in_range(Unit *) const override { - return this->complete; - } - std::string name() const override { - return "gather"; - } - -private: - bool complete, target_resource; - UnitReference target; - gamedata::unit_classes resource_class; - UnitReference nearest_dropsite(game_resource res_type); -}; - -/** - * attacks another unit - */ -class AttackAction : public TargetAction { -public: - AttackAction(Unit *e, UnitReference tar); - virtual ~AttackAction(); - - void update_in_range(unsigned int time, Unit *target_unit) override; - void on_completion_in_range(int) override {} - bool completed_in_range(Unit *) const override; - std::string name() const override { - return "attack"; - } - -private: - IntervalTimer timer; - - /** - * use attack action - */ - void attack(Unit &target); - - /** - * add a projectile game object which moves towards the target - */ - void fire_projectile(const Attribute &att, const coord::phys3 &target); -}; - -/** - * heals another unit - */ -class HealAction : public TargetAction { -public: - HealAction(Unit *e, UnitReference tar); - virtual ~HealAction(); - - void update_in_range(unsigned int time, Unit *target_unit) override; - void on_completion_in_range(int) override {} - bool completed_in_range(Unit *) const override; - std::string name() const override { - return "heal"; - } - -private: - IntervalTimer timer; - - /** - * use heal action - */ - void heal(Unit &target); -}; - -/** - * convert an object - */ -class ConvertAction : public TargetAction { -public: - ConvertAction(Unit *e, UnitReference tar); - virtual ~ConvertAction() {} - - void update_in_range(unsigned int time, Unit *target_unit) override; - void on_completion_in_range(int) override {} - bool completed_in_range(Unit *) const override { - return this->complete >= 1.0f; - } - std::string name() const override { - return "convert"; - } - -private: - float complete; -}; - -/** - * moves object to fly in a parabolic shape - */ -class ProjectileAction : public UnitAction { -public: - ProjectileAction(Unit *e, coord::phys3 target); - virtual ~ProjectileAction(); - - void update(unsigned int time) override; - void on_completion() override; - bool completed() const override; - bool allow_interrupt() const override { - return false; - } - bool allow_control() const override { - return false; - } - std::string name() const override { - return "projectile"; - } - -private: - double grav; - bool has_hit; -}; - -} // namespace openage diff --git a/libopenage/unit/attribute.cpp b/libopenage/unit/attribute.cpp deleted file mode 100644 index 6826bded35..0000000000 --- a/libopenage/unit/attribute.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2016-2017 the openage authors. See copying.md for legal info. - -#include "attribute.h" -#include "unit.h" -#include "unit_type.h" - -namespace openage { - -bool Attribute::accepting_resource(game_resource res) const { - return std::find(resource_types.begin(), resource_types.end(), res) != resource_types.end(); -} - -void Attribute::switchType(const gamedata::unit_classes cls, Unit *unit) const { - auto search = this->types.find(cls); - if (search != this->types.end()) { - auto &player = unit->get_attribute(); - search->second->reinitialise(unit, player.player); - } -} - -} // namespace openage diff --git a/libopenage/unit/attribute.h b/libopenage/unit/attribute.h deleted file mode 100644 index 5a5ce05117..0000000000 --- a/libopenage/unit/attribute.h +++ /dev/null @@ -1,643 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include - -#include "../coord/tile.h" -#include "../gamedata/unit_dummy.h" -#include "../terrain/terrain_object.h" -#include "../gamestate/old/resource.h" -#include "unit_container.h" - -namespace std { - -/** - * hasher for unit classes enum type - */ -template<> struct hash { - typedef underlying_type::type underlying_type; - - size_t operator()(const openage::gamedata::unit_classes &arg) const { - hash hasher; - return hasher(static_cast(arg)); - } -}; - -} // namespace std - -namespace openage { - -/** - * Types of action graphics - */ -enum class graphic_type { - construct, - shadow, - decay, - dying, - standing, - walking, - carrying, - attack, - heal, - work -}; - -/** - * List of unit's attribute types. - */ -enum class attr_type { - owner, - population, - damaged, - hitpoints, - armor, - attack, - formation, - heal, - speed, - direction, - projectile, - building, - dropsite, - resource, - resource_generator, - worker, - storage, - multitype, - garrison -}; - -/** - * List of unit's attack stance. - * Can be used for buildings also. - */ -enum class attack_stance { - aggressive, - defensive, - stand_ground, - do_nothing -}; - -/** - * List of unit's formation. - * Effect applys on a group of units. - */ -enum class attack_formation { - line, - staggered, - box, - flank -}; - - -/** - * this type gets specialized for each attribute - */ -template class Attribute; - -/** - * Wraps a templated attribute - */ -class AttributeContainer { -public: - AttributeContainer() {} - - AttributeContainer(attr_type t) - : - type{t} {} - - virtual ~AttributeContainer() = default; - - attr_type type; - - /** - * shared attributes are common across all units of - * one type, such as max hp, and gather rates - * - * non shared attributes include a units current hp, - * and the amount a villager is carrying - */ - virtual bool shared() const = 0; - - /** - * Produces an copy of the attribute. - */ - virtual std::shared_ptr copy() const = 0; -}; - -/** - * An unordered_map with a int key used as a type id - * and a unsigned int value used as the amount - */ -using typeamount_map = std::unordered_map; - -/** - * Wraps a templated shared attribute - * - * Shared attributes are common across all units of - * one type - */ -class SharedAttributeContainer: public AttributeContainer { -public: - - SharedAttributeContainer(attr_type t) - : - AttributeContainer{t} {} - - bool shared() const override { - return true; - } -}; - -/** - * Wraps a templated unshared attribute - * - * Shared attributes are copied for each unit of - * one type - */ -class UnsharedAttributeContainer: public AttributeContainer { -public: - - UnsharedAttributeContainer(attr_type t) - : - AttributeContainer{t} {} - - bool shared() const override { - return false; - } -}; - -// ----------------------------- -// attribute definitions go here -// ----------------------------- - -class Player; - -template<> class Attribute: public SharedAttributeContainer { -public: - Attribute(Player &p) - : - SharedAttributeContainer{attr_type::owner}, - player(p) {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - Player &player; -}; - -/** - * The max hitpoints and health bar information. - * TODO change bar information stucture - */ -template<> class Attribute: public SharedAttributeContainer { -public: - Attribute(unsigned int i) - : - SharedAttributeContainer{attr_type::hitpoints}, - hp{i} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - /** - * The max hitpoints - */ - unsigned int hp; - float hp_bar_height; -}; - -/** - * The population capacity and the population demand. - */ -template<> class Attribute: public SharedAttributeContainer { -public: - Attribute(int demand, int capacity) - : - SharedAttributeContainer{attr_type::population}, - demand{demand}, - capacity{capacity} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - int demand; - int capacity; -}; - -/** - * The current hitpoints. - * TODO add last damage taken timestamp - */ -template<> class Attribute: public UnsharedAttributeContainer { -public: - Attribute(unsigned int i) - : - UnsharedAttributeContainer{attr_type::damaged}, - hp{i} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - /** - * The current hitpoint - */ - unsigned int hp; -}; - -template<> class Attribute: public SharedAttributeContainer { -public: - Attribute(typeamount_map a) - : - SharedAttributeContainer{attr_type::armor}, - armor{a} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - typeamount_map armor; -}; - -/** - * TODO can a unit have multiple attacks such as villagers hunting map target classes onto attacks - * TODO remove the first constructor and the default values after (keep for now for compatibility) - */ -template<> class Attribute: public SharedAttributeContainer { -public: - // TODO remove (keep for testing) - // 4 = gamedata::hit_class::UNITS_MELEE (not exported at the moment) - Attribute(UnitType *type, coord::phys_t r, coord::phys_t h, unsigned int d) - : - Attribute{type, r, h, {{4, d}}} {} - - Attribute(UnitType *type, coord::phys_t r, coord::phys_t h, typeamount_map d, - coord::phys_t min_range=0, bool friendly_fire=false, - coord::phys_t area_of_effect=0) - : - SharedAttributeContainer{attr_type::attack}, - ptype{type}, - min_range{min_range}, - max_range{r}, - init_height{h}, - damage{d}, - friendly_fire{friendly_fire}, - area_of_effect{area_of_effect} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - /** - * The projectile's unit type - */ - UnitType *ptype; - - /** - * The min range of the attack - * TODO not used - */ - coord::phys_t min_range; - - /** - * The max range of the attack - */ - coord::phys_t max_range; - - /** - * The height from which the projectile starts - */ - coord::phys_t init_height; - - typeamount_map damage; - - /** - * If the attack can damage allied (friendly) units. - * TODO not used - */ - bool friendly_fire; - - /** - * The radius of the area of effect of the attack or 0 if there is no area_of_effect. - * TODO not used - */ - coord::phys_t area_of_effect; -}; - -/** - * The attack stance and formation - * TODO store patrol and follow command information - */ -template<> class Attribute: public UnsharedAttributeContainer { -public: - - Attribute() - : - Attribute{attack_stance::do_nothing} {} - - Attribute(attack_stance stance) - : - UnsharedAttributeContainer{attr_type::formation}, - stance{stance}, - formation{attack_formation::line} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - attack_stance stance; - attack_formation formation; -}; - -/** - * Healing capabilities. - */ -template<> class Attribute: public SharedAttributeContainer { -public: - Attribute(coord::phys_t r, unsigned int l, unsigned int ra) - : - SharedAttributeContainer{attr_type::heal}, - range{r}, - life{l}, - rate{ra} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - /** - * The max range of the healing. - */ - coord::phys_t range; - - /** - * Life healed in each cycle - */ - unsigned int life; - - /** - * The period of each heal cycle - */ - unsigned int rate; -}; - -template<> class Attribute: public SharedAttributeContainer { -public: - Attribute(coord::phys_t sp) - : - SharedAttributeContainer{attr_type::speed}, - unit_speed{sp} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - // TODO rename to default or normal - coord::phys_t unit_speed; -}; - -template<> class Attribute: public UnsharedAttributeContainer { -public: - Attribute(coord::phys3_delta dir) - : - UnsharedAttributeContainer{attr_type::direction}, - unit_dir(dir) {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - coord::phys3_delta unit_dir; -}; - -template<> class Attribute: public UnsharedAttributeContainer { -public: - Attribute(float arc) - : - UnsharedAttributeContainer{attr_type::projectile}, - projectile_arc{arc}, - launched{false} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - float projectile_arc; - UnitReference launcher; - bool launched; -}; - -/** - * TODO revisit after unit training is improved - */ -template<> class Attribute: public UnsharedAttributeContainer { -public: - Attribute(int foundation_terrain, UnitType *pp, coord::phys3 gather_point) - : - UnsharedAttributeContainer{attr_type::building}, - completed{.0f}, - foundation_terrain{foundation_terrain}, - pp{pp}, - gather_point{gather_point} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - float completed; - int foundation_terrain; - - // set the TerrainObject to this state - // once building has been completed - object_state completion_state; - - // TODO: list allowed trainable producers - UnitType *pp; - - /** - * The go to point after a unit is created. - */ - coord::phys3 gather_point; -}; - -/** - * The resources that are accepted to be dropped. - */ -template<> class Attribute: public SharedAttributeContainer { -public: - Attribute(std::vector types) - : - SharedAttributeContainer{attr_type::dropsite}, - resource_types{types} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - bool accepting_resource(game_resource res) const; - - std::vector resource_types; -}; - -/** - * Resource capacity of a trees, mines, animal, worker etc. - */ -template<> class Attribute: public UnsharedAttributeContainer { -public: - Attribute() - : - Attribute{game_resource::food, 0} {} - - Attribute(game_resource type, double init_amount, double decay=0.0, double gather_rate=1.0) - : - UnsharedAttributeContainer{attr_type::resource}, - resource_type{type}, - amount{init_amount}, - decay{decay}, - gather_rate{gather_rate} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - game_resource resource_type; - - double amount; - - /** - * The rate of decay - */ - double decay; - - /** - * The gather rate multiplier (1.0 is the identity) - */ - double gather_rate; - -}; - -/** - * Resource generator eg. relic. - * While a unit is idle and contains this attribute, it will generate resources for its owner. - * - * A rate of zero means that the generation is continuously and not in intervals. - */ -template<> class Attribute: public SharedAttributeContainer { -public: - - Attribute(ResourceBundle resources, double rate=0) - : - SharedAttributeContainer{attr_type::resource_generator}, - resources{resources}, - rate{rate} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - ResourceBundle resources; - - double rate; - -}; - -/** - * The worker's capacity and gather rates. - */ -template<> class Attribute: public SharedAttributeContainer { -public: - Attribute() - : - SharedAttributeContainer{attr_type::worker} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - /** - * The max number of resources that can be carried. - */ - double capacity; - - /** - * The gather rate for each resource. - * The ResourceBundle class is used but instead of amounts it stores gather rates. - */ - ResourceBundle gather_rate; -}; - -/** - * The worker's capacity and gather rates. - */ -template<> class Attribute: public SharedAttributeContainer { -public: - Attribute() - : - SharedAttributeContainer{attr_type::storage} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - /** - * The capacity for each resource. - */ - ResourceBundle capacity; -}; - -class Unit; - -/** - * Stores the collection of unit types based on a unit class. - * It is used mostly for units with multiple graphics (villagers, trebuchets). - */ -template<> class Attribute: public SharedAttributeContainer { -public: - Attribute() - : - SharedAttributeContainer{attr_type::multitype} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - /** - * Switch the type of a unit based on a given unit class - */ - void switchType(const gamedata::unit_classes cls, Unit *unit) const; - - /** - * The collection of unit class to unit type pairs - */ - std::unordered_map types; -}; - -/** - * Units put inside a building. - * TODO add capacity per type of unit - */ -template<> class Attribute: public UnsharedAttributeContainer { -public: - Attribute() - : - UnsharedAttributeContainer{attr_type::garrison} {} - - std::shared_ptr copy() const override { - return std::make_shared>(*this); - } - - /** - * The units that are garrisoned. - */ - std::vector content; -}; - -} // namespace openage diff --git a/libopenage/unit/attributes.cpp b/libopenage/unit/attributes.cpp deleted file mode 100644 index faa4fa0df5..0000000000 --- a/libopenage/unit/attributes.cpp +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2017-2017 the openage authors. See copying.md for legal info. - -#include "attributes.h" - -namespace openage { - -void Attributes::add(const std::shared_ptr attr) { - this->attrs[attr->type] = attr; -} - -void Attributes::add_copies(const Attributes &other) { - this->add_copies(other, true, true); -} - -void Attributes::add_copies(const Attributes &other, bool shared, bool unshared) { - for (auto &i : other.attrs) { - auto &attr = *i.second.get(); - - if (attr.shared()) { - if (shared) { - // pass self - this->add(i.second); - } - } - else if (unshared) { - // create copy - this->add(attr.copy()); - } - } -} - -bool Attributes::remove(const attr_type type) { - return this->attrs.erase(type) > 0; -} - -bool Attributes::has(const attr_type type) const { - return this->attrs.find(type) != this->attrs.end(); -} - -std::shared_ptr Attributes::get(const attr_type type) const { - return this->attrs.at(type); -} - -} // namespace openage diff --git a/libopenage/unit/attributes.h b/libopenage/unit/attributes.h deleted file mode 100644 index cc02ac1193..0000000000 --- a/libopenage/unit/attributes.h +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2017-2017 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "attribute.h" - -namespace openage { - -/** - * Contains a group of attributes. - * Can contain only one attribute of each type. - */ -class Attributes { -public: - Attributes() {} - - /** - * Add an attribute or replace any attribute of the same type. - */ - void add(const std::shared_ptr attr); - - /** - * Add copies of all the attributes from the given Attributes. - */ - void add_copies(const Attributes &attrs); - - /** - * Add copies of all the attributes from the given Attributes. - * If shared is false, shared attributes are ignored. - * If unshared is false, unshared attributes are ignored. - */ - void add_copies(const Attributes &attrs, bool shared, bool unshared); - - /** - * Remove an attribute based on the type. - */ - bool remove(const attr_type type); - - /** - * Check if the attribute of the given type exists. - */ - bool has(const attr_type type) const; - - /** - * Get the attribute based on the type. - */ - std::shared_ptr get(const attr_type type) const; - - /** - * Get the attribute - */ - template - Attribute &get() const { - return *reinterpret_cast *>(this->attrs.at(T).get()); - } - -private: - - std::map> attrs; -}; - -} // namespace openage diff --git a/libopenage/unit/command.cpp b/libopenage/unit/command.cpp deleted file mode 100644 index ec83664394..0000000000 --- a/libopenage/unit/command.cpp +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. - -#include "command.h" - -namespace openage { - -Command::Command(const Player &p, Unit *unit, bool haspos, UnitType *t, Research *res) - : - player(p), - has_pos{haspos}, - u{unit}, - unit_type{t}, - res{res} { - this->modifiers.set(); -} - -Command::Command(const Player &p, Unit *unit) - : - Command{p, unit, false, nullptr, nullptr} { -} - -Command::Command(const Player &p, coord::phys3 position) - : - Command{p, nullptr, true, nullptr, nullptr} { - this->pos = position; -} - -Command::Command(const Player &p, Unit *unit, coord::phys3 position) - : - Command{p, unit, true, nullptr, nullptr} { - this->pos = position; -} - -Command::Command(const Player &p, UnitType *t) - : - Command{p, nullptr, false, t, nullptr} { -} - -Command::Command(const Player &p, Research *res) - : - Command{p, nullptr, false, nullptr, res} { -} - -Command::Command(const Player &p, UnitType *t, coord::phys3 position) - : - Command{p, nullptr, true, t, nullptr} { - this->pos = position; -} - -bool Command::has_unit() const { - return this->u; -} - -bool Command::has_position() const { - return this->has_pos; -} - -bool Command::has_type() const { - return this->unit_type; -} - -bool Command::has_research() const { - return this->res; -} - -Unit *Command::unit() const { - return this->u; -} - -coord::phys3 Command::position() const { - return this->pos; -} - -UnitType *Command::type() const { - return this->unit_type; -} - -Research *Command::research() const { - return this->res; -} - -void Command::set_ability(ability_type t) { - this->modifiers = 0; - this->modifiers[static_cast(t)] = true; -} - -void Command::set_ability_set(ability_set set) { - this->modifiers = set; -} - -const ability_set &Command::ability() const { - return this->modifiers; -} - -void Command::add_flag(command_flag flag) { - this->flags.insert(flag); -} - -bool Command::has_flag(command_flag flag) const { - return 0 < this->flags.count(flag); -} - -} // namespace openage diff --git a/libopenage/unit/command.h b/libopenage/unit/command.h deleted file mode 100644 index 4719e09e0b..0000000000 --- a/libopenage/unit/command.h +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2014-2018 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "../coord/phys.h" -#include "ability.h" - -namespace openage { - -/** - * additional flags which may affect some abilities - */ -enum class command_flag { - interrupt, // the user directly issued this command, stopping other actions - use_range, // move command account for units range - attack_res // allow attack on a resource object -}; - -} // namespace openage - -namespace std { - -/** - * hasher for game command flags - */ -template<> -struct hash { - typedef underlying_type::type underlying_type; - - size_t operator()(const openage::command_flag &arg) const { - hash hasher; - return hasher(static_cast(arg)); - } -}; - -} // namespace std - -namespace openage { - -class Player; -class Research; -class Unit; -class UnitType; - -/* - * Game command from the ui - * TODO reorganize the names of the optional variables and their getters - */ -class Command { -public: - - /** - * target another unit - */ - Command(const Player &, Unit *unit); - - /** - * target a position - */ - Command(const Player &, coord::phys3 position); - - /** - * target another unit or a position - */ - Command(const Player &, Unit *unit, coord::phys3 position); - - /** - * select a type - */ - Command(const Player &, UnitType *t); - - /** - * select a research - */ - Command(const Player &, Research *res); - - /** - * place building foundation - */ - Command(const Player &, UnitType *, coord::phys3); - - bool has_unit() const; - bool has_position() const; - bool has_type() const; - bool has_research() const; - - Unit *unit() const; - coord::phys3 position() const; - UnitType *type() const; - Research *research() const; - - /** - * sets invoked ability type, no other type may be used if - * this gets set - * - * @param type allows a specific ability type to be used - * for example to set a unit to patrol rather than the default move - */ - void set_ability(ability_type t); - - /** - * restricts action to a set of possible ability types - */ - void set_ability_set(ability_set set); - - /** - * the ability types allowed to use this command - */ - const ability_set &ability() const; - - /** - * add addition option to this command - */ - void add_flag(command_flag flag); - - /** - * read the range setting - */ - bool has_flag(command_flag flag) const; - - /** - * player who created the command - */ - const Player &player; - -private: - - /** - * basic constructor, which shouldnt be used directly - */ - Command(const Player &, Unit *unit, bool haspos, UnitType *t, Research *res); - - bool has_pos; - Unit *u; - coord::phys3 pos = {0, 0, 0}; // TODO: make pos a c++17 optional - UnitType *unit_type; - Research *res; - - /** - * additional options - */ - std::unordered_set flags; - - /** - * select actions to use when targeting - */ - ability_set modifiers; - -}; - -} // namespace openage diff --git a/libopenage/unit/producer.cpp b/libopenage/unit/producer.cpp deleted file mode 100644 index 07f6f45463..0000000000 --- a/libopenage/unit/producer.cpp +++ /dev/null @@ -1,695 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#include - -#include "../gamedata/unit_dummy.h" -#include "../log/log.h" -#include "../terrain/terrain.h" -#include "../terrain/terrain_object.h" -#include "../util/strings.h" -#include "ability.h" -#include "action.h" -#include "producer.h" -#include "unit.h" - -/** @file - * Many values in this file are hardcoded, due to limited understanding of how the original - * game files work -- as more becomes known these will be removed. - * - * It is likely the conversion from gamedata to openage units will be done by the nyan - * system in future - */ - -namespace openage { - -std::unordered_set allowed_terrains(const gamedata::ground_type &restriction) { - // returns a terrain whitelist for a given restriction - // you can also define a blacklist for brevity, it will be converted and returned - std::unordered_set whitelist; - std::unordered_set blacklist; - - // 1, 14, and 15 are water, 2 is shore - if (restriction == gamedata::ground_type::WATER || restriction == gamedata::ground_type::WATER_0x0D || restriction == gamedata::ground_type::WATER_SHIP_0x03 || restriction == gamedata::ground_type::WATER_SHIP_0x0F) { - whitelist.insert(1); // water - whitelist.insert(2); // shore - whitelist.insert(4); // shallows - whitelist.insert(14); // medium water - whitelist.insert(15); // deep water - } - else if (restriction == gamedata::ground_type::SOLID) { - blacklist.insert(1); // water - blacklist.insert(4); // shallows - blacklist.insert(14); // medium water - blacklist.insert(15); // deep water - } - else if (restriction == gamedata::ground_type::FOUNDATION || restriction == gamedata::ground_type::NO_ICE_0x08 || restriction == gamedata::ground_type::FOREST) { - blacklist.insert(1); // water - blacklist.insert(4); // shallows - blacklist.insert(14); // medium water - blacklist.insert(15); // deep water - blacklist.insert(18); // ice - } - else { - log::log(MSG(warn) << "undefined terrain restriction, assuming solid"); - blacklist.insert(1); // water - blacklist.insert(4); // shallows - blacklist.insert(14); // medium water - blacklist.insert(15); // deep water - } - - // if we're using a blacklist, fill out a whitelist with everything not on it - if (blacklist.size() > 0) { - // Allow all terrains that are not on blacklist - for (int i = 0; i < 32; ++i) { - if (blacklist.count(i) == 0) { - whitelist.insert(i); - } - } - } - - return whitelist; -} - -ResourceBundle create_resource_cost(game_resource resource, int amount) { - ResourceBundle resources = ResourceBundle(); - resources[resource] = amount; - return resources; -} - -ObjectProducer::ObjectProducer(const Player &owner, const GameSpec &spec, const gamedata::unit_object *ud) : - UnitType(owner), - dataspec(spec), - unit_data(*ud), - dead_unit_id{ud->dead_unit_id} { - // copy the class type - this->unit_class = this->unit_data.unit_class; - this->icon = this->unit_data.icon_id; - - // for now just look for type names ending with "_D" - this->decay = unit_data.name.substr(unit_data.name.length() - 2) == "_D"; - - // find suitable sounds - int creation_sound = this->unit_data.train_sound_id; - int dying_sound = this->unit_data.dying_sound_id; - if (creation_sound == -1) { - creation_sound = this->unit_data.damage_sound_id; - } - if (creation_sound == -1) { - creation_sound = this->unit_data.selection_sound_id; - } - if (dying_sound == -1) { - dying_sound = 323; //generic explosion sound - } - on_create = spec.get_sound(creation_sound); - on_destroy = spec.get_sound(dying_sound); - - // convert the float to the discrete foundation size... - this->foundation_size = { - static_cast(this->unit_data.radius_x * 2), - static_cast(this->unit_data.radius_y * 2), - }; - - // TODO get cost, temp fixed cost of 50 food - this->cost.set(cost_type::constant, create_resource_cost(game_resource::food, 50)); -} - -ObjectProducer::~ObjectProducer() = default; - -int ObjectProducer::id() const { - return this->unit_data.id0; -} - -int ObjectProducer::parent_id() const { - int uid = this->unit_data.id0; - - // male types - if (uid == 156 || uid == 120 || uid == 592 || uid == 123 || uid == 579 || uid == 124) { - return 83; - } - - // female types - else if (uid == 222 || uid == 354 || uid == 590 || uid == 218 || uid == 581 || uid == 220) { - return 293; - } - return uid; -} - -std::string ObjectProducer::name() const { - return this->unit_data.name; -} - -void ObjectProducer::initialise(Unit *unit, Player &player) { - ENSURE(this->owner == player, "unit init from a UnitType of a wrong player which breaks tech levels"); - - // log attributes - unit->log(MSG(dbg) << "setting unit type " << this->unit_data.id0 << " " << this->unit_data.name); - - // reset existing attributes - unit->reset(); - - // initialise unit - unit->unit_type = this; - - // colour - unit->add_attribute(std::make_shared>(player)); - - // hitpoints if available - if (this->unit_data.hit_points > 0) { - unit->add_attribute(std::make_shared>(this->unit_data.hit_points)); - unit->add_attribute(std::make_shared>(this->unit_data.hit_points)); - } - - // collectable resources - if (this->unit_data.unit_class == gamedata::unit_classes::TREES) { - unit->add_attribute(std::make_shared>(game_resource::wood, 125)); - } - else if (this->unit_data.unit_class == gamedata::unit_classes::BERRY_BUSH) { - unit->add_attribute(std::make_shared>(game_resource::food, 100)); - } - else if (this->unit_data.unit_class == gamedata::unit_classes::SEA_FISH) { - unit->add_attribute(std::make_shared>(game_resource::food, 200)); - } - else if (this->unit_data.unit_class == gamedata::unit_classes::PREY_ANIMAL) { - unit->add_attribute(std::make_shared>(game_resource::food, 140)); - } - else if (this->unit_data.unit_class == gamedata::unit_classes::HERDABLE) { - unit->add_attribute(std::make_shared>(game_resource::food, 100, 0.1)); - } - else if (this->unit_data.unit_class == gamedata::unit_classes::GOLD_MINE) { - unit->add_attribute(std::make_shared>(game_resource::gold, 800)); - } - else if (this->unit_data.unit_class == gamedata::unit_classes::STONE_MINE) { - unit->add_attribute(std::make_shared>(game_resource::stone, 350)); - } - - // decaying units have a timed lifespan - if (decay) { - unit->push_action(std::make_unique(unit), true); - } - else { - // the default action - unit->push_action(std::make_unique(unit), true); - } - - // give required abilitys - for (auto &a : this->type_abilities) { - unit->give_ability(a); - } -} - -TerrainObject *ObjectProducer::place(Unit *u, std::shared_ptr terrain, coord::phys3 init_pos) const { - // find set of allowed terrains - std::unordered_set terrains = allowed_terrains(this->unit_data.terrain_restriction); - - /* - * decide what terrain is passable using this lambda - * currently unit avoids water and tiles with another unit - * this function should be true if pos is a valid position of the object - */ - TerrainObject *obj_ptr = u->location.get(); - std::weak_ptr terrain_ptr = terrain; - u->location->passable = [obj_ptr, terrain_ptr, terrains](const coord::phys3 &pos) -> bool { - // if location is deleted, then so is this lambda (deleting terrain implies location is deleted) - // so locking objects here will not return null - auto terrain = terrain_ptr.lock(); - - // look at all tiles in the bases range - for (coord::tile check_pos : tile_list(obj_ptr->get_range(pos, *terrain))) { - TileContent *tc = terrain->get_data(check_pos); - - // invalid tile types - if (!tc || terrains.count(tc->terrain_id) == 0) { - return false; - } - - // compare with objects intersecting the units tile - // ensure no intersections with other objects - for (auto obj_cmp : tc->obj) { - if (obj_ptr != obj_cmp && obj_cmp->check_collisions() && obj_ptr->intersects(*obj_cmp, pos)) { - return false; - } - } - } - return true; - }; - - // u->location->draw = [u, obj_ptr](const Engine &e) { - // if (u->selected) { - // obj_ptr->draw_outline(e.coord); - // } - // u->draw(e); - // }; - - // try to place the obj, it knows best whether it will fit. - auto state = this->decay ? object_state::placed_no_collision : object_state::placed; - if (u->location->place(terrain, init_pos, state)) { - if (this->on_create) { - this->on_create->play(); - } - return u->location.get(); - } - - // placing at the given position failed - u->log(MSG(dbg) << "failed to place object"); - return nullptr; -} - -MovableProducer::MovableProducer(const Player &owner, const GameSpec &spec, const gamedata::projectile_unit *um) : - ObjectProducer(owner, spec, um), - unit_data(*um), - on_move{spec.get_sound(this->unit_data.command_sound_id)}, - on_attack{spec.get_sound(this->unit_data.command_sound_id)}, - projectile{this->unit_data.attack_projectile_primary_unit_id} { - // extra abilities - this->type_abilities.emplace_back(std::make_shared(this->on_move)); - this->type_abilities.emplace_back(std::make_shared(this->on_attack)); -} - -MovableProducer::~MovableProducer() = default; - -void MovableProducer::initialise(Unit *unit, Player &player) { - /* - * call base function - */ - ObjectProducer::initialise(unit, player); - - /* - * basic attributes - */ - if (!unit->has_attribute(attr_type::direction)) { - unit->add_attribute(std::make_shared>(coord::phys3_delta{1, 0, 0})); - } - - /* - * distance per millisecond -- consider original game speed - * where 1.5 in game seconds pass in 1 real second - */ - coord::phys_t sp = this->unit_data.speed / 666; - unit->add_attribute(std::make_shared>(sp)); - - // projectile of melee attacks - UnitType *proj_type = this->owner.get_type(this->projectile); - if (this->unit_data.attack_projectile_primary_unit_id > 0 && proj_type) { - // calculate requirements for ranged attacks - coord::phys_t range_phys = this->unit_data.weapon_range_max; - unit->add_attribute(std::make_shared>(proj_type, range_phys, 48000, 1)); - } - else { - unit->add_attribute(std::make_shared>(nullptr, 0, 0, 1)); - } - unit->add_attribute(std::make_shared>()); -} - -TerrainObject *MovableProducer::place(Unit *unit, std::shared_ptr terrain, coord::phys3 init_pos) const { - return ObjectProducer::place(unit, terrain, init_pos); -} - -LivingProducer::LivingProducer(const Player &owner, const GameSpec &spec, const gamedata::living_unit *ud) : - MovableProducer(owner, spec, ud), - unit_data(*ud) { - // extra abilities - this->type_abilities.emplace_back(std::make_shared(this->on_move)); -} - -LivingProducer::~LivingProducer() = default; - -void LivingProducer::initialise(Unit *unit, Player &player) { - /* - * call base function - */ - MovableProducer::initialise(unit, player); - - // population of 1 for all movable units - if (this->unit_data.unit_class != gamedata::unit_classes::HERDABLE) { - unit->add_attribute(std::make_shared>(1, 0)); - } - - // add worker attributes - if (this->unit_data.unit_class == gamedata::unit_classes::CIVILIAN) { - unit->add_attribute(std::make_shared>()); - unit->add_attribute(std::make_shared>()); - unit->add_attribute(std::make_shared>()); - - // add graphic ids for resource actions - auto &worker_attr = unit->get_attribute(); - worker_attr.capacity = 10.0; - worker_attr.gather_rate[game_resource::wood] = 0.002; - worker_attr.gather_rate[game_resource::food] = 0.002; - worker_attr.gather_rate[game_resource::gold] = 0.002; - worker_attr.gather_rate[game_resource::stone] = 0.002; - - auto &multitype_attr = unit->get_attribute(); - // currently not sure where the game data keeps these values - // todo PREY_ANIMAL SEA_FISH - if (this->parent_id() == 83) { - // male graphics - multitype_attr.types[gamedata::unit_classes::CIVILIAN] = this->parent_type(); // get default villager - multitype_attr.types[gamedata::unit_classes::BUILDING] = this->owner.get_type(156); // builder 118 - multitype_attr.types[gamedata::unit_classes::BERRY_BUSH] = this->owner.get_type(120); // forager - multitype_attr.types[gamedata::unit_classes::HERDABLE] = this->owner.get_type(592); // sheperd - multitype_attr.types[gamedata::unit_classes::TREES] = this->owner.get_type(123); // woodcutter - multitype_attr.types[gamedata::unit_classes::GOLD_MINE] = this->owner.get_type(579); // gold miner - multitype_attr.types[gamedata::unit_classes::STONE_MINE] = this->owner.get_type(124); // stone miner - } - else { - // female graphics - multitype_attr.types[gamedata::unit_classes::CIVILIAN] = this->parent_type(); // get default villager - multitype_attr.types[gamedata::unit_classes::BUILDING] = this->owner.get_type(222); // builder 212 - multitype_attr.types[gamedata::unit_classes::BERRY_BUSH] = this->owner.get_type(354); // forager - multitype_attr.types[gamedata::unit_classes::HERDABLE] = this->owner.get_type(590); // sheperd - multitype_attr.types[gamedata::unit_classes::TREES] = this->owner.get_type(218); // woodcutter - multitype_attr.types[gamedata::unit_classes::GOLD_MINE] = this->owner.get_type(581); // gold miner - multitype_attr.types[gamedata::unit_classes::STONE_MINE] = this->owner.get_type(220); // stone miner - } - unit->give_ability(std::make_shared(this->on_attack)); - unit->give_ability(std::make_shared(this->on_attack)); - unit->give_ability(std::make_shared(this->on_attack)); - } - else if (this->unit_data.unit_class == gamedata::unit_classes::FISHING_BOAT) { - unit->add_attribute(std::make_shared>()); - unit->add_attribute(std::make_shared>()); - - // add fishing abilites - auto &worker_attr = unit->get_attribute(); - worker_attr.capacity = 15.0; - worker_attr.gather_rate[game_resource::food] = 0.002; - - unit->give_ability(std::make_shared(this->on_attack)); - } -} - -TerrainObject *LivingProducer::place(Unit *unit, std::shared_ptr terrain, coord::phys3 init_pos) const { - return MovableProducer::place(unit, terrain, init_pos); -} - -BuildingProducer::BuildingProducer(const Player &owner, const GameSpec &spec, const gamedata::building_unit *ud) : - UnitType(owner), - unit_data{*ud}, - projectile{this->unit_data.attack_projectile_primary_unit_id}, - foundation_terrain{ud->foundation_terrain_id}, - enable_collisions{this->unit_data.id0 != 109} { // 109 = town center - - // copy the class type - this->unit_class = this->unit_data.unit_class; - this->icon = this->unit_data.icon_id; - - // find suitable sounds - int creation_sound = this->unit_data.train_sound_id; - int dying_sound = this->unit_data.dying_sound_id; - if (creation_sound == -1) { - creation_sound = this->unit_data.damage_sound_id; - } - if (creation_sound == -1) { - creation_sound = this->unit_data.selection_sound_id; - } - if (dying_sound == -1) { - dying_sound = 323; //generic explosion sound - } - on_create = spec.get_sound(creation_sound); - on_destroy = spec.get_sound(dying_sound); - - // convert the float to the discrete foundation size... - this->foundation_size = { - static_cast(this->unit_data.radius_x * 2), - static_cast(this->unit_data.radius_y * 2), - }; - - // TODO get cost, temp fixed cost of 100 wood - this->cost.set(cost_type::constant, create_resource_cost(game_resource::wood, 100)); -} - -BuildingProducer::~BuildingProducer() = default; - -int BuildingProducer::id() const { - return this->unit_data.id0; -} - -int BuildingProducer::parent_id() const { - return this->unit_data.id0; -} - -std::string BuildingProducer::name() const { - return this->unit_data.name; -} - -void BuildingProducer::initialise(Unit *unit, Player &player) { - ENSURE(this->owner == player, "unit init from a UnitType of a wrong player which breaks tech levels"); - - // log type - unit->log(MSG(dbg) << "setting unit type " << this->unit_data.id0 << " " << this->unit_data.name); - - // initialize graphic set - unit->unit_type = this; - - auto player_attr = std::make_shared>(player); - unit->add_attribute(player_attr); - - // building specific attribute - auto build_attr = std::make_shared>( - this->foundation_terrain, - this->owner.get_type(293), // fem_villager, male is 83 - unit->location->pos.draw); - build_attr->completion_state = this->enable_collisions ? object_state::placed : object_state::placed_no_collision; - unit->add_attribute(build_attr); - - // garrison and hp for all buildings - unit->add_attribute(std::make_shared>()); - unit->add_attribute(std::make_shared>(this->unit_data.hit_points)); - unit->add_attribute(std::make_shared>(this->unit_data.hit_points)); - - // population - if (this->id() == 109 || this->id() == 70) { // Town center, House - unit->add_attribute(std::make_shared>(0, 5)); - } - else if (this->id() == 82) { // Castle - unit->add_attribute(std::make_shared>(0, 20)); - } - - // limits - if (this->id() == 109 || this->id() == 276) { // Town center, Wonder - this->have_limit = 2; // TODO change to 1, 2 is for testing - } - - UnitType *proj_type = this->owner.get_type(this->projectile); - if (this->unit_data.attack_projectile_primary_unit_id > 0 && proj_type) { - coord::phys_t range_phys = this->unit_data.weapon_range_max; - unit->add_attribute(std::make_shared>(proj_type, range_phys, 350000, 1)); - // formation is used only for the attack_stance - unit->add_attribute(std::make_shared>(attack_stance::aggressive)); - unit->give_ability(std::make_shared()); - } - - // dropsite attribute - std::vector accepted_resources = this->get_accepted_resources(); - if (accepted_resources.size() != 0) { - unit->add_attribute(std::make_shared>(accepted_resources)); - } - - // building can train new units and ungarrison - unit->give_ability(std::make_shared()); - unit->give_ability(std::make_shared()); - unit->give_ability(std::make_shared()); -} - -std::vector BuildingProducer::get_accepted_resources() { - //TODO use a more general approach instead of hard coded ids - - auto id_in = [=, this](std::initializer_list ids) { - return std::any_of(ids.begin(), ids.end(), [=, this](int n) { return n == this->id(); }); - }; - - if (this->id() == 109) { // Town center - return std::vector{ - game_resource::wood, - game_resource::food, - game_resource::gold, - game_resource::stone}; - } - else if (id_in({584, 585, 586, 587})) { // Mine - return std::vector{ - game_resource::gold, - game_resource::stone}; - } - else if (id_in({68, 129, 130, 131})) { // Mill - return std::vector{ - game_resource::food}; - } - else if (id_in({562, 563, 564, 565})) { // Lumberjack camp - return std::vector{ - game_resource::wood}; - } - - return std::vector(); -} - -TerrainObject *BuildingProducer::place(Unit *u, std::shared_ptr terrain, coord::phys3 init_pos) const { - /* - * decide what terrain is passable using this lambda - * currently unit avoids water and tiles with another unit - * this function should be true if pos is a valid position of the object - */ - TerrainObject *obj_ptr = u->location.get(); - std::weak_ptr terrain_ptr = terrain; - - // find set of allowed terrains - std::unordered_set terrains = allowed_terrains(this->unit_data.terrain_restriction); - - u->location->passable = [obj_ptr, terrain_ptr, terrains](const coord::phys3 &pos) -> bool { - auto terrain = terrain_ptr.lock(); - - // look at all tiles in the bases range - for (coord::tile check_pos : tile_list(obj_ptr->get_range(pos, *terrain))) { - TileContent *tc = terrain->get_data(check_pos); - - // check if terrains are suitable and free of content - if (!tc || !terrains.count(tc->terrain_id) || tc->obj.size()) { - return false; - } - } - - return true; - }; - - // drawing function - // bool draw_outline = this->enable_collisions; - // u->location->draw = [u, obj_ptr, draw_outline](const Engine &e) { - // if (u->selected && draw_outline) { - // obj_ptr->draw_outline(e.coord); - // } - // u->draw(e); - // }; - - // try to place the obj, it knows best whether it will fit. - auto state = object_state::floating; - if (!u->location->place(terrain, init_pos, state)) { - return nullptr; - } - - // annex objects - for (unsigned i = 0; i < 4; ++i) { - const gamedata::building_annex &annex = this->unit_data.building_annex.data[i]; - if (annex.unit_id > 0) { - // make objects for annex - coord::phys3 a_pos = u->location->pos.draw; - a_pos.ne += annex.misplaced0; - a_pos.se += annex.misplaced1; - this->make_annex(*u, terrain, annex.unit_id, a_pos, i == 0); - } - } - - // TODO: play sound once built - if (this->on_create) { - this->on_create->play(); - } - return u->location.get(); -} - -TerrainObject *BuildingProducer::make_annex(Unit &u, std::shared_ptr t, int annex_id, coord::phys3 annex_pos, bool c) const { - // for use in lambda drawing functions - auto annex_type = this->owner.get_type(annex_id); - if (!annex_type) { - u.log(MSG(warn) << "Invalid annex type id " << annex_id); - return nullptr; - } - - // foundation size - coord::tile_delta annex_foundation = annex_type->foundation_size; - - // producers place by the nw tile - coord::phys3 start_tile = annex_pos; - start_tile.ne -= annex_foundation.ne / 2.0; - start_tile.se -= annex_foundation.se / 2.0; - - // create and place on terrain - TerrainObject *annex_loc = u.location->make_annex(annex_foundation); - object_state state = c ? object_state::placed : object_state::placed_no_collision; - annex_loc->place(t, start_tile, state); - - return annex_loc; -} - -ProjectileProducer::ProjectileProducer(const Player &owner, const GameSpec &spec, const gamedata::missile_unit *pd) : - UnitType(owner), - unit_data{*pd} { - // copy the class type - this->unit_class = this->unit_data.unit_class; -} - -ProjectileProducer::~ProjectileProducer() = default; - -int ProjectileProducer::id() const { - return this->unit_data.id0; -} - -int ProjectileProducer::parent_id() const { - return this->unit_data.id0; -} - -std::string ProjectileProducer::name() const { - return this->unit_data.name; -} - -void ProjectileProducer::initialise(Unit *unit, Player &player) { - ENSURE(this->owner == player, "unit init from a UnitType of a wrong player which breaks tech levels"); - - // initialize graphic set - unit->unit_type = this; - - auto player_attr = std::make_shared>(player); - unit->add_attribute(player_attr); - - // projectile speed - coord::phys_t sp = this->unit_data.speed / 666; - unit->add_attribute(std::make_shared>(sp)); - unit->add_attribute(std::make_shared>(this->unit_data.projectile_arc)); - unit->add_attribute(std::make_shared>(coord::phys3_delta{1, 0, 0})); -} - -TerrainObject *ProjectileProducer::place(Unit *u, std::shared_ptr terrain, coord::phys3 init_pos) const { - TerrainObject *obj_ptr = u->location.get(); - std::weak_ptr terrain_ptr = terrain; - u->location->passable = [obj_ptr, u, terrain_ptr](const coord::phys3 &pos) -> bool { - if (pos.up > 64000) { - return true; - } - - // avoid intersections with launcher - Unit *launcher = nullptr; - auto terrain = terrain_ptr.lock(); - if (u->has_attribute(attr_type::projectile)) { - auto &pr_attr = u->get_attribute(); - if (pr_attr.launched && pr_attr.launcher.is_valid()) { - launcher = pr_attr.launcher.get(); - } - else { - return true; - } - } - else { - return true; - } - - // look at all tiles in the bases range - for (coord::tile check_pos : tile_list(obj_ptr->get_range(pos, *terrain))) { - TileContent *tc = terrain->get_data(check_pos); - if (!tc) - return false; - - // ensure no intersections with other objects - for (auto obj_cmp : tc->obj) { - if (obj_ptr != obj_cmp && &obj_cmp->unit != launcher && obj_cmp->check_collisions() && obj_ptr->intersects(*obj_cmp, pos)) { - return false; - } - } - } - return true; - }; - - // try to place the obj, it knows best whether it will fit. - if (u->location->place(terrain, init_pos, object_state::placed_no_collision)) { - return u->location.get(); - } - return nullptr; -} - -} // namespace openage diff --git a/libopenage/unit/producer.h b/libopenage/unit/producer.h deleted file mode 100644 index 2ff837a147..0000000000 --- a/libopenage/unit/producer.h +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include - -#include "../coord/tile.h" -#include "../gamedata/gamedata_dummy.h" -#include "../gamedata/graphic_dummy.h" -#include "../gamestate/old/player.h" -#include "unit.h" -#include "unit_type.h" - -namespace openage { - -class GameMain; -class GameSpec; -class Terrain; -class TerrainObject; -class Sound; - -class UnitAbility; -class UnitAction; - - -std::unordered_set allowed_terrains(const gamedata::ground_type &restriction); - -/** - * base game data unit type - */ -class ObjectProducer : public UnitType { -public: - ObjectProducer(const Player &owner, const GameSpec &spec, const gamedata::unit_object *ud); - virtual ~ObjectProducer(); - - int id() const override; - int parent_id() const override; - std::string name() const override; - void initialise(Unit *, Player &) override; - TerrainObject *place(Unit *, std::shared_ptr, coord::phys3) const override; - -protected: - const GameSpec &dataspec; - const gamedata::unit_object unit_data; - - /** - * decaying objects have a timed lifespan - */ - bool decay; - - /** - * Sound id played when object is created or destroyed. - */ - const Sound *on_create; - const Sound *on_destroy; - int dead_unit_id; -}; - -/** - * movable unit types - */ -class MovableProducer : public ObjectProducer { -public: - MovableProducer(const Player &owner, const GameSpec &spec, const gamedata::projectile_unit *); - virtual ~MovableProducer(); - - void initialise(Unit *, Player &) override; - TerrainObject *place(Unit *, std::shared_ptr, coord::phys3) const override; - -protected: - const gamedata::projectile_unit unit_data; - const Sound *on_move; - const Sound *on_attack; - int projectile; -}; - -/** - * temporary class -- will be replaced with nyan system in future - * Stores graphics and attributes for a single unit type - * in aoe living units are derived from objects - */ -class LivingProducer : public MovableProducer { -public: - LivingProducer(const Player &owner, const GameSpec &spec, const gamedata::living_unit *); - virtual ~LivingProducer(); - - void initialise(Unit *, Player &) override; - TerrainObject *place(Unit *, std::shared_ptr, coord::phys3) const override; - -private: - const gamedata::living_unit unit_data; -}; - -/** - * Stores graphics and attributes for a building type - * Will be replaced with nyan system in future - * in aoe buildings are derived from living units - */ -class BuildingProducer : public UnitType { -public: - BuildingProducer(const Player &owner, - const GameSpec &spec, - const gamedata::building_unit *ud); - virtual ~BuildingProducer(); - - int id() const override; - int parent_id() const override; - std::string name() const override; - void initialise(Unit *, Player &) override; - TerrainObject *place(Unit *, std::shared_ptr, coord::phys3) const override; - -private: - const gamedata::building_unit unit_data; - - /** - * Sound id played when object is created or destroyed. - */ - const Sound *on_create; - const Sound *on_destroy; - int projectile; - int foundation_terrain; - std::vector get_accepted_resources(); - - /** - * used for objects like town centers or gates - * where the base does not apply collision checks - */ - bool enable_collisions; - - TerrainObject *make_annex(Unit &u, std::shared_ptr t, int annex_id, coord::phys3 annex_pos, bool c) const; -}; - -/** - * creates projectiles - * todo use MovableProducer as base class - */ -class ProjectileProducer : public UnitType { -public: - ProjectileProducer(const Player &owner, const GameSpec &spec, const gamedata::missile_unit *); - virtual ~ProjectileProducer(); - - int id() const override; - int parent_id() const override; - std::string name() const override; - void initialise(Unit *, Player &) override; - TerrainObject *place(Unit *, std::shared_ptr, coord::phys3) const override; - -private: - const gamedata::missile_unit unit_data; -}; - -} // namespace openage diff --git a/libopenage/unit/research.cpp b/libopenage/unit/research.cpp deleted file mode 100644 index 7161565583..0000000000 --- a/libopenage/unit/research.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. - -#include "../gamestate/old/player.h" -#include "research.h" - - -namespace openage { - -ResearchType::ResearchType(Player &owner) - : - owner{owner} { -} - -std::shared_ptr ResearchType::initialise() const { - return std::make_shared(this); -} - -Research::Research(const ResearchType *type) - : - type{type}, - active_count{0}, - completed_count{0} { -} - -void Research::started() { - this->active_count += 1; -} - -void Research::stopped() { - this->active_count -= 1; -} - -void Research::completed() { - this->active_count -= 1; - this->completed_count += 1; -} - -bool Research::can_start() const { - return this->active_count + this->completed_count < this->type->get_max_repeats(); -} - -bool Research::is_active() const { - return this->active_count > 0; -} - -bool Research::is_researched() const { - return this->completed_count == this->type->get_max_repeats(); -} - -void Research::apply() { - // apply the patch - this->type->apply(); - - // perform category based actions - if (type->category() == research_category::age_advance) { - type->owner.advance_age(); - - } else if (type->category() == research_category::generic) { - // TODO implement a way to handle this category - } -} - -} // namespace openage diff --git a/libopenage/unit/research.h b/libopenage/unit/research.h deleted file mode 100644 index edd73ee8ed..0000000000 --- a/libopenage/unit/research.h +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2017-2017 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - - -namespace openage { - -class Player; -class Research; -class ResourceCost; - -enum class research_category : int { - /** - * Research which modify unit type data. - */ - unit_upgrade, - /** - * Research which modify unit type data and also progresses the next age. - * Separate category for simplification. - */ - age_advance, - /** - * Research which modify unit type data and something else. - * (eg. see enemy line of sight) - */ - generic, - RESEARCH_CATEGORY_COUNT -}; - -/** - * Describes a research for a single player - * - * The get_max_repeats (with a default value of 1) can allow for a research to be - * performed multiple types - */ -class ResearchType { -public: - - ResearchType(Player &owner); - - /** - * Gets the unique id of this research type. - */ - virtual int id() const = 0; - - /** - * Gets the name of the research. - */ - virtual std::string name() const = 0; - - /** - * Gets the research category of the research. - */ - virtual research_category category() const = 0; - - /** - * Creates a single Research object. - * Must be called only once. - */ - std::shared_ptr initialise() const; - - /** - * The player who owns this research type - */ - Player &owner; - - /** - * How many times it can be researched. - * All classic researches have a value of 1. - */ - int get_max_repeats() const { return 1; } - - virtual unsigned int get_research_time() const = 0; - - virtual ResourceCost get_research_cost() const = 0; - - /** - * Performs the modifications (eg. apply patch to the unit types) - */ - virtual void apply() const = 0; - -protected: - -private: - -}; - -class NyanResearchType : public ResearchType { - // TODO POST-NYAN Implement - - NyanResearchType(Player &owner); - -}; - -/** - * At most one Research must exist for each ResearchType. - * - * A research represents how many times the research type has been completed (completed count) - * and also how many unit are researching it now (active count). - */ -class Research { -public: - - Research(const ResearchType *type); - - const ResearchType *type; - - /** - * Called when a unit started researching this research. - */ - void started(); - - /** - * Called when a unit stopped researching this research before completing it. - */ - void stopped(); - - /** - * Called when a unit completed researching this research. - */ - void completed(); - - /** - * Returns true if a unit can start researching this research. - */ - bool can_start() const; - - /** - * Returns true if any unit is researching this research. - */ - bool is_active() const; - - /** - * Returns true if it has nothing more to offer (reached max repeats). - */ - bool is_researched() const; - - /** - * Apply the modifications to the owner player. - */ - void apply(); - -protected: - - /** - * The number of units that are researching this research - */ - int active_count; - - /** - * The number of times this research has been completed - */ - int completed_count; - -private: - -}; - -} // namespace openage diff --git a/libopenage/unit/selection.cpp b/libopenage/unit/selection.cpp deleted file mode 100644 index 354bf8f5ba..0000000000 --- a/libopenage/unit/selection.cpp +++ /dev/null @@ -1,306 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "selection.h" - -#include -#include - -#include "../coord/tile.h" -#include "../log/log.h" -#include "../terrain/terrain.h" -#include "action.h" -#include "command.h" -#include "producer.h" -#include "unit.h" - - -namespace openage { - -UnitSelection::UnitSelection() : - selection_type{selection_type_t::nothing}, - drag_active{false} { -} - -// bool UnitSelection::on_drawhud() { -// // the drag selection box -// if (drag_active) { -// coord::viewport s = this->start.to_viewport(this->engine->coord); -// coord::viewport e = this->end.to_viewport(this->engine->coord); -// glLineWidth(1); -// glColor3f(1.0, 1.0, 1.0); -// glBegin(GL_LINE_LOOP); { -// glVertex3f(s.x, s.y, 0); -// glVertex3f(e.x, s.y, 0); -// glVertex3f(e.x, e.y, 0); -// glVertex3f(s.x, e.y, 0); -// } -// glEnd(); -// } -// -// // draw hp bars for each selected unit -// glLineWidth(3); -// for (auto u : this->units) { -// if (u.second.is_valid()) { -// Unit *unit_ptr = u.second.get(); -// if (unit_ptr->location && -// unit_ptr->has_attribute(attr_type::hitpoints) && -// unit_ptr->has_attribute(attr_type::damaged)) { -// -// auto &hp = unit_ptr->get_attribute(); -// auto &dm = unit_ptr->get_attribute(); -// float percent = static_cast(dm.hp) / static_cast(hp.hp); -// int mid = percent * 28.0f - 14.0f; -// -// coord::phys3 &pos_phys3 = unit_ptr->location->pos.draw; -// auto pos = pos_phys3.to_viewport(this->engine->coord); -// // green part -// glColor3f(0.0, 1.0, 0.0); -// glBegin(GL_LINES); { -// glVertex3f(pos.x - 14, pos.y + 60, 0); -// glVertex3f(pos.x + mid, pos.y + 60, 0); -// } -// glEnd(); -// -// // red part -// glColor3f(1.0, 0.0, 0.0); -// glBegin(GL_LINES); { -// glVertex3f(pos.x + mid, pos.y + 60, 0); -// glVertex3f(pos.x + 14, pos.y + 60, 0); -// } -// glEnd(); -// } -// } -// } -// glColor3f(1.0, 1.0, 1.0); // reset - -// ui graphics 3404 and 3405 -// return true; -// } - -void UnitSelection::drag_begin(coord::camgame pos) { - this->start = pos; - this->end = pos; - this->drag_active = true; -} - -// called as the entry point for the selection -// from ActionMode. -void UnitSelection::drag_update(coord::camgame pos) { - if (!this->drag_active) { - this->drag_begin(pos); - } - this->end = pos; -} - -void UnitSelection::drag_release(const Player &player, Terrain *terrain, bool append) { - if (this->start == this->end) { - this->select_point(player, terrain, this->start, append); - } - else { - this->select_space(player, terrain, this->start, this->end, append); - } - this->drag_active = false; -} - -void UnitSelection::clear() { - for (auto u : this->units) { - if (u.second.is_valid()) { - u.second.get()->selected = false; - } - } - this->units.clear(); - this->selection_type = selection_type_t::nothing; -} - -void UnitSelection::toggle_unit(const Player &player, Unit *u, bool append) { - if (this->units.count(u->id) == 0) { - this->add_unit(player, u, append); - } - else { - this->remove_unit(u); - } -} - -void UnitSelection::add_unit(const Player &player, Unit *u, bool append) { - // Only select resources and units with hitpoints > 0 - if (u->has_attribute(attr_type::resource) || (u->has_attribute(attr_type::damaged) && u->get_attribute().hp > 0)) { - selection_type_t unit_type = get_unit_selection_type(player, u); - int unit_type_i = static_cast(unit_type); - int selection_type_i = static_cast(this->selection_type); - - if (unit_type_i > selection_type_i) { - // Don't select this unit as it has too low priority - return; - } - - if (unit_type_i < selection_type_i) { - // Upgrade selection to a higher priority selection - this->clear(); - this->selection_type = unit_type; - } - - // Can't select multiple enemies at once - if (not(unit_type == selection_type_t::own_units || (unit_type == selection_type_t::own_buildings && append))) { - this->clear(); - this->selection_type = unit_type; // Clear resets selection_type - } - - // Finally, add the unit to the selection - u->selected = true; - this->units[u->id] = u->get_ref(); - } -} - -void UnitSelection::remove_unit(Unit *u) { - u->selected = false; - this->units.erase(u->id); - - if (this->units.empty()) { - this->selection_type = selection_type_t::nothing; - } -} - -selection_type_t UnitSelection::get_selection_type() { - return this->selection_type; -} - -void UnitSelection::kill_unit(const Player &player) { - if (this->units.empty()) { - return; - } - - UnitReference &ref = this->units.begin()->second; - if (!ref.is_valid()) { - this->units.erase(this->units.begin()); - - if (this->units.empty()) { - this->selection_type = selection_type_t::nothing; - } - } - else { - Unit *u = ref.get(); - - // Check color: you can only kill your own units - if (u->is_own_unit(player)) { - this->remove_unit(u); - u->delete_unit(); - } - } -} - -bool UnitSelection::contains_builders(const Player &player) { - for (auto &it : units) { - if (it.second.is_valid() && it.second.get()->get_ability(ability_type::build) && it.second.get()->is_own_unit(player)) { - return true; - } - } - return false; -} - -bool UnitSelection::contains_military(const Player &player) { - for (auto &it : units) { - if (it.second.is_valid() && !it.second.get()->get_ability(ability_type::build) && it.second.get()->is_own_unit(player)) { - return true; - } - } - return false; -} - -void UnitSelection::select_point(const Player &player, Terrain *terrain, coord::camgame point, bool append) { - //if (!append) { - // this->clear(); - //} - // - //if (!terrain) { - // log::log(MSG(warn) << "selection terrain not specified"); - // return; - //} - // - //// find any object at selected point - //auto obj = terrain->obj_at_point(point.to_phys3(this->engine->coord)); - //if (obj) { - // this->toggle_unit(player, &obj->unit); - //} -} - -void UnitSelection::select_space(const Player &player, Terrain *terrain, coord::camgame point0, coord::camgame point1, bool append) { - if (!append) { - this->clear(); - } - - coord::camgame min{std::min(point0.x, point1.x), std::min(point0.y, point1.y)}; - coord::camgame max{std::max(point0.x, point1.x), std::max(point0.y, point1.y)}; - - // look at each tile in the range and find all units - //for (coord::tile check_pos : tiles_in_range(point0, point1, this->engine->coord)) { - // TileContent *tc = terrain->get_data(check_pos); - // if (tc) { - // // find objects within selection box - // for (auto unit_location : tc->obj) { - // coord::camgame pos = unit_location->pos.draw.to_camgame(this->engine->coord); - // - // if ((pos.x > min.x and pos.x < max.x) and (pos.y > min.y and pos.y < max.y)) { - // this->add_unit(player, &unit_location->unit, append); - // } - // } - // } - //} -} - -void UnitSelection::all_invoke(Command &cmd) { - for (auto u : this->units) { - if (u.second.is_valid() && u.second.get()->is_own_unit(cmd.player)) { - // allow unit to find best use of the command - // TODO: queue_cmd returns ability which allows playing of sound - u.second.get()->queue_cmd(cmd); - } - } -} - -selection_type_t UnitSelection::get_unit_selection_type(const Player &player, Unit *u) { - bool is_building = u->has_attribute(attr_type::building); - - // Check owner - // TODO implement allied units - if (u->is_own_unit(player)) { - return is_building ? selection_type_t::own_buildings : selection_type_t::own_units; - } - else { - return is_building ? selection_type_t::enemy_building : selection_type_t::enemy_unit; - } -} - -std::vector tiles_in_range(coord::camgame p1, coord::camgame p2, const coord::CoordManager &coord) { - // the remaining corners - coord::camgame p3 = coord::camgame{p1.x, p2.y}; - coord::camgame p4 = coord::camgame{p2.x, p1.y}; - coord::camgame pts[4]{p1, p2, p3, p4}; - - // find the range of tiles covered - coord::tile t1 = pts[0].to_tile(coord); - coord::tile min = t1; - coord::tile max = t1; - - for (unsigned i = 1; i < 4; ++i) { - coord::tile t = pts[i].to_tile(coord); - min.ne = std::min(min.ne, t.ne); - min.se = std::min(min.se, t.se); - max.ne = std::max(max.ne, t.ne); - max.se = std::max(max.se, t.se); - } - - // find all units in the boxed region - std::vector tiles; - coord::tile check_pos = min; - while (check_pos.ne <= max.ne) { - while (check_pos.se <= max.se) { - tiles.push_back(check_pos); - check_pos.se += 1; - } - check_pos.se = min.se; - check_pos.ne += 1; - } - return tiles; -} - -} // namespace openage diff --git a/libopenage/unit/selection.h b/libopenage/unit/selection.h deleted file mode 100644 index f82fc02bea..0000000000 --- a/libopenage/unit/selection.h +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "../coord/pixel.h" -#include "ability.h" -#include "unit_container.h" - -namespace openage { -class Terrain; - -std::vector tiles_in_range(coord::camgame p1, coord::camgame p2, const coord::CoordManager &coord); - -/** - * A selection of units always has a type - * You can't select units from multiple types at once - * Earlier types have precedence over later types - * - * So you can select a group of units, or a building (or multiple if append is on) - * Enemy units, enemy buildings and other objects may only be selected one at a time - */ -enum class selection_type_t { - own_units, - own_buildings, - enemy_unit, - enemy_building, - nothing -}; - -/** - * a user interface component allowing control of a selected group - */ -class UnitSelection { -public: - UnitSelection(/* LegacyEngine *engine */); - - // bool on_drawhud() override; - void drag_begin(coord::camgame pos); - void drag_update(coord::camgame pos); - void drag_release(const Player &player, Terrain *terrain, bool append = false); - - void clear(); - - void toggle_unit(const Player &player, Unit *u, bool append = false); - void add_unit(const Player &player, Unit *u, bool append = false); - void remove_unit(Unit *u); - - selection_type_t get_selection_type(); - - /** - * kill a single unit in the selection - */ - void kill_unit(const Player &player); - - /** - * checks whether there are any builders in the selection - */ - bool contains_builders(const Player &player); - - /** - * checks whether there are any military units (i.e. non-builders) in the selection - */ - bool contains_military(const Player &player); - - /** - * point unit selection - */ - void select_point(const Player &player, Terrain *terrain, coord::camgame p, bool append = false); - - /** - * boxed unit selection - */ - void select_space(const Player &player, Terrain *terrain, coord::camgame p1, coord::camgame p2, bool append = false); - - /** - * uses command on every selected unit - */ - void all_invoke(Command &cmd); - - int get_units_count() const { - return this->units.size(); - } - - const UnitReference &get_first_unit() const { - return this->units.begin()->second; - } - -private: - /** - * Check whether the currently selected units may be selected at the same time - * If not, deselect some units - * This is the order in which the checks occur: - * Own units > own building(s) > enemy unit > enemy building > any object - * - * So you can select a group of units, or a building (or multiple if append is on) - * Enemy units, enemy buildings and other objects may only be selected one at a time - */ - selection_type_t get_unit_selection_type(const Player &player, Unit *); - - std::unordered_map units; - selection_type_t selection_type; - - bool drag_active; - // TODO: turn these into a C++17 optional - coord::camgame start = {0, 0}, end = {0, 0}; - - /** - * Engine where this selection is attached to. - */ - // LegacyEngine *engine; -}; - -} // namespace openage diff --git a/libopenage/unit/type_pair.cpp b/libopenage/unit/type_pair.cpp deleted file mode 100644 index 725f63c072..0000000000 --- a/libopenage/unit/type_pair.cpp +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#include "type_pair.h" - -namespace openage { - -UnitType::UnitType() {} - -bool UnitType::match(Unit *) { - // TODO: types - return true; -} - -TypePair::TypePair() {} - -} // namespace openage diff --git a/libopenage/unit/type_pair.h b/libopenage/unit/type_pair.h deleted file mode 100644 index 7b96cd45bd..0000000000 --- a/libopenage/unit/type_pair.h +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -namespace openage { - -/** - * units in aoc have a class and a type - */ -class UnitType { -public: - UnitType(); - - /** - * the unit must have either same class or id as this - */ - bool match(class Unit *); - -private: - unsigned int class_id; - unsigned int unit_type_id; -}; - -/** - * many effects in aoc use a pair structure - * such as attack bonuses, armour and selection commands - */ -class TypePair { -public: - TypePair(); - -private: - UnitType a, b; - -}; - -} // namespace openage diff --git a/libopenage/unit/unit.cpp b/libopenage/unit/unit.cpp deleted file mode 100644 index bccd729a8e..0000000000 --- a/libopenage/unit/unit.cpp +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#include -#include -#include - -#include "../terrain/terrain.h" - -#include "ability.h" -#include "action.h" -#include "command.h" -#include "producer.h" -#include "unit.h" - - -namespace openage { - -Unit::Unit(UnitContainer *c, id_t id) : - id{id}, - unit_type{nullptr}, - selected{false}, - pop_destructables{false}, - container(c) {} - -Unit::~Unit() { - // remove any used location from the map - if (this->location) { - this->location->remove(); - } -} - -void Unit::reset() { - this->ability_available.clear(); - this->action_stack.clear(); - this->pop_destructables = false; -} - -bool Unit::has_action() const { - return !this->action_stack.empty(); -} - -bool Unit::accept_commands() const { - return (this->has_action() && this->top()->allow_control()); -} - -bool Unit::is_own_unit(const Player &player) { - return player.owns(*this); -} - -UnitAction *Unit::top() const { - if (this->action_stack.empty()) { - throw Error{MSG(err) << "Unit stack empty - no top action exists"}; - } - return this->action_stack.back().get(); -} - -UnitAction *Unit::before(const UnitAction *action) const { - auto start = std::find_if( - std::begin(this->action_stack), - std::end(this->action_stack), - [action](const std::unique_ptr &a) { - return action == a.get(); - }); - - if (start != std::begin(this->action_stack) && start != std::end(this->action_stack)) { - return (*(start - 1)).get(); - } - return nullptr; -} - -bool Unit::update(time_nsec_t lastframe_duration) { - // if unit is not on the map then do nothing - if (!this->location) { - return true; - } - - // unit is dead (not player controlled) - if (this->pop_destructables) { - this->erase_after( - [](std::unique_ptr &e) { - return e->allow_interrupt() || e->allow_control(); - }, - false); - } - - /* - * the active action is on top - */ - if (this->has_action()) { - // TODO: change the entire unit action timing - // to a higher resolution like nsecs or usecs. - - // time as float, in milliseconds. - auto time_elapsed = lastframe_duration / 1e6; - - this->top()->update(time_elapsed); - - // the top primary action specifies whether - // secondary actions are updated - if (this->top()->allow_control()) { - this->update_secondary(time_elapsed); - } - - // check completion of all primary actions, - // pop completed actions and anything above - this->erase_after( - [](std::unique_ptr &e) { - return e->completed(); - }); - } - - // apply new queued commands - this->apply_all_cmds(); - - return true; -} - -void Unit::update_secondary(int64_t time_elapsed) { - // update secondary actions and remove when completed - auto position_it = std::remove_if( - std::begin(this->action_secondary), - std::end(this->action_secondary), - [time_elapsed](std::unique_ptr &action) { - action->update(time_elapsed); - return action->completed(); - }); - this->action_secondary.erase(position_it, std::end(this->action_secondary)); -} - -void Unit::apply_all_cmds() { - std::lock_guard lock(this->command_queue_lock); - while (!this->command_queue.empty()) { - auto &action = this->command_queue.front(); - this->apply_cmd(action.first, action.second); - this->command_queue.pop(); - } -} - - -void Unit::apply_cmd(std::shared_ptr ability, const Command &cmd) { - // if the interrupt flag is set, discard ongoing actions - bool is_direct = cmd.has_flag(command_flag::interrupt); - if (is_direct) { - this->stop_actions(); - } - if (ability->can_invoke(*this, cmd)) { - ability->invoke(*this, cmd, is_direct); - } -} - -void Unit::give_ability(std::shared_ptr ability) { - this->ability_available.emplace(std::make_pair(ability->type(), ability)); -} - -UnitAbility *Unit::get_ability(ability_type type) { - if (this->ability_available.count(type) > 0) { - return this->ability_available[type].get(); - } - return nullptr; -} - -void Unit::push_action(std::unique_ptr action, bool force) { - // unit not being deleted -- can control unit - if (force || this->accept_commands()) { - this->action_stack.push_back(std::move(action)); - } -} - -void Unit::secondary_action(std::unique_ptr action) { - this->action_secondary.push_back(std::move(action)); -} - -void Unit::add_attribute(std::shared_ptr attr) { - this->attributes.add(attr); -} - -void Unit::add_attributes(const Attributes &attr) { - this->attributes.add_copies(attr); -} - -void Unit::add_attributes(const Attributes &attr, bool shared, bool unshared) { - this->attributes.add_copies(attr, shared, unshared); -} - -bool Unit::has_attribute(attr_type type) const { - return this->attributes.has(type); -} - -std::shared_ptr Unit::queue_cmd(const Command &cmd) { - std::lock_guard lock(this->command_queue_lock); - - // following the specified ability priority - // find suitable ability for this target if available - for (auto &ability : ability_priority) { - auto pair = this->ability_available.find(ability); - if (pair != this->ability_available.end() && cmd.ability()[static_cast(pair->first)] && pair->second->can_invoke(*this, cmd)) { - command_queue.push(std::make_pair(pair->second, cmd)); - return pair->second; - } - } - return nullptr; -} - -void Unit::delete_unit() { - this->pop_destructables = true; -} - -void Unit::stop_gather() { - this->erase_after( - [](std::unique_ptr &e) { - return e->name() == "gather"; - }, - false); -} - -void Unit::stop_actions() { - // work around for workers continuing to work after retasking - if (this->has_attribute(attr_type::worker)) { - this->stop_gather(); - } - - // discard all interruptible tasks - this->erase_after( - [](std::unique_ptr &e) { - return e->allow_interrupt(); - }); -} - -UnitReference Unit::get_ref() { - return UnitReference(this->container, id, this); -} - -UnitContainer *Unit::get_container() const { - return this->container; -} - -std::string Unit::logsource_name() { - return "Unit " + std::to_string(this->id); -} - -void Unit::erase_after(std::function &)> func, bool run_completed) { - auto position_it = std::find_if( - std::begin(this->action_stack), - std::end(this->action_stack), - func); - - if (position_it != std::end(this->action_stack)) { - auto completed_action = std::move(*position_it); - - // erase from the stack - this->action_stack.erase(position_it, std::end(this->action_stack)); - - // perform any completion actions - if (run_completed) { - completed_action->on_completion(); - } - } -} - -} // namespace openage diff --git a/libopenage/unit/unit.h b/libopenage/unit/unit.h deleted file mode 100644 index d832c26d19..0000000000 --- a/libopenage/unit/unit.h +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include -#include -#include - -#include "../coord/phys.h" -#include "../log/logsource.h" -#include "../terrain/terrain_object.h" -#include "../util/timing.h" -#include "ability.h" -#include "attribute.h" -#include "attributes.h" -#include "command.h" -#include "unit_container.h" - - -namespace openage { - -class UnitAbility; -class UnitAction; - -/** - * A game object with current state represented by a stack of actions - * since this class represents both unit and building objects it may be better to - * name as GameObject - * - * it is possible that abilities are not required here and they could be moved - * to selection controller -- units only need the attributes - */ -class Unit : public log::LogSource { -public: - Unit(UnitContainer *c, id_t id); - - /** - * unit cleanup will delete terrain object - */ - virtual ~Unit(); - - /** - * this units unique id value - */ - const id_t id; - - /** - * type of this object, this is set by the the UnitType which - * was most recently applied to this unit - */ - const UnitType *unit_type; - - /** - * should selection features be drawn - * TODO: should be a pointer to selection to be updated - * when unit is removed, or null if not selected - */ - bool selected; - - /** - * space on the map used by this unit - * null if the object is not yet placed or garrisoned - * TODO: make private field - */ - std::unique_ptr location; - - /** - * constructs a new location for this unit replacing any - * existing locatio - * - * uses same args as the location constructor - * except the first which will filled automatically - */ - template - void make_location(Arg... args) { - // remove any existing location first - if (this->location) { - this->location->remove(); - } - - // since Unit is a friend of the location - // make_shared will not work - this->location = std::unique_ptr(new T(*this, args...)); - } - - /** - * removes all actions and abilities - * current attributes are kept - */ - void reset(); - - /** - * checks the entity has an action, if it has no action it should be removed from the game - * @return true if the entity currently has an action - */ - bool has_action() const; - - /** - * true when the unit is alive and able to add new actions - */ - bool accept_commands() const; - - /** - * checks whether the current player is the owner of this unit - */ - bool is_own_unit(const Player &player); - - /** - * returns the current action on top of the stack - */ - UnitAction *top() const; - - /** - * returns action under the passed action in the stack - * returns null if stack size is less than 2 - */ - UnitAction *before(const UnitAction *action) const; - - /** - * update this object using the action currently on top of the stack - */ - bool update(time_nsec_t lastframe_duration); - - /** - * adds an available ability to this unit - * this turns targeted objects into actions which are pushed - * onto the stack, eg. targeting a relic may push a collect relic action - */ - void give_ability(std::shared_ptr); - - /** - * get ability with specified type, null if not available - * - * To invoke commands use the invoke function instead - */ - UnitAbility *get_ability(ability_type type); - - /** - * adds a new action on top of the action stack - * will be performed immediately - */ - void push_action(std::unique_ptr action, bool force = false); - - /** - * adds a secondary action which is always updated - */ - void secondary_action(std::unique_ptr action); - - /** - * give a new attribute this this unit - * this is used to set things like color, hitpoints and speed - */ - void add_attribute(std::shared_ptr attr); - - /** - * Give new attributes to this unit. - * This is used to add the default attributes - */ - void add_attributes(const Attributes &attr); - - /** - * Give new attributes to this unit. - * If shared is false, shared attributes are ignored. - * If unshared is false, unshared attributes are ignored. - */ - void add_attributes(const Attributes &attr, bool shared, bool unshared); - - /** - * returns whether attribute is available - */ - bool has_attribute(attr_type type) const; - - /** - * returns attribute based on templated value - */ - template - Attribute &get_attribute() { - return *reinterpret_cast *>(attributes.get(T).get()); - // TODO change to (templates errors) - //return attributes.get(); - } - - /** - * queues a command to be applied to this unit on the next update - * - * @return the ability which will apply the command if an action was created - * otherwise nullptr is returned when no ability can handle the command - */ - std::shared_ptr queue_cmd(const Command &cmd); - - /** - * removes all gather actions without calling their on_complete actions - * this cancels the gathering action completely - */ - void stop_gather(); - - /** - * removes all actions above and including the first interuptable action - * this will stop any of the units current moving or attacking actions - * a direct command from the user will invoke this function - */ - void stop_actions(); - - /** - * begins unit removal by popping some actions - * - * this is the action that occurs when pressing the delete key - * which plays death sequence and does not remove instantly - */ - void delete_unit(); - - /** - * get a reference which can check against the container - * to ensure this object still exists - */ - UnitReference get_ref(); - - /** - * the container used when constructing this unit - */ - UnitContainer *get_container() const; - - /** - * Returns the unit's name as the LogSource name. - */ - std::string logsource_name() override; - - /** - * Unit attributes include color, hitpoints, speed, objects garrisoned etc - * contains 0 or 1 values for each type - */ - Attributes attributes; - -private: - /** - * ability available -- actions that this entity - * can perform when controlled - */ - std::unordered_map> ability_available; - - - /** - * action stack -- top action determines graphic to be drawn - */ - std::vector> action_stack; - - - /** - * secondary actions are always updated - */ - std::vector> action_secondary; - - - /** - * queue commands to be applied on the next update - */ - std::queue, const Command>> command_queue; - - /** - * mutex controlling updates to the command queue - */ - std::mutex command_queue_lock; - - - /** - * pop any destructable actions on the next update cycle - * and prevent additional actions being added - */ - bool pop_destructables; - - /** - * the container that updates this unit - */ - UnitContainer *container; - - /** - * applies new commands as part of the units update process - */ - void apply_all_cmds(); - - /** - * applies one command using a chosen ability - * locks the command queue mutex while operating - */ - void apply_cmd(std::shared_ptr ability, const Command &cmd); - - /** - * update all secondary actions - */ - void update_secondary(int64_t time_elapsed); - - /** - * erase from action specified by func to the end of the stack - * all actions erased will have the on_complete function called - * - * @param run_completed usually each action has an on_complete() function called when it is removed - * but when run_completed is false this on_complete() function is not called for all popped actions - */ - void erase_after(std::function &)> func, bool run_completed = true); -}; - -} // namespace openage diff --git a/libopenage/unit/unit_container.cpp b/libopenage/unit/unit_container.cpp deleted file mode 100644 index 672fedacd9..0000000000 --- a/libopenage/unit/unit_container.cpp +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright 2014-2019 the openage authors. See copying.md for legal info. - -#include "unit_container.h" - -#include - -#include "../log/log.h" -#include "../terrain/terrain_object.h" -#include "producer.h" -#include "unit.h" - - -namespace openage { - -reference_data::reference_data(const UnitContainer *c, id_t id, Unit *u) - : - container{c}, - unit_id{id}, - unit_ptr{u} { - if (!u) { - throw Error{MSG(err) << "Cannot reference null unit pointer"}; - } -} - - -UnitReference::UnitReference() - : - data{nullptr} {} - - -UnitReference::UnitReference(const UnitContainer *c, id_t id, Unit *u) - : - data{std::make_shared(c, id, u)} {} - - -bool UnitReference::is_valid() const { - return this->data && - this->data->container->valid_id(this->data->unit_id); -} - - -Unit *UnitReference::get() const { - if (!this->is_valid()) { - throw Error{MSG(err) << "Unit reference is no longer valid"}; - } - return this->data->unit_ptr; -} - - -UnitContainer::UnitContainer() - : - next_new_id{1} {} - - -UnitContainer::~UnitContainer() = default; - - -void UnitContainer::reset() { - this->live_units.clear(); -} - -void UnitContainer::set_terrain(std::shared_ptr &t) { - this->terrain = t; -} - -std::shared_ptr UnitContainer::get_terrain() const { - if (this->terrain.expired()) { - throw Error{MSG(err) << "Terrain has expired"}; - } - return this->terrain.lock(); -} - - -bool UnitContainer::valid_id(id_t id) const { - return (this->live_units.count(id) > 0); -} - - -UnitReference UnitContainer::get_unit(id_t id) { - if (this->valid_id(id)) { - return UnitReference(this, id, this->live_units[id].get()); - } - else { - return UnitReference(this, id, nullptr); - } -} - -UnitReference UnitContainer::new_unit() { - auto id = next_new_id; - next_new_id += 1; - - this->live_units.emplace(id, std::make_unique(this, id)); - return this->live_units[id]->get_ref(); -} - -UnitReference UnitContainer::new_unit(UnitType &type, - Player &owner, - coord::phys3 position) { - - auto new_id = next_new_id; - next_new_id += 1; - - auto newobj = std::make_unique(this, new_id); - - // try placing unit at this location - auto terrain_shared = this->get_terrain(); - auto placed = type.place(newobj.get(), terrain_shared, position); - if (placed) { - type.initialise(newobj.get(), owner); - owner.active_unit_added(newobj.get()); // TODO change, move elsewhere - auto id = newobj->id; - this->live_units.emplace(id, std::move(newobj)); - return this->live_units[id]->get_ref(); - } - return UnitReference(); // is not valid -} - -UnitReference UnitContainer::new_unit(UnitType &type, - Player &owner, - TerrainObject *other) { - auto new_id = next_new_id; - next_new_id += 1; - - auto newobj = std::make_unique(this, new_id); - - // try placing unit - TerrainObject *placed = type.place_beside(newobj.get(), other); - if (placed) { - type.initialise(newobj.get(), owner); - owner.active_unit_added(newobj.get()); // TODO change, move elsewhere - auto id = newobj->id; - this->live_units.emplace(id, std::move(newobj)); - return this->live_units[id]->get_ref(); - } - return UnitReference(); // is not valid -} - - -bool dispatch_command(id_t, const Command &) { - return true; -} - -bool UnitContainer::update_all(time_nsec_t lastframe_duration) { - // update everything and find objects with no actions - std::vector to_remove; - - for (auto &obj : this->live_units) { - obj.second->update(lastframe_duration); - - if (not obj.second->has_action()) { - to_remove.push_back(obj.first); - } - } - - // cleanup and removal of objects - for (auto &obj : to_remove) { - - // unique pointer triggers cleanup - this->live_units.erase(obj); - } - return true; -} - -std::vector UnitContainer::all_units() { - std::vector result; - for (auto &u : this->live_units) { - result.push_back(u.second.get()); - } - return result; -} - -} // namespace openage diff --git a/libopenage/unit/unit_container.h b/libopenage/unit/unit_container.h deleted file mode 100644 index 8627c8582f..0000000000 --- a/libopenage/unit/unit_container.h +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include - -#include "../coord/tile.h" -#include "../util/timing.h" - - -namespace openage { - -class Command; -class Player; -class Terrain; -class TerrainObject; -class Unit; -class UnitContainer; -class UnitType; - - -/** - * Type used to identify each single unit in the game. - */ -using id_t = unsigned long int; - - -/** - * immutable reference data - */ -struct reference_data { - reference_data(const UnitContainer *c, id_t id, Unit *); - - const UnitContainer *const container; - const id_t unit_id; - Unit *const unit_ptr; -}; - -/** - * Reference to a single unit, which may have been removed - * from the game, check is_valid() before calling get() - */ -class UnitReference { -public: - /** - * create an invalid reference - */ - UnitReference(); - - /** - * create referece by unit id - */ - UnitReference(const UnitContainer *c, id_t id, Unit *); - - bool is_valid() const; - Unit *get() const; - -private: - /** - * The default copy constructor and assignment - * will just copy the shared pointer - */ - std::shared_ptr data; -}; - -/** - * the list of units that are currently in use - * will also give a view of the current game state for networking in later milestones - */ -class UnitContainer { -public: - UnitContainer(); - ~UnitContainer(); - - void reset(); - - /** - * sets terrain to initialise units on - */ - void set_terrain(std::shared_ptr &t); - - /** - * returns the terrain which units are placed on - */ - std::shared_ptr get_terrain() const; - - /** - * checks the id is valid - */ - bool valid_id(id_t id) const; - - /** - * returns a reference to a unit - */ - UnitReference get_unit(id_t id); - - /** - * creates a new unit without initialising - */ - UnitReference new_unit(); - - /** - * adds a new unit to the container and initialises using a unit type - */ - UnitReference new_unit(UnitType &type, Player &owner, coord::phys3 position); - - /** - * adds a new unit to the container and initialises using a unit type - * places outside an existing object using the player of that object - */ - UnitReference new_unit(UnitType &type, Player &owner, TerrainObject *other); - - /** - * give a command to a unit -- unit creation and deletion should be done as commands - */ - bool dispatch_command(id_t to_id, const Command &cmd); - - /** - * update dispatched by the game engine on each physics tick. - * this will update all game objects. - */ - bool update_all(time_nsec_t lastframe_duration); - - /** - * gets a list of all units in the container - */ - std::vector all_units(); - -private: - id_t next_new_id; - - /** - * mapping unit ids to unit objects - */ - std::unordered_map> live_units; - - /** - * Terrain for initialising new units - */ - std::weak_ptr terrain; -}; - -} // namespace openage diff --git a/libopenage/unit/unit_type.cpp b/libopenage/unit/unit_type.cpp deleted file mode 100644 index 80677ebc75..0000000000 --- a/libopenage/unit/unit_type.cpp +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "unit.h" -#include "../gamestate/old/player.h" -#include "../terrain/terrain_object.h" -#include "../util/math_constants.h" -#include "action.h" -#include "unit_container.h" -#include "unit_type.h" - -#include - -namespace openage { - -UnitTypeMeta::UnitTypeMeta(std::string name, int id, init_func f) : - init{f}, - type_name{std::move(name)}, - type_id{id} { -} - -std::string UnitTypeMeta::name() const { - return this->type_name; -} - -int UnitTypeMeta::id() const { - return this->type_id; -} - -UnitType::UnitType(const Player &owner) : - owner{owner}, - have_limit{math::INT_INF}, - had_limit{math::INT_INF} { -} - -void UnitType::reinitialise(Unit *unit, Player &player) { - // In case reinitialise is not implemented separately - - Attributes tmp; - // copy only unshared - tmp.add_copies(unit->attributes, false, true); - // initialise the new unit - this->initialise(unit, player); - // replace new unshared attributes with the old - unit->attributes.add_copies(tmp); -} - -bool UnitType::operator==(const UnitType &other) const { - return this->type_abilities == other.type_abilities; -} - -bool UnitType::operator!=(const UnitType &other) const { - return !(*this == other); -} - -TerrainObject *UnitType::place_beside(Unit *u, TerrainObject const *other) const { - if (!u || !other) { - return nullptr; - } - - // find the range of possible tiles - tile_range outline{other->pos.start - coord::tile_delta{1, 1}, - other->pos.end + coord::tile_delta{1, 1}, - other->pos.draw}; - - // find a free position adjacent to the object - auto terrain = other->get_terrain(); - for (coord::tile temp_pos : tile_list(outline)) { - TerrainChunk *chunk = terrain->get_chunk(temp_pos); - - if (chunk == nullptr) { - continue; - } - - auto placed = this->place(u, terrain, temp_pos.to_phys3(*terrain)); - if (placed) { - return placed; - } - } - return nullptr; -} - -void UnitType::copy_attributes(Unit *unit) const { - unit->add_attributes(this->default_attributes); -} - -void UnitType::upgrade(const std::shared_ptr &attr) { - this->default_attributes.add(attr); -} - -UnitType *UnitType::parent_type() const { - return this->owner.get_type(this->parent_id()); -} - -NyanType::NyanType(const Player &owner) : - UnitType(owner) { - // TODO: the type should be given attributes and abilities -} - -NyanType::~NyanType() = default; - -int NyanType::id() const { - return 1; -} - -int NyanType::parent_id() const { - return -1; -} - -std::string NyanType::name() const { - return "Nyan"; -} - -void NyanType::initialise(Unit *unit, Player &) { - // removes all actions and abilities - unit->reset(); - - // initialise unit - unit->unit_type = this; - - // the parsed nyan data gives the list of attributes - // and abilities which are given to the unit - for (auto &ability : this->type_abilities) { - unit->give_ability(ability); - } - - // copy all attributes - this->copy_attributes(unit); - - // give idle action - unit->push_action(std::make_unique(unit), true); -} - -TerrainObject *NyanType::place(Unit *unit, std::shared_ptr terrain, coord::phys3 init_pos) const { - // the parsed nyan data gives the rules for terrain placement - // which includes valid terrains, base radius and shape - - unit->make_location(coord::tile_delta{1, 1}); - - // allow unit to go anywhere - unit->location->passable = [](const coord::phys3 &) { - return true; - }; - - // try to place the obj, it knows best whether it will fit. - if (unit->location->place(terrain, init_pos, object_state::placed)) { - return unit->location.get(); - } - - // placing at the given position failed - unit->log(MSG(dbg) << "failed to place object"); - return nullptr; -} - -} // namespace openage diff --git a/libopenage/unit/unit_type.h b/libopenage/unit/unit_type.h deleted file mode 100644 index 57dc2c2b49..0000000000 --- a/libopenage/unit/unit_type.h +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include -#include - -#include "../coord/phys.h" -#include "../gamestate/old/cost.h" -#include "attributes.h" - -namespace openage { - -class Player; -class Terrain; -class TerrainObject; -class Unit; -class UnitAbility; -class UnitContainer; - - -/** - * an abstract unit type which is not yet owned by any player - */ -class UnitTypeMeta { -public: - using type_ptr = std::shared_ptr; - using init_func = std::function; - UnitTypeMeta(std::string name, int id, init_func f); - - std::string name() const; - - int id() const; - - /** - * creates the base unit type for a player - */ - const init_func init; - -private: - const std::string type_name; - const int type_id; -}; - -/** - * UnitType has to main roles: - * - * initialise(unit, player) should be called on a unit to give it a type and the required attributes, abilities and initial actions - * of that type - * - * place(unit, terrain, initial position) is called to customise how the unit gets added to the world -- used to setup the TerrainObject location - * - * UnitType is connected to a player to allow independent tech levels - */ -class UnitType { -public: - UnitType(const Player &owner); - virtual ~UnitType() {} - - /** - * gets the unique id of this unit type - */ - virtual int id() const = 0; - - /** - * gets the parent id of this unit type - * which is used for village base and gather types - */ - virtual int parent_id() const = 0; - - /** - * gets the name of the unit type being produced - */ - virtual std::string name() const = 0; - - /** - * Initialize units attributes to this type spec - * - * This can be called using existing units to modify type - * Ensure that the unit has been placed before seting the units type - * - * TODO: make const - */ - virtual void initialise(Unit *, Player &) = 0; - - /** - * Initialize units shared attributes only to this type spec - * - * This can be called using existing units to modify type if the type - * Ensure that the unit has been placed before seting the units type - * - * TODO define if pure vitrual or not / should be in nyan? - */ - virtual void reinitialise(Unit *, Player &); - - /** - * set unit in place -- return if placement was successful - * - * This should be used when initially creating a unit or - * when a unit is ungarrsioned from a building or object - * TODO: make const - */ - virtual TerrainObject *place(Unit *, std::shared_ptr, coord::phys3) const = 0; - - /** - * compare if two types are the same - */ - bool operator==(const UnitType &other) const; - bool operator!=(const UnitType &other) const; - - /** - * similar to place but places adjacent to an existing object - */ - TerrainObject *place_beside(Unit *, TerrainObject const *) const; - - /** - * copy attributes of this unit type to a new unit instance - */ - void copy_attributes(Unit *unit) const; - - /** - * upgrades one attribute of this unit type - */ - void upgrade(const std::shared_ptr &attr); - - /** - * returns type matching parent_id() - */ - UnitType *parent_type() const; - - /** - * the player who owns this unit type - */ - const Player &owner; - - /** - * all instances of units made from this unit type - * this could allow all units of a type to be upgraded - */ - std::vector instances; - - /** - * abilities given to all instances - */ - std::vector> type_abilities; - - /** - * default attributes which get copied to new units - */ - Attributes default_attributes; - - /** - * The cost of the unit. - */ - ResourceCost cost; - - /** - * The max number of units of this type that a player can have at an instance. - * Use negative values for special cases. - */ - int have_limit; - - /** - * The max number of units of this type that a player can create. - * Use negative values for special cases. - */ - int had_limit; - - /** - * The index of the icon representing this unit - */ - int icon; - - /** - * the square dimensions of the placement - */ - coord::tile_delta foundation_size; - - /** - * raw game data class of this unit instance - */ - gamedata::unit_classes unit_class; -}; - -/** - * An example of how nyan can work with the type system - */ -class NyanType : public UnitType { -public: - /** - * TODO: give the parsed nyan attributes - * to the constructor - */ - NyanType(const Player &owner); - virtual ~NyanType(); - - int id() const override; - int parent_id() const override; - std::string name() const override; - void initialise(Unit *, Player &) override; - TerrainObject *place(Unit *, std::shared_ptr, coord::phys3) const override; -}; - -} // namespace openage From 1b6224ed80b971dc6bdc42edaf1d8e1b8aafa4f8 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Sep 2023 00:00:51 +0200 Subject: [PATCH 041/771] gamedata: Remove old dummy data. --- libopenage/CMakeLists.txt | 1 - libopenage/console/console.cpp | 16 +- libopenage/console/console.h | 3 +- libopenage/gamedata/CMakeLists.txt | 16 - libopenage/gamedata/about | 3 - libopenage/gamedata/blending_mode_dummy.cpp | 46 - libopenage/gamedata/blending_mode_dummy.h | 32 - libopenage/gamedata/civilisation_dummy.cpp | 273 -- libopenage/gamedata/civilisation_dummy.h | 57 - libopenage/gamedata/color_dummy.cpp | 50 - libopenage/gamedata/color_dummy.h | 36 - libopenage/gamedata/gamedata_dummy.cpp | 110 - libopenage/gamedata/gamedata_dummy.h | 88 - libopenage/gamedata/graphic_dummy.cpp | 166 - libopenage/gamedata/graphic_dummy.h | 97 - libopenage/gamedata/player_color_dummy.cpp | 54 - libopenage/gamedata/player_color_dummy.h | 40 - libopenage/gamedata/research_dummy.cpp | 101 - libopenage/gamedata/research_dummy.h | 61 - libopenage/gamedata/sound_dummy.cpp | 81 - libopenage/gamedata/sound_dummy.h | 50 - libopenage/gamedata/string_resource_dummy.cpp | 48 - libopenage/gamedata/string_resource_dummy.h | 34 - libopenage/gamedata/tech_dummy.cpp | 264 -- libopenage/gamedata/tech_dummy.h | 140 - libopenage/gamedata/terrain_dummy.cpp | 274 -- libopenage/gamedata/terrain_dummy.h | 141 - libopenage/gamedata/texture_dummy.cpp | 51 - libopenage/gamedata/texture_dummy.h | 40 - libopenage/gamedata/unit_dummy.cpp | 2820 ----------------- libopenage/gamedata/unit_dummy.h | 515 --- libopenage/gamedata/util_dummy.cpp | 46 - libopenage/gamedata/util_dummy.h | 32 - libopenage/util/color.cpp | 11 +- libopenage/util/color.h | 10 +- 35 files changed, 16 insertions(+), 5791 deletions(-) delete mode 100644 libopenage/gamedata/CMakeLists.txt delete mode 100644 libopenage/gamedata/about delete mode 100644 libopenage/gamedata/blending_mode_dummy.cpp delete mode 100644 libopenage/gamedata/blending_mode_dummy.h delete mode 100644 libopenage/gamedata/civilisation_dummy.cpp delete mode 100644 libopenage/gamedata/civilisation_dummy.h delete mode 100644 libopenage/gamedata/color_dummy.cpp delete mode 100644 libopenage/gamedata/color_dummy.h delete mode 100644 libopenage/gamedata/gamedata_dummy.cpp delete mode 100644 libopenage/gamedata/gamedata_dummy.h delete mode 100644 libopenage/gamedata/graphic_dummy.cpp delete mode 100644 libopenage/gamedata/graphic_dummy.h delete mode 100644 libopenage/gamedata/player_color_dummy.cpp delete mode 100644 libopenage/gamedata/player_color_dummy.h delete mode 100644 libopenage/gamedata/research_dummy.cpp delete mode 100644 libopenage/gamedata/research_dummy.h delete mode 100644 libopenage/gamedata/sound_dummy.cpp delete mode 100644 libopenage/gamedata/sound_dummy.h delete mode 100644 libopenage/gamedata/string_resource_dummy.cpp delete mode 100644 libopenage/gamedata/string_resource_dummy.h delete mode 100644 libopenage/gamedata/tech_dummy.cpp delete mode 100644 libopenage/gamedata/tech_dummy.h delete mode 100644 libopenage/gamedata/terrain_dummy.cpp delete mode 100644 libopenage/gamedata/terrain_dummy.h delete mode 100644 libopenage/gamedata/texture_dummy.cpp delete mode 100644 libopenage/gamedata/texture_dummy.h delete mode 100644 libopenage/gamedata/unit_dummy.cpp delete mode 100644 libopenage/gamedata/unit_dummy.h delete mode 100644 libopenage/gamedata/util_dummy.cpp delete mode 100644 libopenage/gamedata/util_dummy.h diff --git a/libopenage/CMakeLists.txt b/libopenage/CMakeLists.txt index bda4bc1611..3e12a5766c 100644 --- a/libopenage/CMakeLists.txt +++ b/libopenage/CMakeLists.txt @@ -344,7 +344,6 @@ add_subdirectory("datastructure") add_subdirectory("engine") add_subdirectory("error") add_subdirectory("event") -add_subdirectory("gamedata") add_subdirectory("gamestate") add_subdirectory("gui") add_subdirectory("input") diff --git a/libopenage/console/console.cpp b/libopenage/console/console.cpp index 20e414e13d..04cf29d4d1 100644 --- a/libopenage/console/console.cpp +++ b/libopenage/console/console.cpp @@ -44,15 +44,15 @@ Console::Console(/* presenter::LegacyDisplay *display */) : Console::~Console() {} -void Console::load_colors(std::vector &colortable) { - for (auto &c : colortable) { - this->termcolors.emplace_back(c); - } +// void Console::load_colors(std::vector &colortable) { +// for (auto &c : colortable) { +// this->termcolors.emplace_back(c); +// } - if (termcolors.size() != 256) { - throw Error(MSG(err) << "Exactly 256 terminal colors are required."); - } -} +// if (termcolors.size() != 256) { +// throw Error(MSG(err) << "Exactly 256 terminal colors are required."); +// } +// } void Console::register_to_engine() { // TODO: Use new renderer diff --git a/libopenage/console/console.h b/libopenage/console/console.h index 6994c4cbe9..839467e6a1 100644 --- a/libopenage/console/console.h +++ b/libopenage/console/console.h @@ -6,7 +6,6 @@ #include #include "../coord/pixel.h" -#include "../gamedata/color_dummy.h" #include "../renderer/font/font.h" #include "../util/color.h" #include "buf.h" @@ -28,7 +27,7 @@ class Console { /** * load the consoles color table */ - void load_colors(std::vector &colortable); + // void load_colors(std::vector &colortable); /** * register this console to the renderer. diff --git a/libopenage/gamedata/CMakeLists.txt b/libopenage/gamedata/CMakeLists.txt deleted file mode 100644 index 9eb8632148..0000000000 --- a/libopenage/gamedata/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -add_sources(libopenage - blending_mode_dummy.cpp - civilisation_dummy.cpp - color_dummy.cpp - gamedata_dummy.cpp - graphic_dummy.cpp - player_color_dummy.cpp - research_dummy.cpp - sound_dummy.cpp - string_resource_dummy.cpp - tech_dummy.cpp - terrain_dummy.cpp - texture_dummy.cpp - unit_dummy.cpp - util_dummy.cpp -) diff --git a/libopenage/gamedata/about b/libopenage/gamedata/about deleted file mode 100644 index f9e60f268e..0000000000 --- a/libopenage/gamedata/about +++ /dev/null @@ -1,3 +0,0 @@ -all source files in this directory are auto-generated during the build process. - -this file mainly exists to ensure that the directory isn't empty. diff --git a/libopenage/gamedata/blending_mode_dummy.cpp b/libopenage/gamedata/blending_mode_dummy.cpp deleted file mode 100644 index 468c55c84e..0000000000 --- a/libopenage/gamedata/blending_mode_dummy.cpp +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - - -#include -#include -#include "blending_mode_dummy.h" -#include "error/error.h" -#include "util/strings.h" - - -namespace openage { -namespace gamedata { - -constexpr size_t blending_mode::member_count; -int blending_mode::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', blending_mode::member_count - ); - - if (buf.size() != blending_mode::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing blending_mode led to " - << buf.size() - << " columns (expected " - << blending_mode::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%d", &this->blend_mode) != 1) { return 0; } - - return -1; -} - -bool blending_mode::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -} // gamedata -} // openage diff --git a/libopenage/gamedata/blending_mode_dummy.h b/libopenage/gamedata/blending_mode_dummy.h deleted file mode 100644 index 55b29788ae..0000000000 --- a/libopenage/gamedata/blending_mode_dummy.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - -#pragma once - -#include -#include -#include -#include "util/csv.h" - - - -namespace openage { -namespace gamedata { - -/** - * describes one blending mode, a blending transition shape between two different terrain types. - */ -struct blending_mode { - int32_t blend_mode; - static constexpr size_t member_count = 1; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -} // gamedata -} // openage diff --git a/libopenage/gamedata/civilisation_dummy.cpp b/libopenage/gamedata/civilisation_dummy.cpp deleted file mode 100644 index e4b0300edf..0000000000 --- a/libopenage/gamedata/civilisation_dummy.cpp +++ /dev/null @@ -1,273 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - - -#include -#include -#include -#include "civilisation_dummy.h" -#include "error/error.h" -#include "unit_dummy.h" -#include "util_dummy.h" -#include "util/csv.h" -#include "util/path.h" -#include "util/strings.h" - - -namespace openage { -namespace gamedata { - -constexpr size_t civilisation::member_count; -int civilisation::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', civilisation::member_count - ); - - if (buf.size() != civilisation::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing civilisation led to " - << buf.size() - << " columns (expected " - << civilisation::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hhd", &this->player_type) != 1) { return 0; } - this->name = buf[1]; - if (sscanf(buf[2].c_str(), "%hd", &this->tech_tree_id) != 1) { return 2; } - if (sscanf(buf[3].c_str(), "%hd", &this->team_bonus_id) != 1) { return 3; } - if (sscanf(buf[5].c_str(), "%hhd", &this->icon_set) != 1) { return 5; } - this->units.subdata_meta.filename = buf[6]; - - return -1; -} - -bool civilisation::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->units.recurse(storage, basedir); - - return true; -} - -int unit_types::fill(const std::string & /*line*/) { - return -1; -} -bool unit_types::recurse(const openage::util::CSVCollection &storage, - const std::string &basedir) { - - // the .filename was set by the previous entry parser already - // so now read the index-file entries - this->subdata_meta.read(storage, basedir); - - int subtype_count = this->subdata_meta.data.size(); - if (subtype_count != 9) { - throw openage::error::Error( - ERR << "multisubtype index file entry count mismatched!" - << subtype_count << " != 9" - ); - } - - // the recursed data files are relative to the subdata_meta filename - std::string metadata_dir = basedir + openage::util::fslike::PATHSEP + openage::util::dirname(this->subdata_meta.filename); - int idx; - int idxtry; - - // read subtype 'action' - idx = -1; - idxtry = 0; - // find the index of the subdata in the metadata - for (auto &file_reference : this->subdata_meta.data) { - if (file_reference.subtype == "action") { - idx = idxtry; - break; - } - idxtry += 1; - } - if (idx == -1) { - throw openage::error::Error( - ERR << "multisubtype index file contains no entry for action!" - ); - } - - // the filename is relative to the metadata file! - this->action.filename = this->subdata_meta.data[idx].filename; - this->action.read(storage, metadata_dir); - - // read subtype 'animated' - idx = -1; - idxtry = 0; - // find the index of the subdata in the metadata - for (auto &file_reference : this->subdata_meta.data) { - if (file_reference.subtype == "animated") { - idx = idxtry; - break; - } - idxtry += 1; - } - if (idx == -1) { - throw openage::error::Error( - ERR << "multisubtype index file contains no entry for animated!" - ); - } - - // the filename is relative to the metadata file! - this->animated.filename = this->subdata_meta.data[idx].filename; - this->animated.read(storage, metadata_dir); - - // read subtype 'building' - idx = -1; - idxtry = 0; - // find the index of the subdata in the metadata - for (auto &file_reference : this->subdata_meta.data) { - if (file_reference.subtype == "building") { - idx = idxtry; - break; - } - idxtry += 1; - } - if (idx == -1) { - throw openage::error::Error( - ERR << "multisubtype index file contains no entry for building!" - ); - } - - // the filename is relative to the metadata file! - this->building.filename = this->subdata_meta.data[idx].filename; - this->building.read(storage, metadata_dir); - - // read subtype 'doppelganger' - idx = -1; - idxtry = 0; - // find the index of the subdata in the metadata - for (auto &file_reference : this->subdata_meta.data) { - if (file_reference.subtype == "doppelganger") { - idx = idxtry; - break; - } - idxtry += 1; - } - if (idx == -1) { - throw openage::error::Error( - ERR << "multisubtype index file contains no entry for doppelganger!" - ); - } - - // the filename is relative to the metadata file! - this->doppelganger.filename = this->subdata_meta.data[idx].filename; - this->doppelganger.read(storage, metadata_dir); - - // read subtype 'living' - idx = -1; - idxtry = 0; - // find the index of the subdata in the metadata - for (auto &file_reference : this->subdata_meta.data) { - if (file_reference.subtype == "living") { - idx = idxtry; - break; - } - idxtry += 1; - } - if (idx == -1) { - throw openage::error::Error( - ERR << "multisubtype index file contains no entry for living!" - ); - } - - // the filename is relative to the metadata file! - this->living.filename = this->subdata_meta.data[idx].filename; - this->living.read(storage, metadata_dir); - - // read subtype 'missile' - idx = -1; - idxtry = 0; - // find the index of the subdata in the metadata - for (auto &file_reference : this->subdata_meta.data) { - if (file_reference.subtype == "missile") { - idx = idxtry; - break; - } - idxtry += 1; - } - if (idx == -1) { - throw openage::error::Error( - ERR << "multisubtype index file contains no entry for missile!" - ); - } - - // the filename is relative to the metadata file! - this->missile.filename = this->subdata_meta.data[idx].filename; - this->missile.read(storage, metadata_dir); - - // read subtype 'moving' - idx = -1; - idxtry = 0; - // find the index of the subdata in the metadata - for (auto &file_reference : this->subdata_meta.data) { - if (file_reference.subtype == "moving") { - idx = idxtry; - break; - } - idxtry += 1; - } - if (idx == -1) { - throw openage::error::Error( - ERR << "multisubtype index file contains no entry for moving!" - ); - } - - // the filename is relative to the metadata file! - this->moving.filename = this->subdata_meta.data[idx].filename; - this->moving.read(storage, metadata_dir); - - // read subtype 'object' - idx = -1; - idxtry = 0; - // find the index of the subdata in the metadata - for (auto &file_reference : this->subdata_meta.data) { - if (file_reference.subtype == "object") { - idx = idxtry; - break; - } - idxtry += 1; - } - if (idx == -1) { - throw openage::error::Error( - ERR << "multisubtype index file contains no entry for object!" - ); - } - - // the filename is relative to the metadata file! - this->object.filename = this->subdata_meta.data[idx].filename; - this->object.read(storage, metadata_dir); - - // read subtype 'tree' - idx = -1; - idxtry = 0; - // find the index of the subdata in the metadata - for (auto &file_reference : this->subdata_meta.data) { - if (file_reference.subtype == "tree") { - idx = idxtry; - break; - } - idxtry += 1; - } - if (idx == -1) { - throw openage::error::Error( - ERR << "multisubtype index file contains no entry for tree!" - ); - } - - // the filename is relative to the metadata file! - this->tree.filename = this->subdata_meta.data[idx].filename; - this->tree.read(storage, metadata_dir); - - return true; -} - -} // gamedata -} // openage diff --git a/libopenage/gamedata/civilisation_dummy.h b/libopenage/gamedata/civilisation_dummy.h deleted file mode 100644 index 1627b4112f..0000000000 --- a/libopenage/gamedata/civilisation_dummy.h +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - -#pragma once - -#include -#include -#include -#include -#include "unit_dummy.h" -#include "util_dummy.h" -#include "util/csv.h" - - - -namespace openage { -namespace gamedata { - -struct unit_types { - struct openage::util::csv_subdata action; - struct openage::util::csv_subdata animated; - struct openage::util::csv_subdata building; - struct openage::util::csv_subdata doppelganger; - struct openage::util::csv_subdata living; - struct openage::util::csv_subdata missile; - struct openage::util::csv_subdata moving; - struct openage::util::csv_subdata object; - struct openage::util::csv_subdata tree; - struct openage::util::csv_subdata subdata_meta; - - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * describes a civilisation. - */ -struct civilisation { - int8_t player_type; - std::string name; - int16_t tech_tree_id; - int16_t team_bonus_id; - int8_t icon_set; - unit_types units; - static constexpr size_t member_count = 7; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -} // gamedata -} // openage diff --git a/libopenage/gamedata/color_dummy.cpp b/libopenage/gamedata/color_dummy.cpp deleted file mode 100644 index 9985fb0b9d..0000000000 --- a/libopenage/gamedata/color_dummy.cpp +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - - -#include -#include -#include "color_dummy.h" -#include "error/error.h" -#include "util/strings.h" - - -namespace openage { -namespace gamedata { - -constexpr size_t palette_color::member_count; -int palette_color::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', palette_color::member_count - ); - - if (buf.size() != palette_color::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing palette_color led to " - << buf.size() - << " columns (expected " - << palette_color::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%d", &this->idx) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hhu", &this->r) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hhu", &this->g) != 1) { return 2; } - if (sscanf(buf[3].c_str(), "%hhu", &this->b) != 1) { return 3; } - if (sscanf(buf[4].c_str(), "%hhu", &this->a) != 1) { return 4; } - - return -1; -} - -bool palette_color::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -} // gamedata -} // openage diff --git a/libopenage/gamedata/color_dummy.h b/libopenage/gamedata/color_dummy.h deleted file mode 100644 index 4c6915e5cb..0000000000 --- a/libopenage/gamedata/color_dummy.h +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - -#pragma once - -#include -#include -#include -#include "util/csv.h" - - - -namespace openage { -namespace gamedata { - -/** - * indexed color storage. - */ -struct palette_color { - int32_t idx; - uint8_t r; - uint8_t g; - uint8_t b; - uint8_t a; - static constexpr size_t member_count = 5; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -} // gamedata -} // openage diff --git a/libopenage/gamedata/gamedata_dummy.cpp b/libopenage/gamedata/gamedata_dummy.cpp deleted file mode 100644 index 959df5237a..0000000000 --- a/libopenage/gamedata/gamedata_dummy.cpp +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - - -#include -#include -#include "error/error.h" -#include "gamedata_dummy.h" -#include "util/strings.h" - - -namespace openage { -namespace gamedata { - -constexpr size_t empiresdat::member_count; -int empiresdat::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', empiresdat::member_count - ); - - if (buf.size() != empiresdat::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing empiresdat led to " - << buf.size() - << " columns (expected " - << empiresdat::member_count - << ")!" - ); - } - - this->versionstr = buf[0]; - this->terrain_restrictions.filename = buf[1]; - this->player_colors.filename = buf[2]; - this->sounds.filename = buf[3]; - this->graphics.filename = buf[4]; - if (sscanf(buf[5].c_str(), "%d", &this->virt_function_ptr) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%d", &this->map_pointer) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%d", &this->map_width) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%d", &this->map_height) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%d", &this->world_width) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%d", &this->world_height) != 1) { return 10; } - this->tile_sizes.filename = buf[11]; - if (sscanf(buf[12].c_str(), "%hd", &this->padding1) != 1) { return 12; } - this->terrains.filename = buf[13]; - this->terrain_border.filename = buf[14]; - if (sscanf(buf[15].c_str(), "%d", &this->map_row_offset) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%f", &this->map_min_x) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%f", &this->map_min_y) != 1) { return 17; } - if (sscanf(buf[18].c_str(), "%f", &this->map_max_x) != 1) { return 18; } - if (sscanf(buf[19].c_str(), "%f", &this->map_max_y) != 1) { return 19; } - if (sscanf(buf[20].c_str(), "%f", &this->map_max_xplus1) != 1) { return 20; } - if (sscanf(buf[21].c_str(), "%f", &this->map_min_yplus1) != 1) { return 21; } - if (sscanf(buf[22].c_str(), "%hd", &this->current_row) != 1) { return 22; } - if (sscanf(buf[23].c_str(), "%hd", &this->current_column) != 1) { return 23; } - if (sscanf(buf[24].c_str(), "%hd", &this->block_beginn_row) != 1) { return 24; } - if (sscanf(buf[25].c_str(), "%hd", &this->block_end_row) != 1) { return 25; } - if (sscanf(buf[26].c_str(), "%hd", &this->block_begin_column) != 1) { return 26; } - if (sscanf(buf[27].c_str(), "%hd", &this->block_end_column) != 1) { return 27; } - if (sscanf(buf[28].c_str(), "%d", &this->search_map_ptr) != 1) { return 28; } - if (sscanf(buf[29].c_str(), "%d", &this->search_map_rows_ptr) != 1) { return 29; } - if (sscanf(buf[30].c_str(), "%hhd", &this->any_frame_change) != 1) { return 30; } - if (sscanf(buf[31].c_str(), "%hhd", &this->map_visible_flag) != 1) { return 31; } - if (sscanf(buf[32].c_str(), "%hhd", &this->fog_flag) != 1) { return 32; } - this->effect_bundles.filename = buf[33]; - this->unit_headers.filename = buf[34]; - this->civs.filename = buf[35]; - this->researches.filename = buf[36]; - if (sscanf(buf[37].c_str(), "%d", &this->time_slice) != 1) { return 37; } - if (sscanf(buf[38].c_str(), "%d", &this->unit_kill_rate) != 1) { return 38; } - if (sscanf(buf[39].c_str(), "%d", &this->unit_kill_total) != 1) { return 39; } - if (sscanf(buf[40].c_str(), "%d", &this->unit_hitpoint_rate) != 1) { return 40; } - if (sscanf(buf[41].c_str(), "%d", &this->unit_hitpoint_total) != 1) { return 41; } - if (sscanf(buf[42].c_str(), "%d", &this->razing_kill_rate) != 1) { return 42; } - if (sscanf(buf[43].c_str(), "%d", &this->razing_kill_total) != 1) { return 43; } - if (sscanf(buf[44].c_str(), "%d", &this->total_unit_tech_groups) != 1) { return 44; } - this->age_connections.filename = buf[45]; - this->building_connections.filename = buf[46]; - this->unit_connections.filename = buf[47]; - this->tech_connections.filename = buf[48]; - - return -1; -} - -bool empiresdat::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->terrain_restrictions.read(storage, basedir); - this->player_colors.read(storage, basedir); - this->sounds.read(storage, basedir); - this->graphics.read(storage, basedir); - this->tile_sizes.read(storage, basedir); - this->terrains.read(storage, basedir); - this->terrain_border.read(storage, basedir); - this->effect_bundles.read(storage, basedir); - this->unit_headers.read(storage, basedir); - this->civs.read(storage, basedir); - this->researches.read(storage, basedir); - this->age_connections.read(storage, basedir); - this->building_connections.read(storage, basedir); - this->unit_connections.read(storage, basedir); - this->tech_connections.read(storage, basedir); - - return true; -} - -} // gamedata -} // openage diff --git a/libopenage/gamedata/gamedata_dummy.h b/libopenage/gamedata/gamedata_dummy.h deleted file mode 100644 index d702b8cd4f..0000000000 --- a/libopenage/gamedata/gamedata_dummy.h +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - -#pragma once - -#include -#include -#include -#include "civilisation_dummy.h" -#include "graphic_dummy.h" -#include "player_color_dummy.h" -#include "research_dummy.h" -#include "sound_dummy.h" -#include "tech_dummy.h" -#include "terrain_dummy.h" -#include "unit_dummy.h" -#include "util/csv.h" - - - -namespace openage { -namespace gamedata { - -/** - * empires2_x1_p1.dat structure - */ -struct empiresdat { - std::string versionstr; - openage::util::csv_subdata terrain_restrictions; - openage::util::csv_subdata player_colors; - openage::util::csv_subdata sounds; - openage::util::csv_subdata graphics; - int32_t virt_function_ptr; - int32_t map_pointer; - int32_t map_width; - int32_t map_height; - int32_t world_width; - int32_t world_height; - openage::util::csv_subdata tile_sizes; - int16_t padding1; - openage::util::csv_subdata terrains; - openage::util::csv_subdata terrain_border; - int32_t map_row_offset; - float map_min_x; - float map_min_y; - float map_max_x; - float map_max_y; - float map_max_xplus1; - float map_min_yplus1; - int16_t current_row; - int16_t current_column; - int16_t block_beginn_row; - int16_t block_end_row; - int16_t block_begin_column; - int16_t block_end_column; - int32_t search_map_ptr; - int32_t search_map_rows_ptr; - int8_t any_frame_change; - int8_t map_visible_flag; - int8_t fog_flag; - openage::util::csv_subdata effect_bundles; - openage::util::csv_subdata unit_headers; - openage::util::csv_subdata civs; - openage::util::csv_subdata researches; - int32_t time_slice; - int32_t unit_kill_rate; - int32_t unit_kill_total; - int32_t unit_hitpoint_rate; - int32_t unit_hitpoint_total; - int32_t razing_kill_rate; - int32_t razing_kill_total; - int32_t total_unit_tech_groups; - openage::util::csv_subdata age_connections; - openage::util::csv_subdata building_connections; - openage::util::csv_subdata unit_connections; - openage::util::csv_subdata tech_connections; - static constexpr size_t member_count = 49; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -} // gamedata -} // openage diff --git a/libopenage/gamedata/graphic_dummy.cpp b/libopenage/gamedata/graphic_dummy.cpp deleted file mode 100644 index 6a0d6b63cb..0000000000 --- a/libopenage/gamedata/graphic_dummy.cpp +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - - -#include -#include -#include "error/error.h" -#include "graphic_dummy.h" -#include "util/strings.h" - - -namespace openage { -namespace gamedata { - -constexpr size_t graphic::member_count; -constexpr size_t graphic_attack_sound::member_count; -constexpr size_t graphic_delta::member_count; -constexpr size_t sound_prop::member_count; -int graphic_attack_sound::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', graphic_attack_sound::member_count - ); - - if (buf.size() != graphic_attack_sound::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing graphic_attack_sound led to " - << buf.size() - << " columns (expected " - << graphic_attack_sound::member_count - << ")!" - ); - } - - this->sound_props.filename = buf[0]; - - return -1; -} - -bool graphic_attack_sound::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->sound_props.read(storage, basedir); - - return true; -} - -int graphic_delta::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', graphic_delta::member_count - ); - - if (buf.size() != graphic_delta::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing graphic_delta led to " - << buf.size() - << " columns (expected " - << graphic_delta::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->graphic_id) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hd", &this->padding_1) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%d", &this->sprite_ptr) != 1) { return 2; } - if (sscanf(buf[3].c_str(), "%hd", &this->offset_x) != 1) { return 3; } - if (sscanf(buf[4].c_str(), "%hd", &this->offset_y) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%hd", &this->display_angle) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%hd", &this->padding_2) != 1) { return 6; } - - return -1; -} - -bool graphic_delta::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -int graphic::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', graphic::member_count - ); - - if (buf.size() != graphic::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing graphic led to " - << buf.size() - << " columns (expected " - << graphic::member_count - << ")!" - ); - } - - this->name = buf[0]; - this->filename = buf[1]; - if (sscanf(buf[2].c_str(), "%d", &this->slp_id) != 1) { return 2; } - if (sscanf(buf[3].c_str(), "%hhd", &this->is_loaded) != 1) { return 3; } - if (sscanf(buf[4].c_str(), "%hhd", &this->old_color_flag) != 1) { return 4; } - // parse enum graphics_layer - if (buf[5] == "DUMMY") { - this->layer = graphics_layer::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[5] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[6].c_str(), "%hhd", &this->player_color_force_id) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%hhd", &this->adapt_color) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%hhu", &this->transparent_selection) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hd", &this->sound_id) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%hu", &this->frame_count) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%hu", &this->angle_count) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%f", &this->speed_adjust) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%f", &this->frame_rate) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%f", &this->replay_delay) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%hhd", &this->sequence_type) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%hd", &this->graphic_id) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%hhd", &this->mirroring_mode) != 1) { return 17; } - if (sscanf(buf[18].c_str(), "%hhd", &this->editor_flag) != 1) { return 18; } - this->graphic_deltas.filename = buf[19]; - this->graphic_attack_sounds.filename = buf[20]; - - return -1; -} - -bool graphic::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->graphic_deltas.read(storage, basedir); - this->graphic_attack_sounds.read(storage, basedir); - - return true; -} - -int sound_prop::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', sound_prop::member_count - ); - - if (buf.size() != sound_prop::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing sound_prop led to " - << buf.size() - << " columns (expected " - << sound_prop::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->sound_delay) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hd", &this->sound_id) != 1) { return 1; } - - return -1; -} - -bool sound_prop::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -} // gamedata -} // openage diff --git a/libopenage/gamedata/graphic_dummy.h b/libopenage/gamedata/graphic_dummy.h deleted file mode 100644 index 34f0a2a82f..0000000000 --- a/libopenage/gamedata/graphic_dummy.h +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - -#pragma once - -#include -#include -#include -#include "util/csv.h" - - - -namespace openage { -namespace gamedata { - -/** - * delta definitions for ingame graphics files. - */ -struct graphic_delta { - int16_t graphic_id; - int16_t padding_1; - int32_t sprite_ptr; - int16_t offset_x; - int16_t offset_y; - int16_t display_angle; - int16_t padding_2; - static constexpr size_t member_count = 7; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -enum class graphics_layer { - DUMMY -}; - - -/** - * sound id and delay definition for graphics sounds. - */ -struct sound_prop { - int16_t sound_delay; - int16_t sound_id; - static constexpr size_t member_count = 2; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * attack sounds for a given graphics file. - */ -struct graphic_attack_sound { - openage::util::csv_subdata sound_props; - static constexpr size_t member_count = 1; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * metadata for ingame graphics files. - */ -struct graphic { - std::string name; - std::string filename; - int32_t slp_id; - int8_t is_loaded; - int8_t old_color_flag; - graphics_layer layer; - int8_t player_color_force_id; - int8_t adapt_color; - uint8_t transparent_selection; - int16_t sound_id; - uint16_t frame_count; - uint16_t angle_count; - float speed_adjust; - float frame_rate; - float replay_delay; - int8_t sequence_type; - int16_t graphic_id; - int8_t mirroring_mode; - int8_t editor_flag; - openage::util::csv_subdata graphic_deltas; - openage::util::csv_subdata graphic_attack_sounds; - static constexpr size_t member_count = 21; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -} // gamedata -} // openage diff --git a/libopenage/gamedata/player_color_dummy.cpp b/libopenage/gamedata/player_color_dummy.cpp deleted file mode 100644 index 949b0de3c1..0000000000 --- a/libopenage/gamedata/player_color_dummy.cpp +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - - -#include -#include -#include "error/error.h" -#include "player_color_dummy.h" -#include "util/strings.h" - - -namespace openage { -namespace gamedata { - -constexpr size_t player_color::member_count; -int player_color::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', player_color::member_count - ); - - if (buf.size() != player_color::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing player_color led to " - << buf.size() - << " columns (expected " - << player_color::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%d", &this->id) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%d", &this->player_color_base) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%d", &this->outline_color) != 1) { return 2; } - if (sscanf(buf[3].c_str(), "%d", &this->unit_selection_color1) != 1) { return 3; } - if (sscanf(buf[4].c_str(), "%d", &this->unit_selection_color2) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%d", &this->minimap_color1) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%d", &this->minimap_color2) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%d", &this->minimap_color3) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%d", &this->statistics_text_color) != 1) { return 8; } - - return -1; -} - -bool player_color::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -} // gamedata -} // openage diff --git a/libopenage/gamedata/player_color_dummy.h b/libopenage/gamedata/player_color_dummy.h deleted file mode 100644 index ced379fba4..0000000000 --- a/libopenage/gamedata/player_color_dummy.h +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - -#pragma once - -#include -#include -#include -#include "util/csv.h" - - - -namespace openage { -namespace gamedata { - -/** - * describes player color settings. - */ -struct player_color { - int32_t id; - int32_t player_color_base; - int32_t outline_color; - int32_t unit_selection_color1; - int32_t unit_selection_color2; - int32_t minimap_color1; - int32_t minimap_color2; - int32_t minimap_color3; - int32_t statistics_text_color; - static constexpr size_t member_count = 9; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -} // gamedata -} // openage diff --git a/libopenage/gamedata/research_dummy.cpp b/libopenage/gamedata/research_dummy.cpp deleted file mode 100644 index 3b3a9af15b..0000000000 --- a/libopenage/gamedata/research_dummy.cpp +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - - -#include -#include -#include "error/error.h" -#include "research_dummy.h" -#include "util/strings.h" - - -namespace openage { -namespace gamedata { - -constexpr size_t tech::member_count; -constexpr size_t tech_resource_cost::member_count; -int tech::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', tech::member_count - ); - - if (buf.size() != tech::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing tech led to " - << buf.size() - << " columns (expected " - << tech::member_count - << ")!" - ); - } - - this->research_resource_costs.filename = buf[1]; - if (sscanf(buf[2].c_str(), "%hd", &this->required_tech_count) != 1) { return 2; } - if (sscanf(buf[3].c_str(), "%hd", &this->civilization_id) != 1) { return 3; } - if (sscanf(buf[4].c_str(), "%hd", &this->full_tech_mode) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%hd", &this->research_location_id) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%hu", &this->language_dll_name) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%hu", &this->language_dll_description) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%hd", &this->research_time) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hd", &this->tech_effect_id) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%hd", &this->tech_type) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%hd", &this->icon_id) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%hhd", &this->button_id) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%d", &this->language_dll_help) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%d", &this->language_dll_techtree) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%d", &this->hotkey) != 1) { return 15; } - this->name = buf[16]; - - return -1; -} - -bool tech::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->research_resource_costs.read(storage, basedir); - - return true; -} - -int tech_resource_cost::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', tech_resource_cost::member_count - ); - - if (buf.size() != tech_resource_cost::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing tech_resource_cost led to " - << buf.size() - << " columns (expected " - << tech_resource_cost::member_count - << ")!" - ); - } - - // parse enum resource_types - if (buf[0] == "DUMMY") { - this->type_id = resource_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[0] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[1].c_str(), "%hd", &this->amount) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hhd", &this->enabled) != 1) { return 2; } - - return -1; -} - -bool tech_resource_cost::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -} // gamedata -} // openage diff --git a/libopenage/gamedata/research_dummy.h b/libopenage/gamedata/research_dummy.h deleted file mode 100644 index f1a3205c16..0000000000 --- a/libopenage/gamedata/research_dummy.h +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - -#pragma once - -#include -#include -#include -#include "unit_dummy.h" -#include "util/csv.h" - - - -namespace openage { -namespace gamedata { - -/** - * amount definition for a single type resource for researches. - */ -struct tech_resource_cost { - resource_types type_id; - int16_t amount; - int8_t enabled; - static constexpr size_t member_count = 3; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * one researchable technology. - */ -struct tech { - openage::util::csv_subdata research_resource_costs; - int16_t required_tech_count; - int16_t civilization_id; - int16_t full_tech_mode; - int16_t research_location_id; - uint16_t language_dll_name; - uint16_t language_dll_description; - int16_t research_time; - int16_t tech_effect_id; - int16_t tech_type; - int16_t icon_id; - int8_t button_id; - int32_t language_dll_help; - int32_t language_dll_techtree; - int32_t hotkey; - std::string name; - static constexpr size_t member_count = 17; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -} // gamedata -} // openage diff --git a/libopenage/gamedata/sound_dummy.cpp b/libopenage/gamedata/sound_dummy.cpp deleted file mode 100644 index 67d781d0c0..0000000000 --- a/libopenage/gamedata/sound_dummy.cpp +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - - -#include -#include -#include "error/error.h" -#include "sound_dummy.h" -#include "util/strings.h" - - -namespace openage { -namespace gamedata { - -constexpr size_t sound::member_count; -constexpr size_t sound_item::member_count; -int sound::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', sound::member_count - ); - - if (buf.size() != sound::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing sound led to " - << buf.size() - << " columns (expected " - << sound::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->sound_id) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hd", &this->play_delay) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%d", &this->cache_time) != 1) { return 2; } - this->sound_items.filename = buf[3]; - - return -1; -} - -int sound_item::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', sound_item::member_count - ); - - if (buf.size() != sound_item::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing sound_item led to " - << buf.size() - << " columns (expected " - << sound_item::member_count - << ")!" - ); - } - - this->filename = buf[0]; - if (sscanf(buf[1].c_str(), "%d", &this->resource_id) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hd", &this->probablilty) != 1) { return 2; } - if (sscanf(buf[3].c_str(), "%hd", &this->civilization_id) != 1) { return 3; } - if (sscanf(buf[4].c_str(), "%hd", &this->icon_set) != 1) { return 4; } - - return -1; -} - -bool sound_item::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -bool sound::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->sound_items.read(storage, basedir); - - return true; -} - -} // gamedata -} // openage diff --git a/libopenage/gamedata/sound_dummy.h b/libopenage/gamedata/sound_dummy.h deleted file mode 100644 index 7c004cba5a..0000000000 --- a/libopenage/gamedata/sound_dummy.h +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - -#pragma once - -#include -#include -#include -#include "util/csv.h" - - - -namespace openage { -namespace gamedata { - -/** - * one possible file for a sound. - */ -struct sound_item { - std::string filename; - int32_t resource_id; - int16_t probablilty; - int16_t civilization_id; - int16_t icon_set; - static constexpr size_t member_count = 5; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * describes a sound, consisting of several sound items. - */ -struct sound { - int16_t sound_id; - int16_t play_delay; - int32_t cache_time; - openage::util::csv_subdata sound_items; - static constexpr size_t member_count = 4; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -} // gamedata -} // openage diff --git a/libopenage/gamedata/string_resource_dummy.cpp b/libopenage/gamedata/string_resource_dummy.cpp deleted file mode 100644 index 7030d15150..0000000000 --- a/libopenage/gamedata/string_resource_dummy.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - - -#include -#include -#include "error/error.h" -#include "string_resource_dummy.h" -#include "util/strings.h" - - -namespace openage { -namespace gamedata { - -constexpr size_t string_resource::member_count; -int string_resource::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', string_resource::member_count - ); - - if (buf.size() != string_resource::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing string_resource led to " - << buf.size() - << " columns (expected " - << string_resource::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%d", &this->id) != 1) { return 0; } - this->lang = buf[1]; - this->text = buf[2]; - - return -1; -} - -bool string_resource::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -} // gamedata -} // openage diff --git a/libopenage/gamedata/string_resource_dummy.h b/libopenage/gamedata/string_resource_dummy.h deleted file mode 100644 index 7673489566..0000000000 --- a/libopenage/gamedata/string_resource_dummy.h +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - -#pragma once - -#include -#include -#include -#include "util/csv.h" - - - -namespace openage { -namespace gamedata { - -/** - * string id/language to text mapping, extracted from language.dll file. - */ -struct string_resource { - int32_t id; - std::string lang; - std::string text; - static constexpr size_t member_count = 3; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -} // gamedata -} // openage diff --git a/libopenage/gamedata/tech_dummy.cpp b/libopenage/gamedata/tech_dummy.cpp deleted file mode 100644 index f2697695db..0000000000 --- a/libopenage/gamedata/tech_dummy.cpp +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - - -#include -#include -#include "error/error.h" -#include "tech_dummy.h" -#include "util/strings.h" - - -namespace openage { -namespace gamedata { - -constexpr size_t age_tech_tree::member_count; -constexpr size_t building_connection::member_count; -constexpr size_t effect_bundle::member_count; -constexpr size_t other_connection::member_count; -constexpr size_t research_connection::member_count; -constexpr size_t tech_effect::member_count; -constexpr size_t unit_connection::member_count; -int age_tech_tree::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', age_tech_tree::member_count - ); - - if (buf.size() != age_tech_tree::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing age_tech_tree led to " - << buf.size() - << " columns (expected " - << age_tech_tree::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%d", &this->id) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hhd", &this->status) != 1) { return 1; } - if (sscanf(buf[5].c_str(), "%d", &this->connected_slots_used) != 1) { return 5; } - this->other_connections.filename = buf[7]; - if (sscanf(buf[8].c_str(), "%hhd", &this->building_level_count) != 1) { return 8; } - if (sscanf(buf[11].c_str(), "%hhd", &this->max_age_length) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%d", &this->line_mode) != 1) { return 12; } - - return -1; -} - -bool age_tech_tree::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->other_connections.read(storage, basedir); - - return true; -} - -int building_connection::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', building_connection::member_count - ); - - if (buf.size() != building_connection::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing building_connection led to " - << buf.size() - << " columns (expected " - << building_connection::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%d", &this->id) != 1) { return 0; } - if (sscanf(buf[4].c_str(), "%d", &this->connected_slots_used) != 1) { return 4; } - this->other_connections.filename = buf[6]; - if (sscanf(buf[7].c_str(), "%hhd", &this->location_in_age) != 1) { return 7; } - if (sscanf(buf[10].c_str(), "%d", &this->line_mode) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%d", &this->enabling_research) != 1) { return 11; } - - return -1; -} - -bool building_connection::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->other_connections.read(storage, basedir); - - return true; -} - -int effect_bundle::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', effect_bundle::member_count - ); - - if (buf.size() != effect_bundle::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing effect_bundle led to " - << buf.size() - << " columns (expected " - << effect_bundle::member_count - << ")!" - ); - } - - this->name = buf[0]; - this->effects.filename = buf[1]; - - return -1; -} - -bool effect_bundle::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->effects.read(storage, basedir); - - return true; -} - -int other_connection::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', other_connection::member_count - ); - - if (buf.size() != other_connection::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing other_connection led to " - << buf.size() - << " columns (expected " - << other_connection::member_count - << ")!" - ); - } - - // parse enum connection_mode - if (buf[0] == "DUMMY") { - this->other_connection = connection_mode::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[0] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - - return -1; -} - -bool other_connection::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -int research_connection::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', research_connection::member_count - ); - - if (buf.size() != research_connection::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing research_connection led to " - << buf.size() - << " columns (expected " - << research_connection::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%d", &this->id) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hhd", &this->status) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%d", &this->upper_building) != 1) { return 2; } - if (sscanf(buf[6].c_str(), "%d", &this->connected_slots_used) != 1) { return 6; } - this->other_connections.filename = buf[8]; - if (sscanf(buf[9].c_str(), "%d", &this->vertical_line) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%d", &this->location_in_age) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%d", &this->line_mode) != 1) { return 11; } - - return -1; -} - -bool research_connection::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->other_connections.read(storage, basedir); - - return true; -} - -int tech_effect::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', tech_effect::member_count - ); - - if (buf.size() != tech_effect::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing tech_effect led to " - << buf.size() - << " columns (expected " - << tech_effect::member_count - << ")!" - ); - } - - // parse enum effect_apply_type - if (buf[0] == "DUMMY") { - this->type_id = effect_apply_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[0] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[1].c_str(), "%hd", &this->attr_a) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hd", &this->attr_b) != 1) { return 2; } - if (sscanf(buf[3].c_str(), "%hd", &this->attr_c) != 1) { return 3; } - if (sscanf(buf[4].c_str(), "%f", &this->attr_d) != 1) { return 4; } - - return -1; -} - -bool tech_effect::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -int unit_connection::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', unit_connection::member_count - ); - - if (buf.size() != unit_connection::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing unit_connection led to " - << buf.size() - << " columns (expected " - << unit_connection::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%d", &this->id) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hhd", &this->status) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%d", &this->upper_building) != 1) { return 2; } - if (sscanf(buf[3].c_str(), "%d", &this->connected_slots_used) != 1) { return 3; } - this->other_connections.filename = buf[5]; - if (sscanf(buf[6].c_str(), "%d", &this->vertical_line) != 1) { return 6; } - if (sscanf(buf[8].c_str(), "%d", &this->location_in_age) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%d", &this->required_research) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%d", &this->line_mode) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%d", &this->enabling_research) != 1) { return 11; } - - return -1; -} - -bool unit_connection::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->other_connections.read(storage, basedir); - - return true; -} - -} // gamedata -} // openage diff --git a/libopenage/gamedata/tech_dummy.h b/libopenage/gamedata/tech_dummy.h deleted file mode 100644 index bac2cc13ff..0000000000 --- a/libopenage/gamedata/tech_dummy.h +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - -#pragma once - -#include -#include -#include -#include "util/csv.h" - - - -namespace openage { -namespace gamedata { - -enum class connection_mode { - DUMMY -}; - - -enum class effect_apply_type { - DUMMY -}; - - -/** - * misc connection for a building/unit/research connection - */ -struct other_connection { - connection_mode other_connection; - static constexpr size_t member_count = 1; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * items available when this age was reached. - */ -struct age_tech_tree { - int32_t id; - int8_t status; - int32_t connected_slots_used; - openage::util::csv_subdata other_connections; - int8_t building_level_count; - int8_t max_age_length; - int32_t line_mode; - static constexpr size_t member_count = 13; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * new available buildings/units/researches when this building was created. - */ -struct building_connection { - int32_t id; - int32_t connected_slots_used; - openage::util::csv_subdata other_connections; - int8_t location_in_age; - int32_t line_mode; - int32_t enabling_research; - static constexpr size_t member_count = 12; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * applied effect for a research technology. - */ -struct tech_effect { - effect_apply_type type_id; - int16_t attr_a; - int16_t attr_b; - int16_t attr_c; - float attr_d; - static constexpr size_t member_count = 5; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * a bundle of effects. - */ -struct effect_bundle { - std::string name; - openage::util::csv_subdata effects; - static constexpr size_t member_count = 2; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * research updates to apply when activating the technology. - */ -struct research_connection { - int32_t id; - int8_t status; - int32_t upper_building; - int32_t connected_slots_used; - openage::util::csv_subdata other_connections; - int32_t vertical_line; - int32_t location_in_age; - int32_t line_mode; - static constexpr size_t member_count = 12; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * unit updates to apply when activating the technology. - */ -struct unit_connection { - int32_t id; - int8_t status; - int32_t upper_building; - int32_t connected_slots_used; - openage::util::csv_subdata other_connections; - int32_t vertical_line; - int32_t location_in_age; - int32_t required_research; - int32_t line_mode; - int32_t enabling_research; - static constexpr size_t member_count = 12; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -} // gamedata -} // openage diff --git a/libopenage/gamedata/terrain_dummy.cpp b/libopenage/gamedata/terrain_dummy.cpp deleted file mode 100644 index d99f430a4f..0000000000 --- a/libopenage/gamedata/terrain_dummy.cpp +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - - -#include -#include -#include "error/error.h" -#include "terrain_dummy.h" -#include "util/strings.h" - - -namespace openage { -namespace gamedata { - -constexpr size_t frame_data::member_count; -constexpr size_t terrain_animation::member_count; -constexpr size_t terrain_border::member_count; -constexpr size_t terrain_pass_graphic::member_count; -constexpr size_t terrain_restriction::member_count; -constexpr size_t terrain_type::member_count; -constexpr size_t tile_size::member_count; -int frame_data::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', frame_data::member_count - ); - - if (buf.size() != frame_data::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing frame_data led to " - << buf.size() - << " columns (expected " - << frame_data::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->frame_count) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hd", &this->angle_count) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hd", &this->shape_id) != 1) { return 2; } - - return -1; -} - -bool frame_data::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -int terrain_animation::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', terrain_animation::member_count - ); - - if (buf.size() != terrain_animation::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing terrain_animation led to " - << buf.size() - << " columns (expected " - << terrain_animation::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hhd", &this->is_animated) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hd", &this->animation_frame_count) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hd", &this->pause_frame_count) != 1) { return 2; } - if (sscanf(buf[3].c_str(), "%f", &this->interval) != 1) { return 3; } - if (sscanf(buf[4].c_str(), "%f", &this->pause_between_loops) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%hd", &this->frame) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%hd", &this->draw_frame) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%f", &this->animate_last) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%hhd", &this->frame_changed) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hhd", &this->drawn) != 1) { return 9; } - - return -1; -} - -bool terrain_animation::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -int terrain_border::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', terrain_border::member_count - ); - - if (buf.size() != terrain_border::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing terrain_border led to " - << buf.size() - << " columns (expected " - << terrain_border::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hhd", &this->enabled) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hhd", &this->random) != 1) { return 1; } - this->internal_name = buf[2]; - this->filename = buf[3]; - if (sscanf(buf[4].c_str(), "%d", &this->slp_id) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%d", &this->shape_ptr) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%d", &this->sound_id) != 1) { return 6; } - if (sscanf(buf[8].c_str(), "%hhd", &this->is_animated) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hd", &this->animation_frame_count) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%hd", &this->pause_frame_count) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%f", &this->interval) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%f", &this->pause_between_loops) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%hd", &this->frame) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%hd", &this->draw_frame) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%f", &this->animate_last) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%hhd", &this->frame_changed) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%hhd", &this->drawn) != 1) { return 17; } - this->frames.filename = buf[18]; - if (sscanf(buf[19].c_str(), "%hd", &this->draw_tile) != 1) { return 19; } - if (sscanf(buf[20].c_str(), "%hd", &this->underlay_terrain) != 1) { return 20; } - if (sscanf(buf[21].c_str(), "%hd", &this->border_style) != 1) { return 21; } - - return -1; -} - -bool terrain_border::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->frames.read(storage, basedir); - - return true; -} - -int terrain_pass_graphic::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', terrain_pass_graphic::member_count - ); - - if (buf.size() != terrain_pass_graphic::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing terrain_pass_graphic led to " - << buf.size() - << " columns (expected " - << terrain_pass_graphic::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%d", &this->slp_id_exit_tile) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%d", &this->slp_id_enter_tile) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%d", &this->slp_id_walk_tile) != 1) { return 2; } - if (sscanf(buf[3].c_str(), "%d", &this->replication_amount) != 1) { return 3; } - - return -1; -} - -bool terrain_pass_graphic::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -int terrain_restriction::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', terrain_restriction::member_count - ); - - if (buf.size() != terrain_restriction::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing terrain_restriction led to " - << buf.size() - << " columns (expected " - << terrain_restriction::member_count - << ")!" - ); - } - - this->pass_graphics.filename = buf[1]; - - return -1; -} - -bool terrain_restriction::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->pass_graphics.read(storage, basedir); - - return true; -} - -int terrain_type::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', terrain_type::member_count - ); - - if (buf.size() != terrain_type::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing terrain_type led to " - << buf.size() - << " columns (expected " - << terrain_type::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hhd", &this->enabled) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hhd", &this->random) != 1) { return 1; } - this->internal_name = buf[2]; - this->filename = buf[3]; - if (sscanf(buf[4].c_str(), "%d", &this->slp_id) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%d", &this->shape_ptr) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%d", &this->sound_id) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%d", &this->blend_priority) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%d", &this->blend_mode) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hhu", &this->map_color_hi) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%hhu", &this->map_color_med) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%hhu", &this->map_color_low) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%hhu", &this->map_color_cliff_lt) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%hhu", &this->map_color_cliff_rt) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%hhd", &this->passable_terrain) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%hhd", &this->impassable_terrain) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%hhd", &this->is_animated) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%hd", &this->animation_frame_count) != 1) { return 17; } - if (sscanf(buf[18].c_str(), "%hd", &this->pause_frame_count) != 1) { return 18; } - if (sscanf(buf[19].c_str(), "%f", &this->interval) != 1) { return 19; } - if (sscanf(buf[20].c_str(), "%f", &this->pause_between_loops) != 1) { return 20; } - if (sscanf(buf[21].c_str(), "%hd", &this->frame) != 1) { return 21; } - if (sscanf(buf[22].c_str(), "%hd", &this->draw_frame) != 1) { return 22; } - if (sscanf(buf[23].c_str(), "%f", &this->animate_last) != 1) { return 23; } - if (sscanf(buf[24].c_str(), "%hhd", &this->frame_changed) != 1) { return 24; } - if (sscanf(buf[25].c_str(), "%hhd", &this->drawn) != 1) { return 25; } - this->elevation_graphics.filename = buf[26]; - if (sscanf(buf[27].c_str(), "%hd", &this->terrain_replacement_id) != 1) { return 27; } - if (sscanf(buf[28].c_str(), "%hd", &this->terrain_to_draw0) != 1) { return 28; } - if (sscanf(buf[29].c_str(), "%hd", &this->terrain_to_draw1) != 1) { return 29; } - if (sscanf(buf[34].c_str(), "%hd", &this->terrain_units_used_count) != 1) { return 34; } - - return -1; -} - -bool terrain_type::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->elevation_graphics.read(storage, basedir); - - return true; -} - -int tile_size::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', tile_size::member_count - ); - - if (buf.size() != tile_size::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing tile_size led to " - << buf.size() - << " columns (expected " - << tile_size::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->width) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hd", &this->height) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hd", &this->delta_z) != 1) { return 2; } - - return -1; -} - -bool tile_size::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -} // gamedata -} // openage diff --git a/libopenage/gamedata/terrain_dummy.h b/libopenage/gamedata/terrain_dummy.h deleted file mode 100644 index 6985296ed0..0000000000 --- a/libopenage/gamedata/terrain_dummy.h +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - -#pragma once - -#include -#include -#include -#include "util/csv.h" - - - -namespace openage { -namespace gamedata { - -/** - * specification of terrain frames. - */ -struct frame_data { - int16_t frame_count; - int16_t angle_count; - int16_t shape_id; - static constexpr size_t member_count = 3; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * describes animation properties of a terrain type - */ -struct terrain_animation { - int8_t is_animated; - int16_t animation_frame_count; - int16_t pause_frame_count; - float interval; - float pause_between_loops; - int16_t frame; - int16_t draw_frame; - float animate_last; - int8_t frame_changed; - int8_t drawn; - static constexpr size_t member_count = 10; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -struct terrain_pass_graphic { - int32_t slp_id_exit_tile; - int32_t slp_id_enter_tile; - int32_t slp_id_walk_tile; - int32_t replication_amount; - static constexpr size_t member_count = 4; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * size definition of one terrain tile. - */ -struct tile_size { - int16_t width; - int16_t height; - int16_t delta_z; - static constexpr size_t member_count = 3; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * TODO - */ -struct terrain_restriction { - openage::util::csv_subdata pass_graphics; - static constexpr size_t member_count = 2; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * one inter-terraintile border specification. - */ -struct terrain_border : terrain_animation { - int8_t enabled; - int8_t random; - std::string internal_name; - std::string filename; - int32_t slp_id; - int32_t shape_ptr; - int32_t sound_id; - openage::util::csv_subdata frames; - int16_t draw_tile; - int16_t underlay_terrain; - int16_t border_style; - static constexpr size_t member_count = 22; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * describes a terrain type, like water, ice, etc. - */ -struct terrain_type : terrain_animation { - int8_t enabled; - int8_t random; - std::string internal_name; - std::string filename; - int32_t slp_id; - int32_t shape_ptr; - int32_t sound_id; - int32_t blend_priority; - int32_t blend_mode; - uint8_t map_color_hi; - uint8_t map_color_med; - uint8_t map_color_low; - uint8_t map_color_cliff_lt; - uint8_t map_color_cliff_rt; - int8_t passable_terrain; - int8_t impassable_terrain; - openage::util::csv_subdata elevation_graphics; - int16_t terrain_replacement_id; - int16_t terrain_to_draw0; - int16_t terrain_to_draw1; - int16_t terrain_units_used_count; - static constexpr size_t member_count = 35; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -} // gamedata -} // openage diff --git a/libopenage/gamedata/texture_dummy.cpp b/libopenage/gamedata/texture_dummy.cpp deleted file mode 100644 index 6d599c663b..0000000000 --- a/libopenage/gamedata/texture_dummy.cpp +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - - -#include -#include -#include "error/error.h" -#include "texture_dummy.h" -#include "util/strings.h" - - -namespace openage { -namespace gamedata { - -constexpr size_t subtexture::member_count; -int subtexture::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', subtexture::member_count - ); - - if (buf.size() != subtexture::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing subtexture led to " - << buf.size() - << " columns (expected " - << subtexture::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%d", &this->x) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%d", &this->y) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%d", &this->w) != 1) { return 2; } - if (sscanf(buf[3].c_str(), "%d", &this->h) != 1) { return 3; } - if (sscanf(buf[4].c_str(), "%d", &this->cx) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%d", &this->cy) != 1) { return 5; } - - return -1; -} - -bool subtexture::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -} // gamedata -} // openage diff --git a/libopenage/gamedata/texture_dummy.h b/libopenage/gamedata/texture_dummy.h deleted file mode 100644 index 9ac92d4e3e..0000000000 --- a/libopenage/gamedata/texture_dummy.h +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - -#pragma once - -#include -#include -#include -#include "util/csv.h" - - - -namespace openage { -namespace gamedata { - -/** - * one sprite, as part of a texture atlas. - * - * this struct stores information about positions and sizes - * of sprites included in the 'big texture'. - */ -struct subtexture { - int32_t x; - int32_t y; - int32_t w; - int32_t h; - int32_t cx; - int32_t cy; - static constexpr size_t member_count = 6; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -} // gamedata -} // openage diff --git a/libopenage/gamedata/unit_dummy.cpp b/libopenage/gamedata/unit_dummy.cpp deleted file mode 100644 index 190e6774a3..0000000000 --- a/libopenage/gamedata/unit_dummy.cpp +++ /dev/null @@ -1,2820 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - - -#include -#include -#include "error/error.h" -#include "unit_dummy.h" -#include "util/strings.h" - - -namespace openage { -namespace gamedata { - -constexpr size_t action_unit::member_count; -constexpr size_t animated_unit::member_count; -constexpr size_t building_annex::member_count; -constexpr size_t building_unit::member_count; -constexpr size_t damage_graphic::member_count; -constexpr size_t doppelganger_unit::member_count; -constexpr size_t hit_type::member_count; -constexpr size_t living_unit::member_count; -constexpr size_t missile_unit::member_count; -constexpr size_t moving_unit::member_count; -constexpr size_t projectile_unit::member_count; -constexpr size_t resource_cost::member_count; -constexpr size_t resource_storage::member_count; -constexpr size_t tree_unit::member_count; -constexpr size_t unit_command::member_count; -constexpr size_t unit_header::member_count; -constexpr size_t unit_object::member_count; -int action_unit::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', action_unit::member_count - ); - - if (buf.size() != action_unit::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing action_unit led to " - << buf.size() - << " columns (expected " - << action_unit::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->id0) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hu", &this->language_dll_name) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hu", &this->language_dll_creation) != 1) { return 2; } - // parse enum unit_classes - if (buf[3] == "DUMMY") { - this->unit_class = unit_classes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[3] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[4].c_str(), "%hd", &this->idle_graphic0) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%hd", &this->idle_graphic1) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%hd", &this->dying_graphic) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%hd", &this->undead_graphic) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%hhd", &this->death_mode) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hd", &this->hit_points) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%f", &this->line_of_sight) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%hhd", &this->garrison_capacity) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%f", &this->radius_x) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%f", &this->radius_y) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%f", &this->radius_z) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%hd", &this->train_sound_id) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%hd", &this->damage_sound_id) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%hd", &this->dead_unit_id) != 1) { return 17; } - if (sscanf(buf[18].c_str(), "%hhd", &this->placement_mode) != 1) { return 18; } - if (sscanf(buf[19].c_str(), "%hhd", &this->can_be_built_on) != 1) { return 19; } - if (sscanf(buf[20].c_str(), "%hd", &this->icon_id) != 1) { return 20; } - if (sscanf(buf[21].c_str(), "%hhd", &this->hidden_in_editor) != 1) { return 21; } - if (sscanf(buf[22].c_str(), "%hd", &this->old_portrait_icon_id) != 1) { return 22; } - if (sscanf(buf[23].c_str(), "%hhd", &this->enabled) != 1) { return 23; } - if (sscanf(buf[24].c_str(), "%hd", &this->placement_side_terrain0) != 1) { return 24; } - if (sscanf(buf[25].c_str(), "%hd", &this->placement_side_terrain1) != 1) { return 25; } - if (sscanf(buf[26].c_str(), "%hd", &this->placement_terrain0) != 1) { return 26; } - if (sscanf(buf[27].c_str(), "%hd", &this->placement_terrain1) != 1) { return 27; } - if (sscanf(buf[28].c_str(), "%f", &this->clearance_size_x) != 1) { return 28; } - if (sscanf(buf[29].c_str(), "%f", &this->clearance_size_y) != 1) { return 29; } - // parse enum elevation_modes - if (buf[30] == "DUMMY") { - this->elevation_mode = elevation_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[30] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum fog_visibility - if (buf[31] == "DUMMY") { - this->visible_in_fog = fog_visibility::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[31] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum ground_type - if (buf[32] == "DUMMY") { - this->terrain_restriction = ground_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[32] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[33].c_str(), "%hhd", &this->fly_mode) != 1) { return 33; } - if (sscanf(buf[34].c_str(), "%hd", &this->resource_capacity) != 1) { return 34; } - if (sscanf(buf[35].c_str(), "%f", &this->resource_decay) != 1) { return 35; } - // parse enum blast_types - if (buf[36] == "DUMMY") { - this->blast_defense_level = blast_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[36] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum combat_levels - if (buf[37] == "DUMMY") { - this->combat_level = combat_levels::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[37] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum interaction_modes - if (buf[38] == "DUMMY") { - this->interaction_mode = interaction_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[38] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum minimap_modes - if (buf[39] == "DUMMY") { - this->map_draw_level = minimap_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[39] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum command_attributes - if (buf[40] == "DUMMY") { - this->unit_level = command_attributes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[40] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[41].c_str(), "%f", &this->attack_reaction) != 1) { return 41; } - if (sscanf(buf[42].c_str(), "%hhd", &this->minimap_color) != 1) { return 42; } - if (sscanf(buf[43].c_str(), "%d", &this->language_dll_help) != 1) { return 43; } - if (sscanf(buf[44].c_str(), "%d", &this->language_dll_hotkey_text) != 1) { return 44; } - if (sscanf(buf[45].c_str(), "%d", &this->hot_keys) != 1) { return 45; } - if (sscanf(buf[46].c_str(), "%hhd", &this->recyclable) != 1) { return 46; } - if (sscanf(buf[47].c_str(), "%hhd", &this->enable_auto_gather) != 1) { return 47; } - if (sscanf(buf[48].c_str(), "%hhd", &this->doppelgaenger_on_death) != 1) { return 48; } - if (sscanf(buf[49].c_str(), "%hhd", &this->resource_gather_drop) != 1) { return 49; } - if (sscanf(buf[50].c_str(), "%hhu", &this->occlusion_mode) != 1) { return 50; } - // parse enum obstruction_types - if (buf[51] == "DUMMY") { - this->obstruction_type = obstruction_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[51] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[52].c_str(), "%hhd", &this->obstruction_class) != 1) { return 52; } - if (sscanf(buf[53].c_str(), "%hhu", &this->trait) != 1) { return 53; } - if (sscanf(buf[54].c_str(), "%hhd", &this->civilization_id) != 1) { return 54; } - if (sscanf(buf[55].c_str(), "%hd", &this->attribute_piece) != 1) { return 55; } - // parse enum selection_effects - if (buf[56] == "DUMMY") { - this->selection_effect = selection_effects::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[56] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[65].c_str(), "%hhd", &this->convert_terrain) != 1) { return 65; } - this->name = buf[66]; - if (sscanf(buf[67].c_str(), "%hd", &this->id1) != 1) { return 67; } - if (sscanf(buf[68].c_str(), "%hd", &this->id2) != 1) { return 68; } - if (sscanf(buf[69].c_str(), "%f", &this->speed) != 1) { return 69; } - if (sscanf(buf[70].c_str(), "%hd", &this->move_graphics) != 1) { return 70; } - if (sscanf(buf[71].c_str(), "%hd", &this->run_graphics) != 1) { return 71; } - if (sscanf(buf[72].c_str(), "%f", &this->turn_speed) != 1) { return 72; } - if (sscanf(buf[73].c_str(), "%hhd", &this->old_size_class) != 1) { return 73; } - if (sscanf(buf[74].c_str(), "%hd", &this->trail_unit_id) != 1) { return 74; } - if (sscanf(buf[75].c_str(), "%hhu", &this->trail_opsions) != 1) { return 75; } - if (sscanf(buf[76].c_str(), "%f", &this->trail_spacing) != 1) { return 76; } - if (sscanf(buf[77].c_str(), "%hhd", &this->old_move_algorithm) != 1) { return 77; } - if (sscanf(buf[78].c_str(), "%f", &this->turn_radius) != 1) { return 78; } - if (sscanf(buf[79].c_str(), "%f", &this->turn_radius_speed) != 1) { return 79; } - if (sscanf(buf[80].c_str(), "%f", &this->max_yaw_per_sec_moving) != 1) { return 80; } - if (sscanf(buf[81].c_str(), "%f", &this->stationary_yaw_revolution_time) != 1) { return 81; } - if (sscanf(buf[82].c_str(), "%f", &this->max_yaw_per_sec_stationary) != 1) { return 82; } - if (sscanf(buf[83].c_str(), "%hd", &this->default_task_id) != 1) { return 83; } - if (sscanf(buf[84].c_str(), "%f", &this->search_radius) != 1) { return 84; } - if (sscanf(buf[85].c_str(), "%f", &this->work_rate) != 1) { return 85; } - if (sscanf(buf[87].c_str(), "%hhd", &this->task_group) != 1) { return 87; } - if (sscanf(buf[88].c_str(), "%hd", &this->command_sound_id) != 1) { return 88; } - if (sscanf(buf[89].c_str(), "%hd", &this->stop_sound_id) != 1) { return 89; } - if (sscanf(buf[90].c_str(), "%hhd", &this->run_pattern) != 1) { return 90; } - - return -1; -} - -bool action_unit::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->resource_storage.read(storage, basedir); - this->damage_graphics.read(storage, basedir); - - return true; -} - -int animated_unit::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', animated_unit::member_count - ); - - if (buf.size() != animated_unit::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing animated_unit led to " - << buf.size() - << " columns (expected " - << animated_unit::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->id0) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hu", &this->language_dll_name) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hu", &this->language_dll_creation) != 1) { return 2; } - // parse enum unit_classes - if (buf[3] == "DUMMY") { - this->unit_class = unit_classes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[3] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[4].c_str(), "%hd", &this->idle_graphic0) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%hd", &this->idle_graphic1) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%hd", &this->dying_graphic) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%hd", &this->undead_graphic) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%hhd", &this->death_mode) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hd", &this->hit_points) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%f", &this->line_of_sight) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%hhd", &this->garrison_capacity) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%f", &this->radius_x) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%f", &this->radius_y) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%f", &this->radius_z) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%hd", &this->train_sound_id) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%hd", &this->damage_sound_id) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%hd", &this->dead_unit_id) != 1) { return 17; } - if (sscanf(buf[18].c_str(), "%hhd", &this->placement_mode) != 1) { return 18; } - if (sscanf(buf[19].c_str(), "%hhd", &this->can_be_built_on) != 1) { return 19; } - if (sscanf(buf[20].c_str(), "%hd", &this->icon_id) != 1) { return 20; } - if (sscanf(buf[21].c_str(), "%hhd", &this->hidden_in_editor) != 1) { return 21; } - if (sscanf(buf[22].c_str(), "%hd", &this->old_portrait_icon_id) != 1) { return 22; } - if (sscanf(buf[23].c_str(), "%hhd", &this->enabled) != 1) { return 23; } - if (sscanf(buf[24].c_str(), "%hd", &this->placement_side_terrain0) != 1) { return 24; } - if (sscanf(buf[25].c_str(), "%hd", &this->placement_side_terrain1) != 1) { return 25; } - if (sscanf(buf[26].c_str(), "%hd", &this->placement_terrain0) != 1) { return 26; } - if (sscanf(buf[27].c_str(), "%hd", &this->placement_terrain1) != 1) { return 27; } - if (sscanf(buf[28].c_str(), "%f", &this->clearance_size_x) != 1) { return 28; } - if (sscanf(buf[29].c_str(), "%f", &this->clearance_size_y) != 1) { return 29; } - // parse enum elevation_modes - if (buf[30] == "DUMMY") { - this->elevation_mode = elevation_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[30] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum fog_visibility - if (buf[31] == "DUMMY") { - this->visible_in_fog = fog_visibility::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[31] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum ground_type - if (buf[32] == "DUMMY") { - this->terrain_restriction = ground_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[32] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[33].c_str(), "%hhd", &this->fly_mode) != 1) { return 33; } - if (sscanf(buf[34].c_str(), "%hd", &this->resource_capacity) != 1) { return 34; } - if (sscanf(buf[35].c_str(), "%f", &this->resource_decay) != 1) { return 35; } - // parse enum blast_types - if (buf[36] == "DUMMY") { - this->blast_defense_level = blast_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[36] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum combat_levels - if (buf[37] == "DUMMY") { - this->combat_level = combat_levels::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[37] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum interaction_modes - if (buf[38] == "DUMMY") { - this->interaction_mode = interaction_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[38] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum minimap_modes - if (buf[39] == "DUMMY") { - this->map_draw_level = minimap_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[39] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum command_attributes - if (buf[40] == "DUMMY") { - this->unit_level = command_attributes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[40] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[41].c_str(), "%f", &this->attack_reaction) != 1) { return 41; } - if (sscanf(buf[42].c_str(), "%hhd", &this->minimap_color) != 1) { return 42; } - if (sscanf(buf[43].c_str(), "%d", &this->language_dll_help) != 1) { return 43; } - if (sscanf(buf[44].c_str(), "%d", &this->language_dll_hotkey_text) != 1) { return 44; } - if (sscanf(buf[45].c_str(), "%d", &this->hot_keys) != 1) { return 45; } - if (sscanf(buf[46].c_str(), "%hhd", &this->recyclable) != 1) { return 46; } - if (sscanf(buf[47].c_str(), "%hhd", &this->enable_auto_gather) != 1) { return 47; } - if (sscanf(buf[48].c_str(), "%hhd", &this->doppelgaenger_on_death) != 1) { return 48; } - if (sscanf(buf[49].c_str(), "%hhd", &this->resource_gather_drop) != 1) { return 49; } - if (sscanf(buf[50].c_str(), "%hhu", &this->occlusion_mode) != 1) { return 50; } - // parse enum obstruction_types - if (buf[51] == "DUMMY") { - this->obstruction_type = obstruction_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[51] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[52].c_str(), "%hhd", &this->obstruction_class) != 1) { return 52; } - if (sscanf(buf[53].c_str(), "%hhu", &this->trait) != 1) { return 53; } - if (sscanf(buf[54].c_str(), "%hhd", &this->civilization_id) != 1) { return 54; } - if (sscanf(buf[55].c_str(), "%hd", &this->attribute_piece) != 1) { return 55; } - // parse enum selection_effects - if (buf[56] == "DUMMY") { - this->selection_effect = selection_effects::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[56] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[57].c_str(), "%f", &this->selection_shape_x) != 1) { return 57; } - if (sscanf(buf[58].c_str(), "%f", &this->selection_shape_y) != 1) { return 58; } - if (sscanf(buf[59].c_str(), "%f", &this->selection_shape_z) != 1) { return 59; } - this->resource_storage.filename = buf[60]; - this->damage_graphics.filename = buf[61]; - if (sscanf(buf[62].c_str(), "%hd", &this->selection_sound_id) != 1) { return 62; } - if (sscanf(buf[63].c_str(), "%hd", &this->dying_sound_id) != 1) { return 63; } - // parse enum attack_modes - if (buf[64] == "DUMMY") { - this->old_attack_mode = attack_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[64] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[65].c_str(), "%hhd", &this->convert_terrain) != 1) { return 65; } - this->name = buf[66]; - if (sscanf(buf[67].c_str(), "%hd", &this->id1) != 1) { return 67; } - if (sscanf(buf[68].c_str(), "%hd", &this->id2) != 1) { return 68; } - if (sscanf(buf[69].c_str(), "%f", &this->speed) != 1) { return 69; } - - return -1; -} - -bool animated_unit::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->resource_storage.read(storage, basedir); - this->damage_graphics.read(storage, basedir); - - return true; -} - -int building_annex::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', building_annex::member_count - ); - - if (buf.size() != building_annex::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing building_annex led to " - << buf.size() - << " columns (expected " - << building_annex::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->unit_id) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%f", &this->misplaced0) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%f", &this->misplaced1) != 1) { return 2; } - - return -1; -} - -bool building_annex::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -int building_unit::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', building_unit::member_count - ); - - if (buf.size() != building_unit::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing building_unit led to " - << buf.size() - << " columns (expected " - << building_unit::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->id0) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hu", &this->language_dll_name) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hu", &this->language_dll_creation) != 1) { return 2; } - // parse enum unit_classes - if (buf[3] == "DUMMY") { - this->unit_class = unit_classes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[3] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[4].c_str(), "%hd", &this->idle_graphic0) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%hd", &this->idle_graphic1) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%hd", &this->dying_graphic) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%hd", &this->undead_graphic) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%hhd", &this->death_mode) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hd", &this->hit_points) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%f", &this->line_of_sight) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%hhd", &this->garrison_capacity) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%f", &this->radius_x) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%f", &this->radius_y) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%f", &this->radius_z) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%hd", &this->train_sound_id) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%hd", &this->damage_sound_id) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%hd", &this->dead_unit_id) != 1) { return 17; } - if (sscanf(buf[18].c_str(), "%hhd", &this->placement_mode) != 1) { return 18; } - if (sscanf(buf[19].c_str(), "%hhd", &this->can_be_built_on) != 1) { return 19; } - if (sscanf(buf[20].c_str(), "%hd", &this->icon_id) != 1) { return 20; } - if (sscanf(buf[21].c_str(), "%hhd", &this->hidden_in_editor) != 1) { return 21; } - if (sscanf(buf[22].c_str(), "%hd", &this->old_portrait_icon_id) != 1) { return 22; } - if (sscanf(buf[23].c_str(), "%hhd", &this->enabled) != 1) { return 23; } - if (sscanf(buf[24].c_str(), "%hd", &this->placement_side_terrain0) != 1) { return 24; } - if (sscanf(buf[25].c_str(), "%hd", &this->placement_side_terrain1) != 1) { return 25; } - if (sscanf(buf[26].c_str(), "%hd", &this->placement_terrain0) != 1) { return 26; } - if (sscanf(buf[27].c_str(), "%hd", &this->placement_terrain1) != 1) { return 27; } - if (sscanf(buf[28].c_str(), "%f", &this->clearance_size_x) != 1) { return 28; } - if (sscanf(buf[29].c_str(), "%f", &this->clearance_size_y) != 1) { return 29; } - // parse enum elevation_modes - if (buf[30] == "DUMMY") { - this->elevation_mode = elevation_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[30] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum fog_visibility - if (buf[31] == "DUMMY") { - this->visible_in_fog = fog_visibility::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[31] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum ground_type - if (buf[32] == "DUMMY") { - this->terrain_restriction = ground_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[32] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[33].c_str(), "%hhd", &this->fly_mode) != 1) { return 33; } - if (sscanf(buf[34].c_str(), "%hd", &this->resource_capacity) != 1) { return 34; } - if (sscanf(buf[35].c_str(), "%f", &this->resource_decay) != 1) { return 35; } - // parse enum blast_types - if (buf[36] == "DUMMY") { - this->blast_defense_level = blast_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[36] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum combat_levels - if (buf[37] == "DUMMY") { - this->combat_level = combat_levels::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[37] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum interaction_modes - if (buf[38] == "DUMMY") { - this->interaction_mode = interaction_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[38] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum minimap_modes - if (buf[39] == "DUMMY") { - this->map_draw_level = minimap_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[39] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum command_attributes - if (buf[40] == "DUMMY") { - this->unit_level = command_attributes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[40] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[41].c_str(), "%f", &this->attack_reaction) != 1) { return 41; } - if (sscanf(buf[42].c_str(), "%hhd", &this->minimap_color) != 1) { return 42; } - if (sscanf(buf[43].c_str(), "%d", &this->language_dll_help) != 1) { return 43; } - if (sscanf(buf[44].c_str(), "%d", &this->language_dll_hotkey_text) != 1) { return 44; } - if (sscanf(buf[45].c_str(), "%d", &this->hot_keys) != 1) { return 45; } - if (sscanf(buf[46].c_str(), "%hhd", &this->recyclable) != 1) { return 46; } - if (sscanf(buf[47].c_str(), "%hhd", &this->enable_auto_gather) != 1) { return 47; } - if (sscanf(buf[48].c_str(), "%hhd", &this->doppelgaenger_on_death) != 1) { return 48; } - if (sscanf(buf[49].c_str(), "%hhd", &this->resource_gather_drop) != 1) { return 49; } - if (sscanf(buf[50].c_str(), "%hhu", &this->occlusion_mode) != 1) { return 50; } - // parse enum obstruction_types - if (buf[51] == "DUMMY") { - this->obstruction_type = obstruction_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[51] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[52].c_str(), "%hhd", &this->obstruction_class) != 1) { return 52; } - if (sscanf(buf[53].c_str(), "%hhu", &this->trait) != 1) { return 53; } - if (sscanf(buf[54].c_str(), "%hhd", &this->civilization_id) != 1) { return 54; } - if (sscanf(buf[55].c_str(), "%hd", &this->attribute_piece) != 1) { return 55; } - // parse enum selection_effects - if (buf[56] == "DUMMY") { - this->selection_effect = selection_effects::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[56] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[57].c_str(), "%f", &this->selection_shape_x) != 1) { return 57; } - if (sscanf(buf[58].c_str(), "%f", &this->selection_shape_y) != 1) { return 58; } - if (sscanf(buf[59].c_str(), "%f", &this->selection_shape_z) != 1) { return 59; } - this->resource_storage.filename = buf[60]; - this->damage_graphics.filename = buf[61]; - if (sscanf(buf[62].c_str(), "%hd", &this->selection_sound_id) != 1) { return 62; } - if (sscanf(buf[63].c_str(), "%hd", &this->dying_sound_id) != 1) { return 63; } - // parse enum attack_modes - if (buf[64] == "DUMMY") { - this->old_attack_mode = attack_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[64] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[65].c_str(), "%hhd", &this->convert_terrain) != 1) { return 65; } - this->name = buf[66]; - if (sscanf(buf[67].c_str(), "%hd", &this->id1) != 1) { return 67; } - if (sscanf(buf[68].c_str(), "%hd", &this->id2) != 1) { return 68; } - if (sscanf(buf[69].c_str(), "%f", &this->speed) != 1) { return 69; } - if (sscanf(buf[70].c_str(), "%hd", &this->move_graphics) != 1) { return 70; } - if (sscanf(buf[71].c_str(), "%hd", &this->run_graphics) != 1) { return 71; } - if (sscanf(buf[72].c_str(), "%f", &this->turn_speed) != 1) { return 72; } - if (sscanf(buf[73].c_str(), "%hhd", &this->old_size_class) != 1) { return 73; } - if (sscanf(buf[74].c_str(), "%hd", &this->trail_unit_id) != 1) { return 74; } - if (sscanf(buf[75].c_str(), "%hhu", &this->trail_opsions) != 1) { return 75; } - if (sscanf(buf[76].c_str(), "%f", &this->trail_spacing) != 1) { return 76; } - if (sscanf(buf[77].c_str(), "%hhd", &this->old_move_algorithm) != 1) { return 77; } - if (sscanf(buf[78].c_str(), "%f", &this->turn_radius) != 1) { return 78; } - if (sscanf(buf[79].c_str(), "%f", &this->turn_radius_speed) != 1) { return 79; } - if (sscanf(buf[80].c_str(), "%f", &this->max_yaw_per_sec_moving) != 1) { return 80; } - if (sscanf(buf[81].c_str(), "%f", &this->stationary_yaw_revolution_time) != 1) { return 81; } - if (sscanf(buf[82].c_str(), "%f", &this->max_yaw_per_sec_stationary) != 1) { return 82; } - if (sscanf(buf[83].c_str(), "%hd", &this->default_task_id) != 1) { return 83; } - if (sscanf(buf[84].c_str(), "%f", &this->search_radius) != 1) { return 84; } - if (sscanf(buf[85].c_str(), "%f", &this->work_rate) != 1) { return 85; } - if (sscanf(buf[87].c_str(), "%hhd", &this->task_group) != 1) { return 87; } - if (sscanf(buf[88].c_str(), "%hd", &this->command_sound_id) != 1) { return 88; } - if (sscanf(buf[89].c_str(), "%hd", &this->stop_sound_id) != 1) { return 89; } - if (sscanf(buf[90].c_str(), "%hhd", &this->run_pattern) != 1) { return 90; } - if (sscanf(buf[91].c_str(), "%hd", &this->default_armor) != 1) { return 91; } - this->attacks.filename = buf[92]; - this->armors.filename = buf[93]; - // parse enum boundary_ids - if (buf[94] == "DUMMY") { - this->boundary_id = boundary_ids::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[94] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[95].c_str(), "%f", &this->weapon_range_max) != 1) { return 95; } - if (sscanf(buf[96].c_str(), "%f", &this->blast_range) != 1) { return 96; } - if (sscanf(buf[97].c_str(), "%f", &this->attack_speed) != 1) { return 97; } - if (sscanf(buf[98].c_str(), "%hd", &this->attack_projectile_primary_unit_id) != 1) { return 98; } - if (sscanf(buf[99].c_str(), "%hd", &this->accuracy) != 1) { return 99; } - if (sscanf(buf[100].c_str(), "%hhd", &this->break_off_combat) != 1) { return 100; } - if (sscanf(buf[101].c_str(), "%hd", &this->frame_delay) != 1) { return 101; } - // parse enum range_damage_type - if (buf[103] == "DUMMY") { - this->blast_level_offence = range_damage_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[103] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[104].c_str(), "%f", &this->weapon_range_min) != 1) { return 104; } - if (sscanf(buf[105].c_str(), "%f", &this->accuracy_dispersion) != 1) { return 105; } - if (sscanf(buf[106].c_str(), "%hd", &this->attack_sprite_id) != 1) { return 106; } - if (sscanf(buf[107].c_str(), "%hd", &this->melee_armor_displayed) != 1) { return 107; } - if (sscanf(buf[108].c_str(), "%hd", &this->attack_displayed) != 1) { return 108; } - if (sscanf(buf[109].c_str(), "%f", &this->range_displayed) != 1) { return 109; } - if (sscanf(buf[110].c_str(), "%f", &this->reload_time_displayed) != 1) { return 110; } - this->resource_cost.filename = buf[111]; - if (sscanf(buf[112].c_str(), "%hd", &this->creation_time) != 1) { return 112; } - if (sscanf(buf[113].c_str(), "%hd", &this->train_location_id) != 1) { return 113; } - if (sscanf(buf[114].c_str(), "%f", &this->rear_attack_modifier) != 1) { return 114; } - if (sscanf(buf[115].c_str(), "%f", &this->flank_attack_modifier) != 1) { return 115; } - // parse enum creatable_types - if (buf[116] == "DUMMY") { - this->creatable_type = creatable_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[116] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[117].c_str(), "%hhd", &this->hero_mode) != 1) { return 117; } - if (sscanf(buf[118].c_str(), "%d", &this->garrison_graphic) != 1) { return 118; } - if (sscanf(buf[119].c_str(), "%f", &this->attack_projectile_count) != 1) { return 119; } - if (sscanf(buf[120].c_str(), "%hhd", &this->attack_projectile_max_count) != 1) { return 120; } - if (sscanf(buf[121].c_str(), "%f", &this->attack_projectile_spawning_area_width) != 1) { return 121; } - if (sscanf(buf[122].c_str(), "%f", &this->attack_projectile_spawning_area_length) != 1) { return 122; } - if (sscanf(buf[123].c_str(), "%f", &this->attack_projectile_spawning_area_randomness) != 1) { return 123; } - if (sscanf(buf[124].c_str(), "%d", &this->attack_projectile_secondary_unit_id) != 1) { return 124; } - if (sscanf(buf[125].c_str(), "%d", &this->special_graphic_id) != 1) { return 125; } - if (sscanf(buf[126].c_str(), "%hhd", &this->special_activation) != 1) { return 126; } - if (sscanf(buf[127].c_str(), "%hd", &this->pierce_armor_displayed) != 1) { return 127; } - if (sscanf(buf[128].c_str(), "%hd", &this->construction_graphic_id) != 1) { return 128; } - if (sscanf(buf[129].c_str(), "%hd", &this->snow_graphic_id) != 1) { return 129; } - if (sscanf(buf[130].c_str(), "%hhd", &this->adjacent_mode) != 1) { return 130; } - if (sscanf(buf[131].c_str(), "%hd", &this->graphics_angle) != 1) { return 131; } - if (sscanf(buf[132].c_str(), "%hhd", &this->disappears_when_built) != 1) { return 132; } - if (sscanf(buf[133].c_str(), "%hd", &this->stack_unit_id) != 1) { return 133; } - if (sscanf(buf[134].c_str(), "%hd", &this->foundation_terrain_id) != 1) { return 134; } - if (sscanf(buf[135].c_str(), "%hd", &this->old_overlay_id) != 1) { return 135; } - if (sscanf(buf[136].c_str(), "%hd", &this->research_id) != 1) { return 136; } - if (sscanf(buf[137].c_str(), "%hhd", &this->can_burn) != 1) { return 137; } - this->building_annex.filename = buf[138]; - if (sscanf(buf[139].c_str(), "%hd", &this->head_unit_id) != 1) { return 139; } - if (sscanf(buf[140].c_str(), "%hd", &this->transform_unit_id) != 1) { return 140; } - if (sscanf(buf[141].c_str(), "%hd", &this->transform_sound_id) != 1) { return 141; } - if (sscanf(buf[142].c_str(), "%hd", &this->construction_sound_id) != 1) { return 142; } - // parse enum garrison_types - if (buf[143] == "DUMMY") { - this->garrison_type = garrison_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[143] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[144].c_str(), "%f", &this->garrison_heal_rate) != 1) { return 144; } - if (sscanf(buf[145].c_str(), "%f", &this->garrison_repair_rate) != 1) { return 145; } - if (sscanf(buf[146].c_str(), "%hd", &this->salvage_unit_id) != 1) { return 146; } - - return -1; -} - -bool building_unit::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->resource_storage.read(storage, basedir); - this->damage_graphics.read(storage, basedir); - this->attacks.read(storage, basedir); - this->armors.read(storage, basedir); - this->resource_cost.read(storage, basedir); - this->building_annex.read(storage, basedir); - - return true; -} - -int damage_graphic::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', damage_graphic::member_count - ); - - if (buf.size() != damage_graphic::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing damage_graphic led to " - << buf.size() - << " columns (expected " - << damage_graphic::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->graphic_id) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hhd", &this->damage_percent) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hhd", &this->old_apply_mode) != 1) { return 2; } - // parse enum damage_draw_type - if (buf[3] == "DUMMY") { - this->apply_mode = damage_draw_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[3] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - - return -1; -} - -bool damage_graphic::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -int doppelganger_unit::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', doppelganger_unit::member_count - ); - - if (buf.size() != doppelganger_unit::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing doppelganger_unit led to " - << buf.size() - << " columns (expected " - << doppelganger_unit::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->id0) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hu", &this->language_dll_name) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hu", &this->language_dll_creation) != 1) { return 2; } - // parse enum unit_classes - if (buf[3] == "DUMMY") { - this->unit_class = unit_classes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[3] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[4].c_str(), "%hd", &this->idle_graphic0) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%hd", &this->idle_graphic1) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%hd", &this->dying_graphic) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%hd", &this->undead_graphic) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%hhd", &this->death_mode) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hd", &this->hit_points) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%f", &this->line_of_sight) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%hhd", &this->garrison_capacity) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%f", &this->radius_x) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%f", &this->radius_y) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%f", &this->radius_z) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%hd", &this->train_sound_id) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%hd", &this->damage_sound_id) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%hd", &this->dead_unit_id) != 1) { return 17; } - if (sscanf(buf[18].c_str(), "%hhd", &this->placement_mode) != 1) { return 18; } - if (sscanf(buf[19].c_str(), "%hhd", &this->can_be_built_on) != 1) { return 19; } - if (sscanf(buf[20].c_str(), "%hd", &this->icon_id) != 1) { return 20; } - if (sscanf(buf[21].c_str(), "%hhd", &this->hidden_in_editor) != 1) { return 21; } - if (sscanf(buf[22].c_str(), "%hd", &this->old_portrait_icon_id) != 1) { return 22; } - if (sscanf(buf[23].c_str(), "%hhd", &this->enabled) != 1) { return 23; } - if (sscanf(buf[24].c_str(), "%hd", &this->placement_side_terrain0) != 1) { return 24; } - if (sscanf(buf[25].c_str(), "%hd", &this->placement_side_terrain1) != 1) { return 25; } - if (sscanf(buf[26].c_str(), "%hd", &this->placement_terrain0) != 1) { return 26; } - if (sscanf(buf[27].c_str(), "%hd", &this->placement_terrain1) != 1) { return 27; } - if (sscanf(buf[28].c_str(), "%f", &this->clearance_size_x) != 1) { return 28; } - if (sscanf(buf[29].c_str(), "%f", &this->clearance_size_y) != 1) { return 29; } - // parse enum elevation_modes - if (buf[30] == "DUMMY") { - this->elevation_mode = elevation_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[30] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum fog_visibility - if (buf[31] == "DUMMY") { - this->visible_in_fog = fog_visibility::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[31] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum ground_type - if (buf[32] == "DUMMY") { - this->terrain_restriction = ground_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[32] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[33].c_str(), "%hhd", &this->fly_mode) != 1) { return 33; } - if (sscanf(buf[34].c_str(), "%hd", &this->resource_capacity) != 1) { return 34; } - if (sscanf(buf[35].c_str(), "%f", &this->resource_decay) != 1) { return 35; } - // parse enum blast_types - if (buf[36] == "DUMMY") { - this->blast_defense_level = blast_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[36] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum combat_levels - if (buf[37] == "DUMMY") { - this->combat_level = combat_levels::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[37] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum interaction_modes - if (buf[38] == "DUMMY") { - this->interaction_mode = interaction_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[38] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum minimap_modes - if (buf[39] == "DUMMY") { - this->map_draw_level = minimap_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[39] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum command_attributes - if (buf[40] == "DUMMY") { - this->unit_level = command_attributes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[40] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[41].c_str(), "%f", &this->attack_reaction) != 1) { return 41; } - if (sscanf(buf[42].c_str(), "%hhd", &this->minimap_color) != 1) { return 42; } - if (sscanf(buf[43].c_str(), "%d", &this->language_dll_help) != 1) { return 43; } - if (sscanf(buf[44].c_str(), "%d", &this->language_dll_hotkey_text) != 1) { return 44; } - if (sscanf(buf[45].c_str(), "%d", &this->hot_keys) != 1) { return 45; } - if (sscanf(buf[46].c_str(), "%hhd", &this->recyclable) != 1) { return 46; } - if (sscanf(buf[47].c_str(), "%hhd", &this->enable_auto_gather) != 1) { return 47; } - if (sscanf(buf[48].c_str(), "%hhd", &this->doppelgaenger_on_death) != 1) { return 48; } - if (sscanf(buf[49].c_str(), "%hhd", &this->resource_gather_drop) != 1) { return 49; } - if (sscanf(buf[50].c_str(), "%hhu", &this->occlusion_mode) != 1) { return 50; } - // parse enum obstruction_types - if (buf[51] == "DUMMY") { - this->obstruction_type = obstruction_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[51] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[52].c_str(), "%hhd", &this->obstruction_class) != 1) { return 52; } - if (sscanf(buf[53].c_str(), "%hhu", &this->trait) != 1) { return 53; } - if (sscanf(buf[54].c_str(), "%hhd", &this->civilization_id) != 1) { return 54; } - if (sscanf(buf[55].c_str(), "%hd", &this->attribute_piece) != 1) { return 55; } - // parse enum selection_effects - if (buf[56] == "DUMMY") { - this->selection_effect = selection_effects::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[56] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[57].c_str(), "%f", &this->selection_shape_x) != 1) { return 57; } - if (sscanf(buf[58].c_str(), "%f", &this->selection_shape_y) != 1) { return 58; } - if (sscanf(buf[59].c_str(), "%f", &this->selection_shape_z) != 1) { return 59; } - this->resource_storage.filename = buf[60]; - this->damage_graphics.filename = buf[61]; - if (sscanf(buf[62].c_str(), "%hd", &this->selection_sound_id) != 1) { return 62; } - if (sscanf(buf[63].c_str(), "%hd", &this->dying_sound_id) != 1) { return 63; } - // parse enum attack_modes - if (buf[64] == "DUMMY") { - this->old_attack_mode = attack_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[64] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[65].c_str(), "%hhd", &this->convert_terrain) != 1) { return 65; } - this->name = buf[66]; - if (sscanf(buf[67].c_str(), "%hd", &this->id1) != 1) { return 67; } - if (sscanf(buf[68].c_str(), "%hd", &this->id2) != 1) { return 68; } - if (sscanf(buf[69].c_str(), "%f", &this->speed) != 1) { return 69; } - - return -1; -} - -bool doppelganger_unit::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->resource_storage.read(storage, basedir); - this->damage_graphics.read(storage, basedir); - - return true; -} - -int hit_type::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', hit_type::member_count - ); - - if (buf.size() != hit_type::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing hit_type led to " - << buf.size() - << " columns (expected " - << hit_type::member_count - << ")!" - ); - } - - // parse enum hit_class - if (buf[0] == "DUMMY") { - this->type_id = hit_class::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[0] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[1].c_str(), "%hd", &this->amount) != 1) { return 1; } - - return -1; -} - -bool hit_type::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -int living_unit::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', living_unit::member_count - ); - - if (buf.size() != living_unit::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing living_unit led to " - << buf.size() - << " columns (expected " - << living_unit::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->id0) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hu", &this->language_dll_name) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hu", &this->language_dll_creation) != 1) { return 2; } - // parse enum unit_classes - if (buf[3] == "DUMMY") { - this->unit_class = unit_classes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[3] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[4].c_str(), "%hd", &this->idle_graphic0) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%hd", &this->idle_graphic1) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%hd", &this->dying_graphic) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%hd", &this->undead_graphic) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%hhd", &this->death_mode) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hd", &this->hit_points) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%f", &this->line_of_sight) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%hhd", &this->garrison_capacity) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%f", &this->radius_x) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%f", &this->radius_y) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%f", &this->radius_z) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%hd", &this->train_sound_id) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%hd", &this->damage_sound_id) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%hd", &this->dead_unit_id) != 1) { return 17; } - if (sscanf(buf[18].c_str(), "%hhd", &this->placement_mode) != 1) { return 18; } - if (sscanf(buf[19].c_str(), "%hhd", &this->can_be_built_on) != 1) { return 19; } - if (sscanf(buf[20].c_str(), "%hd", &this->icon_id) != 1) { return 20; } - if (sscanf(buf[21].c_str(), "%hhd", &this->hidden_in_editor) != 1) { return 21; } - if (sscanf(buf[22].c_str(), "%hd", &this->old_portrait_icon_id) != 1) { return 22; } - if (sscanf(buf[23].c_str(), "%hhd", &this->enabled) != 1) { return 23; } - if (sscanf(buf[24].c_str(), "%hd", &this->placement_side_terrain0) != 1) { return 24; } - if (sscanf(buf[25].c_str(), "%hd", &this->placement_side_terrain1) != 1) { return 25; } - if (sscanf(buf[26].c_str(), "%hd", &this->placement_terrain0) != 1) { return 26; } - if (sscanf(buf[27].c_str(), "%hd", &this->placement_terrain1) != 1) { return 27; } - if (sscanf(buf[28].c_str(), "%f", &this->clearance_size_x) != 1) { return 28; } - if (sscanf(buf[29].c_str(), "%f", &this->clearance_size_y) != 1) { return 29; } - // parse enum elevation_modes - if (buf[30] == "DUMMY") { - this->elevation_mode = elevation_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[30] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum fog_visibility - if (buf[31] == "DUMMY") { - this->visible_in_fog = fog_visibility::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[31] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum ground_type - if (buf[32] == "DUMMY") { - this->terrain_restriction = ground_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[32] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[33].c_str(), "%hhd", &this->fly_mode) != 1) { return 33; } - if (sscanf(buf[34].c_str(), "%hd", &this->resource_capacity) != 1) { return 34; } - if (sscanf(buf[35].c_str(), "%f", &this->resource_decay) != 1) { return 35; } - // parse enum blast_types - if (buf[36] == "DUMMY") { - this->blast_defense_level = blast_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[36] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum combat_levels - if (buf[37] == "DUMMY") { - this->combat_level = combat_levels::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[37] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum interaction_modes - if (buf[38] == "DUMMY") { - this->interaction_mode = interaction_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[38] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum minimap_modes - if (buf[39] == "DUMMY") { - this->map_draw_level = minimap_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[39] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum command_attributes - if (buf[40] == "DUMMY") { - this->unit_level = command_attributes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[40] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[41].c_str(), "%f", &this->attack_reaction) != 1) { return 41; } - if (sscanf(buf[42].c_str(), "%hhd", &this->minimap_color) != 1) { return 42; } - if (sscanf(buf[43].c_str(), "%d", &this->language_dll_help) != 1) { return 43; } - if (sscanf(buf[44].c_str(), "%d", &this->language_dll_hotkey_text) != 1) { return 44; } - if (sscanf(buf[45].c_str(), "%d", &this->hot_keys) != 1) { return 45; } - if (sscanf(buf[46].c_str(), "%hhd", &this->recyclable) != 1) { return 46; } - if (sscanf(buf[47].c_str(), "%hhd", &this->enable_auto_gather) != 1) { return 47; } - if (sscanf(buf[48].c_str(), "%hhd", &this->doppelgaenger_on_death) != 1) { return 48; } - if (sscanf(buf[49].c_str(), "%hhd", &this->resource_gather_drop) != 1) { return 49; } - if (sscanf(buf[50].c_str(), "%hhu", &this->occlusion_mode) != 1) { return 50; } - // parse enum obstruction_types - if (buf[51] == "DUMMY") { - this->obstruction_type = obstruction_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[51] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[52].c_str(), "%hhd", &this->obstruction_class) != 1) { return 52; } - if (sscanf(buf[53].c_str(), "%hhu", &this->trait) != 1) { return 53; } - if (sscanf(buf[54].c_str(), "%hhd", &this->civilization_id) != 1) { return 54; } - if (sscanf(buf[55].c_str(), "%hd", &this->attribute_piece) != 1) { return 55; } - // parse enum selection_effects - if (buf[56] == "DUMMY") { - this->selection_effect = selection_effects::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[56] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[57].c_str(), "%f", &this->selection_shape_x) != 1) { return 57; } - if (sscanf(buf[58].c_str(), "%f", &this->selection_shape_y) != 1) { return 58; } - if (sscanf(buf[59].c_str(), "%f", &this->selection_shape_z) != 1) { return 59; } - this->resource_storage.filename = buf[60]; - this->damage_graphics.filename = buf[61]; - if (sscanf(buf[62].c_str(), "%hd", &this->selection_sound_id) != 1) { return 62; } - if (sscanf(buf[63].c_str(), "%hd", &this->dying_sound_id) != 1) { return 63; } - // parse enum attack_modes - if (buf[64] == "DUMMY") { - this->old_attack_mode = attack_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[64] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[65].c_str(), "%hhd", &this->convert_terrain) != 1) { return 65; } - this->name = buf[66]; - if (sscanf(buf[67].c_str(), "%hd", &this->id1) != 1) { return 67; } - if (sscanf(buf[68].c_str(), "%hd", &this->id2) != 1) { return 68; } - if (sscanf(buf[69].c_str(), "%f", &this->speed) != 1) { return 69; } - if (sscanf(buf[70].c_str(), "%hd", &this->move_graphics) != 1) { return 70; } - if (sscanf(buf[71].c_str(), "%hd", &this->run_graphics) != 1) { return 71; } - if (sscanf(buf[72].c_str(), "%f", &this->turn_speed) != 1) { return 72; } - if (sscanf(buf[73].c_str(), "%hhd", &this->old_size_class) != 1) { return 73; } - if (sscanf(buf[74].c_str(), "%hd", &this->trail_unit_id) != 1) { return 74; } - if (sscanf(buf[75].c_str(), "%hhu", &this->trail_opsions) != 1) { return 75; } - if (sscanf(buf[76].c_str(), "%f", &this->trail_spacing) != 1) { return 76; } - if (sscanf(buf[77].c_str(), "%hhd", &this->old_move_algorithm) != 1) { return 77; } - if (sscanf(buf[78].c_str(), "%f", &this->turn_radius) != 1) { return 78; } - if (sscanf(buf[79].c_str(), "%f", &this->turn_radius_speed) != 1) { return 79; } - if (sscanf(buf[80].c_str(), "%f", &this->max_yaw_per_sec_moving) != 1) { return 80; } - if (sscanf(buf[81].c_str(), "%f", &this->stationary_yaw_revolution_time) != 1) { return 81; } - if (sscanf(buf[82].c_str(), "%f", &this->max_yaw_per_sec_stationary) != 1) { return 82; } - if (sscanf(buf[83].c_str(), "%hd", &this->default_task_id) != 1) { return 83; } - if (sscanf(buf[84].c_str(), "%f", &this->search_radius) != 1) { return 84; } - if (sscanf(buf[85].c_str(), "%f", &this->work_rate) != 1) { return 85; } - if (sscanf(buf[87].c_str(), "%hhd", &this->task_group) != 1) { return 87; } - if (sscanf(buf[88].c_str(), "%hd", &this->command_sound_id) != 1) { return 88; } - if (sscanf(buf[89].c_str(), "%hd", &this->stop_sound_id) != 1) { return 89; } - if (sscanf(buf[90].c_str(), "%hhd", &this->run_pattern) != 1) { return 90; } - if (sscanf(buf[91].c_str(), "%hd", &this->default_armor) != 1) { return 91; } - this->attacks.filename = buf[92]; - this->armors.filename = buf[93]; - // parse enum boundary_ids - if (buf[94] == "DUMMY") { - this->boundary_id = boundary_ids::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[94] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[95].c_str(), "%f", &this->weapon_range_max) != 1) { return 95; } - if (sscanf(buf[96].c_str(), "%f", &this->blast_range) != 1) { return 96; } - if (sscanf(buf[97].c_str(), "%f", &this->attack_speed) != 1) { return 97; } - if (sscanf(buf[98].c_str(), "%hd", &this->attack_projectile_primary_unit_id) != 1) { return 98; } - if (sscanf(buf[99].c_str(), "%hd", &this->accuracy) != 1) { return 99; } - if (sscanf(buf[100].c_str(), "%hhd", &this->break_off_combat) != 1) { return 100; } - if (sscanf(buf[101].c_str(), "%hd", &this->frame_delay) != 1) { return 101; } - // parse enum range_damage_type - if (buf[103] == "DUMMY") { - this->blast_level_offence = range_damage_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[103] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[104].c_str(), "%f", &this->weapon_range_min) != 1) { return 104; } - if (sscanf(buf[105].c_str(), "%f", &this->accuracy_dispersion) != 1) { return 105; } - if (sscanf(buf[106].c_str(), "%hd", &this->attack_sprite_id) != 1) { return 106; } - if (sscanf(buf[107].c_str(), "%hd", &this->melee_armor_displayed) != 1) { return 107; } - if (sscanf(buf[108].c_str(), "%hd", &this->attack_displayed) != 1) { return 108; } - if (sscanf(buf[109].c_str(), "%f", &this->range_displayed) != 1) { return 109; } - if (sscanf(buf[110].c_str(), "%f", &this->reload_time_displayed) != 1) { return 110; } - this->resource_cost.filename = buf[111]; - if (sscanf(buf[112].c_str(), "%hd", &this->creation_time) != 1) { return 112; } - if (sscanf(buf[113].c_str(), "%hd", &this->train_location_id) != 1) { return 113; } - if (sscanf(buf[114].c_str(), "%f", &this->rear_attack_modifier) != 1) { return 114; } - if (sscanf(buf[115].c_str(), "%f", &this->flank_attack_modifier) != 1) { return 115; } - // parse enum creatable_types - if (buf[116] == "DUMMY") { - this->creatable_type = creatable_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[116] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[117].c_str(), "%hhd", &this->hero_mode) != 1) { return 117; } - if (sscanf(buf[118].c_str(), "%d", &this->garrison_graphic) != 1) { return 118; } - if (sscanf(buf[119].c_str(), "%f", &this->attack_projectile_count) != 1) { return 119; } - if (sscanf(buf[120].c_str(), "%hhd", &this->attack_projectile_max_count) != 1) { return 120; } - if (sscanf(buf[121].c_str(), "%f", &this->attack_projectile_spawning_area_width) != 1) { return 121; } - if (sscanf(buf[122].c_str(), "%f", &this->attack_projectile_spawning_area_length) != 1) { return 122; } - if (sscanf(buf[123].c_str(), "%f", &this->attack_projectile_spawning_area_randomness) != 1) { return 123; } - if (sscanf(buf[124].c_str(), "%d", &this->attack_projectile_secondary_unit_id) != 1) { return 124; } - if (sscanf(buf[125].c_str(), "%d", &this->special_graphic_id) != 1) { return 125; } - if (sscanf(buf[126].c_str(), "%hhd", &this->special_activation) != 1) { return 126; } - if (sscanf(buf[127].c_str(), "%hd", &this->pierce_armor_displayed) != 1) { return 127; } - - return -1; -} - -bool living_unit::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->resource_storage.read(storage, basedir); - this->damage_graphics.read(storage, basedir); - this->attacks.read(storage, basedir); - this->armors.read(storage, basedir); - this->resource_cost.read(storage, basedir); - - return true; -} - -int missile_unit::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', missile_unit::member_count - ); - - if (buf.size() != missile_unit::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing missile_unit led to " - << buf.size() - << " columns (expected " - << missile_unit::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->id0) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hu", &this->language_dll_name) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hu", &this->language_dll_creation) != 1) { return 2; } - // parse enum unit_classes - if (buf[3] == "DUMMY") { - this->unit_class = unit_classes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[3] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[4].c_str(), "%hd", &this->idle_graphic0) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%hd", &this->idle_graphic1) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%hd", &this->dying_graphic) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%hd", &this->undead_graphic) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%hhd", &this->death_mode) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hd", &this->hit_points) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%f", &this->line_of_sight) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%hhd", &this->garrison_capacity) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%f", &this->radius_x) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%f", &this->radius_y) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%f", &this->radius_z) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%hd", &this->train_sound_id) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%hd", &this->damage_sound_id) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%hd", &this->dead_unit_id) != 1) { return 17; } - if (sscanf(buf[18].c_str(), "%hhd", &this->placement_mode) != 1) { return 18; } - if (sscanf(buf[19].c_str(), "%hhd", &this->can_be_built_on) != 1) { return 19; } - if (sscanf(buf[20].c_str(), "%hd", &this->icon_id) != 1) { return 20; } - if (sscanf(buf[21].c_str(), "%hhd", &this->hidden_in_editor) != 1) { return 21; } - if (sscanf(buf[22].c_str(), "%hd", &this->old_portrait_icon_id) != 1) { return 22; } - if (sscanf(buf[23].c_str(), "%hhd", &this->enabled) != 1) { return 23; } - if (sscanf(buf[24].c_str(), "%hd", &this->placement_side_terrain0) != 1) { return 24; } - if (sscanf(buf[25].c_str(), "%hd", &this->placement_side_terrain1) != 1) { return 25; } - if (sscanf(buf[26].c_str(), "%hd", &this->placement_terrain0) != 1) { return 26; } - if (sscanf(buf[27].c_str(), "%hd", &this->placement_terrain1) != 1) { return 27; } - if (sscanf(buf[28].c_str(), "%f", &this->clearance_size_x) != 1) { return 28; } - if (sscanf(buf[29].c_str(), "%f", &this->clearance_size_y) != 1) { return 29; } - // parse enum elevation_modes - if (buf[30] == "DUMMY") { - this->elevation_mode = elevation_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[30] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum fog_visibility - if (buf[31] == "DUMMY") { - this->visible_in_fog = fog_visibility::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[31] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum ground_type - if (buf[32] == "DUMMY") { - this->terrain_restriction = ground_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[32] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[33].c_str(), "%hhd", &this->fly_mode) != 1) { return 33; } - if (sscanf(buf[34].c_str(), "%hd", &this->resource_capacity) != 1) { return 34; } - if (sscanf(buf[35].c_str(), "%f", &this->resource_decay) != 1) { return 35; } - // parse enum blast_types - if (buf[36] == "DUMMY") { - this->blast_defense_level = blast_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[36] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum combat_levels - if (buf[37] == "DUMMY") { - this->combat_level = combat_levels::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[37] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum interaction_modes - if (buf[38] == "DUMMY") { - this->interaction_mode = interaction_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[38] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum minimap_modes - if (buf[39] == "DUMMY") { - this->map_draw_level = minimap_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[39] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum command_attributes - if (buf[40] == "DUMMY") { - this->unit_level = command_attributes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[40] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[41].c_str(), "%f", &this->attack_reaction) != 1) { return 41; } - if (sscanf(buf[42].c_str(), "%hhd", &this->minimap_color) != 1) { return 42; } - if (sscanf(buf[43].c_str(), "%d", &this->language_dll_help) != 1) { return 43; } - if (sscanf(buf[44].c_str(), "%d", &this->language_dll_hotkey_text) != 1) { return 44; } - if (sscanf(buf[45].c_str(), "%d", &this->hot_keys) != 1) { return 45; } - if (sscanf(buf[46].c_str(), "%hhd", &this->recyclable) != 1) { return 46; } - if (sscanf(buf[47].c_str(), "%hhd", &this->enable_auto_gather) != 1) { return 47; } - if (sscanf(buf[48].c_str(), "%hhd", &this->doppelgaenger_on_death) != 1) { return 48; } - if (sscanf(buf[49].c_str(), "%hhd", &this->resource_gather_drop) != 1) { return 49; } - if (sscanf(buf[50].c_str(), "%hhu", &this->occlusion_mode) != 1) { return 50; } - // parse enum obstruction_types - if (buf[51] == "DUMMY") { - this->obstruction_type = obstruction_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[51] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[52].c_str(), "%hhd", &this->obstruction_class) != 1) { return 52; } - if (sscanf(buf[53].c_str(), "%hhu", &this->trait) != 1) { return 53; } - if (sscanf(buf[54].c_str(), "%hhd", &this->civilization_id) != 1) { return 54; } - if (sscanf(buf[55].c_str(), "%hd", &this->attribute_piece) != 1) { return 55; } - // parse enum selection_effects - if (buf[56] == "DUMMY") { - this->selection_effect = selection_effects::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[56] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[57].c_str(), "%f", &this->selection_shape_x) != 1) { return 57; } - if (sscanf(buf[58].c_str(), "%f", &this->selection_shape_y) != 1) { return 58; } - if (sscanf(buf[59].c_str(), "%f", &this->selection_shape_z) != 1) { return 59; } - this->resource_storage.filename = buf[60]; - this->damage_graphics.filename = buf[61]; - if (sscanf(buf[62].c_str(), "%hd", &this->selection_sound_id) != 1) { return 62; } - if (sscanf(buf[63].c_str(), "%hd", &this->dying_sound_id) != 1) { return 63; } - // parse enum attack_modes - if (buf[64] == "DUMMY") { - this->old_attack_mode = attack_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[64] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[65].c_str(), "%hhd", &this->convert_terrain) != 1) { return 65; } - this->name = buf[66]; - if (sscanf(buf[67].c_str(), "%hd", &this->id1) != 1) { return 67; } - if (sscanf(buf[68].c_str(), "%hd", &this->id2) != 1) { return 68; } - if (sscanf(buf[69].c_str(), "%f", &this->speed) != 1) { return 69; } - if (sscanf(buf[70].c_str(), "%hd", &this->move_graphics) != 1) { return 70; } - if (sscanf(buf[71].c_str(), "%hd", &this->run_graphics) != 1) { return 71; } - if (sscanf(buf[72].c_str(), "%f", &this->turn_speed) != 1) { return 72; } - if (sscanf(buf[73].c_str(), "%hhd", &this->old_size_class) != 1) { return 73; } - if (sscanf(buf[74].c_str(), "%hd", &this->trail_unit_id) != 1) { return 74; } - if (sscanf(buf[75].c_str(), "%hhu", &this->trail_opsions) != 1) { return 75; } - if (sscanf(buf[76].c_str(), "%f", &this->trail_spacing) != 1) { return 76; } - if (sscanf(buf[77].c_str(), "%hhd", &this->old_move_algorithm) != 1) { return 77; } - if (sscanf(buf[78].c_str(), "%f", &this->turn_radius) != 1) { return 78; } - if (sscanf(buf[79].c_str(), "%f", &this->turn_radius_speed) != 1) { return 79; } - if (sscanf(buf[80].c_str(), "%f", &this->max_yaw_per_sec_moving) != 1) { return 80; } - if (sscanf(buf[81].c_str(), "%f", &this->stationary_yaw_revolution_time) != 1) { return 81; } - if (sscanf(buf[82].c_str(), "%f", &this->max_yaw_per_sec_stationary) != 1) { return 82; } - if (sscanf(buf[83].c_str(), "%hd", &this->default_task_id) != 1) { return 83; } - if (sscanf(buf[84].c_str(), "%f", &this->search_radius) != 1) { return 84; } - if (sscanf(buf[85].c_str(), "%f", &this->work_rate) != 1) { return 85; } - if (sscanf(buf[87].c_str(), "%hhd", &this->task_group) != 1) { return 87; } - if (sscanf(buf[88].c_str(), "%hd", &this->command_sound_id) != 1) { return 88; } - if (sscanf(buf[89].c_str(), "%hd", &this->stop_sound_id) != 1) { return 89; } - if (sscanf(buf[90].c_str(), "%hhd", &this->run_pattern) != 1) { return 90; } - if (sscanf(buf[91].c_str(), "%hd", &this->default_armor) != 1) { return 91; } - this->attacks.filename = buf[92]; - this->armors.filename = buf[93]; - // parse enum boundary_ids - if (buf[94] == "DUMMY") { - this->boundary_id = boundary_ids::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[94] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[95].c_str(), "%f", &this->weapon_range_max) != 1) { return 95; } - if (sscanf(buf[96].c_str(), "%f", &this->blast_range) != 1) { return 96; } - if (sscanf(buf[97].c_str(), "%f", &this->attack_speed) != 1) { return 97; } - if (sscanf(buf[98].c_str(), "%hd", &this->attack_projectile_primary_unit_id) != 1) { return 98; } - if (sscanf(buf[99].c_str(), "%hd", &this->accuracy) != 1) { return 99; } - if (sscanf(buf[100].c_str(), "%hhd", &this->break_off_combat) != 1) { return 100; } - if (sscanf(buf[101].c_str(), "%hd", &this->frame_delay) != 1) { return 101; } - // parse enum range_damage_type - if (buf[103] == "DUMMY") { - this->blast_level_offence = range_damage_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[103] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[104].c_str(), "%f", &this->weapon_range_min) != 1) { return 104; } - if (sscanf(buf[105].c_str(), "%f", &this->accuracy_dispersion) != 1) { return 105; } - if (sscanf(buf[106].c_str(), "%hd", &this->attack_sprite_id) != 1) { return 106; } - if (sscanf(buf[107].c_str(), "%hd", &this->melee_armor_displayed) != 1) { return 107; } - if (sscanf(buf[108].c_str(), "%hd", &this->attack_displayed) != 1) { return 108; } - if (sscanf(buf[109].c_str(), "%f", &this->range_displayed) != 1) { return 109; } - if (sscanf(buf[110].c_str(), "%f", &this->reload_time_displayed) != 1) { return 110; } - if (sscanf(buf[111].c_str(), "%hhd", &this->projectile_type) != 1) { return 111; } - if (sscanf(buf[112].c_str(), "%hhd", &this->smart_mode) != 1) { return 112; } - if (sscanf(buf[113].c_str(), "%hhd", &this->drop_animation_mode) != 1) { return 113; } - if (sscanf(buf[114].c_str(), "%hhd", &this->penetration_mode) != 1) { return 114; } - if (sscanf(buf[115].c_str(), "%hhd", &this->area_of_effect_special) != 1) { return 115; } - if (sscanf(buf[116].c_str(), "%f", &this->projectile_arc) != 1) { return 116; } - - return -1; -} - -bool missile_unit::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->resource_storage.read(storage, basedir); - this->damage_graphics.read(storage, basedir); - this->attacks.read(storage, basedir); - this->armors.read(storage, basedir); - - return true; -} - -int moving_unit::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', moving_unit::member_count - ); - - if (buf.size() != moving_unit::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing moving_unit led to " - << buf.size() - << " columns (expected " - << moving_unit::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->id0) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hu", &this->language_dll_name) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hu", &this->language_dll_creation) != 1) { return 2; } - // parse enum unit_classes - if (buf[3] == "DUMMY") { - this->unit_class = unit_classes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[3] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[4].c_str(), "%hd", &this->idle_graphic0) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%hd", &this->idle_graphic1) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%hd", &this->dying_graphic) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%hd", &this->undead_graphic) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%hhd", &this->death_mode) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hd", &this->hit_points) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%f", &this->line_of_sight) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%hhd", &this->garrison_capacity) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%f", &this->radius_x) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%f", &this->radius_y) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%f", &this->radius_z) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%hd", &this->train_sound_id) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%hd", &this->damage_sound_id) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%hd", &this->dead_unit_id) != 1) { return 17; } - if (sscanf(buf[18].c_str(), "%hhd", &this->placement_mode) != 1) { return 18; } - if (sscanf(buf[19].c_str(), "%hhd", &this->can_be_built_on) != 1) { return 19; } - if (sscanf(buf[20].c_str(), "%hd", &this->icon_id) != 1) { return 20; } - if (sscanf(buf[21].c_str(), "%hhd", &this->hidden_in_editor) != 1) { return 21; } - if (sscanf(buf[22].c_str(), "%hd", &this->old_portrait_icon_id) != 1) { return 22; } - if (sscanf(buf[23].c_str(), "%hhd", &this->enabled) != 1) { return 23; } - if (sscanf(buf[24].c_str(), "%hd", &this->placement_side_terrain0) != 1) { return 24; } - if (sscanf(buf[25].c_str(), "%hd", &this->placement_side_terrain1) != 1) { return 25; } - if (sscanf(buf[26].c_str(), "%hd", &this->placement_terrain0) != 1) { return 26; } - if (sscanf(buf[27].c_str(), "%hd", &this->placement_terrain1) != 1) { return 27; } - if (sscanf(buf[28].c_str(), "%f", &this->clearance_size_x) != 1) { return 28; } - if (sscanf(buf[29].c_str(), "%f", &this->clearance_size_y) != 1) { return 29; } - // parse enum elevation_modes - if (buf[30] == "DUMMY") { - this->elevation_mode = elevation_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[30] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum fog_visibility - if (buf[31] == "DUMMY") { - this->visible_in_fog = fog_visibility::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[31] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum ground_type - if (buf[32] == "DUMMY") { - this->terrain_restriction = ground_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[32] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[33].c_str(), "%hhd", &this->fly_mode) != 1) { return 33; } - if (sscanf(buf[34].c_str(), "%hd", &this->resource_capacity) != 1) { return 34; } - if (sscanf(buf[35].c_str(), "%f", &this->resource_decay) != 1) { return 35; } - // parse enum blast_types - if (buf[36] == "DUMMY") { - this->blast_defense_level = blast_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[36] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum combat_levels - if (buf[37] == "DUMMY") { - this->combat_level = combat_levels::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[37] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum interaction_modes - if (buf[38] == "DUMMY") { - this->interaction_mode = interaction_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[38] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum minimap_modes - if (buf[39] == "DUMMY") { - this->map_draw_level = minimap_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[39] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum command_attributes - if (buf[40] == "DUMMY") { - this->unit_level = command_attributes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[40] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[41].c_str(), "%f", &this->attack_reaction) != 1) { return 41; } - if (sscanf(buf[42].c_str(), "%hhd", &this->minimap_color) != 1) { return 42; } - if (sscanf(buf[43].c_str(), "%d", &this->language_dll_help) != 1) { return 43; } - if (sscanf(buf[44].c_str(), "%d", &this->language_dll_hotkey_text) != 1) { return 44; } - if (sscanf(buf[45].c_str(), "%d", &this->hot_keys) != 1) { return 45; } - if (sscanf(buf[46].c_str(), "%hhd", &this->recyclable) != 1) { return 46; } - if (sscanf(buf[47].c_str(), "%hhd", &this->enable_auto_gather) != 1) { return 47; } - if (sscanf(buf[48].c_str(), "%hhd", &this->doppelgaenger_on_death) != 1) { return 48; } - if (sscanf(buf[49].c_str(), "%hhd", &this->resource_gather_drop) != 1) { return 49; } - if (sscanf(buf[50].c_str(), "%hhu", &this->occlusion_mode) != 1) { return 50; } - // parse enum obstruction_types - if (buf[51] == "DUMMY") { - this->obstruction_type = obstruction_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[51] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[52].c_str(), "%hhd", &this->obstruction_class) != 1) { return 52; } - if (sscanf(buf[53].c_str(), "%hhu", &this->trait) != 1) { return 53; } - if (sscanf(buf[54].c_str(), "%hhd", &this->civilization_id) != 1) { return 54; } - if (sscanf(buf[55].c_str(), "%hd", &this->attribute_piece) != 1) { return 55; } - // parse enum selection_effects - if (buf[56] == "DUMMY") { - this->selection_effect = selection_effects::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[56] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[57].c_str(), "%f", &this->selection_shape_x) != 1) { return 57; } - if (sscanf(buf[58].c_str(), "%f", &this->selection_shape_y) != 1) { return 58; } - if (sscanf(buf[59].c_str(), "%f", &this->selection_shape_z) != 1) { return 59; } - this->resource_storage.filename = buf[60]; - this->damage_graphics.filename = buf[61]; - if (sscanf(buf[62].c_str(), "%hd", &this->selection_sound_id) != 1) { return 62; } - if (sscanf(buf[63].c_str(), "%hd", &this->dying_sound_id) != 1) { return 63; } - // parse enum attack_modes - if (buf[64] == "DUMMY") { - this->old_attack_mode = attack_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[64] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[65].c_str(), "%hhd", &this->convert_terrain) != 1) { return 65; } - this->name = buf[66]; - if (sscanf(buf[67].c_str(), "%hd", &this->id1) != 1) { return 67; } - if (sscanf(buf[68].c_str(), "%hd", &this->id2) != 1) { return 68; } - if (sscanf(buf[69].c_str(), "%f", &this->speed) != 1) { return 69; } - if (sscanf(buf[70].c_str(), "%hd", &this->move_graphics) != 1) { return 70; } - if (sscanf(buf[71].c_str(), "%hd", &this->run_graphics) != 1) { return 71; } - if (sscanf(buf[72].c_str(), "%f", &this->turn_speed) != 1) { return 72; } - if (sscanf(buf[73].c_str(), "%hhd", &this->old_size_class) != 1) { return 73; } - if (sscanf(buf[74].c_str(), "%hd", &this->trail_unit_id) != 1) { return 74; } - if (sscanf(buf[75].c_str(), "%hhu", &this->trail_opsions) != 1) { return 75; } - if (sscanf(buf[76].c_str(), "%f", &this->trail_spacing) != 1) { return 76; } - if (sscanf(buf[77].c_str(), "%hhd", &this->old_move_algorithm) != 1) { return 77; } - if (sscanf(buf[78].c_str(), "%f", &this->turn_radius) != 1) { return 78; } - if (sscanf(buf[79].c_str(), "%f", &this->turn_radius_speed) != 1) { return 79; } - if (sscanf(buf[80].c_str(), "%f", &this->max_yaw_per_sec_moving) != 1) { return 80; } - if (sscanf(buf[81].c_str(), "%f", &this->stationary_yaw_revolution_time) != 1) { return 81; } - if (sscanf(buf[82].c_str(), "%f", &this->max_yaw_per_sec_stationary) != 1) { return 82; } - - return -1; -} - -bool moving_unit::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->resource_storage.read(storage, basedir); - this->damage_graphics.read(storage, basedir); - - return true; -} - -int projectile_unit::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', projectile_unit::member_count - ); - - if (buf.size() != projectile_unit::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing projectile_unit led to " - << buf.size() - << " columns (expected " - << projectile_unit::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->id0) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hu", &this->language_dll_name) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hu", &this->language_dll_creation) != 1) { return 2; } - // parse enum unit_classes - if (buf[3] == "DUMMY") { - this->unit_class = unit_classes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[3] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[4].c_str(), "%hd", &this->idle_graphic0) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%hd", &this->idle_graphic1) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%hd", &this->dying_graphic) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%hd", &this->undead_graphic) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%hhd", &this->death_mode) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hd", &this->hit_points) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%f", &this->line_of_sight) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%hhd", &this->garrison_capacity) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%f", &this->radius_x) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%f", &this->radius_y) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%f", &this->radius_z) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%hd", &this->train_sound_id) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%hd", &this->damage_sound_id) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%hd", &this->dead_unit_id) != 1) { return 17; } - if (sscanf(buf[18].c_str(), "%hhd", &this->placement_mode) != 1) { return 18; } - if (sscanf(buf[19].c_str(), "%hhd", &this->can_be_built_on) != 1) { return 19; } - if (sscanf(buf[20].c_str(), "%hd", &this->icon_id) != 1) { return 20; } - if (sscanf(buf[21].c_str(), "%hhd", &this->hidden_in_editor) != 1) { return 21; } - if (sscanf(buf[22].c_str(), "%hd", &this->old_portrait_icon_id) != 1) { return 22; } - if (sscanf(buf[23].c_str(), "%hhd", &this->enabled) != 1) { return 23; } - if (sscanf(buf[24].c_str(), "%hd", &this->placement_side_terrain0) != 1) { return 24; } - if (sscanf(buf[25].c_str(), "%hd", &this->placement_side_terrain1) != 1) { return 25; } - if (sscanf(buf[26].c_str(), "%hd", &this->placement_terrain0) != 1) { return 26; } - if (sscanf(buf[27].c_str(), "%hd", &this->placement_terrain1) != 1) { return 27; } - if (sscanf(buf[28].c_str(), "%f", &this->clearance_size_x) != 1) { return 28; } - if (sscanf(buf[29].c_str(), "%f", &this->clearance_size_y) != 1) { return 29; } - // parse enum elevation_modes - if (buf[30] == "DUMMY") { - this->elevation_mode = elevation_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[30] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum fog_visibility - if (buf[31] == "DUMMY") { - this->visible_in_fog = fog_visibility::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[31] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum ground_type - if (buf[32] == "DUMMY") { - this->terrain_restriction = ground_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[32] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[33].c_str(), "%hhd", &this->fly_mode) != 1) { return 33; } - if (sscanf(buf[34].c_str(), "%hd", &this->resource_capacity) != 1) { return 34; } - if (sscanf(buf[35].c_str(), "%f", &this->resource_decay) != 1) { return 35; } - // parse enum blast_types - if (buf[36] == "DUMMY") { - this->blast_defense_level = blast_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[36] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum combat_levels - if (buf[37] == "DUMMY") { - this->combat_level = combat_levels::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[37] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum interaction_modes - if (buf[38] == "DUMMY") { - this->interaction_mode = interaction_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[38] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum minimap_modes - if (buf[39] == "DUMMY") { - this->map_draw_level = minimap_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[39] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum command_attributes - if (buf[40] == "DUMMY") { - this->unit_level = command_attributes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[40] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[41].c_str(), "%f", &this->attack_reaction) != 1) { return 41; } - if (sscanf(buf[42].c_str(), "%hhd", &this->minimap_color) != 1) { return 42; } - if (sscanf(buf[43].c_str(), "%d", &this->language_dll_help) != 1) { return 43; } - if (sscanf(buf[44].c_str(), "%d", &this->language_dll_hotkey_text) != 1) { return 44; } - if (sscanf(buf[45].c_str(), "%d", &this->hot_keys) != 1) { return 45; } - if (sscanf(buf[46].c_str(), "%hhd", &this->recyclable) != 1) { return 46; } - if (sscanf(buf[47].c_str(), "%hhd", &this->enable_auto_gather) != 1) { return 47; } - if (sscanf(buf[48].c_str(), "%hhd", &this->doppelgaenger_on_death) != 1) { return 48; } - if (sscanf(buf[49].c_str(), "%hhd", &this->resource_gather_drop) != 1) { return 49; } - if (sscanf(buf[50].c_str(), "%hhu", &this->occlusion_mode) != 1) { return 50; } - // parse enum obstruction_types - if (buf[51] == "DUMMY") { - this->obstruction_type = obstruction_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[51] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[52].c_str(), "%hhd", &this->obstruction_class) != 1) { return 52; } - if (sscanf(buf[53].c_str(), "%hhu", &this->trait) != 1) { return 53; } - if (sscanf(buf[54].c_str(), "%hhd", &this->civilization_id) != 1) { return 54; } - if (sscanf(buf[55].c_str(), "%hd", &this->attribute_piece) != 1) { return 55; } - // parse enum selection_effects - if (buf[56] == "DUMMY") { - this->selection_effect = selection_effects::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[56] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[57].c_str(), "%f", &this->selection_shape_x) != 1) { return 57; } - if (sscanf(buf[58].c_str(), "%f", &this->selection_shape_y) != 1) { return 58; } - if (sscanf(buf[59].c_str(), "%f", &this->selection_shape_z) != 1) { return 59; } - this->resource_storage.filename = buf[60]; - this->damage_graphics.filename = buf[61]; - if (sscanf(buf[62].c_str(), "%hd", &this->selection_sound_id) != 1) { return 62; } - if (sscanf(buf[63].c_str(), "%hd", &this->dying_sound_id) != 1) { return 63; } - // parse enum attack_modes - if (buf[64] == "DUMMY") { - this->old_attack_mode = attack_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[64] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[65].c_str(), "%hhd", &this->convert_terrain) != 1) { return 65; } - this->name = buf[66]; - if (sscanf(buf[67].c_str(), "%hd", &this->id1) != 1) { return 67; } - if (sscanf(buf[68].c_str(), "%hd", &this->id2) != 1) { return 68; } - if (sscanf(buf[69].c_str(), "%f", &this->speed) != 1) { return 69; } - if (sscanf(buf[70].c_str(), "%hd", &this->move_graphics) != 1) { return 70; } - if (sscanf(buf[71].c_str(), "%hd", &this->run_graphics) != 1) { return 71; } - if (sscanf(buf[72].c_str(), "%f", &this->turn_speed) != 1) { return 72; } - if (sscanf(buf[73].c_str(), "%hhd", &this->old_size_class) != 1) { return 73; } - if (sscanf(buf[74].c_str(), "%hd", &this->trail_unit_id) != 1) { return 74; } - if (sscanf(buf[75].c_str(), "%hhu", &this->trail_opsions) != 1) { return 75; } - if (sscanf(buf[76].c_str(), "%f", &this->trail_spacing) != 1) { return 76; } - if (sscanf(buf[77].c_str(), "%hhd", &this->old_move_algorithm) != 1) { return 77; } - if (sscanf(buf[78].c_str(), "%f", &this->turn_radius) != 1) { return 78; } - if (sscanf(buf[79].c_str(), "%f", &this->turn_radius_speed) != 1) { return 79; } - if (sscanf(buf[80].c_str(), "%f", &this->max_yaw_per_sec_moving) != 1) { return 80; } - if (sscanf(buf[81].c_str(), "%f", &this->stationary_yaw_revolution_time) != 1) { return 81; } - if (sscanf(buf[82].c_str(), "%f", &this->max_yaw_per_sec_stationary) != 1) { return 82; } - if (sscanf(buf[83].c_str(), "%hd", &this->default_task_id) != 1) { return 83; } - if (sscanf(buf[84].c_str(), "%f", &this->search_radius) != 1) { return 84; } - if (sscanf(buf[85].c_str(), "%f", &this->work_rate) != 1) { return 85; } - if (sscanf(buf[87].c_str(), "%hhd", &this->task_group) != 1) { return 87; } - if (sscanf(buf[88].c_str(), "%hd", &this->command_sound_id) != 1) { return 88; } - if (sscanf(buf[89].c_str(), "%hd", &this->stop_sound_id) != 1) { return 89; } - if (sscanf(buf[90].c_str(), "%hhd", &this->run_pattern) != 1) { return 90; } - if (sscanf(buf[91].c_str(), "%hd", &this->default_armor) != 1) { return 91; } - this->attacks.filename = buf[92]; - this->armors.filename = buf[93]; - // parse enum boundary_ids - if (buf[94] == "DUMMY") { - this->boundary_id = boundary_ids::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[94] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[95].c_str(), "%f", &this->weapon_range_max) != 1) { return 95; } - if (sscanf(buf[96].c_str(), "%f", &this->blast_range) != 1) { return 96; } - if (sscanf(buf[97].c_str(), "%f", &this->attack_speed) != 1) { return 97; } - if (sscanf(buf[98].c_str(), "%hd", &this->attack_projectile_primary_unit_id) != 1) { return 98; } - if (sscanf(buf[99].c_str(), "%hd", &this->accuracy) != 1) { return 99; } - if (sscanf(buf[100].c_str(), "%hhd", &this->break_off_combat) != 1) { return 100; } - if (sscanf(buf[101].c_str(), "%hd", &this->frame_delay) != 1) { return 101; } - // parse enum range_damage_type - if (buf[103] == "DUMMY") { - this->blast_level_offence = range_damage_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[103] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[104].c_str(), "%f", &this->weapon_range_min) != 1) { return 104; } - if (sscanf(buf[105].c_str(), "%f", &this->accuracy_dispersion) != 1) { return 105; } - if (sscanf(buf[106].c_str(), "%hd", &this->attack_sprite_id) != 1) { return 106; } - if (sscanf(buf[107].c_str(), "%hd", &this->melee_armor_displayed) != 1) { return 107; } - if (sscanf(buf[108].c_str(), "%hd", &this->attack_displayed) != 1) { return 108; } - if (sscanf(buf[109].c_str(), "%f", &this->range_displayed) != 1) { return 109; } - if (sscanf(buf[110].c_str(), "%f", &this->reload_time_displayed) != 1) { return 110; } - - return -1; -} - -bool projectile_unit::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->resource_storage.read(storage, basedir); - this->damage_graphics.read(storage, basedir); - this->attacks.read(storage, basedir); - this->armors.read(storage, basedir); - - return true; -} - -int resource_cost::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', resource_cost::member_count - ); - - if (buf.size() != resource_cost::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing resource_cost led to " - << buf.size() - << " columns (expected " - << resource_cost::member_count - << ")!" - ); - } - - // parse enum resource_types - if (buf[0] == "DUMMY") { - this->type_id = resource_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[0] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[1].c_str(), "%hd", &this->amount) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hd", &this->enabled) != 1) { return 2; } - - return -1; -} - -bool resource_cost::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -int resource_storage::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', resource_storage::member_count - ); - - if (buf.size() != resource_storage::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing resource_storage led to " - << buf.size() - << " columns (expected " - << resource_storage::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->type) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%f", &this->amount) != 1) { return 1; } - // parse enum resource_handling - if (buf[2] == "DUMMY") { - this->used_mode = resource_handling::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[2] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - - return -1; -} - -bool resource_storage::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -int tree_unit::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', tree_unit::member_count - ); - - if (buf.size() != tree_unit::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing tree_unit led to " - << buf.size() - << " columns (expected " - << tree_unit::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->id0) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hu", &this->language_dll_name) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hu", &this->language_dll_creation) != 1) { return 2; } - // parse enum unit_classes - if (buf[3] == "DUMMY") { - this->unit_class = unit_classes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[3] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[4].c_str(), "%hd", &this->idle_graphic0) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%hd", &this->idle_graphic1) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%hd", &this->dying_graphic) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%hd", &this->undead_graphic) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%hhd", &this->death_mode) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hd", &this->hit_points) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%f", &this->line_of_sight) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%hhd", &this->garrison_capacity) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%f", &this->radius_x) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%f", &this->radius_y) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%f", &this->radius_z) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%hd", &this->train_sound_id) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%hd", &this->damage_sound_id) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%hd", &this->dead_unit_id) != 1) { return 17; } - if (sscanf(buf[18].c_str(), "%hhd", &this->placement_mode) != 1) { return 18; } - if (sscanf(buf[19].c_str(), "%hhd", &this->can_be_built_on) != 1) { return 19; } - if (sscanf(buf[20].c_str(), "%hd", &this->icon_id) != 1) { return 20; } - if (sscanf(buf[21].c_str(), "%hhd", &this->hidden_in_editor) != 1) { return 21; } - if (sscanf(buf[22].c_str(), "%hd", &this->old_portrait_icon_id) != 1) { return 22; } - if (sscanf(buf[23].c_str(), "%hhd", &this->enabled) != 1) { return 23; } - if (sscanf(buf[24].c_str(), "%hd", &this->placement_side_terrain0) != 1) { return 24; } - if (sscanf(buf[25].c_str(), "%hd", &this->placement_side_terrain1) != 1) { return 25; } - if (sscanf(buf[26].c_str(), "%hd", &this->placement_terrain0) != 1) { return 26; } - if (sscanf(buf[27].c_str(), "%hd", &this->placement_terrain1) != 1) { return 27; } - if (sscanf(buf[28].c_str(), "%f", &this->clearance_size_x) != 1) { return 28; } - if (sscanf(buf[29].c_str(), "%f", &this->clearance_size_y) != 1) { return 29; } - // parse enum elevation_modes - if (buf[30] == "DUMMY") { - this->elevation_mode = elevation_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[30] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum fog_visibility - if (buf[31] == "DUMMY") { - this->visible_in_fog = fog_visibility::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[31] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum ground_type - if (buf[32] == "DUMMY") { - this->terrain_restriction = ground_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[32] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[33].c_str(), "%hhd", &this->fly_mode) != 1) { return 33; } - if (sscanf(buf[34].c_str(), "%hd", &this->resource_capacity) != 1) { return 34; } - if (sscanf(buf[35].c_str(), "%f", &this->resource_decay) != 1) { return 35; } - // parse enum blast_types - if (buf[36] == "DUMMY") { - this->blast_defense_level = blast_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[36] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum combat_levels - if (buf[37] == "DUMMY") { - this->combat_level = combat_levels::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[37] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum interaction_modes - if (buf[38] == "DUMMY") { - this->interaction_mode = interaction_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[38] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum minimap_modes - if (buf[39] == "DUMMY") { - this->map_draw_level = minimap_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[39] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum command_attributes - if (buf[40] == "DUMMY") { - this->unit_level = command_attributes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[40] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[41].c_str(), "%f", &this->attack_reaction) != 1) { return 41; } - if (sscanf(buf[42].c_str(), "%hhd", &this->minimap_color) != 1) { return 42; } - if (sscanf(buf[43].c_str(), "%d", &this->language_dll_help) != 1) { return 43; } - if (sscanf(buf[44].c_str(), "%d", &this->language_dll_hotkey_text) != 1) { return 44; } - if (sscanf(buf[45].c_str(), "%d", &this->hot_keys) != 1) { return 45; } - if (sscanf(buf[46].c_str(), "%hhd", &this->recyclable) != 1) { return 46; } - if (sscanf(buf[47].c_str(), "%hhd", &this->enable_auto_gather) != 1) { return 47; } - if (sscanf(buf[48].c_str(), "%hhd", &this->doppelgaenger_on_death) != 1) { return 48; } - if (sscanf(buf[49].c_str(), "%hhd", &this->resource_gather_drop) != 1) { return 49; } - if (sscanf(buf[50].c_str(), "%hhu", &this->occlusion_mode) != 1) { return 50; } - // parse enum obstruction_types - if (buf[51] == "DUMMY") { - this->obstruction_type = obstruction_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[51] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[52].c_str(), "%hhd", &this->obstruction_class) != 1) { return 52; } - if (sscanf(buf[53].c_str(), "%hhu", &this->trait) != 1) { return 53; } - if (sscanf(buf[54].c_str(), "%hhd", &this->civilization_id) != 1) { return 54; } - if (sscanf(buf[55].c_str(), "%hd", &this->attribute_piece) != 1) { return 55; } - // parse enum selection_effects - if (buf[56] == "DUMMY") { - this->selection_effect = selection_effects::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[56] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[57].c_str(), "%f", &this->selection_shape_x) != 1) { return 57; } - if (sscanf(buf[58].c_str(), "%f", &this->selection_shape_y) != 1) { return 58; } - if (sscanf(buf[59].c_str(), "%f", &this->selection_shape_z) != 1) { return 59; } - this->resource_storage.filename = buf[60]; - this->damage_graphics.filename = buf[61]; - if (sscanf(buf[62].c_str(), "%hd", &this->selection_sound_id) != 1) { return 62; } - if (sscanf(buf[63].c_str(), "%hd", &this->dying_sound_id) != 1) { return 63; } - // parse enum attack_modes - if (buf[64] == "DUMMY") { - this->old_attack_mode = attack_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[64] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[65].c_str(), "%hhd", &this->convert_terrain) != 1) { return 65; } - this->name = buf[66]; - if (sscanf(buf[67].c_str(), "%hd", &this->id1) != 1) { return 67; } - if (sscanf(buf[68].c_str(), "%hd", &this->id2) != 1) { return 68; } - - return -1; -} - -bool tree_unit::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->resource_storage.read(storage, basedir); - this->damage_graphics.read(storage, basedir); - - return true; -} - -int unit_command::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', unit_command::member_count - ); - - if (buf.size() != unit_command::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing unit_command led to " - << buf.size() - << " columns (expected " - << unit_command::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->command_used) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hd", &this->command_id) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hhd", &this->is_default) != 1) { return 2; } - // parse enum command_ability - if (buf[3] == "DUMMY") { - this->type = command_ability::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[3] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[4].c_str(), "%hd", &this->class_id) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%hd", &this->unit_id) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%hd", &this->terrain_id) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%hd", &this->resource_in) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%hd", &this->resource_multiplier) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hd", &this->resource_out) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%hd", &this->unused_resource) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%f", &this->work_value1) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%f", &this->work_value2) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%f", &this->work_range) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%hhd", &this->search_mode) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%f", &this->search_time) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%hhd", &this->enable_targeting) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%hhd", &this->combat_level_flag) != 1) { return 17; } - if (sscanf(buf[18].c_str(), "%hd", &this->gather_type) != 1) { return 18; } - // parse enum selection_type - if (buf[19] == "DUMMY") { - this->owner_type = selection_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[19] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[20].c_str(), "%hhd", &this->carry_check) != 1) { return 20; } - if (sscanf(buf[21].c_str(), "%hhd", &this->state_build) != 1) { return 21; } - if (sscanf(buf[22].c_str(), "%hd", &this->move_sprite_id) != 1) { return 22; } - if (sscanf(buf[23].c_str(), "%hd", &this->proceed_sprite_id) != 1) { return 23; } - if (sscanf(buf[24].c_str(), "%hd", &this->work_sprite_id) != 1) { return 24; } - if (sscanf(buf[25].c_str(), "%hd", &this->carry_sprite_id) != 1) { return 25; } - if (sscanf(buf[26].c_str(), "%hd", &this->resource_gather_sound_id) != 1) { return 26; } - if (sscanf(buf[27].c_str(), "%hd", &this->resource_deposit_sound_id) != 1) { return 27; } - - return -1; -} - -bool unit_command::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -int unit_header::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', unit_header::member_count - ); - - if (buf.size() != unit_header::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing unit_header led to " - << buf.size() - << " columns (expected " - << unit_header::member_count - << ")!" - ); - } - - // remember if the following members are undefined - if (buf[0] == "data_absent") { - this->exists = 0; - } else if (buf[0] == "data_exists") { - this->exists = 1; - } else { - throw openage::error::Error(ERR << "unexpected value '"<< buf[0] << "' for ContinueReadMember"); - } - this->unit_commands.filename = buf[1]; - - return -1; -} - -bool unit_header::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->unit_commands.read(storage, basedir); - - return true; -} - -int unit_object::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', unit_object::member_count - ); - - if (buf.size() != unit_object::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing unit_object led to " - << buf.size() - << " columns (expected " - << unit_object::member_count - << ")!" - ); - } - - if (sscanf(buf[0].c_str(), "%hd", &this->id0) != 1) { return 0; } - if (sscanf(buf[1].c_str(), "%hu", &this->language_dll_name) != 1) { return 1; } - if (sscanf(buf[2].c_str(), "%hu", &this->language_dll_creation) != 1) { return 2; } - // parse enum unit_classes - if (buf[3] == "DUMMY") { - this->unit_class = unit_classes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[3] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[4].c_str(), "%hd", &this->idle_graphic0) != 1) { return 4; } - if (sscanf(buf[5].c_str(), "%hd", &this->idle_graphic1) != 1) { return 5; } - if (sscanf(buf[6].c_str(), "%hd", &this->dying_graphic) != 1) { return 6; } - if (sscanf(buf[7].c_str(), "%hd", &this->undead_graphic) != 1) { return 7; } - if (sscanf(buf[8].c_str(), "%hhd", &this->death_mode) != 1) { return 8; } - if (sscanf(buf[9].c_str(), "%hd", &this->hit_points) != 1) { return 9; } - if (sscanf(buf[10].c_str(), "%f", &this->line_of_sight) != 1) { return 10; } - if (sscanf(buf[11].c_str(), "%hhd", &this->garrison_capacity) != 1) { return 11; } - if (sscanf(buf[12].c_str(), "%f", &this->radius_x) != 1) { return 12; } - if (sscanf(buf[13].c_str(), "%f", &this->radius_y) != 1) { return 13; } - if (sscanf(buf[14].c_str(), "%f", &this->radius_z) != 1) { return 14; } - if (sscanf(buf[15].c_str(), "%hd", &this->train_sound_id) != 1) { return 15; } - if (sscanf(buf[16].c_str(), "%hd", &this->damage_sound_id) != 1) { return 16; } - if (sscanf(buf[17].c_str(), "%hd", &this->dead_unit_id) != 1) { return 17; } - if (sscanf(buf[18].c_str(), "%hhd", &this->placement_mode) != 1) { return 18; } - if (sscanf(buf[19].c_str(), "%hhd", &this->can_be_built_on) != 1) { return 19; } - if (sscanf(buf[20].c_str(), "%hd", &this->icon_id) != 1) { return 20; } - if (sscanf(buf[21].c_str(), "%hhd", &this->hidden_in_editor) != 1) { return 21; } - if (sscanf(buf[22].c_str(), "%hd", &this->old_portrait_icon_id) != 1) { return 22; } - if (sscanf(buf[23].c_str(), "%hhd", &this->enabled) != 1) { return 23; } - if (sscanf(buf[24].c_str(), "%hd", &this->placement_side_terrain0) != 1) { return 24; } - if (sscanf(buf[25].c_str(), "%hd", &this->placement_side_terrain1) != 1) { return 25; } - if (sscanf(buf[26].c_str(), "%hd", &this->placement_terrain0) != 1) { return 26; } - if (sscanf(buf[27].c_str(), "%hd", &this->placement_terrain1) != 1) { return 27; } - if (sscanf(buf[28].c_str(), "%f", &this->clearance_size_x) != 1) { return 28; } - if (sscanf(buf[29].c_str(), "%f", &this->clearance_size_y) != 1) { return 29; } - // parse enum elevation_modes - if (buf[30] == "DUMMY") { - this->elevation_mode = elevation_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[30] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum fog_visibility - if (buf[31] == "DUMMY") { - this->visible_in_fog = fog_visibility::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[31] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum ground_type - if (buf[32] == "DUMMY") { - this->terrain_restriction = ground_type::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[32] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[33].c_str(), "%hhd", &this->fly_mode) != 1) { return 33; } - if (sscanf(buf[34].c_str(), "%hd", &this->resource_capacity) != 1) { return 34; } - if (sscanf(buf[35].c_str(), "%f", &this->resource_decay) != 1) { return 35; } - // parse enum blast_types - if (buf[36] == "DUMMY") { - this->blast_defense_level = blast_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[36] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum combat_levels - if (buf[37] == "DUMMY") { - this->combat_level = combat_levels::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[37] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum interaction_modes - if (buf[38] == "DUMMY") { - this->interaction_mode = interaction_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[38] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum minimap_modes - if (buf[39] == "DUMMY") { - this->map_draw_level = minimap_modes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[39] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - // parse enum command_attributes - if (buf[40] == "DUMMY") { - this->unit_level = command_attributes::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[40] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[41].c_str(), "%f", &this->attack_reaction) != 1) { return 41; } - if (sscanf(buf[42].c_str(), "%hhd", &this->minimap_color) != 1) { return 42; } - if (sscanf(buf[43].c_str(), "%d", &this->language_dll_help) != 1) { return 43; } - if (sscanf(buf[44].c_str(), "%d", &this->language_dll_hotkey_text) != 1) { return 44; } - if (sscanf(buf[45].c_str(), "%d", &this->hot_keys) != 1) { return 45; } - if (sscanf(buf[46].c_str(), "%hhd", &this->recyclable) != 1) { return 46; } - if (sscanf(buf[47].c_str(), "%hhd", &this->enable_auto_gather) != 1) { return 47; } - if (sscanf(buf[48].c_str(), "%hhd", &this->doppelgaenger_on_death) != 1) { return 48; } - if (sscanf(buf[49].c_str(), "%hhd", &this->resource_gather_drop) != 1) { return 49; } - if (sscanf(buf[50].c_str(), "%hhu", &this->occlusion_mode) != 1) { return 50; } - // parse enum obstruction_types - if (buf[51] == "DUMMY") { - this->obstruction_type = obstruction_types::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[51] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[52].c_str(), "%hhd", &this->obstruction_class) != 1) { return 52; } - if (sscanf(buf[53].c_str(), "%hhu", &this->trait) != 1) { return 53; } - if (sscanf(buf[54].c_str(), "%hhd", &this->civilization_id) != 1) { return 54; } - if (sscanf(buf[55].c_str(), "%hd", &this->attribute_piece) != 1) { return 55; } - // parse enum selection_effects - if (buf[56] == "DUMMY") { - this->selection_effect = selection_effects::DUMMY; - } - else { - throw openage::error::Error( - MSG(err) - << "unknown enum value '" << buf[56] - << "' encountered. valid are: " - "DUMMY\n---\n" - );} - if (sscanf(buf[65].c_str(), "%hhd", &this->convert_terrain) != 1) { return 65; } - this->name = buf[66]; - if (sscanf(buf[67].c_str(), "%hd", &this->id1) != 1) { return 67; } - if (sscanf(buf[68].c_str(), "%hd", &this->id2) != 1) { return 68; } - - return -1; -} - -bool unit_object::recurse(const openage::util::CSVCollection &storage, const std::string &basedir) { - this->resource_storage.read(storage, basedir); - this->damage_graphics.read(storage, basedir); - - return true; -} - -} // gamedata -} // openage diff --git a/libopenage/gamedata/unit_dummy.h b/libopenage/gamedata/unit_dummy.h deleted file mode 100644 index 4398fa0d11..0000000000 --- a/libopenage/gamedata/unit_dummy.h +++ /dev/null @@ -1,515 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - -#pragma once - -#include -#include -#include -#include "util/csv.h" - - - -namespace openage { -namespace gamedata { - -enum class attack_modes { - DUMMY -}; - - -enum class blast_types { - DUMMY -}; - - -enum class boundary_ids { - DUMMY -}; - - -/** - * a possible building annex. - */ -struct building_annex { - int16_t unit_id; - float misplaced0; - float misplaced1; - static constexpr size_t member_count = 3; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -enum class combat_levels { - DUMMY -}; - - -enum class command_ability { - DUMMY -}; - - -enum class command_attributes { - DUMMY -}; - - -enum class creatable_types { - DUMMY -}; - - -enum class damage_draw_type { - DUMMY -}; - - -enum class elevation_modes { - DUMMY -}; - - -enum class fog_visibility { - DUMMY -}; - - -enum class garrison_types { - DUMMY -}; - - -enum class ground_type { - DUMMY, - WATER, - WATER_0x0D, - WATER_SHIP_0x03, - WATER_SHIP_0x0F, - SOLID, - FOUNDATION, - NO_ICE_0x08, - FOREST -}; - - -enum class hit_class { - DUMMY -}; - - -enum class interaction_modes { - DUMMY -}; - - -enum class minimap_modes { - DUMMY -}; - - -enum class obstruction_types { - DUMMY -}; - - -enum class range_damage_type { - DUMMY -}; - - -enum class resource_handling { - DUMMY -}; - - -enum class resource_types { - DUMMY -}; - - -enum class selection_effects { - DUMMY -}; - - -enum class selection_type { - DUMMY -}; - - -enum class unit_classes { - DUMMY, - BUILDING, - CIVILIAN, - TREES, - HERDABLE, - GOLD_MINE, - STONE_MINE, - BERRY_BUSH, - PREY_ANIMAL, - SEA_FISH, - FISHING_BOAT -}; - - -/** - * stores one possible unit image that is displayed at a given damage percentage. - */ -struct damage_graphic { - int16_t graphic_id; - int8_t damage_percent; - int8_t old_apply_mode; - damage_draw_type apply_mode; - static constexpr size_t member_count = 4; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * determines the resource storage capacity for one unit mode. - */ -struct resource_storage { - int16_t type; - float amount; - resource_handling used_mode; - static constexpr size_t member_count = 3; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * base properties for all units. - */ -struct unit_object { - int16_t id0; - uint16_t language_dll_name; - uint16_t language_dll_creation; - unit_classes unit_class; - int16_t idle_graphic0; - int16_t idle_graphic1; - int16_t dying_graphic; - int16_t undead_graphic; - int8_t death_mode; - int16_t hit_points; - float line_of_sight; - int8_t garrison_capacity; - float radius_x; - float radius_y; - float radius_z; - int16_t train_sound_id; - int16_t damage_sound_id; - int16_t dead_unit_id; - int8_t placement_mode; - int8_t can_be_built_on; - int16_t icon_id; - int8_t hidden_in_editor; - int16_t old_portrait_icon_id; - int8_t enabled; - int16_t placement_side_terrain0; - int16_t placement_side_terrain1; - int16_t placement_terrain0; - int16_t placement_terrain1; - float clearance_size_x; - float clearance_size_y; - elevation_modes elevation_mode; - fog_visibility visible_in_fog; - ground_type terrain_restriction; - int8_t fly_mode; - int16_t resource_capacity; - float resource_decay; - blast_types blast_defense_level; - combat_levels combat_level; - interaction_modes interaction_mode; - minimap_modes map_draw_level; - command_attributes unit_level; - float attack_reaction; - int8_t minimap_color; - int32_t language_dll_help; - int32_t language_dll_hotkey_text; - int32_t hot_keys; - int8_t recyclable; - int8_t enable_auto_gather; - int8_t doppelgaenger_on_death; - int8_t resource_gather_drop; - uint8_t occlusion_mode; - obstruction_types obstruction_type; - int8_t obstruction_class; - uint8_t trait; - int8_t civilization_id; - int16_t attribute_piece; - selection_effects selection_effect; - float selection_shape_x; - float selection_shape_y; - float selection_shape_z; - openage::util::csv_subdata resource_storage; - openage::util::csv_subdata damage_graphics; - int16_t selection_sound_id; - int16_t dying_sound_id; - attack_modes old_attack_mode; - int8_t convert_terrain; - std::string name; - int16_t id1; - int16_t id2; - static constexpr size_t member_count = 69; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * adds speed property to units. - */ -struct animated_unit : unit_object { - float speed; - static constexpr size_t member_count = 70; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * weird doppelganger unit thats actually the same as an animated unit. - */ -struct doppelganger_unit : animated_unit { - static constexpr size_t member_count = 70; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * adds walking graphics, rotations and tracking properties to units. - */ -struct moving_unit : doppelganger_unit { - int16_t move_graphics; - int16_t run_graphics; - float turn_speed; - int8_t old_size_class; - int16_t trail_unit_id; - uint8_t trail_opsions; - float trail_spacing; - int8_t old_move_algorithm; - float turn_radius; - float turn_radius_speed; - float max_yaw_per_sec_moving; - float stationary_yaw_revolution_time; - float max_yaw_per_sec_stationary; - static constexpr size_t member_count = 83; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * adds search radius and work properties, as well as movement sounds. - */ -struct action_unit : moving_unit { - int16_t default_task_id; - float search_radius; - float work_rate; - int8_t task_group; - int16_t command_sound_id; - int16_t stop_sound_id; - int8_t run_pattern; - static constexpr size_t member_count = 91; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * stores attack amount for a damage type. - */ -struct hit_type { - hit_class type_id; - int16_t amount; - static constexpr size_t member_count = 2; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * adds attack and armor properties to units. - */ -struct projectile_unit : action_unit { - int16_t default_armor; - openage::util::csv_subdata attacks; - openage::util::csv_subdata armors; - boundary_ids boundary_id; - float weapon_range_max; - float blast_range; - float attack_speed; - int16_t attack_projectile_primary_unit_id; - int16_t accuracy; - int8_t break_off_combat; - int16_t frame_delay; - range_damage_type blast_level_offence; - float weapon_range_min; - float accuracy_dispersion; - int16_t attack_sprite_id; - int16_t melee_armor_displayed; - int16_t attack_displayed; - float range_displayed; - float reload_time_displayed; - static constexpr size_t member_count = 111; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * adds missile specific unit properties. - */ -struct missile_unit : projectile_unit { - int8_t projectile_type; - int8_t smart_mode; - int8_t drop_animation_mode; - int8_t penetration_mode; - int8_t area_of_effect_special; - float projectile_arc; - static constexpr size_t member_count = 117; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * stores cost for one resource for creating the unit. - */ -struct resource_cost { - resource_types type_id; - int16_t amount; - int16_t enabled; - static constexpr size_t member_count = 3; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * just a tree unit. - */ -struct tree_unit : unit_object { - static constexpr size_t member_count = 69; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * a command a single unit may receive by script or human. - */ -struct unit_command { - int16_t command_used; - int16_t command_id; - int8_t is_default; - command_ability type; - int16_t class_id; - int16_t unit_id; - int16_t terrain_id; - int16_t resource_in; - int16_t resource_multiplier; - int16_t resource_out; - int16_t unused_resource; - float work_value1; - float work_value2; - float work_range; - int8_t search_mode; - float search_time; - int8_t enable_targeting; - int8_t combat_level_flag; - int16_t gather_type; - selection_type owner_type; - int8_t carry_check; - int8_t state_build; - int16_t move_sprite_id; - int16_t proceed_sprite_id; - int16_t work_sprite_id; - int16_t carry_sprite_id; - int16_t resource_gather_sound_id; - int16_t resource_deposit_sound_id; - static constexpr size_t member_count = 28; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * stores a bunch of unit commands. - */ -struct unit_header { - uint8_t exists; - openage::util::csv_subdata unit_commands; - static constexpr size_t member_count = 2; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * adds creation location and garrison unit properties. - */ -struct living_unit : projectile_unit { - openage::util::csv_subdata resource_cost; - int16_t creation_time; - int16_t train_location_id; - float rear_attack_modifier; - float flank_attack_modifier; - creatable_types creatable_type; - int8_t hero_mode; - int32_t garrison_graphic; - float attack_projectile_count; - int8_t attack_projectile_max_count; - float attack_projectile_spawning_area_width; - float attack_projectile_spawning_area_length; - float attack_projectile_spawning_area_randomness; - int32_t attack_projectile_secondary_unit_id; - int32_t special_graphic_id; - int8_t special_activation; - int16_t pierce_armor_displayed; - static constexpr size_t member_count = 128; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -/** - * construction graphics and garrison building properties for units. - */ -struct building_unit : living_unit { - int16_t construction_graphic_id; - int16_t snow_graphic_id; - int8_t adjacent_mode; - int16_t graphics_angle; - int8_t disappears_when_built; - int16_t stack_unit_id; - int16_t foundation_terrain_id; - int16_t old_overlay_id; - int16_t research_id; - int8_t can_burn; - openage::util::csv_subdata building_annex; - int16_t head_unit_id; - int16_t transform_unit_id; - int16_t transform_sound_id; - int16_t construction_sound_id; - garrison_types garrison_type; - float garrison_heal_rate; - float garrison_repair_rate; - int16_t salvage_unit_id; - static constexpr size_t member_count = 148; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -} // gamedata -} // openage diff --git a/libopenage/gamedata/util_dummy.cpp b/libopenage/gamedata/util_dummy.cpp deleted file mode 100644 index ee4c97bb50..0000000000 --- a/libopenage/gamedata/util_dummy.cpp +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - - -#include -#include "error/error.h" -#include "util_dummy.h" -#include "util/strings.h" - - -namespace openage { -namespace gamedata { - -constexpr size_t multisubtype_ref::member_count; -int multisubtype_ref::fill(const std::string &line) { - std::vector buf = openage::util::split_escape( - line, ',', multisubtype_ref::member_count - ); - - if (buf.size() != multisubtype_ref::member_count) { - throw openage::error::Error( - ERR - << "Tokenizing multisubtype_ref led to " - << buf.size() - << " columns (expected " - << multisubtype_ref::member_count - << ")!" - ); - } - - this->subtype = buf[0]; - this->filename = buf[1]; - - return -1; -} - -bool multisubtype_ref::recurse(const openage::util::CSVCollection & /*storage*/, const std::string & /*basedir*/) { - return true; -} - -} // gamedata -} // openage diff --git a/libopenage/gamedata/util_dummy.h b/libopenage/gamedata/util_dummy.h deleted file mode 100644 index 87c6b21868..0000000000 --- a/libopenage/gamedata/util_dummy.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. - -// Warning: this file is a dummy file and was auto-generated by the v0.4.1 converter; -// its purpose is to keep the deprecated gamestate compilable and intact; -// these files keep only the minimum functionality and should not be changed; -// For details, see buildsystem/codegen.cmake and openage/codegen. - -#pragma once - -#include -#include -#include "util/csv.h" - - - -namespace openage { -namespace gamedata { - -/** - * format for multi-subtype references - */ -struct multisubtype_ref { - std::string subtype; - std::string filename; - static constexpr size_t member_count = 2; - int fill(const std::string &line); - bool recurse(const openage::util::CSVCollection &storage, const std::string &basedir); - -}; - -} // gamedata -} // openage diff --git a/libopenage/util/color.cpp b/libopenage/util/color.cpp index 5254606f74..fed90a54a4 100644 --- a/libopenage/util/color.cpp +++ b/libopenage/util/color.cpp @@ -1,4 +1,4 @@ -// Copyright 2013-2019 the openage authors. See copying.md for legal info. +// Copyright 2013-2023 the openage authors. See copying.md for legal info. #include "color.h" @@ -6,13 +6,6 @@ namespace openage::util { -col::col(gamedata::palette_color c) { - this->r = c.r; - this->g = c.g; - this->b = c.b; - this->a = c.a; -} - void col::use() { //TODO use glColor4b glColor4f(r / 255.f, g / 255.f, b / 255.f, a / 255.f); @@ -22,4 +15,4 @@ void col::use(float alpha) { glColor4f(r / 255.f, g / 255.f, b / 255.f, alpha); } -} // openage::util +} // namespace openage::util diff --git a/libopenage/util/color.h b/libopenage/util/color.h index 326b1a1e2d..ee7199a8b7 100644 --- a/libopenage/util/color.h +++ b/libopenage/util/color.h @@ -1,15 +1,14 @@ -// Copyright 2013-2021 the openage authors. See copying.md for legal info. +// Copyright 2013-2023 the openage authors. See copying.md for legal info. #pragma once -#include "../gamedata/color_dummy.h" namespace openage { namespace util { struct col { - col(unsigned r, unsigned g, unsigned b, unsigned a) : r{r}, g{g}, b{b}, a{a} {} - col(gamedata::palette_color c); + col(unsigned r, unsigned g, unsigned b, unsigned a) : + r{r}, g{g}, b{b}, a{a} {} unsigned r, g, b, a; @@ -17,4 +16,5 @@ struct col { void use(float alpha); }; -}} // openage::util +} // namespace util +} // namespace openage From 32e968f15f965bd6787e4cedf7ca9684db5c6400 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Sep 2023 00:26:01 +0200 Subject: [PATCH 042/771] coord: Remove deprecated conversions and types. --- libopenage/console/console.cpp | 2 +- libopenage/coord/CMakeLists.txt | 1 - libopenage/coord/coordmanager.cpp | 10 --- libopenage/coord/coordmanager.h | 56 --------------- libopenage/coord/declarations.h | 3 - libopenage/coord/phys.cpp | 43 ----------- libopenage/coord/phys.h | 11 --- libopenage/coord/pixel.cpp | 114 +++--------------------------- libopenage/coord/pixel.h | 62 ++-------------- libopenage/coord/scene.cpp | 3 +- libopenage/coord/tile.cpp | 29 -------- libopenage/coord/tile.h | 9 --- libopenage/pathfinding/path.h | 4 -- 13 files changed, 14 insertions(+), 333 deletions(-) delete mode 100644 libopenage/coord/coordmanager.cpp delete mode 100644 libopenage/coord/coordmanager.h diff --git a/libopenage/console/console.cpp b/libopenage/console/console.cpp index 04cf29d4d1..274cbc087e 100644 --- a/libopenage/console/console.cpp +++ b/libopenage/console/console.cpp @@ -106,7 +106,7 @@ void Console::register_to_engine() { // this->input_context.utf8_mode = true; } -void Console::set_visible(bool make_visible) { +void Console::set_visible(bool /* make_visible */) { // TODO: Use new renderer // if (make_visible) { diff --git a/libopenage/coord/CMakeLists.txt b/libopenage/coord/CMakeLists.txt index 5123577052..7f01010d8a 100644 --- a/libopenage/coord/CMakeLists.txt +++ b/libopenage/coord/CMakeLists.txt @@ -3,7 +3,6 @@ add_sources(libopenage chunk.cpp coord_test.cpp - coordmanager.cpp declarations.cpp phys.cpp pixel.cpp diff --git a/libopenage/coord/coordmanager.cpp b/libopenage/coord/coordmanager.cpp deleted file mode 100644 index 7d175b0702..0000000000 --- a/libopenage/coord/coordmanager.cpp +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. - -#include "coordmanager.h" - -namespace openage { -namespace coord { - -// no implementation needed - -}} diff --git a/libopenage/coord/coordmanager.h b/libopenage/coord/coordmanager.h deleted file mode 100644 index 19195de83e..0000000000 --- a/libopenage/coord/coordmanager.h +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include "phys.h" -#include "pixel.h" -#include "tile.h" - -namespace openage { -namespace coord { - - -/** - * Holds all coordinate-related state and metadata. - * - * Among other things, this stores the camera positions. - * - * TODO: rename to CoordState! - */ -class CoordManager final { -public: - explicit CoordManager() {}; - - /** - * What's the current size of the viewport? - * (what's the coordinate of the top right pixel + (1, 1)?) - */ - viewport_delta viewport_size{800, 600}; - - /** - * What place (in Phys3) is the origin of the CamGame coordinate system looking at? - */ - phys3 camgame_phys{10, 10, 0}; - - /** - * Where in the viewport is the origin of the CamGame coordinate system? - * (this is usually the center of the viewport). - */ - viewport camgame_viewport{400, 300}; - - /** - * Where in the viewport is the origin of the CamHUD coordinate system? - * (this is usually the bottom left corner of the viewport) - */ - viewport camhud_viewport{0, 0}; - - /** - * The size of a terrain tile, in pixels. - * Rile diamonds are 96 pixels wide and 48 pixels high. - * The area of each tile is 96 * 48 * 0.5 square pixels. - */ - // TODO: dynamically get from nyan data - camgame_delta tile_size{96, 48}; -}; - -}} // namespace openage::coord diff --git a/libopenage/coord/declarations.h b/libopenage/coord/declarations.h index 05b89cb4fa..698b594aac 100644 --- a/libopenage/coord/declarations.h +++ b/libopenage/coord/declarations.h @@ -74,8 +74,5 @@ using term_t = int; struct term_delta; struct term; -// forward declaration of the coord manager -class CoordManager; - } // namespace coord } // namespace openage diff --git a/libopenage/coord/phys.cpp b/libopenage/coord/phys.cpp index bb5e89498d..2daf15ab20 100644 --- a/libopenage/coord/phys.cpp +++ b/libopenage/coord/phys.cpp @@ -2,7 +2,6 @@ #include "phys.h" -#include "coord/coordmanager.h" #include "coord/pixel.h" #include "coord/scene.h" #include "coord/tile.h" @@ -65,16 +64,6 @@ scene2 phys2::to_scene2() const { return scene2(this->ne, this->se); } - -[[deprecated]] phys3 phys2::to_phys3(const Terrain & /* terrain */, phys_t altitude) const { - // TODO: once terrain elevations have been implemented, - // query the terrain elevation at {ne, se}. - phys_t elevation = 0; - - return phys3{this->ne, this->se, elevation + altitude}; -} - - double phys3_delta::length() const { return math::hypot3(this->ne.to_double(), this->se.to_double(), this->up.to_double()); } @@ -108,22 +97,6 @@ phys_angle_t phys3_delta::to_angle(const coord::phys2_delta &other) const { return phys_angle_t::from_float(angle); } - -[[deprecated]] camgame_delta phys3_delta::to_camgame(const CoordManager &mgr) const { - // apply transformation matrix to phys3_delta, to get 'scaled': - // (ne) - // (x) = (+1 +1 +0) (se) - // (y) = (+1 -1 +1) (up) - phys_t x = phys_t{this->ne} * (+1) + this->se * (+1) + this->up * (+0); - phys_t y = phys_t{this->ne} * (+1) + this->se * (-1) + this->up * (+1); - - // add scaling (w/2 for x, h/2 for y) - return camgame_delta{ - static_cast((x * (mgr.tile_size.x / 2)).to_int()), - static_cast((y * (mgr.tile_size.y / 2)).to_int())}; -} - - tile3 phys3::to_tile3() const { return tile3{this->ne.to_int(), this->se.to_int(), this->up.to_int()}; } @@ -143,20 +116,4 @@ scene3 phys3::to_scene3() const { return scene3{this->ne, this->se, this->up}; } - -[[deprecated]] camgame phys3::to_camgame(const CoordManager &mgr) const { - return (*this - mgr.camgame_phys).to_camgame(mgr) + camgame{0, 0}; -} - - -[[deprecated]] viewport phys3::to_viewport(const CoordManager &mgr) const { - return this->to_camgame(mgr).to_viewport(mgr); -} - - -[[deprecated]] camhud phys3::to_camhud(const CoordManager &mgr) const { - return this->to_viewport(mgr).to_camhud(mgr); -} - - } // namespace openage::coord diff --git a/libopenage/coord/phys.h b/libopenage/coord/phys.h index 3a88072fff..7d933410f3 100644 --- a/libopenage/coord/phys.h +++ b/libopenage/coord/phys.h @@ -44,9 +44,6 @@ struct phys2 : CoordNeSeAbsolute { tile to_tile() const; phys3 to_phys3(phys_t up = 0) const; scene2 to_scene2() const; - - // TODO: Remove - [[deprecated]] phys3 to_phys3(const Terrain &terrain, phys_t altitude = 0) const; }; struct phys3_delta : CoordNeSeUpRelative { @@ -66,9 +63,6 @@ struct phys3_delta : CoordNeSeUpRelative { // TODO: This DOES NOT use fixed point math currently phys_angle_t to_angle(const coord::phys2_delta &other = {-1, 1}) const; - - // TODO: Remove - [[deprecated]] camgame_delta to_camgame(const CoordManager &mgr) const; }; struct phys3 : CoordNeSeUpAbsolute { @@ -79,11 +73,6 @@ struct phys3 : CoordNeSeUpAbsolute { tile to_tile() const; phys2 to_phys2() const; scene3 to_scene3() const; - - // TODO: Remove - [[deprecated]] camgame to_camgame(const CoordManager &mgr) const; - [[deprecated]] viewport to_viewport(const CoordManager &mgr) const; - [[deprecated]] camhud to_camhud(const CoordManager &mgr) const; }; diff --git a/libopenage/coord/pixel.cpp b/libopenage/coord/pixel.cpp index 24ad6a507f..c7dac5f815 100644 --- a/libopenage/coord/pixel.cpp +++ b/libopenage/coord/pixel.cpp @@ -2,86 +2,25 @@ #include "pixel.h" -#include "coord/coordmanager.h" #include "coord/phys.h" #include "renderer/camera/camera.h" namespace openage { namespace coord { - -phys3_delta camgame_delta::to_phys3(const CoordManager &mgr, phys_t up) const { - // apply scaling factor; w/2 for x, h/2 for y - phys_t x = phys_t::from_int(this->x) / static_cast(mgr.tile_size.x / 2); - phys_t y = phys_t::from_int(this->y) / static_cast(mgr.tile_size.y / 2); - - // apply transformation matrix to 'scaled', - // to get the relative phys3 position - // - // a camgame position represents a line in the 3D phys space - // a camgame delta of 0 might actually correlate to an arbitrarily - // large phys delta. - // we select one specific point on that line by explicitly specifying the - // 'up' value of the result. - // - // the transformation matrix is: - // - // (ne) = (+0.5 +0.5 +0.5) ( x) - // (se) = (+0.5 -0.5 -0.5) ( y) - // (up) = (+0.0 +0.0 +1.0) (up) - phys3_delta result; - result.ne = (x + y + up) / 2L; - result.se = (x - y - up) / 2L; - result.up = (up); - - return result; -} - - -viewport_delta camgame_delta::to_viewport() const { - return viewport_delta{this->x, this->y}; -} - - -viewport camgame::to_viewport(const CoordManager &mgr) const { - // reverse of viewport::to_camgame - return (*this - camgame{0, 0}).to_viewport() + mgr.camgame_viewport; -} - - -camhud camgame::to_camhud(const CoordManager &mgr) const { - return this->to_viewport(mgr).to_camhud(mgr); -} - - -phys3 camgame::to_phys3(const CoordManager &mgr, phys_t up) const { - return (*this - camgame{0, 0}).to_phys3(mgr, up) + mgr.camgame_phys; -} - - -tile camgame::to_tile(const CoordManager &mgr, phys_t up) const { - return this->to_phys3(mgr, up).to_tile(); -} - - viewport_delta camhud_delta::to_viewport() const { return viewport_delta{this->x, this->y}; } -viewport camhud::to_viewport(const CoordManager &mgr) const { +viewport camhud::to_viewport() const { // reverse of viewport::to_camhud - return (*this - camhud{0, 0}).to_viewport() + mgr.camhud_viewport; + return (*this - camhud{0, 0}).to_viewport() + viewport{0, 0}; } -[[deprecated]] phys3_delta viewport_delta::to_phys3(const CoordManager &mgr, phys_t up) const { - return this->to_camgame().to_phys3(mgr, up); -} - - -camhud viewport::to_camhud(const CoordManager &mgr) const { - return camhud{0, 0} + (*this - mgr.camhud_viewport).to_camhud(); +camhud viewport::to_camhud() const { + return camhud{0, 0} + (*this - viewport{0, 0}).to_camhud(); } @@ -92,44 +31,18 @@ Eigen::Vector2f viewport::to_ndc_space(const std::shared_ptrto_camgame(mgr).to_phys3(mgr, up); -} - - -[[deprecated]] tile viewport::to_tile(const CoordManager &mgr, phys_t up) const { - return this->to_camgame(mgr).to_tile(mgr, up); -} - - -viewport_delta input_delta::to_viewport(const CoordManager &mgr) const { +viewport_delta input_delta::to_viewport(const std::shared_ptr &camera) const { viewport_delta result; result.x = this->x; - result.y = mgr.viewport_size.y - this->y; + result.y = camera->get_viewport_size()[1] - this->y; return result; } -[[deprecated]] camgame_delta input_delta::to_camgame(const CoordManager &mgr) const { - return this->to_viewport(mgr).to_camgame(); -} - - -[[deprecated]] phys3_delta input_delta::to_phys3(const CoordManager &mgr, phys_t up) const { - return this->to_viewport(mgr).to_camgame().to_phys3(mgr, up); -} - - -viewport input::to_viewport(const CoordManager &mgr) const { - return viewport{0, 0} + (*this - input{0, 0}).to_viewport(mgr); +viewport input::to_viewport(const std::shared_ptr &camera) const { + return viewport{0, 0} + (*this - input{0, 0}).to_viewport(camera); } @@ -155,16 +68,5 @@ scene3 input::to_scene3(const std::shared_ptr &camera) return scene3(-p_intersect.z(), p_intersect.x(), 0.0f); } - -[[deprecated]] phys3 input::to_phys3(const CoordManager &mgr, phys_t up) const { - return this->to_viewport(mgr).to_camgame(mgr).to_phys3(mgr, up); -} - - -[[deprecated]] camgame input::to_camgame(const CoordManager &mgr) const { - return this->to_viewport(mgr).to_camgame(mgr); -} - - } // namespace coord } // namespace openage diff --git a/libopenage/coord/pixel.h b/libopenage/coord/pixel.h index 5c42b92a4a..e97106224b 100644 --- a/libopenage/coord/pixel.h +++ b/libopenage/coord/pixel.h @@ -21,41 +21,6 @@ namespace coord { * See doc/code/coordinate-systems.md for more information. */ - -// TODO: Remove -struct [[deprecated]] camgame_delta : CoordXYRelative { - using CoordXYRelative::CoordXYRelative; - - /** - * There are infinite solutions to this conversion problem because - * a 2D coordinate is converted into a 3D coordinate. - * - * The user needs to manually give the 'up' value of the phys3 result. - */ - phys3_delta to_phys3(const CoordManager &mgr, phys_t up = phys_t::zero()) const; - viewport_delta to_viewport() const; -}; - - -// TODO: Remove -struct [[deprecated]] camgame : CoordXYAbsolute { - using CoordXYAbsolute::CoordXYAbsolute; - - /** - * See the comments for camgame_delta::to_phys3. - * - * TODO: Once we have terrain elevation, 'up' will not mean the absolute - * elevation, but instead the returned phys3 coordinate will be - * the intersection between the camgame line and the 3d terrain + - * up altitude. - */ - viewport to_viewport(const CoordManager &mgr) const; - camhud to_camhud(const CoordManager &mgr) const; - phys3 to_phys3(const CoordManager &mgr, phys_t up = phys_t::zero()) const; - tile to_tile(const CoordManager &mgr, phys_t up = phys_t::zero()) const; -}; - - struct camhud_delta : CoordXYRelative { using CoordXYRelative::CoordXYRelative; @@ -68,7 +33,7 @@ struct camhud : CoordXYAbsolute { using CoordXYAbsolute::CoordXYAbsolute; // coordinate conversions - viewport to_viewport(const CoordManager &mgr) const; + viewport to_viewport() const; }; @@ -79,12 +44,6 @@ struct viewport_delta : CoordXYRelative { camhud_delta to_camhud() const { return camhud_delta{this->x, this->y}; } - - // TODO: Remove - [[deprecated]] constexpr camgame_delta to_camgame() const { - return camgame_delta{this->x, this->y}; - } - [[deprecated]] phys3_delta to_phys3(const CoordManager &mgr, phys_t up) const; }; @@ -92,15 +51,10 @@ struct viewport : CoordXYAbsolute { using CoordXYAbsolute::CoordXYAbsolute; // coordinate conversions - camhud to_camhud(const CoordManager &mgr) const; + camhud to_camhud() const; // renderer conversions Eigen::Vector2f to_ndc_space(const std::shared_ptr &camera) const; - - // TODO: Remove - [[deprecated]] camgame to_camgame(const CoordManager &mgr) const; - [[deprecated]] phys3 to_phys3(const CoordManager &mgr, phys_t up = phys_t::zero()) const; - [[deprecated]] tile to_tile(const CoordManager &mgr, phys_t up = phys_t::zero()) const; }; @@ -108,11 +62,7 @@ struct input_delta : CoordXYRelative { using CoordXYRelative::CoordXYRelative; // coordinate conversions - viewport_delta to_viewport(const CoordManager &mgr) const; - - // TODO: Remove - [[deprecated]] camgame_delta to_camgame(const CoordManager &mgr) const; - [[deprecated]] phys3_delta to_phys3(const CoordManager &mgr, phys_t up = phys_t::zero()) const; + viewport_delta to_viewport(const std::shared_ptr &camera) const; }; @@ -120,13 +70,9 @@ struct input : CoordXYAbsolute { using CoordXYAbsolute::CoordXYAbsolute; // coordinate conversions - viewport to_viewport(const CoordManager &mgr) const; + viewport to_viewport(const std::shared_ptr &camera) const; phys3 to_phys3(const std::shared_ptr &camera) const; scene3 to_scene3(const std::shared_ptr &camera) const; - - // TODO: Remove - [[deprecated]] phys3 to_phys3(const CoordManager &mgr, phys_t up = phys_t::zero()) const; - [[deprecated]] camgame to_camgame(const CoordManager &mgr) const; }; diff --git a/libopenage/coord/scene.cpp b/libopenage/coord/scene.cpp index d34519bfe4..45e262d519 100644 --- a/libopenage/coord/scene.cpp +++ b/libopenage/coord/scene.cpp @@ -4,7 +4,6 @@ #include -#include "coord/coordmanager.h" #include "coord/pixel.h" #include "coord/tile.h" #include "util/math.h" @@ -108,7 +107,7 @@ Eigen::Vector3f scene3_delta::to_world_space() const { } float scene3_delta::to_angle(const coord::scene2_delta &other) const { - return this->to_scene2().to_angle(); + return this->to_scene2().to_angle(other); } scene2 scene3::to_scene2() const { diff --git a/libopenage/coord/tile.cpp b/libopenage/coord/tile.cpp index dbd68a0d5a..b89f1e9578 100644 --- a/libopenage/coord/tile.cpp +++ b/libopenage/coord/tile.cpp @@ -3,7 +3,6 @@ #include "tile.h" #include "../terrain/terrain.h" -#include "coordmanager.h" namespace openage::coord { @@ -37,34 +36,6 @@ tile_delta tile::get_pos_on_chunk() const { } -[[deprecated]] tile3 tile::to_tile3(const Terrain & /*terrain*/, tile_t altitude) const { - // TODO: once terrain elevations have been implemented, - // query the terrain elevation at {ne, se}. - tile_t elevation = 0; - - return tile3{this->ne, this->se, elevation + altitude}; -} - - -[[deprecated]] phys3 tile::to_phys3(const Terrain &terrain, tile_t altitude) const { - return this->to_tile3(terrain, altitude).to_phys3(); -} - - -[[deprecated]] camgame tile::to_camgame(const CoordManager &mgr, - const Terrain &terrain, - tile_t altitude) const { - return this->to_phys3(terrain, altitude).to_camgame(mgr); -} - - -[[deprecated]] viewport tile::to_viewport(const CoordManager &mgr, - const Terrain &terrain, - tile_t altitude) const { - return this->to_camgame(mgr, terrain, altitude).to_viewport(mgr); -} - - phys2 tile3::to_phys2() const { return this->to_tile().to_phys2(); } diff --git a/libopenage/coord/tile.h b/libopenage/coord/tile.h index 4570564d32..df08045e21 100644 --- a/libopenage/coord/tile.h +++ b/libopenage/coord/tile.h @@ -14,9 +14,6 @@ class Terrain; namespace coord { -class CoordManager; - - /* * Gameworld tile-related coordinate systems. * See doc/code/coordinate-systems.md for more information. @@ -41,12 +38,6 @@ struct tile : CoordNeSeAbsolute { phys3 to_phys3(tile_t up = 0) const; chunk to_chunk() const; tile_delta get_pos_on_chunk() const; - - // TODO: Remove - [[deprecated]] tile3 to_tile3(const Terrain &terrain, tile_t altitude = 0) const; - [[deprecated]] phys3 to_phys3(const Terrain &terrain, tile_t altitude = 0) const; - [[deprecated]] camgame to_camgame(const CoordManager &mgr, const Terrain &terrain, tile_t altitude = 0) const; - [[deprecated]] viewport to_viewport(const CoordManager &mgr, const Terrain &terrain, tile_t altitude = 0) const; }; struct tile3_delta : CoordNeSeUpRelative { diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/path.h index 033090ca6b..e41ddbcc28 100644 --- a/libopenage/pathfinding/path.h +++ b/libopenage/pathfinding/path.h @@ -16,10 +16,6 @@ namespace openage { -namespace coord { -class CoordManager; -} - namespace path { class Node; From 66b317ef1c77b9f23cfc48f4ded765e3a93ddee2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Sep 2023 00:33:32 +0200 Subject: [PATCH 043/771] refactor: Remove nyan directory. --- libopenage/nyan/db.h | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 libopenage/nyan/db.h diff --git a/libopenage/nyan/db.h b/libopenage/nyan/db.h deleted file mode 100644 index 23a39980a6..0000000000 --- a/libopenage/nyan/db.h +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2018-2019 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "../log/log.h" -#include "../error/error.h" - -namespace openage::nyan { - -} From ab404ca601313ec440d7a244c698f92c355392bf Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Sep 2023 00:45:34 +0200 Subject: [PATCH 044/771] refactor: Remove SDL from remaining engine code. --- libopenage/console/console.h | 1 - libopenage/event/demo/main.cpp | 10 +++++-- libopenage/versions/versions.cpp | 49 ++++---------------------------- 3 files changed, 13 insertions(+), 47 deletions(-) diff --git a/libopenage/console/console.h b/libopenage/console/console.h index 839467e6a1..380a563e17 100644 --- a/libopenage/console/console.h +++ b/libopenage/console/console.h @@ -2,7 +2,6 @@ #pragma once -#include #include #include "../coord/pixel.h" diff --git a/libopenage/event/demo/main.cpp b/libopenage/event/demo/main.cpp index 442e9910c7..3ccbed5ac6 100644 --- a/libopenage/event/demo/main.cpp +++ b/libopenage/event/demo/main.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include "config.h" #include "event/demo/aicontroller.h" @@ -13,6 +13,7 @@ #include "event/demo/physics.h" #include "event/event.h" #include "event/event_loop.h" +#include "renderer/gui/integration/public/gui_application_with_logger.h" #if WITH_NCURSES #ifdef __MINGW32__ @@ -41,6 +42,7 @@ enum class timescale { void curvepong(bool disable_gui, bool no_human) { + renderer::gui::GuiApplicationWithLogger gui_app{}; bool enable_gui = not disable_gui; bool human_player = not no_human; @@ -99,6 +101,8 @@ void curvepong(bool disable_gui, bool no_human) { while (state->p1->lives->get(now) > 0 and state->p2->lives->get(now) > 0) { auto loop_start = Clock::now(); + gui_app.process_events(); + #if WITH_NCURSES if (enable_gui) { gui->clear(); @@ -157,7 +161,7 @@ void curvepong(bool disable_gui, bool no_human) { if (speed == timescale::NOSLEEP) { // increase the simulation loop time a bit - SDL_Delay(5); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); } dt_ms_t dt_us = Clock::now() - loop_start; @@ -166,7 +170,7 @@ void curvepong(bool disable_gui, bool no_human) { dt_ms_t wait_time = per_frame - dt_us; if (wait_time > dt_ms_t::zero()) { - SDL_Delay(wait_time.count()); + std::this_thread::sleep_for(wait_time); } } diff --git a/libopenage/versions/versions.cpp b/libopenage/versions/versions.cpp index c94d898f33..8e2e63d215 100644 --- a/libopenage/versions/versions.cpp +++ b/libopenage/versions/versions.cpp @@ -11,37 +11,21 @@ #include #include #include -#include #include -#include "versions/compiletime.h" #include "../util/strings.h" +#include "versions/compiletime.h" namespace openage::versions { std::map get_version_numbers() { - std::map version_numbers; - // SDL runtime version number - SDL_version sdl_runtime_version; - SDL_GetVersion(&sdl_runtime_version); - version_numbers.emplace("SDL-runtime", util::sformat("%d.%d.%d", - sdl_runtime_version.major, - sdl_runtime_version.minor, - sdl_runtime_version.patch)); - // Eigen compiletime version number - version_numbers.emplace("Eigen", util::sformat("%d.%d.%d", - EIGEN_WORLD_VERSION, - EIGEN_MAJOR_VERSION, - EIGEN_MINOR_VERSION)); + version_numbers.emplace("Eigen", util::sformat("%d.%d.%d", EIGEN_WORLD_VERSION, EIGEN_MAJOR_VERSION, EIGEN_MINOR_VERSION)); // Harfbuzz compiletime version number - version_numbers.emplace("Harfbuzz", util::sformat("%d.%d.%d", - HB_VERSION_MAJOR, - HB_VERSION_MINOR, - HB_VERSION_MICRO)); + version_numbers.emplace("Harfbuzz", util::sformat("%d.%d.%d", HB_VERSION_MAJOR, HB_VERSION_MINOR, HB_VERSION_MICRO)); // Add Qt version number version_numbers.emplace("Qt", QT_VERSION_STR); @@ -49,38 +33,17 @@ std::map get_version_numbers() { // Add nyan version number version_numbers.emplace("nyan", nyan_version); - // Add OpenGL version number - // TODO: set the same SDL_GL_CONTEXT_MAJOR_VERSION as the real renderer - if (SDL_Init(SDL_INIT_VIDEO) != 0) { - version_numbers.emplace("OpenGL", SDL_GetError()); - } - else { - SDL_Window *window = SDL_CreateWindow("Query OpenGL version", - 0, 0, 640, 480, - SDL_WINDOW_OPENGL|SDL_WINDOW_HIDDEN); - if (window != nullptr) { - SDL_GLContext glcontext = SDL_GL_CreateContext(window); - if (glcontext != nullptr) { - version_numbers.emplace("OpenGL", - reinterpret_cast(glGetString(GL_VERSION))); - SDL_GL_DeleteContext(glcontext); - } - SDL_DestroyWindow(window); - } - SDL_Quit(); - } - // Add Opus version number std::string opus_version = opus_get_version_string(); version_numbers.emplace("Opus", opus_version.substr(opus_version.find(' ') + 1)); + // TODO: Add OpenGL version number + #ifdef __linux__ // Add libc version number if not MacOSX version_numbers.emplace("libc-runtime", gnu_get_libc_version()); - version_numbers.emplace("libc-compile", util::sformat("%d.%d", - __GLIBC__, - __GLIBC_MINOR__)); + version_numbers.emplace("libc-compile", util::sformat("%d.%d", __GLIBC__, __GLIBC_MINOR__)); #endif #ifdef __APPLE__ From 931a672ed15f77d6d9baa61479f495cab00fd1c6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Sep 2023 14:43:08 +0200 Subject: [PATCH 045/771] audio: Port audio manager to Qt6. This needs some additional work! --- libopenage/audio/audio_manager.cpp | 188 +++++++++++------------------ libopenage/audio/audio_manager.h | 51 ++++---- 2 files changed, 95 insertions(+), 144 deletions(-) diff --git a/libopenage/audio/audio_manager.cpp b/libopenage/audio/audio_manager.cpp index 5bda65e6e9..ce19e77073 100644 --- a/libopenage/audio/audio_manager.cpp +++ b/libopenage/audio/audio_manager.cpp @@ -3,94 +3,59 @@ #include "audio_manager.h" #include -#include #include +#include +#include +#include +#include + +#include "../log/log.h" +#include "../util/misc.h" #include "error.h" #include "hash_functions.h" #include "resource.h" -#include "../log/log.h" -#include "../util/misc.h" namespace openage { namespace audio { - - - -/** - * Wrapper class for the sdl audio device locking so - * the device doesn't deadlock because of funny exceptions. - */ -class SDLDeviceLock { -public: - explicit SDLDeviceLock(const SDL_AudioDeviceID &id) - : - dev_id{id} { - SDL_LockAudioDevice(this->dev_id); - } - - ~SDLDeviceLock() { - SDL_UnlockAudioDevice(this->dev_id); - } - - SDLDeviceLock(SDLDeviceLock &&) = delete; - SDLDeviceLock(const SDLDeviceLock &) = delete; - SDLDeviceLock& operator =(SDLDeviceLock &&) = delete; - SDLDeviceLock& operator =(const SDLDeviceLock &) = delete; - -private: - SDL_AudioDeviceID dev_id; -}; - - -AudioManager::AudioManager(job::JobManager *job_manager, - const std::string &device_name) - : +AudioManager::AudioManager(const std::shared_ptr &job_manager, + const std::string &device_name) : available{false}, job_manager{job_manager}, - device_name{device_name} { + device_name{device_name}, + device_format{std::make_shared()}, + device{nullptr}, + audio_sink{nullptr} { + // set desired audio output format + this->device_format->setSampleRate(48000); + this->device_format->setSampleFormat(QAudioFormat::SampleFormat::Int16); + this->device_format->setChannelCount(2); + + // find the device with the given name + for (auto &device : QMediaDevices::audioOutputs()) { + if (device.description().toStdString() == device_name) { + this->device = std::make_shared(device); + break; + } + } - if (SDL_Init(SDL_INIT_AUDIO) < 0) { - log::log(MSG(err) - << "SDL audio initialization failed: " - << SDL_GetError()); + // select the default device if the name was not found + if (not this->device) { + this->device = std::make_shared(QMediaDevices::defaultAudioOutput()); return; - } else { - log::log(MSG(info) << "SDL audio subsystems initialized"); } - // set desired audio output format - SDL_AudioSpec desired_spec; - SDL_zero(desired_spec); - desired_spec.freq = 48000; - desired_spec.format = AUDIO_S16LSB; - desired_spec.channels = 2; - desired_spec.samples = 4096; - desired_spec.userdata = this; - - // call back that is invoked once SDL needs the next chunk of data - desired_spec.callback = [] (void *userdata, uint8_t *stream, int len) { - auto *audio_manager = static_cast(userdata); - audio_manager->audio_callback(reinterpret_cast(stream), len / 2); - }; - - // convert device name to valid parameter for sdl call - // if the device name is empty, use a nullptr in order to indicate that the - // default device should be used - const char *c_device_name = device_name.empty() ? - nullptr : device_name.c_str(); - // open audio playback device - device_id = SDL_OpenAudioDevice(c_device_name, 0, &desired_spec, - &device_spec, 0); - - // no device could be opened - if (device_id == 0) { - log::log(MSG(err) << "Error opening audio device: " << SDL_GetError()); + if (not this->device->isFormatSupported(*this->device_format)) { + log::log(MSG(err) << "Audio device does not support the desired format!"); return; } + // create audio sink + this->audio_sink = std::make_unique(*this->device.get(), *this->device_format.get()); + // TODO: connect callback to get audio data to device + // initialize playing sounds vectors using sound_vector = std::vector>; playing_sounds.insert({category_t::GAME, sound_vector{}}); @@ -100,25 +65,23 @@ AudioManager::AudioManager(job::JobManager *job_manager, // create buffer for mixing this->mix_buffer = std::make_unique( - 4 * device_spec.samples * device_spec.channels - ); + 4 * device_format->bytesPerSample() * device_format->channelCount()); - log::log(MSG(info) << - "Using audio device: " - << (device_name.empty() ? "default" : device_name) - << " [freq=" << device_spec.freq - << ", format=" << device_spec.format - << ", channels=" << static_cast(device_spec.channels) - << ", samples=" << device_spec.samples - << "]"); + log::log(MSG(info) << "Using audio device: " + << (device_name.empty() ? "default" : device_name) + << " [sample rate=" << device_format->sampleRate() + << ", format=" << device_format->sampleFormat() + << ", channels=" << device_format->channelCount() + << ", samples=" << device_format->bytesPerSample() + << "]"); - SDL_PauseAudioDevice(device_id, 0); + this->audio_sink->stop(); this->available = true; } AudioManager::~AudioManager() { - SDL_CloseAudioDevice(device_id); + this->audio_sink->stop(); } void AudioManager::load_resources(const std::vector &sound_files) { @@ -148,11 +111,10 @@ Sound AudioManager::get_sound(category_t category, int id) { auto resource = resources.find(std::make_tuple(category, id)); if (resource == std::end(resources)) { throw audio::Error{ - MSG(err) << - "Sound resource does not exist: " - "category=" << category << ", " << - "id=" << id - }; + MSG(err) << "Sound resource does not exist: " + "category=" + << category << ", " + << "id=" << id}; } auto sound_impl = std::make_shared(resource->second); @@ -161,7 +123,7 @@ Sound AudioManager::get_sound(category_t category, int id) { void AudioManager::audio_callback(int16_t *stream, int length) { - std::memset(mix_buffer.get(), 0, length*4); + std::memset(mix_buffer.get(), 0, length * 4); // iterate over all categories for (auto &entry : this->playing_sounds) { @@ -181,28 +143,25 @@ void AudioManager::audio_callback(int16_t *stream, int length) { // write the mix buffer to the output stream and adjust volume for (int i = 0; i < length; i++) { - auto value = mix_buffer[i]/256; + auto value = mix_buffer[i] / 256; if (value > 32767) { value = 32767; - } else if (value < -32768) { + } + else if (value < -32768) { value = -32768; } stream[i] = static_cast(value); } } -void AudioManager::add_sound(const std::shared_ptr& sound) { - SDLDeviceLock lock{this->device_id}; - +void AudioManager::add_sound(const std::shared_ptr &sound) { auto category = sound->get_category(); auto &playing_list = this->playing_sounds.find(category)->second; // TODO probably check if sound already exists in playing list playing_list.push_back(sound); } -void AudioManager::remove_sound(const std::shared_ptr& sound) { - SDLDeviceLock lock{this->device_id}; - +void AudioManager::remove_sound(const std::shared_ptr &sound) { auto category = sound->get_category(); auto &playing_list = this->playing_sounds.find(category)->second; @@ -214,11 +173,11 @@ void AudioManager::remove_sound(const std::shared_ptr& sound) { } } -SDL_AudioSpec AudioManager::get_device_spec() const { - return this->device_spec; +const std::shared_ptr &AudioManager::get_device_spec() const { + return this->device_format; } -job::JobManager *AudioManager::get_job_manager() const { +const std::shared_ptr &AudioManager::get_job_manager() const { return this->job_manager; } @@ -228,32 +187,21 @@ bool AudioManager::is_available() const { std::vector AudioManager::get_devices() { + auto devices = QMediaDevices::audioOutputs(); + std::vector device_list; - auto num_devices = SDL_GetNumAudioDevices(0); - device_list.reserve(num_devices); - for (int i = 0; i < num_devices; i++) { - device_list.emplace_back(SDL_GetAudioDeviceName(i, 0)); - } - return device_list; -} + device_list.reserve(devices.size()); -std::vector AudioManager::get_drivers() { - std::vector driver_list; - auto num_drivers = SDL_GetNumAudioDrivers(); - driver_list.reserve(num_drivers); - for (int i = 0; i < num_drivers; i++) { - driver_list.emplace_back(SDL_GetAudioDriver(i)); + for (auto &device : devices) { + device_list.emplace_back(device.description().toStdString()); } - return driver_list; + + return device_list; } -std::string AudioManager::get_current_driver() { - const char *c_driver = SDL_GetCurrentAudioDriver(); - if (c_driver == nullptr) { - return ""; - } else { - return c_driver; - } +std::string AudioManager::get_default_device() { + return QMediaDevices::defaultAudioOutput().description().toStdString(); } -}} // openage::audio +} // namespace audio +} // namespace openage diff --git a/libopenage/audio/audio_manager.h b/libopenage/audio/audio_manager.h index af14d5ebb2..8d3269a5a2 100644 --- a/libopenage/audio/audio_manager.h +++ b/libopenage/audio/audio_manager.h @@ -7,13 +7,16 @@ #include #include -#include +#include #include "category.h" #include "hash_functions.h" #include "resource_def.h" #include "sound.h" +Q_FORWARD_DECLARE_OBJC_CLASS(QAudioDevice); +Q_FORWARD_DECLARE_OBJC_CLASS(QAudioFormat); +Q_FORWARD_DECLARE_OBJC_CLASS(QAudioSink); namespace openage { @@ -26,6 +29,8 @@ namespace audio { /** * This class provides audio functionality for openage. + * + * TODO: Finish porting to Qt. */ class AudioManager { public: @@ -33,7 +38,7 @@ class AudioManager { * Initializes the audio manager with the given device name. * If the name is empty, the default device is used. */ - AudioManager(job::JobManager *job_manager, + AudioManager(const std::shared_ptr &job_manager, const std::string &device_name = ""); ~AudioManager(); @@ -66,12 +71,12 @@ class AudioManager { /** * Returns the currently used audio output format. */ - SDL_AudioSpec get_device_spec() const; + const std::shared_ptr &get_device_spec() const; /** * Return the game engine the audio manager is attached to. */ - job::JobManager *get_job_manager() const; + const std::shared_ptr &get_job_manager() const; /** * If this audio manager is available. @@ -79,6 +84,16 @@ class AudioManager { */ bool is_available() const; + /** + * Returns a vector of all available device names. + */ + static std::vector get_devices(); + + /** + * Returns the default device name. + */ + static std::string get_default_device(); + private: void add_sound(const std::shared_ptr &sound); void remove_sound(const std::shared_ptr &sound); @@ -96,7 +111,7 @@ class AudioManager { /** * The job manager used in this audio manager for job queuing. */ - job::JobManager *job_manager; + std::shared_ptr job_manager; /** * the used audio device's name @@ -106,12 +121,17 @@ class AudioManager { /** * the audio output format */ - SDL_AudioSpec device_spec; + std::shared_ptr device_format; /** * the used audio device's id */ - SDL_AudioDeviceID device_id; + std::shared_ptr device; + + /** + * the audio sink + */ + std::shared_ptr audio_sink; /** * Buffer used for mixing audio to one stream. @@ -121,23 +141,6 @@ class AudioManager { std::unordered_map, std::shared_ptr> resources; std::unordered_map>> playing_sounds; - - // static functions -public: - /** - * Returns a vector of all available device names. - */ - static std::vector get_devices(); - - /** - * Returns a vector of all available driver names. - */ - static std::vector get_drivers(); - - /** - * Returns the name of the currently used driver. - */ - static std::string get_current_driver(); }; } // namespace audio From ada257f3cc144d9139b68af577548e3343faff99 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Sep 2023 14:54:27 +0200 Subject: [PATCH 046/771] buildsys: Add QtMultimedia dependency. --- doc/build_instructions/arch_linux.md | 2 +- doc/build_instructions/debian.md | 2 +- doc/build_instructions/fedora.md | 2 +- doc/build_instructions/opensuse.md | 2 +- doc/build_instructions/ubuntu.md | 2 +- doc/build_instructions/windows_msvc.md | 2 +- doc/building.md | 2 +- libopenage/CMakeLists.txt | 5 +++-- packaging/docker/devenv/Dockerfile.ubuntu.2204 | 2 ++ 9 files changed, 12 insertions(+), 9 deletions(-) diff --git a/doc/build_instructions/arch_linux.md b/doc/build_instructions/arch_linux.md index 7665cb9888..cd6af02cd7 100644 --- a/doc/build_instructions/arch_linux.md +++ b/doc/build_instructions/arch_linux.md @@ -4,7 +4,7 @@ This command should provide required packages from the Arch Linux repositories: -`sudo pacman -S --needed eigen python python-mako python-pillow python-numpy python-lz4 python-pygments cython libepoxy libogg libpng ttf-dejavu freetype2 fontconfig harfbuzz cmake sdl2 sdl2_image opusfile opus python-pylint python-toml qt6-declarative` +`sudo pacman -S --needed eigen python python-mako python-pillow python-numpy python-lz4 python-pygments cython libepoxy libogg libpng ttf-dejavu freetype2 fontconfig harfbuzz cmake sdl2 sdl2_image opusfile opus python-pylint python-toml qt6-declarative qt6-multimedia` Additionally, you have to install [`toml11`](https://aur.archlinux.org/packages/toml11) from the AUR. If you have `yay`, you can run this command: diff --git a/doc/build_instructions/debian.md b/doc/build_instructions/debian.md index 5381ef165e..d9be204189 100644 --- a/doc/build_instructions/debian.md +++ b/doc/build_instructions/debian.md @@ -1,6 +1,6 @@ # Prerequisite steps for Debian users - `sudo apt-get update` - - `sudo apt-get install cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-dev libsdl2-dev libsdl2-image-dev libtoml11-dev python3-dev python3-mako python3-numpy python3-lz4 python3-pil python3-pip python3-pygments python3-toml qml6-module-qtquick-controls qt6-declarative-dev` + - `sudo apt-get install cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-dev libsdl2-dev libsdl2-image-dev libtoml11-dev python3-dev python3-mako python3-numpy python3-lz4 python3-pil python3-pip python3-pygments python3-toml qml6-module-qtquick-controls qt6-declarative-dev qt6-multimedia-dev qml6-module-qtquick3d-spatialaudio` You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies. diff --git a/doc/build_instructions/fedora.md b/doc/build_instructions/fedora.md index c710a57282..c911ec8eb9 100644 --- a/doc/build_instructions/fedora.md +++ b/doc/build_instructions/fedora.md @@ -2,6 +2,6 @@ Run the following command: -`sudo dnf install clang cmake eigen3-devel fontconfig-devel gcc-c harfbuzz-devel libepoxy-devel libogg-devel libopusenc-devel libpng-devel opusfile-devel python3-Cython python3-devel python3-mako python3-numpy python3-lz4 python3-pillow python3-pygments python3-toml SDL2-devel SDL2_image-devel++ toml11-devel qt6-qtdeclarative-devel` +`sudo dnf install clang cmake eigen3-devel fontconfig-devel gcc-c harfbuzz-devel libepoxy-devel libogg-devel libopusenc-devel libpng-devel opusfile-devel python3-Cython python3-devel python3-mako python3-numpy python3-lz4 python3-pillow python3-pygments python3-toml SDL2-devel SDL2_image-devel++ toml11-devel qt6-qtdeclarative-devel qt6-qtmultimedia-devel` You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies. diff --git a/doc/build_instructions/opensuse.md b/doc/build_instructions/opensuse.md index e2b51c41ef..15166d5032 100644 --- a/doc/build_instructions/opensuse.md +++ b/doc/build_instructions/opensuse.md @@ -1,5 +1,5 @@ # Prerequisite steps for openSUSE users -- `zypper install --no-recommends cmake doxygen eigen3-devel fontconfig-devel gcc-c graphviz++ harfbuzz-devel libSDL2-devel libSDL2_image-devel libepoxy-devel libfreetype-dev libogg-devel libopus-devel libpng-devel libtoml11-dev libqt6-qtdeclarative-devel libqt6-qtquickcontrols opusfile-devel python3-Cython python3-Mako python3-lz4 python3-Pillow python3-Pygments python3-toml python3-devel` +- `zypper install --no-recommends cmake doxygen eigen3-devel fontconfig-devel gcc-c graphviz++ harfbuzz-devel libSDL2-devel libSDL2_image-devel libepoxy-devel libfreetype-dev libogg-devel libopus-devel libpng-devel libtoml11-dev qt6-declarative-dev qt6-quickcontrols2 qt6-multimedia-dev opusfile-devel python3-Cython python3-Mako python3-lz4 python3-Pillow python3-Pygments python3-toml python3-devel` You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies. diff --git a/doc/build_instructions/ubuntu.md b/doc/build_instructions/ubuntu.md index bc151975e2..6565169827 100644 --- a/doc/build_instructions/ubuntu.md +++ b/doc/build_instructions/ubuntu.md @@ -3,7 +3,7 @@ Run the following commands: - `sudo apt-get update` - - `sudo apt-get install g++ cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-dev libsdl2-dev libsdl2-image-dev libtoml11-dev python3-dev python3-mako python3-numpy python3-lz4 python3-pil python3-pip python3-pygments python3-toml qml6-module-qtquick-controls qt6-declarative-dev` + - `sudo apt-get install g++ cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-dev libsdl2-dev libsdl2-image-dev libtoml11-dev python3-dev python3-mako python3-numpy python3-lz4 python3-pil python3-pip python3-pygments python3-toml qml6-module-qtquick-controls qt6-declarative-dev qt6-multimedia-dev qml6-module-qtquick3d-spatialaudio` You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies. diff --git a/doc/build_instructions/windows_msvc.md b/doc/build_instructions/windows_msvc.md index 4c742e0e2c..9ba1c51234 100644 --- a/doc/build_instructions/windows_msvc.md +++ b/doc/build_instructions/windows_msvc.md @@ -45,7 +45,7 @@ _Note:_ Also ensure that `python` and `python3` both point to the correct and th ### vcpkg packages Set up [vcpkg](https://github.com/Microsoft/vcpkg#quick-start). Open a command prompt at `` - vcpkg install dirent eigen3 fontconfig freetype harfbuzz libepoxy libogg libpng opus opusfile qtbase qtdeclarative sdl2 sdl2-image toml11 + vcpkg install dirent eigen3 fontconfig freetype harfbuzz libepoxy libogg libpng opus opusfile qtbase qtdeclarative qtmultimedia sdl2 sdl2-image toml11 _Note:_ The `qt6` port in vcpkg has been split into multiple packages, build times are acceptable now. If you want, you can still use [the prebuilt version](https://www.qt.io/download-open-source/) instead. diff --git a/doc/building.md b/doc/building.md index c5b8af6180..77a87008f0 100644 --- a/doc/building.md +++ b/doc/building.md @@ -56,7 +56,7 @@ Dependency list: S pycodestyle C pygments S pylint - CR qt6 >=6.2 (Core, Quick, QuickControls modules) + CR qt6 >=6.2 (Core, Quick, QuickControls, Multimedia modules) CR toml11 CR O vulkan diff --git a/libopenage/CMakeLists.txt b/libopenage/CMakeLists.txt index 3e12a5766c..7095576dc1 100644 --- a/libopenage/CMakeLists.txt +++ b/libopenage/CMakeLists.txt @@ -65,8 +65,8 @@ set(CMAKE_THREAD_PREFER_PTHREAD TRUE) find_package(Threads REQUIRED) set(QT_VERSION_REQ "6.2") -find_package(Qt6Core ${QT_VERSION_REQ} REQUIRED) -find_package(Qt6Quick ${QT_VERSION_REQ} REQUIRED) +find_package(Qt6 ${QT_VERSION_REQ} REQUIRED COMPONENTS Core Quick Multimedia) +# find_package(Qt6Multimedia ${QT_VERSION_REQ} REQUIRED) if(WANT_BACKTRACE) find_package(GCCBacktrace) @@ -300,6 +300,7 @@ target_link_libraries(libopenage ${EXECINFO_LIB} Qt6::Core Qt6::Quick + Qt6::Multimedia ) ################################################## diff --git a/packaging/docker/devenv/Dockerfile.ubuntu.2204 b/packaging/docker/devenv/Dockerfile.ubuntu.2204 index 40b1dd3abc..0dc50a1960 100644 --- a/packaging/docker/devenv/Dockerfile.ubuntu.2204 +++ b/packaging/docker/devenv/Dockerfile.ubuntu.2204 @@ -35,5 +35,7 @@ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y sudo \ python3-toml \ qml6-module-qtquick-controls \ qt6-declarative-dev \ + qt6-multimedia-dev \ + qml6-module-qtquick3d-spatialaudio \ && sudo apt-get clean \ && truncate -s 0 ~/.bash_history From 4faf7179bbc2c3eabd94940196e38096d28f373e Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Sep 2023 15:38:49 +0200 Subject: [PATCH 047/771] renderer: Fix clang build not compiling. clang cannot correctly substitute for emplacement apparently. --- libopenage/renderer/opengl/shader_data.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libopenage/renderer/opengl/shader_data.cpp b/libopenage/renderer/opengl/shader_data.cpp index d4f7e040e2..1bfd912518 100644 --- a/libopenage/renderer/opengl/shader_data.cpp +++ b/libopenage/renderer/opengl/shader_data.cpp @@ -1,3 +1,10 @@ // Copyright 2023-2023 the openage authors. See copying.md for legal info. #include "shader_data.h" + + +namespace openage::renderer::opengl { + +// this file is intentionally empty + +} // namespace openage::renderer::opengl From c557ff00792d4dbe989e1fe0b8ba1e841d6ee462 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Sep 2023 16:05:25 +0200 Subject: [PATCH 048/771] gui: Remove SDL2 from classes. --- libopenage/gui/gui.cpp | 4 +- libopenage/gui/gui.h | 2 +- .../gui/guisys/private/gui_ctx_setup.cpp | 4 +- libopenage/gui/guisys/private/gui_ctx_setup.h | 14 +- .../gui/guisys/private/gui_input_impl.cpp | 232 +++++++++--------- .../gui/guisys/private/gui_input_impl.h | 8 +- .../gui/guisys/private/gui_renderer_impl.cpp | 4 +- .../gui/guisys/private/gui_renderer_impl.h | 4 +- .../private/gui_rendering_setup_routines.cpp | 25 +- .../private/gui_rendering_setup_routines.h | 14 +- .../private/platforms/context_extraction.h | 10 +- .../platforms/context_extraction_win32.cpp | 66 ++--- .../platforms/context_extraction_x11.cpp | 2 +- libopenage/gui/guisys/public/gui_input.cpp | 7 +- libopenage/gui/guisys/public/gui_input.h | 4 +- libopenage/gui/guisys/public/gui_renderer.cpp | 5 +- libopenage/gui/guisys/public/gui_renderer.h | 4 +- .../renderer/resources/texture_data.cpp | 2 +- libopenage/renderer/resources/texture_data.h | 4 +- .../docker/devenv/Dockerfile.ubuntu.2204 | 2 - 20 files changed, 197 insertions(+), 220 deletions(-) diff --git a/libopenage/gui/gui.cpp b/libopenage/gui/gui.cpp index 590e11124c..eae5553572 100644 --- a/libopenage/gui/gui.cpp +++ b/libopenage/gui/gui.cpp @@ -9,13 +9,13 @@ namespace openage { namespace gui { -GUI::GUI(SDL_Window *window, +GUI::GUI(/* SDL_Window *window, */ const std::string &source, const std::string &rootdir, EngineQMLInfo *info) : application{}, render_updater{}, - renderer{window}, + renderer{/* window */}, game_logic_updater{}, engine{&renderer}, subtree{ diff --git a/libopenage/gui/gui.h b/libopenage/gui/gui.h index 600e4be52a..36273e5462 100644 --- a/libopenage/gui/gui.h +++ b/libopenage/gui/gui.h @@ -30,7 +30,7 @@ class EngineQMLInfo; */ class GUI { public: - explicit GUI(SDL_Window *window, + explicit GUI(/* SDL_Window *window, */ const std::string &source, const std::string &rootdir, EngineQMLInfo *info = nullptr); diff --git a/libopenage/gui/guisys/private/gui_ctx_setup.cpp b/libopenage/gui/guisys/private/gui_ctx_setup.cpp index aea120b89c..3cc34d35fe 100644 --- a/libopenage/gui/guisys/private/gui_ctx_setup.cpp +++ b/libopenage/gui/guisys/private/gui_ctx_setup.cpp @@ -19,7 +19,7 @@ QOpenGLContext *CtxExtractionMode::get_ctx() { return &this->ctx; } -GuiUniqueRenderingContext::GuiUniqueRenderingContext(SDL_Window *window) : +GuiUniqueRenderingContext::GuiUniqueRenderingContext(/* SDL_Window *window */) : CtxExtractionMode{} { QVariant handle; WId id; @@ -50,7 +50,7 @@ void GuiUniqueRenderingContext::pre_render() { void GuiUniqueRenderingContext::post_render() { } -GuiSeparateRenderingContext::GuiSeparateRenderingContext(SDL_Window *window) : +GuiSeparateRenderingContext::GuiSeparateRenderingContext(/* SDL_Window *window */) : CtxExtractionMode{} { QVariant handle; diff --git a/libopenage/gui/guisys/private/gui_ctx_setup.h b/libopenage/gui/guisys/private/gui_ctx_setup.h index bcc88e932a..28d5feb847 100644 --- a/libopenage/gui/guisys/private/gui_ctx_setup.h +++ b/libopenage/gui/guisys/private/gui_ctx_setup.h @@ -2,14 +2,12 @@ #pragma once -#include -#include #include +#include +#include -#include #include - -struct SDL_Window; +#include QT_FORWARD_DECLARE_CLASS(QOpenGLDebugLogger) @@ -31,7 +29,7 @@ class CtxExtractionMode { /** * @return context that can be used by Qt */ - QOpenGLContext* get_ctx(); + QOpenGLContext *get_ctx(); /** * Function that must be called before rendering the GUI. @@ -52,7 +50,7 @@ class CtxExtractionMode { */ class GuiUniqueRenderingContext : public CtxExtractionMode { public: - explicit GuiUniqueRenderingContext(SDL_Window *window); + explicit GuiUniqueRenderingContext(/* SDL_Window *window */); virtual void pre_render() override; virtual void post_render() override; @@ -63,7 +61,7 @@ class GuiUniqueRenderingContext : public CtxExtractionMode { */ class GuiSeparateRenderingContext : public CtxExtractionMode { public: - explicit GuiSeparateRenderingContext(SDL_Window *window); + explicit GuiSeparateRenderingContext(/* SDL_Window *window */); virtual ~GuiSeparateRenderingContext(); virtual void pre_render() override; diff --git a/libopenage/gui/guisys/private/gui_input_impl.cpp b/libopenage/gui/guisys/private/gui_input_impl.cpp index a92daf4134..c0d177ace9 100644 --- a/libopenage/gui/guisys/private/gui_input_impl.cpp +++ b/libopenage/gui/guisys/private/gui_input_impl.cpp @@ -6,8 +6,6 @@ #include #include -#include - #include "../public/gui_event_queue.h" #include "../public/gui_renderer.h" #include "gui_event_queue_impl.h" @@ -29,123 +27,123 @@ GuiInputImpl::GuiInputImpl(GuiRenderer *renderer, GuiEventQueue *game_logic_upda GuiInputImpl::~GuiInputImpl() = default; namespace { -static_assert(!(Qt::LeftButton & (static_cast(Qt::LeftButton) - 1)), "Qt non-one-bit mask."); -static_assert(!(Qt::RightButton & (static_cast(Qt::RightButton) - 1)), "Qt non-one-bit mask."); -static_assert(!(Qt::MiddleButton & (static_cast(Qt::MiddleButton) - 1)), "Qt non-one-bit mask."); -static_assert(!(Qt::XButton1 & (static_cast(Qt::XButton1) - 1)), "Qt non-one-bit mask."); -static_assert(!(Qt::XButton2 & (static_cast(Qt::XButton2) - 1)), "Qt non-one-bit mask."); - -static_assert(SDL_BUTTON_LMASK == Qt::LeftButton, "SDL/Qt mouse button mask incompatibility."); -static_assert(1 << (SDL_BUTTON_LEFT - 1) == Qt::LeftButton, "SDL/Qt mouse button mask incompatibility."); - -// Right and middle are swapped. -static_assert(SDL_BUTTON_RMASK == Qt::MiddleButton, "SDL/Qt mouse button mask incompatibility."); -static_assert(1 << (SDL_BUTTON_RIGHT - 1) == Qt::MiddleButton, "SDL/Qt mouse button mask incompatibility."); - -static_assert(SDL_BUTTON_MMASK == Qt::RightButton, "SDL/Qt mouse button mask incompatibility."); -static_assert(1 << (SDL_BUTTON_MIDDLE - 1) == Qt::RightButton, "SDL/Qt mouse button mask incompatibility."); - -static_assert(SDL_BUTTON_X1MASK == Qt::XButton1, "SDL/Qt mouse button mask incompatibility."); -static_assert(1 << (SDL_BUTTON_X1 - 1) == Qt::XButton1, "SDL/Qt mouse button mask incompatibility."); - -static_assert(SDL_BUTTON_X2MASK == Qt::XButton2, "SDL/Qt mouse button mask incompatibility."); -static_assert(1 << (SDL_BUTTON_X2 - 1) == Qt::XButton2, "SDL/Qt mouse button mask incompatibility."); - -static_assert(Qt::MiddleButton >> 1 == Qt::RightButton, "Qt::RightButton or Qt::MiddleButton has moved."); - -int sdl_mouse_mask_to_qt(Uint32 state) { - return (state & (Qt::LeftButton | Qt::XButton1 | Qt::XButton2)) | ((state & Qt::RightButton) << 1) | ((state & Qt::MiddleButton) >> 1); -} - -Qt::MouseButtons sdl_mouse_state_to_qt(Uint32 state) { - return static_cast(sdl_mouse_mask_to_qt(state)); -} - -Qt::MouseButton sdl_mouse_btn_to_qt(Uint8 button) { - return static_cast(sdl_mouse_mask_to_qt(1 << (button - 1))); -} - -int sdl_key_to_qt(SDL_Keycode sym) { - switch (sym) { - case SDLK_BACKSPACE: - return Qt::Key_Backspace; - case SDLK_DELETE: - return Qt::Key_Delete; - default: - return 0; - } -} +// static_assert(!(Qt::LeftButton & (static_cast(Qt::LeftButton) - 1)), "Qt non-one-bit mask."); +// static_assert(!(Qt::RightButton & (static_cast(Qt::RightButton) - 1)), "Qt non-one-bit mask."); +// static_assert(!(Qt::MiddleButton & (static_cast(Qt::MiddleButton) - 1)), "Qt non-one-bit mask."); +// static_assert(!(Qt::XButton1 & (static_cast(Qt::XButton1) - 1)), "Qt non-one-bit mask."); +// static_assert(!(Qt::XButton2 & (static_cast(Qt::XButton2) - 1)), "Qt non-one-bit mask."); + +// static_assert(SDL_BUTTON_LMASK == Qt::LeftButton, "SDL/Qt mouse button mask incompatibility."); +// static_assert(1 << (SDL_BUTTON_LEFT - 1) == Qt::LeftButton, "SDL/Qt mouse button mask incompatibility."); + +// // Right and middle are swapped. +// static_assert(SDL_BUTTON_RMASK == Qt::MiddleButton, "SDL/Qt mouse button mask incompatibility."); +// static_assert(1 << (SDL_BUTTON_RIGHT - 1) == Qt::MiddleButton, "SDL/Qt mouse button mask incompatibility."); + +// static_assert(SDL_BUTTON_MMASK == Qt::RightButton, "SDL/Qt mouse button mask incompatibility."); +// static_assert(1 << (SDL_BUTTON_MIDDLE - 1) == Qt::RightButton, "SDL/Qt mouse button mask incompatibility."); + +// static_assert(SDL_BUTTON_X1MASK == Qt::XButton1, "SDL/Qt mouse button mask incompatibility."); +// static_assert(1 << (SDL_BUTTON_X1 - 1) == Qt::XButton1, "SDL/Qt mouse button mask incompatibility."); + +// static_assert(SDL_BUTTON_X2MASK == Qt::XButton2, "SDL/Qt mouse button mask incompatibility."); +// static_assert(1 << (SDL_BUTTON_X2 - 1) == Qt::XButton2, "SDL/Qt mouse button mask incompatibility."); + +// static_assert(Qt::MiddleButton >> 1 == Qt::RightButton, "Qt::RightButton or Qt::MiddleButton has moved."); + +// int sdl_mouse_mask_to_qt(Uint32 state) { +// return (state & (Qt::LeftButton | Qt::XButton1 | Qt::XButton2)) | ((state & Qt::RightButton) << 1) | ((state & Qt::MiddleButton) >> 1); +// } + +// Qt::MouseButtons sdl_mouse_state_to_qt(Uint32 state) { +// return static_cast(sdl_mouse_mask_to_qt(state)); +// } + +// Qt::MouseButton sdl_mouse_btn_to_qt(Uint8 button) { +// return static_cast(sdl_mouse_mask_to_qt(1 << (button - 1))); +// } + +// int sdl_key_to_qt(SDL_Keycode sym) { +// switch (sym) { +// case SDLK_BACKSPACE: +// return Qt::Key_Backspace; +// case SDLK_DELETE: +// return Qt::Key_Delete; +// default: +// return 0; +// } +// } } // namespace -bool GuiInputImpl::process(SDL_Event *e) { - switch (e->type) { - case SDL_MOUSEMOTION: { - QMouseEvent ev{QEvent::MouseMove, QPoint{e->motion.x, e->motion.y}, Qt::MouseButton::NoButton, this->mouse_buttons_state = sdl_mouse_state_to_qt(e->motion.state), Qt::KeyboardModifier::NoModifier}; - ev.setAccepted(false); - - // Allow dragging stuff under the gui overlay. - return relay_input_event(&ev, e->motion.state & (SDL_BUTTON_LMASK | SDL_BUTTON_MMASK | SDL_BUTTON_RMASK)); - } - - case SDL_MOUSEBUTTONDOWN: { - auto button = sdl_mouse_btn_to_qt(e->button.button); - QMouseEvent ev{QEvent::MouseButtonPress, QPoint{e->button.x, e->button.y}, button, this->mouse_buttons_state |= button, Qt::KeyboardModifier::NoModifier}; - ev.setAccepted(false); - - bool accepted = relay_input_event(&ev); - - if (e->button.clicks == 2) { - QMouseEvent ev_dbl{QEvent::MouseButtonDblClick, QPoint{e->button.x, e->button.y}, button, this->mouse_buttons_state, Qt::KeyboardModifier::NoModifier}; - ev_dbl.setAccepted(false); - accepted = relay_input_event(&ev_dbl) || accepted; - } - - return accepted; - } - - case SDL_MOUSEBUTTONUP: { - auto button = sdl_mouse_btn_to_qt(e->button.button); - QMouseEvent ev{QEvent::MouseButtonRelease, QPoint{e->button.x, e->button.y}, button, this->mouse_buttons_state &= ~button, Qt::KeyboardModifier::NoModifier}; - ev.setAccepted(false); - - // Allow dragging stuff under the gui overlay: when no item is grabbed, it probably means that initial MousButtonPress was outside gui. - return relay_input_event(&ev, true); - } - - case SDL_MOUSEWHEEL: { - QPoint pos; - SDL_GetMouseState(&pos.rx(), &pos.ry()); - - QWheelEvent ev{ - pos, - pos, - QPoint{}, - QPoint{e->wheel.x, e->wheel.y}, - this->mouse_buttons_state, - Qt::KeyboardModifier::NoModifier, // Correct states? - Qt::ScrollPhase::NoScrollPhase, // ^ - false, - }; - ev.setAccepted(false); - - return relay_input_event(&ev); - } - - case SDL_KEYDOWN: { - QKeyEvent ev{QEvent::KeyPress, sdl_key_to_qt(e->key.keysym.sym), Qt::NoModifier, QChar(static_cast(e->key.keysym.sym))}; - ev.setAccepted(false); - return relay_input_event(&ev); - } - - case SDL_KEYUP: { - QKeyEvent ev{QEvent::KeyRelease, sdl_key_to_qt(e->key.keysym.sym), Qt::NoModifier, QChar(static_cast(e->key.keysym.sym))}; - ev.setAccepted(false); - return relay_input_event(&ev); - } - - default: - return false; - } +bool GuiInputImpl::process(/* SDL_Event *e */) { + // switch (e->type) { + // case SDL_MOUSEMOTION: { + // QMouseEvent ev{QEvent::MouseMove, QPoint{e->motion.x, e->motion.y}, Qt::MouseButton::NoButton, this->mouse_buttons_state = sdl_mouse_state_to_qt(e->motion.state), Qt::KeyboardModifier::NoModifier}; + // ev.setAccepted(false); + + // // Allow dragging stuff under the gui overlay. + // return relay_input_event(&ev, e->motion.state & (SDL_BUTTON_LMASK | SDL_BUTTON_MMASK | SDL_BUTTON_RMASK)); + // } + + // case SDL_MOUSEBUTTONDOWN: { + // auto button = sdl_mouse_btn_to_qt(e->button.button); + // QMouseEvent ev{QEvent::MouseButtonPress, QPoint{e->button.x, e->button.y}, button, this->mouse_buttons_state |= button, Qt::KeyboardModifier::NoModifier}; + // ev.setAccepted(false); + + // bool accepted = relay_input_event(&ev); + + // if (e->button.clicks == 2) { + // QMouseEvent ev_dbl{QEvent::MouseButtonDblClick, QPoint{e->button.x, e->button.y}, button, this->mouse_buttons_state, Qt::KeyboardModifier::NoModifier}; + // ev_dbl.setAccepted(false); + // accepted = relay_input_event(&ev_dbl) || accepted; + // } + + // return accepted; + // } + + // case SDL_MOUSEBUTTONUP: { + // auto button = sdl_mouse_btn_to_qt(e->button.button); + // QMouseEvent ev{QEvent::MouseButtonRelease, QPoint{e->button.x, e->button.y}, button, this->mouse_buttons_state &= ~button, Qt::KeyboardModifier::NoModifier}; + // ev.setAccepted(false); + + // // Allow dragging stuff under the gui overlay: when no item is grabbed, it probably means that initial MousButtonPress was outside gui. + // return relay_input_event(&ev, true); + // } + + // case SDL_MOUSEWHEEL: { + // QPoint pos; + // SDL_GetMouseState(&pos.rx(), &pos.ry()); + + // QWheelEvent ev{ + // pos, + // pos, + // QPoint{}, + // QPoint{e->wheel.x, e->wheel.y}, + // this->mouse_buttons_state, + // Qt::KeyboardModifier::NoModifier, // Correct states? + // Qt::ScrollPhase::NoScrollPhase, // ^ + // false, + // }; + // ev.setAccepted(false); + + // return relay_input_event(&ev); + // } + + // case SDL_KEYDOWN: { + // QKeyEvent ev{QEvent::KeyPress, sdl_key_to_qt(e->key.keysym.sym), Qt::NoModifier, QChar(static_cast(e->key.keysym.sym))}; + // ev.setAccepted(false); + // return relay_input_event(&ev); + // } + + // case SDL_KEYUP: { + // QKeyEvent ev{QEvent::KeyRelease, sdl_key_to_qt(e->key.keysym.sym), Qt::NoModifier, QChar(static_cast(e->key.keysym.sym))}; + // ev.setAccepted(false); + // return relay_input_event(&ev); + // } + + // default: + // return false; + // } } bool GuiInputImpl::relay_input_event(QEvent *ev, bool only_if_grabbed) { diff --git a/libopenage/gui/guisys/private/gui_input_impl.h b/libopenage/gui/guisys/private/gui_input_impl.h index c3665159a5..8272188c7f 100644 --- a/libopenage/gui/guisys/private/gui_input_impl.h +++ b/libopenage/gui/guisys/private/gui_input_impl.h @@ -6,8 +6,6 @@ #include -#include - namespace qtsdl { class GuiRenderer; @@ -24,13 +22,13 @@ class GuiInputImpl : public QObject { /** * Returns true if the event was accepted. */ - bool process(SDL_Event *e); + bool process(/* SDL_Event *e */); signals: - void input_event(std::atomic *processed, QEvent *ev, bool only_if_grabbed=false); + void input_event(std::atomic *processed, QEvent *ev, bool only_if_grabbed = false); private: - bool relay_input_event(QEvent *ev, bool only_if_grabbed=false); + bool relay_input_event(QEvent *ev, bool only_if_grabbed = false); Qt::MouseButtons mouse_buttons_state; GuiEventQueueImpl *game_logic_updater; diff --git a/libopenage/gui/guisys/private/gui_renderer_impl.cpp b/libopenage/gui/guisys/private/gui_renderer_impl.cpp index cf3bed6a63..a75974655f 100644 --- a/libopenage/gui/guisys/private/gui_renderer_impl.cpp +++ b/libopenage/gui/guisys/private/gui_renderer_impl.cpp @@ -74,9 +74,9 @@ void EventHandlingQuickWindow::on_resized(const QSize &size) { this->resize(size); } -GuiRendererImpl::GuiRendererImpl(SDL_Window *window) : +GuiRendererImpl::GuiRendererImpl(/* SDL_Window *window */) : QObject{}, - gui_rendering_setup_routines{window}, + gui_rendering_setup_routines{/* window */}, need_fbo_resize{true}, need_sync{}, need_render{}, diff --git a/libopenage/gui/guisys/private/gui_renderer_impl.h b/libopenage/gui/guisys/private/gui_renderer_impl.h index 31c1330eea..45186f08c3 100644 --- a/libopenage/gui/guisys/private/gui_renderer_impl.h +++ b/libopenage/gui/guisys/private/gui_renderer_impl.h @@ -25,8 +25,6 @@ #include "gui_rendering_setup_routines.h" -struct SDL_Window; - QT_FORWARD_DECLARE_CLASS(QOpenGLFramebufferObject) namespace qtsdl { @@ -56,7 +54,7 @@ class GuiRendererImpl : public QObject { Q_OBJECT public: - explicit GuiRendererImpl(SDL_Window *window); + explicit GuiRendererImpl(/* SDL_Window *window */); ~GuiRendererImpl(); static GuiRendererImpl *impl(GuiRenderer *renderer); diff --git a/libopenage/gui/guisys/private/gui_rendering_setup_routines.cpp b/libopenage/gui/guisys/private/gui_rendering_setup_routines.cpp index cfb6745438..3fdf554f98 100644 --- a/libopenage/gui/guisys/private/gui_rendering_setup_routines.cpp +++ b/libopenage/gui/guisys/private/gui_rendering_setup_routines.cpp @@ -10,16 +10,17 @@ namespace qtsdl { -GuiRenderingSetupRoutines::GuiRenderingSetupRoutines(SDL_Window *window) { +GuiRenderingSetupRoutines::GuiRenderingSetupRoutines(/* SDL_Window *window */) { try { - this->ctx_extraction_mode = std::make_unique(window); - } catch (const CtxExtractionException&) { - + this->ctx_extraction_mode = std::make_unique(/* window */); + } + catch (const CtxExtractionException &) { qInfo() << "Falling back to separate render context for GUI"; try { - this->ctx_extraction_mode = std::make_unique(window); - } catch (const CtxExtractionException&) { + this->ctx_extraction_mode = std::make_unique(/* window */); + } + catch (const CtxExtractionException &) { assert(false && "setting up context for GUI failed"); } } @@ -27,7 +28,7 @@ GuiRenderingSetupRoutines::GuiRenderingSetupRoutines(SDL_Window *window) { GuiRenderingSetupRoutines::~GuiRenderingSetupRoutines() = default; -QOpenGLContext* GuiRenderingSetupRoutines::get_ctx() { +QOpenGLContext *GuiRenderingSetupRoutines::get_ctx() { return this->ctx_extraction_mode->get_ctx(); } @@ -39,10 +40,8 @@ void GuiRenderingSetupRoutines::post_render() { this->ctx_extraction_mode->post_render(); } -GuiRenderingCtxActivator::GuiRenderingCtxActivator(GuiRenderingSetupRoutines &rendering_setup_routines) - : +GuiRenderingCtxActivator::GuiRenderingCtxActivator(GuiRenderingSetupRoutines &rendering_setup_routines) : rendering_setup_routines{&rendering_setup_routines} { - this->rendering_setup_routines->pre_render(); } @@ -51,14 +50,12 @@ GuiRenderingCtxActivator::~GuiRenderingCtxActivator() { this->rendering_setup_routines->post_render(); } -GuiRenderingCtxActivator::GuiRenderingCtxActivator(GuiRenderingCtxActivator&& o) - : +GuiRenderingCtxActivator::GuiRenderingCtxActivator(GuiRenderingCtxActivator &&o) : rendering_setup_routines{o.rendering_setup_routines} { - o.rendering_setup_routines = nullptr; } -GuiRenderingCtxActivator& GuiRenderingCtxActivator::operator=(GuiRenderingCtxActivator&& o) { +GuiRenderingCtxActivator &GuiRenderingCtxActivator::operator=(GuiRenderingCtxActivator &&o) { this->rendering_setup_routines = o.rendering_setup_routines; o.rendering_setup_routines = nullptr; return *this; diff --git a/libopenage/gui/guisys/private/gui_rendering_setup_routines.h b/libopenage/gui/guisys/private/gui_rendering_setup_routines.h index e392d70cb1..b813f85745 100644 --- a/libopenage/gui/guisys/private/gui_rendering_setup_routines.h +++ b/libopenage/gui/guisys/private/gui_rendering_setup_routines.h @@ -6,8 +6,6 @@ #include -struct SDL_Window; - QT_FORWARD_DECLARE_CLASS(QOpenGLContext) namespace qtsdl { @@ -22,10 +20,10 @@ class GuiRenderingCtxActivator; */ class GuiRenderingSetupRoutines { public: - explicit GuiRenderingSetupRoutines(SDL_Window *window); + explicit GuiRenderingSetupRoutines(/* SDL_Window *window */); ~GuiRenderingSetupRoutines(); - QOpenGLContext* get_ctx(); + QOpenGLContext *get_ctx(); private: friend class GuiRenderingCtxActivator; @@ -44,12 +42,12 @@ class GuiRenderingCtxActivator { explicit GuiRenderingCtxActivator(GuiRenderingSetupRoutines &rendering_setup_routines); ~GuiRenderingCtxActivator(); - GuiRenderingCtxActivator(GuiRenderingCtxActivator&& o); - GuiRenderingCtxActivator& operator=(GuiRenderingCtxActivator&& o); + GuiRenderingCtxActivator(GuiRenderingCtxActivator &&o); + GuiRenderingCtxActivator &operator=(GuiRenderingCtxActivator &&o); private: - GuiRenderingCtxActivator(const GuiRenderingCtxActivator&) = delete; - GuiRenderingCtxActivator& operator=(const GuiRenderingCtxActivator&) = delete; + GuiRenderingCtxActivator(const GuiRenderingCtxActivator &) = delete; + GuiRenderingCtxActivator &operator=(const GuiRenderingCtxActivator &) = delete; GuiRenderingSetupRoutines *rendering_setup_routines; }; diff --git a/libopenage/gui/guisys/private/platforms/context_extraction.h b/libopenage/gui/guisys/private/platforms/context_extraction.h index baf13818bb..1918a3d49c 100644 --- a/libopenage/gui/guisys/private/platforms/context_extraction.h +++ b/libopenage/gui/guisys/private/platforms/context_extraction.h @@ -2,24 +2,22 @@ #pragma once -#include #include +#include -#include #include - -struct SDL_Window; +#include namespace qtsdl { /** * @return current context (or null) and id of the window */ -std::tuple extract_native_context(SDL_Window *window); +// std::tuple extract_native_context(SDL_Window *window); /** * @return current context (or null) and function to get it back to the window */ -std::tuple> extract_native_context_and_switchback_func(SDL_Window *window); +// std::tuple> extract_native_context_and_switchback_func(SDL_Window *window); } // namespace qtsdl diff --git a/libopenage/gui/guisys/private/platforms/context_extraction_win32.cpp b/libopenage/gui/guisys/private/platforms/context_extraction_win32.cpp index 86aba09703..87f4dbf953 100644 --- a/libopenage/gui/guisys/private/platforms/context_extraction_win32.cpp +++ b/libopenage/gui/guisys/private/platforms/context_extraction_win32.cpp @@ -2,46 +2,46 @@ #include -#include #include "context_extraction.h" +#include #include -#include +// #include #include namespace qtsdl { -std::tuple extract_native_context(SDL_Window *window) { - assert(window); - - HGLRC current_context; - SDL_SysWMinfo wm_info; - SDL_VERSION(&wm_info.version); - if (SDL_GetWindowWMInfo(window, &wm_info)) { - current_context = wglGetCurrentContext(); - assert(current_context); - } - QWGLNativeContext nativeContext(current_context, wm_info.info.win.window); - return {QVariant::fromValue(nativeContext), reinterpret_cast(wm_info.info.win.window)}; -} - -std::tuple> extract_native_context_and_switchback_func(SDL_Window *window) { - assert(window); - - HGLRC current_context; - SDL_SysWMinfo wm_info; - SDL_VERSION(&wm_info.version); - if (SDL_GetWindowWMInfo(window, &wm_info)) { - current_context = wglGetCurrentContext(); - assert(current_context); - - return std::make_tuple(QVariant::fromValue(QWGLNativeContext(current_context, wm_info.info.win.window)), [wm_info, current_context] { - wglMakeCurrent(wm_info.info.win.hdc, current_context); - }); - } - - return std::tuple>{}; -} +// std::tuple extract_native_context(/* SDL_Window *window */) { +// assert(window); + +// HGLRC current_context; +// SDL_SysWMinfo wm_info; +// SDL_VERSION(&wm_info.version); +// if (SDL_GetWindowWMInfo(window, &wm_info)) { +// current_context = wglGetCurrentContext(); +// assert(current_context); +// } +// QWGLNativeContext nativeContext(current_context, wm_info.info.win.window); +// return {QVariant::fromValue(nativeContext), reinterpret_cast(wm_info.info.win.window)}; +// } + +// std::tuple> extract_native_context_and_switchback_func(/* SDL_Window *window */) { +// assert(window); + +// HGLRC current_context; +// SDL_SysWMinfo wm_info; +// SDL_VERSION(&wm_info.version); +// if (SDL_GetWindowWMInfo(window, &wm_info)) { +// current_context = wglGetCurrentContext(); +// assert(current_context); + +// return std::make_tuple(QVariant::fromValue(QWGLNativeContext(current_context, wm_info.info.win.window)), [wm_info, current_context] { +// wglMakeCurrent(wm_info.info.win.hdc, current_context); +// }); +// } + +// return std::tuple>{}; +// } } // namespace qtsdl diff --git a/libopenage/gui/guisys/private/platforms/context_extraction_x11.cpp b/libopenage/gui/guisys/private/platforms/context_extraction_x11.cpp index e320c9b6e4..333307c1f9 100644 --- a/libopenage/gui/guisys/private/platforms/context_extraction_x11.cpp +++ b/libopenage/gui/guisys/private/platforms/context_extraction_x11.cpp @@ -6,7 +6,7 @@ // #include #include -#include +// #include // DO NOT INCLUDE ANYTHING HERE, X11 HEADERS BREAK STUFF diff --git a/libopenage/gui/guisys/public/gui_input.cpp b/libopenage/gui/guisys/public/gui_input.cpp index c2e007d944..736c2df8cf 100644 --- a/libopenage/gui/guisys/public/gui_input.cpp +++ b/libopenage/gui/guisys/public/gui_input.cpp @@ -6,15 +6,14 @@ namespace qtsdl { -GuiInput:: GuiInput(GuiRenderer *renderer, GuiEventQueue *game_logic_updater) - : +GuiInput::GuiInput(GuiRenderer *renderer, GuiEventQueue *game_logic_updater) : impl{std::make_unique(renderer, game_logic_updater)} { } GuiInput::~GuiInput() = default; -bool GuiInput::process(SDL_Event *event) { - return this->impl->process(event); +bool GuiInput::process(/* SDL_Event *event */) { + return this->impl->process(/* event */); } } // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_input.h b/libopenage/gui/guisys/public/gui_input.h index 74e8adb118..557c53e67f 100644 --- a/libopenage/gui/guisys/public/gui_input.h +++ b/libopenage/gui/guisys/public/gui_input.h @@ -4,8 +4,6 @@ #include -#include - namespace qtsdl { class GuiRenderer; @@ -23,7 +21,7 @@ class GuiInput { /** * Returns true if the event was accepted. */ - bool process(SDL_Event *event); + bool process(/* SDL_Event *event */); private: friend class GuiInputImpl; diff --git a/libopenage/gui/guisys/public/gui_renderer.cpp b/libopenage/gui/guisys/public/gui_renderer.cpp index 7fb182f0b0..3a0fb99092 100644 --- a/libopenage/gui/guisys/public/gui_renderer.cpp +++ b/libopenage/gui/guisys/public/gui_renderer.cpp @@ -8,9 +8,8 @@ namespace qtsdl { -GuiRenderer::GuiRenderer(SDL_Window *window) - : - impl{std::make_unique(window)} { +GuiRenderer::GuiRenderer(/* SDL_Window *window */) : + impl{std::make_unique(/* window */)} { } GuiRenderer::~GuiRenderer() = default; diff --git a/libopenage/gui/guisys/public/gui_renderer.h b/libopenage/gui/guisys/public/gui_renderer.h index 7139dee405..93ff6dfd89 100644 --- a/libopenage/gui/guisys/public/gui_renderer.h +++ b/libopenage/gui/guisys/public/gui_renderer.h @@ -14,8 +14,6 @@ #include #endif -struct SDL_Window; - namespace qtsdl { class GuiRendererImpl; @@ -26,7 +24,7 @@ class GuiRendererImpl; class GuiRenderer { public: // TODO: allow FBO variant - explicit GuiRenderer(SDL_Window *window); + explicit GuiRenderer(/* SDL_Window *window */); ~GuiRenderer(); GLuint render(); diff --git a/libopenage/renderer/resources/texture_data.cpp b/libopenage/renderer/resources/texture_data.cpp index 0e765b61d7..fe11c19ce5 100644 --- a/libopenage/renderer/resources/texture_data.cpp +++ b/libopenage/renderer/resources/texture_data.cpp @@ -177,7 +177,7 @@ void Texture2dData::store(const util::Path &file) const { QImage image{this->data.data(), size.first, size.second, pix_fmt}; - // Call sdl_image for saving the screenshot to PNG + // Call QImage for saving the screenshot to PNG std::string path = file.resolve_native_path_w(); image.save(path.c_str()); } diff --git a/libopenage/renderer/resources/texture_data.h b/libopenage/renderer/resources/texture_data.h index 0107959d36..004a2bda1e 100644 --- a/libopenage/renderer/resources/texture_data.h +++ b/libopenage/renderer/resources/texture_data.h @@ -31,8 +31,8 @@ class Texture2dData { /// Create a texture from info. /// - /// Uses SDL Image internally. For supported image file types, - /// see the SDL_Image initialization in the engine. + /// 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. diff --git a/packaging/docker/devenv/Dockerfile.ubuntu.2204 b/packaging/docker/devenv/Dockerfile.ubuntu.2204 index 0dc50a1960..4976e0c693 100644 --- a/packaging/docker/devenv/Dockerfile.ubuntu.2204 +++ b/packaging/docker/devenv/Dockerfile.ubuntu.2204 @@ -20,8 +20,6 @@ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y sudo \ libopus-dev \ libopusfile-dev \ libpng-dev \ - libsdl2-dev \ - libsdl2-image-dev \ libtoml11-dev \ make \ ninja-build \ From 1899b16185c5fd0da1cf87b9091285d783d77fb3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Sep 2023 16:05:48 +0200 Subject: [PATCH 049/771] buildsys: Remove SDL2 from dependencies. --- .github/workflows/macosx-ci.yml | 2 +- buildsystem/modules/FindSDL2Image.cmake | 29 ------------------- doc/build_instructions/arch_linux.md | 2 +- doc/build_instructions/debian.md | 2 +- doc/build_instructions/fedora.md | 2 +- doc/build_instructions/freebsd.md | 2 +- doc/build_instructions/macos.md | 2 +- doc/build_instructions/opensuse.md | 2 +- doc/build_instructions/ubuntu.md | 2 +- doc/build_instructions/windows_msvc.md | 2 +- doc/building.md | 6 ++-- doc/code_style/mom.cpp | 9 ++++-- doc/troubleshooting.md | 15 ---------- libopenage/CMakeLists.txt | 6 ---- libopenage/audio/audio_manager.cpp | 2 +- libopenage/gui/guisys/private/gui_ctx_setup.h | 2 +- .../gui/guisys/private/gui_input_impl.h | 2 +- .../private/gui_rendering_setup_routines.cpp | 2 +- .../private/gui_rendering_setup_routines.h | 2 +- .../private/platforms/context_extraction.h | 2 +- .../platforms/context_extraction_win32.cpp | 2 +- libopenage/gui/guisys/public/gui_input.cpp | 2 +- libopenage/gui/guisys/public/gui_input.h | 2 +- libopenage/gui/guisys/public/gui_renderer.cpp | 2 +- libopenage/gui/guisys/public/gui_renderer.h | 2 +- libopenage/versions/versions.cpp | 2 +- shell.nix | 5 ++-- 27 files changed, 31 insertions(+), 81 deletions(-) delete mode 100644 buildsystem/modules/FindSDL2Image.cmake diff --git a/.github/workflows/macosx-ci.yml b/.github/workflows/macosx-ci.yml index d7b86776db..e38c9e3e6e 100644 --- a/.github/workflows/macosx-ci.yml +++ b/.github/workflows/macosx-ci.yml @@ -59,7 +59,7 @@ jobs: - name: Install environment helpers with homebrew run: brew install ccache - name: Install dependencies with homebrew - run: brew install libepoxy freetype fontconfig harfbuzz sdl2 sdl2_image opus opusfile qt6 libogg libpng toml11 eigen + run: brew install libepoxy freetype fontconfig harfbuzz opus opusfile qt6 libogg libpng toml11 eigen - name: Install nyan dependencies with homebrew run: brew install flex make - name: Install python3 packages diff --git a/buildsystem/modules/FindSDL2Image.cmake b/buildsystem/modules/FindSDL2Image.cmake deleted file mode 100644 index 74b36be8ba..0000000000 --- a/buildsystem/modules/FindSDL2Image.cmake +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2014-2017 the openage authors. See copying.md for legal info. - -find_package(PackageHandleStandardArgs) - -if(APPLE) - find_library(SDL2IMAGE_LIBRARIES SDL2_image DOC "SDL2 library framework for MacOS X") - find_path(SDL2IMAGE_INCLUDE_DIRS SDL_image.h - HINTS - $ENV{SDL2DIR} - PATH_SUFFIXES include/SDL2 include - PATHS - ~/Library/Frameworks - /Library/Frameworks - /usr/local/include/SDL2 - /usr/include/SDL2 - /sw # Fink - /opt/local # DarwinPorts - /opt/csw # Blastwave - /opt - DOC "Include directory for SDL2_image under MacOS X" - ) -else() - find_library(SDL2IMAGE_LIBRARIES SDL2_image DOC "SDL2 library") - find_path(SDL2IMAGE_INCLUDE_DIRS SDL2/SDL_image.h DOC "Include directory for SDL2_image") -endif() - -# handle the QUIETLY and REQUIRED arguments and set SDL2Image_FOUND to TRUE if -# all listed variables are TRUE -find_package_handle_standard_args(SDL2Image DEFAULT_MSG SDL2IMAGE_LIBRARIES) diff --git a/doc/build_instructions/arch_linux.md b/doc/build_instructions/arch_linux.md index cd6af02cd7..9c42ff64fd 100644 --- a/doc/build_instructions/arch_linux.md +++ b/doc/build_instructions/arch_linux.md @@ -4,7 +4,7 @@ This command should provide required packages from the Arch Linux repositories: -`sudo pacman -S --needed eigen python python-mako python-pillow python-numpy python-lz4 python-pygments cython libepoxy libogg libpng ttf-dejavu freetype2 fontconfig harfbuzz cmake sdl2 sdl2_image opusfile opus python-pylint python-toml qt6-declarative qt6-multimedia` +`sudo pacman -S --needed eigen python python-mako python-pillow python-numpy python-lz4 python-pygments cython libepoxy libogg libpng ttf-dejavu freetype2 fontconfig harfbuzz cmake opusfile opus python-pylint python-toml qt6-declarative qt6-multimedia` Additionally, you have to install [`toml11`](https://aur.archlinux.org/packages/toml11) from the AUR. If you have `yay`, you can run this command: diff --git a/doc/build_instructions/debian.md b/doc/build_instructions/debian.md index d9be204189..de74a192fc 100644 --- a/doc/build_instructions/debian.md +++ b/doc/build_instructions/debian.md @@ -1,6 +1,6 @@ # Prerequisite steps for Debian users - `sudo apt-get update` - - `sudo apt-get install cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-dev libsdl2-dev libsdl2-image-dev libtoml11-dev python3-dev python3-mako python3-numpy python3-lz4 python3-pil python3-pip python3-pygments python3-toml qml6-module-qtquick-controls qt6-declarative-dev qt6-multimedia-dev qml6-module-qtquick3d-spatialaudio` + - `sudo apt-get install cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-dev libtoml11-dev python3-dev python3-mako python3-numpy python3-lz4 python3-pil python3-pip python3-pygments python3-toml qml6-module-qtquick-controls qt6-declarative-dev qt6-multimedia-dev qml6-module-qtquick3d-spatialaudio` You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies. diff --git a/doc/build_instructions/fedora.md b/doc/build_instructions/fedora.md index c911ec8eb9..e18e5f74c2 100644 --- a/doc/build_instructions/fedora.md +++ b/doc/build_instructions/fedora.md @@ -2,6 +2,6 @@ Run the following command: -`sudo dnf install clang cmake eigen3-devel fontconfig-devel gcc-c harfbuzz-devel libepoxy-devel libogg-devel libopusenc-devel libpng-devel opusfile-devel python3-Cython python3-devel python3-mako python3-numpy python3-lz4 python3-pillow python3-pygments python3-toml SDL2-devel SDL2_image-devel++ toml11-devel qt6-qtdeclarative-devel qt6-qtmultimedia-devel` +`sudo dnf install clang cmake eigen3-devel fontconfig-devel gcc-c harfbuzz-devel libepoxy-devel libogg-devel libopusenc-devel libpng-devel opusfile-devel python3-Cython python3-devel python3-mako python3-numpy python3-lz4 python3-pillow python3-pygments python3-toml toml11-devel qt6-qtdeclarative-devel qt6-qtmultimedia-devel` You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies. diff --git a/doc/build_instructions/freebsd.md b/doc/build_instructions/freebsd.md index b52d7e6177..fb53e1e9e2 100644 --- a/doc/build_instructions/freebsd.md +++ b/doc/build_instructions/freebsd.md @@ -2,7 +2,7 @@ This command should provide required packages for FreeBSD installation: -`sudo pkg install cmake cython eigen3 harfbuzz opus-tools opusfile png py-mako py-numpy py-lz4 py-pillow py-pygments py-toml pylint python qt6 sdl2 sdl2_image toml11` +`sudo pkg install cmake cython eigen3 harfbuzz opus-tools opusfile png py-mako py-numpy py-lz4 py-pillow py-pygments py-toml pylint python qt6 toml11` You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies. diff --git a/doc/build_instructions/macos.md b/doc/build_instructions/macos.md index ffe6e64f67..ba139c3b12 100644 --- a/doc/build_instructions/macos.md +++ b/doc/build_instructions/macos.md @@ -8,7 +8,7 @@ brew update-reset && brew update brew tap homebrew/cask-fonts brew install font-dejavu -brew install cmake python3 libepoxy freetype fontconfig harfbuzz sdl2 sdl2_image opus opusfile qt6 libogg libpng toml11 eigen +brew install cmake python3 libepoxy freetype fontconfig harfbuzz opus opusfile qt6 libogg libpng toml11 eigen brew install llvm pip3 install cython numpy mako lz4 pillow pygments toml diff --git a/doc/build_instructions/opensuse.md b/doc/build_instructions/opensuse.md index 15166d5032..8b967b1779 100644 --- a/doc/build_instructions/opensuse.md +++ b/doc/build_instructions/opensuse.md @@ -1,5 +1,5 @@ # Prerequisite steps for openSUSE users -- `zypper install --no-recommends cmake doxygen eigen3-devel fontconfig-devel gcc-c graphviz++ harfbuzz-devel libSDL2-devel libSDL2_image-devel libepoxy-devel libfreetype-dev libogg-devel libopus-devel libpng-devel libtoml11-dev qt6-declarative-dev qt6-quickcontrols2 qt6-multimedia-dev opusfile-devel python3-Cython python3-Mako python3-lz4 python3-Pillow python3-Pygments python3-toml python3-devel` +- `zypper install --no-recommends cmake doxygen eigen3-devel fontconfig-devel gcc-c graphviz++ harfbuzz-devel libepoxy-devel libfreetype-dev libogg-devel libopus-devel libpng-devel libtoml11-dev qt6-declarative-dev qt6-quickcontrols2 qt6-multimedia-dev opusfile-devel python3-Cython python3-Mako python3-lz4 python3-Pillow python3-Pygments python3-toml python3-devel` You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies. diff --git a/doc/build_instructions/ubuntu.md b/doc/build_instructions/ubuntu.md index 6565169827..08fc635abb 100644 --- a/doc/build_instructions/ubuntu.md +++ b/doc/build_instructions/ubuntu.md @@ -3,7 +3,7 @@ Run the following commands: - `sudo apt-get update` - - `sudo apt-get install g++ cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-dev libsdl2-dev libsdl2-image-dev libtoml11-dev python3-dev python3-mako python3-numpy python3-lz4 python3-pil python3-pip python3-pygments python3-toml qml6-module-qtquick-controls qt6-declarative-dev qt6-multimedia-dev qml6-module-qtquick3d-spatialaudio` + - `sudo apt-get install g++ cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-dev libtoml11-dev python3-dev python3-mako python3-numpy python3-lz4 python3-pil python3-pip python3-pygments python3-toml qml6-module-qtquick-controls qt6-declarative-dev qt6-multimedia-dev qml6-module-qtquick3d-spatialaudio` You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies. diff --git a/doc/build_instructions/windows_msvc.md b/doc/build_instructions/windows_msvc.md index 9ba1c51234..5be6d9167c 100644 --- a/doc/build_instructions/windows_msvc.md +++ b/doc/build_instructions/windows_msvc.md @@ -45,7 +45,7 @@ _Note:_ Also ensure that `python` and `python3` both point to the correct and th ### vcpkg packages Set up [vcpkg](https://github.com/Microsoft/vcpkg#quick-start). Open a command prompt at `` - vcpkg install dirent eigen3 fontconfig freetype harfbuzz libepoxy libogg libpng opus opusfile qtbase qtdeclarative qtmultimedia sdl2 sdl2-image toml11 + vcpkg install dirent eigen3 fontconfig freetype harfbuzz libepoxy libogg libpng opus opusfile qtbase qtdeclarative qtmultimedia toml11 _Note:_ The `qt6` port in vcpkg has been split into multiple packages, build times are acceptable now. If you want, you can still use [the prebuilt version](https://www.qt.io/download-open-source/) instead. diff --git a/doc/building.md b/doc/building.md index 77a87008f0..e7e0339d64 100644 --- a/doc/building.md +++ b/doc/building.md @@ -48,8 +48,6 @@ Dependency list: CR nyan (https://github.com/SFTtech/nyan) CR O ncurses C mako - CR sdl2 - CR sdl2_image CR opusfile CRA opus CRA ogg @@ -167,10 +165,10 @@ The reference package is [created for Gentoo](https://github.com/SFTtech/gentoo- - I wanna see compiler invocations - `make VERBOSE=1` -- My `SDL2_Image`/`Python`/whatever is installed somewhere, but `cmake` can't find it! +- My `Qt`/`Python`/whatever is installed somewhere, but `cmake` can't find it! - Run `ccmake` or `cmake-gui` in the build directory to see and change config variables. - You can manually tell `cmake` where to look. Try something along the lines of - - `./configure -- -DSDL2IMAGE_INCLUDE_DIRS=/whereever/sdl2_image/include/` + - `./configure -- -DPYTHON_INCLUDE_DIRS=/whereever/python/include/` - `-DPython3_EXECUTABLE=/your/py3/directory/` - I get compiler errors about missing header files diff --git a/doc/code_style/mom.cpp b/doc/code_style/mom.cpp index 6655d0956d..ad2bb5d07d 100644 --- a/doc/code_style/mom.cpp +++ b/doc/code_style/mom.cpp @@ -17,13 +17,16 @@ // The associated header file comes first! #include "mom.h" -// System includes follow, sorted alphabetically +// C++ std library includes follow, sorted alphabetically #include #include #include -#include -// Local includes next, sorted alphabetically +// External libraries are next, sorted alphabetically +#include +#include + +// Local includes come last, sorted alphabetically #include "../valve.h" #include "half_life.h" #include "log/log.h" diff --git a/doc/troubleshooting.md b/doc/troubleshooting.md index 5ed9cc440a..a955eca018 100644 --- a/doc/troubleshooting.md +++ b/doc/troubleshooting.md @@ -26,18 +26,3 @@ A workaround would be to make a backup of your AGE2 directory and let the conver backup at subfolder `AGE2/resources` delete all files ***except*** folders. Another workaround would be to backup your AGE2 folder and redownload it to have a clean install. After conversion you can replace it with the backup. - -## Installation - -### Cannot specify compile definitions for target "SDL2::SDL2" which is not built by this project - -This error is specific to a few operating systems. The main cause is that your SDL2 version is too old and does not -include the necessary CMake files defining the target. There is an indepth discussion about this -[here](https://discourse.libsdl.org/t/how-is-sdl2-supposed-to-be-used-with-cmake/31275/16). -As a solution, you should update your SDL packages to SDL >=2.0.12 or **compile the latest SDL2 and SDL2-image from -source**. The latest version includes the necessary CMake files to expose the `SDL2::SDL2` target. - -## Building on Debian 12 -On Debian you might get an error saying that it couldn't find SDL2 library. This happens because the CMAKE prefix and SDL2 path are not set correctly. -The solution is to append at the end of the `./configure` command the cmake variables for both the prefix and SDL2 path, like so: -`./configure -- -DCMAKE_PREFIX_PATH=/usr -DSDL2_DIR=/usr/include/SDL2` (you can use `find` to look for the correct paths) diff --git a/libopenage/CMakeLists.txt b/libopenage/CMakeLists.txt index 7095576dc1..46fc15da13 100644 --- a/libopenage/CMakeLists.txt +++ b/libopenage/CMakeLists.txt @@ -53,9 +53,6 @@ find_library(FONTCONFIG_LIB fontconfig) find_package(toml11 REQUIRED) find_package(Freetype REQUIRED) find_package(PNG REQUIRED) -find_package(SDL2 CONFIG REQUIRED) -target_compile_definitions(SDL2::SDL2 INTERFACE SDL_MAIN_HANDLED) -find_package(SDL2Image REQUIRED) find_package(Opusfile REQUIRED) find_package(Epoxy REQUIRED) find_package(HarfBuzz 1.0.0 REQUIRED) @@ -268,7 +265,6 @@ target_include_directories(libopenage ${EPOXY_INCLUDE_DIRS} ${OPUS_INCLUDE_DIRS} ${PNG_INCLUDE_DIRS} - ${SDL2IMAGE_INCLUDE_DIRS} ${HarfBuzz_INCLUDE_DIRS} ${QTPLATFORM_INCLUDE_DIRS} ) @@ -292,8 +288,6 @@ target_link_libraries(libopenage ${FREETYPE_LIBRARIES} ${EPOXY_LIBRARIES} ${MATH_LIB} - ${SDL2IMAGE_LIBRARIES} - SDL2::SDL2 ${UTIL_LIB} ${HarfBuzz_LIBRARIES} ${RT_LIB} diff --git a/libopenage/audio/audio_manager.cpp b/libopenage/audio/audio_manager.cpp index ce19e77073..b46fbcf7e2 100644 --- a/libopenage/audio/audio_manager.cpp +++ b/libopenage/audio/audio_manager.cpp @@ -1,4 +1,4 @@ -// Copyright 2014-2019 the openage authors. See copying.md for legal info. +// Copyright 2014-2023 the openage authors. See copying.md for legal info. #include "audio_manager.h" diff --git a/libopenage/gui/guisys/private/gui_ctx_setup.h b/libopenage/gui/guisys/private/gui_ctx_setup.h index 28d5feb847..8b5d1952d9 100644 --- a/libopenage/gui/guisys/private/gui_ctx_setup.h +++ b/libopenage/gui/guisys/private/gui_ctx_setup.h @@ -1,4 +1,4 @@ -// Copyright 2017-2017 the openage authors. See copying.md for legal info. +// Copyright 2017-2023 the openage authors. See copying.md for legal info. #pragma once diff --git a/libopenage/gui/guisys/private/gui_input_impl.h b/libopenage/gui/guisys/private/gui_input_impl.h index 8272188c7f..4863b239e8 100644 --- a/libopenage/gui/guisys/private/gui_input_impl.h +++ b/libopenage/gui/guisys/private/gui_input_impl.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once diff --git a/libopenage/gui/guisys/private/gui_rendering_setup_routines.cpp b/libopenage/gui/guisys/private/gui_rendering_setup_routines.cpp index 3fdf554f98..059cc3f7b2 100644 --- a/libopenage/gui/guisys/private/gui_rendering_setup_routines.cpp +++ b/libopenage/gui/guisys/private/gui_rendering_setup_routines.cpp @@ -1,4 +1,4 @@ -// Copyright 2017-2019 the openage authors. See copying.md for legal info. +// Copyright 2017-2023 the openage authors. See copying.md for legal info. #include "gui_rendering_setup_routines.h" diff --git a/libopenage/gui/guisys/private/gui_rendering_setup_routines.h b/libopenage/gui/guisys/private/gui_rendering_setup_routines.h index b813f85745..a262228c25 100644 --- a/libopenage/gui/guisys/private/gui_rendering_setup_routines.h +++ b/libopenage/gui/guisys/private/gui_rendering_setup_routines.h @@ -1,4 +1,4 @@ -// Copyright 2017-2017 the openage authors. See copying.md for legal info. +// Copyright 2017-2023 the openage authors. See copying.md for legal info. #pragma once diff --git a/libopenage/gui/guisys/private/platforms/context_extraction.h b/libopenage/gui/guisys/private/platforms/context_extraction.h index 1918a3d49c..7bbf515d8b 100644 --- a/libopenage/gui/guisys/private/platforms/context_extraction.h +++ b/libopenage/gui/guisys/private/platforms/context_extraction.h @@ -1,4 +1,4 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once diff --git a/libopenage/gui/guisys/private/platforms/context_extraction_win32.cpp b/libopenage/gui/guisys/private/platforms/context_extraction_win32.cpp index 87f4dbf953..1c413ff926 100644 --- a/libopenage/gui/guisys/private/platforms/context_extraction_win32.cpp +++ b/libopenage/gui/guisys/private/platforms/context_extraction_win32.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include diff --git a/libopenage/gui/guisys/public/gui_input.cpp b/libopenage/gui/guisys/public/gui_input.cpp index 736c2df8cf..861e6a5f6e 100644 --- a/libopenage/gui/guisys/public/gui_input.cpp +++ b/libopenage/gui/guisys/public/gui_input.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "../public/gui_input.h" diff --git a/libopenage/gui/guisys/public/gui_input.h b/libopenage/gui/guisys/public/gui_input.h index 557c53e67f..1a46b227be 100644 --- a/libopenage/gui/guisys/public/gui_input.h +++ b/libopenage/gui/guisys/public/gui_input.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once diff --git a/libopenage/gui/guisys/public/gui_renderer.cpp b/libopenage/gui/guisys/public/gui_renderer.cpp index 3a0fb99092..a11063e7e5 100644 --- a/libopenage/gui/guisys/public/gui_renderer.cpp +++ b/libopenage/gui/guisys/public/gui_renderer.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "../public/gui_renderer.h" diff --git a/libopenage/gui/guisys/public/gui_renderer.h b/libopenage/gui/guisys/public/gui_renderer.h index 93ff6dfd89..bc8e93dd99 100644 --- a/libopenage/gui/guisys/public/gui_renderer.h +++ b/libopenage/gui/guisys/public/gui_renderer.h @@ -1,4 +1,4 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once diff --git a/libopenage/versions/versions.cpp b/libopenage/versions/versions.cpp index 8e2e63d215..d2e1fd251a 100644 --- a/libopenage/versions/versions.cpp +++ b/libopenage/versions/versions.cpp @@ -1,4 +1,4 @@ -// Copyright 2020-2020 the openage authors. See copying.md for legal info. +// Copyright 2020-2023 the openage authors. See copying.md for legal info. #include "versions.h" diff --git a/shell.nix b/shell.nix index 5b466174ef..5d80e0669f 100644 --- a/shell.nix +++ b/shell.nix @@ -9,7 +9,7 @@ pkgs.mkShell { #pkgs.gdb pkgs.cmake pkgs.gnumake - pkgs.qt5.full + pkgs.qt6.full #pkgs.qtcreator pkgs.eigen @@ -27,13 +27,12 @@ pkgs.mkShell { pkgs.ftgl pkgs.fontconfig pkgs.harfbuzz - pkgs.SDL2 - pkgs.SDL2_image pkgs.opusfile pkgs.libopus pkgs.python39Packages.pylint pkgs.python39Packages.toml pkgs.libsForQt6.qt6.qtdeclarative pkgs.libsForQt6.qt6.qtquickcontrols + pkgs.libsForQt6.qt6.qtmultimedia ]; } From 475143eb5f6926b5605a0c6bffa4a4abb6841ff8 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Sep 2023 18:37:44 +0200 Subject: [PATCH 050/771] ci: Use Github Actions checkout v4. --- .github/workflows/ubuntu-22.04.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ubuntu-22.04.yml b/.github/workflows/ubuntu-22.04.yml index 3343df5506..b6eaa70230 100644 --- a/.github/workflows/ubuntu-22.04.yml +++ b/.github/workflows/ubuntu-22.04.yml @@ -32,7 +32,7 @@ jobs: run: mkdir -p /tmp/image shell: bash - name: Download devenv image - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: devenv-image-compressed.tar.gz path: '/tmp/image' From c7f3d70759cde94e0ebdf158fdfe6df94007a9fe Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Sep 2023 22:10:17 +0200 Subject: [PATCH 051/771] ci: Update cached Windows dependencies. --- .github/workflows/windows-server-2019.yml | 2 +- .github/workflows/windows-server-2022.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/windows-server-2019.yml b/.github/workflows/windows-server-2019.yml index 93a9ecf867..7a2262c00f 100644 --- a/.github/workflows/windows-server-2019.yml +++ b/.github/workflows/windows-server-2019.yml @@ -24,7 +24,7 @@ jobs: mkdir download cd download $zipfile = "openage-dep-x64-windows.zip" - Invoke-WebRequest https://github.com/SFTtech/openage-dependencies/releases/download/v0.5.0/openage-dep-x64-windows.zip -OutFile $zipfile + Invoke-WebRequest https://github.com/SFTtech/openage-dependencies/releases/download/v0.5.1/openage-dep-x64-windows.zip -OutFile $zipfile Expand-Archive -Path $zipfile -DestinationPath . -Force Remove-Item $zipfile (Get-ChildItem . -Recurse -File).FullName diff --git a/.github/workflows/windows-server-2022.yml b/.github/workflows/windows-server-2022.yml index c4deba0d46..f0172b103e 100644 --- a/.github/workflows/windows-server-2022.yml +++ b/.github/workflows/windows-server-2022.yml @@ -24,7 +24,7 @@ jobs: mkdir download cd download $zipfile = "openage-dep-x64-windows.zip" - Invoke-WebRequest https://github.com/SFTtech/openage-dependencies/releases/download/v0.5.0/openage-dep-x64-windows.zip -OutFile $zipfile + Invoke-WebRequest https://github.com/SFTtech/openage-dependencies/releases/download/v0.5.1/openage-dep-x64-windows.zip -OutFile $zipfile Expand-Archive -Path $zipfile -DestinationPath . -Force Remove-Item $zipfile (Get-ChildItem . -Recurse -File).FullName From dbe3c30535a139df4ed3f0fd65762484ff833af5 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 27 Sep 2023 17:21:08 +0200 Subject: [PATCH 052/771] gui: Remove BlendPreserver object. --- libopenage/gui/gui.cpp | 70 +++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/libopenage/gui/gui.cpp b/libopenage/gui/gui.cpp index eae5553572..9f2df8a983 100644 --- a/libopenage/gui/gui.cpp +++ b/libopenage/gui/gui.cpp @@ -47,41 +47,41 @@ void GUI::process_events() { // return not this->input.process(event); // } -namespace { -/** - * Restores blending function. - */ -class BlendPreserver { -public: - BlendPreserver() : - was_on{}, - src{}, - dst{} { - glGetBooleanv(GL_BLEND, &this->was_on); - - if (this->was_on != GL_FALSE) { - glGetIntegerv(GL_BLEND_SRC_ALPHA, &this->src); - glGetIntegerv(GL_BLEND_DST_ALPHA, &this->dst); - } - } - - ~BlendPreserver() { - if (this->was_on != GL_FALSE) { - glEnable(GL_BLEND); - glBlendFunc(this->src, this->dst); - } - else { - glDisable(GL_BLEND); - } - } - -private: - GLboolean was_on; - GLint src; - GLint dst; -}; - -} // namespace +// namespace { +// /** +// * Restores blending function. +// */ +// class BlendPreserver { +// public: +// BlendPreserver() : +// was_on{}, +// src{}, +// dst{} { +// glGetBooleanv(GL_BLEND, &this->was_on); + +// if (this->was_on != GL_FALSE) { +// glGetIntegerv(GL_BLEND_SRC_ALPHA, &this->src); +// glGetIntegerv(GL_BLEND_DST_ALPHA, &this->dst); +// } +// } + +// ~BlendPreserver() { +// if (this->was_on != GL_FALSE) { +// glEnable(GL_BLEND); +// glBlendFunc(this->src, this->dst); +// } +// else { +// glDisable(GL_BLEND); +// } +// } + +// private: +// GLboolean was_on; +// GLint src; +// GLint dst; +// }; + +// } // namespace // bool GUI::on_drawhud() { // this->render_updater.process_callbacks(); From 9af686fd5b37cc237c43930043a4f0fd67a1d3f0 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 27 Sep 2023 19:00:40 +0200 Subject: [PATCH 053/771] gui: Fix missing return value. --- libopenage/gui/guisys/private/gui_input_impl.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libopenage/gui/guisys/private/gui_input_impl.cpp b/libopenage/gui/guisys/private/gui_input_impl.cpp index c0d177ace9..8675785596 100644 --- a/libopenage/gui/guisys/private/gui_input_impl.cpp +++ b/libopenage/gui/guisys/private/gui_input_impl.cpp @@ -144,6 +144,7 @@ bool GuiInputImpl::process(/* SDL_Event *e */) { // default: // return false; // } + return false; } bool GuiInputImpl::relay_input_event(QEvent *ev, bool only_if_grabbed) { From 351a6d70ec444d40263207b5f375df79735d23d1 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 22 Oct 2023 19:11:21 +0200 Subject: [PATCH 054/771] Add clang-tidy recommendations. --- libopenage/gamestate/player.h | 4 +- libopenage/gui/guisys/link/gui_item_link.h | 55 +++++++++++---------- libopenage/renderer/vulkan/render_target.h | 8 ++- libopenage/renderer/vulkan/shader_program.h | 40 +++++++++------ libopenage/rng/global_rng.h | 6 ++- 5 files changed, 66 insertions(+), 47 deletions(-) diff --git a/libopenage/gamestate/player.h b/libopenage/gamestate/player.h index db6c53b128..d9bdbe3bd2 100644 --- a/libopenage/gamestate/player.h +++ b/libopenage/gamestate/player.h @@ -29,10 +29,10 @@ class Player { // players can't be copied to prevent duplicate IDs Player(const Player &) = delete; - Player(Player &&) = default; + Player(Player &&) = delete; Player &operator=(const Player &) = delete; - Player &operator=(Player &&) = default; + Player &operator=(Player &&) = delete; ~Player() = default; diff --git a/libopenage/gui/guisys/link/gui_item_link.h b/libopenage/gui/guisys/link/gui_item_link.h index 43191ea2ec..60cc7e5572 100644 --- a/libopenage/gui/guisys/link/gui_item_link.h +++ b/libopenage/gui/guisys/link/gui_item_link.h @@ -1,8 +1,9 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once #include +#include #include "qtsdl_checked_static_cast.h" @@ -22,30 +23,30 @@ class GuiItemLink { /** * If the core 'MyClass' has a shell 'MyClassLink' then 'Wrap' must have a 'using Type = MyClassLink' */ -template +template struct Wrap { }; /** * If the core 'MyClass' has a shell 'MyClassLink' then 'Unwrap' must have a 'using Type = MyClass' */ -template +template struct Unwrap { }; -template -typename Unwrap::Type* unwrap(T *t) { +template +typename Unwrap::Type *unwrap(T *t) { return t ? t->template get::Type>() : nullptr; } -template -const typename Unwrap::Type* unwrap(const T *t) { +template +const typename Unwrap::Type *unwrap(const T *t) { return t ? t->template get::Type>() : nullptr; } -template -const typename Wrap::Type* wrap(const U *u) { - return checked_static_cast::Type*>(u->gui_link); +template +const typename Wrap::Type *wrap(const U *u) { + return checked_static_cast::Type *>(u->gui_link); } /** @@ -53,33 +54,33 @@ const typename Wrap::Type* wrap(const U *u) { */ class GuiSingletonItem; -template -typename Wrap::Type* wrap(U *u, typename std::enable_if::Type>::value>::type* = nullptr) { - return u ? checked_static_cast::Type*>(u->gui_link) : nullptr; +template +typename Wrap::Type *wrap(U *u, typename std::enable_if::Type>::value>::type * = nullptr) { + return u ? checked_static_cast::Type *>(u->gui_link) : nullptr; } -template -typename Wrap::Type* wrap(U *u, typename std::enable_if::Type>::value>::type* = nullptr) { - return u ? checked_static_cast::Type*>(u->gui_link) : nullptr; +template +typename Wrap::Type *wrap(U *u, typename std::enable_if::Type>::value>::type * = nullptr) { + return u ? checked_static_cast::Type *>(u->gui_link) : nullptr; } -template -constexpr P&& wrap_if_can(typename std::remove_reference

::type&& p) noexcept { +template +constexpr P &&wrap_if_can(typename std::remove_reference

::type &&p) noexcept { return std::forward

(p); } -template +template T wrap_if_can(typename Unwrap::type>::Type *u) { return wrap(u); } -template -P unwrap_if_can(P& p) { +template +P unwrap_if_can(P &p) { return p; } -template::Type* = nullptr> -typename Unwrap::Type* unwrap_if_can(T *t) { +template ::Type * = nullptr> +typename Unwrap::Type *unwrap_if_can(T *t) { return unwrap(t); } @@ -87,10 +88,10 @@ typename Unwrap::Type* unwrap_if_can(T *t) { * Checking that callable can be called with given argument types. */ struct can_call_test { - template + template static decltype(std::declval()(std::declval()...), std::true_type()) f(int); - template + template static std::false_type f(...); }; @@ -100,7 +101,7 @@ struct can_call_test { * @tparam F callable * @tparam A arguments to test against the callable */ -template +template struct can_call : decltype(can_call_test::f(0)) { }; @@ -113,7 +114,7 @@ struct can_call : decltype(can_call_test::f(0)) { * @tparam F callable * @tparam A arguments to test against the callable */ -template +template constexpr void static_assert_about_unwrapping() { static_assert(can_call{}, "One of possible causes: if you're passing SomethingLink*, then don't forget to #include \"something_link.h\"."); } diff --git a/libopenage/renderer/vulkan/render_target.h b/libopenage/renderer/vulkan/render_target.h index 49db9e5ef1..c7c3ab2bed 100644 --- a/libopenage/renderer/vulkan/render_target.h +++ b/libopenage/renderer/vulkan/render_target.h @@ -3,10 +3,14 @@ #pragma once #include +#include -#include "../renderer.h" +#include -#include "graphics_device.h" +#include "log/log.h" + +#include "renderer/renderer.h" +#include "renderer/vulkan/graphics_device.h" namespace openage { diff --git a/libopenage/renderer/vulkan/shader_program.h b/libopenage/renderer/vulkan/shader_program.h index e46030dd13..e1b2b7c0b2 100644 --- a/libopenage/renderer/vulkan/shader_program.h +++ b/libopenage/renderer/vulkan/shader_program.h @@ -1,12 +1,14 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. +// Copyright 2017-2023 the openage authors. See copying.md for legal info. #pragma once -#include "../../error/error.h" -#include "../../log/log.h" +#include "error/error.h" +#include "log/log.h" -#include "../resources/shader_source.h" -#include "../shader_program.h" +#include + +#include "renderer/resources/shader_source.h" +#include "renderer/shader_program.h" namespace openage { @@ -15,12 +17,18 @@ namespace vulkan { static VkShaderStageFlagBits vk_shader_stage(resources::shader_stage_t stage) { switch (stage) { - case resources::shader_stage_t::vertex: return VK_SHADER_STAGE_VERTEX_BIT; - case resources::shader_stage_t::geometry: return VK_SHADER_STAGE_GEOMETRY_BIT; - case resources::shader_stage_t::tesselation_control: return VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT; - case resources::shader_stage_t::tesselation_evaluation: return VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT; - case resources::shader_stage_t::fragment: return VK_SHADER_STAGE_FRAGMENT_BIT; - default: throw Error(MSG(err) << "Unknown shader stage."); + case resources::shader_stage_t::vertex: + return VK_SHADER_STAGE_VERTEX_BIT; + case resources::shader_stage_t::geometry: + return VK_SHADER_STAGE_GEOMETRY_BIT; + case resources::shader_stage_t::tesselation_control: + return VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT; + case resources::shader_stage_t::tesselation_evaluation: + return VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT; + case resources::shader_stage_t::fragment: + return VK_SHADER_STAGE_FRAGMENT_BIT; + default: + throw Error(MSG(err) << "Unknown shader stage."); } } @@ -29,11 +37,11 @@ class VlkShaderProgram /* final : public ShaderProgram */ { std::vector modules; std::vector pipeline_stage_infos; - explicit VlkShaderProgram(VkDevice dev, std::vector const& srcs) { + explicit VlkShaderProgram(VkDevice dev, std::vector const &srcs) { // TODO reflect with spirv-cross // TODO if glsl, compile to spirv with libshaderc - for (auto const& src : srcs) { + for (auto const &src : srcs) { if (src.get_lang() != resources::shader_lang_t::spirv) { throw Error(MSG(err) << "Unsupported shader language in Vulkan shader."); } @@ -41,7 +49,7 @@ class VlkShaderProgram /* final : public ShaderProgram */ { VkShaderModuleCreateInfo cr_shdr = {}; cr_shdr.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; cr_shdr.codeSize = src.get_source().size(); - cr_shdr.pCode = reinterpret_cast(src.get_source().data()); + cr_shdr.pCode = reinterpret_cast(src.get_source().data()); VkShaderModule mod; VK_CALL_CHECKED(vkCreateShaderModule, dev, &cr_shdr, nullptr, &mod); @@ -61,4 +69,6 @@ class VlkShaderProgram /* final : public ShaderProgram */ { } }; -}}} // openage::renderer::vulkan +} // namespace vulkan +} // namespace renderer +} // namespace openage diff --git a/libopenage/rng/global_rng.h b/libopenage/rng/global_rng.h index 8ed475ae22..9fbe0f3f47 100644 --- a/libopenage/rng/global_rng.h +++ b/libopenage/rng/global_rng.h @@ -1,6 +1,10 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. + #pragma once +#include + + /** @file * This file contains functions for the global random number generator. * From 99d804388cfc3edb08bf5c0c9b4b05315e2fc007 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 2 Nov 2023 00:59:43 +0100 Subject: [PATCH 055/771] gui: Tranfer remaining GUI classes to renderer. --- libopenage/CMakeLists.txt | 1 - libopenage/gui/CMakeLists.txt | 13 - libopenage/gui/actions_list_model.cpp | 74 ----- libopenage/gui/actions_list_model.h | 71 ----- libopenage/gui/gui.cpp | 125 -------- libopenage/gui/gui.h | 56 ---- libopenage/gui/guisys/CMakeLists.txt | 43 --- libopenage/gui/guisys/link/gui_item.cpp | 20 -- .../gui/guisys/link/gui_singleton_item.cpp | 15 - .../gui/guisys/link/gui_singleton_item.h | 19 -- .../qml_engine_with_singleton_items_info.cpp | 45 --- .../qml_engine_with_singleton_items_info.h | 41 --- .../guisys/link/qtsdl_checked_static_cast.h | 15 - .../gui/guisys/private/game_logic_caller.cpp | 21 -- .../gui/guisys/private/game_logic_caller.h | 32 -- .../guisys/private/gui_application_impl.cpp | 62 ---- .../gui/guisys/private/gui_application_impl.h | 40 --- .../gui/guisys/private/gui_callback.cpp | 25 -- libopenage/gui/guisys/private/gui_callback.h | 26 -- .../gui/guisys/private/gui_ctx_setup.cpp | 101 ------- libopenage/gui/guisys/private/gui_ctx_setup.h | 92 ------ .../guisys/private/gui_dedicated_thread.cpp | 82 ------ .../gui/guisys/private/gui_dedicated_thread.h | 35 --- .../gui/guisys/private/gui_engine_impl.cpp | 77 ----- .../gui/guisys/private/gui_engine_impl.h | 54 ---- .../guisys/private/gui_event_queue_impl.cpp | 40 --- .../gui/guisys/private/gui_event_queue_impl.h | 33 --- .../private/gui_image_provider_impl.cpp | 32 -- .../guisys/private/gui_image_provider_impl.h | 27 -- .../gui/guisys/private/gui_input_impl.cpp | 171 ----------- .../gui/guisys/private/gui_input_impl.h | 37 --- .../gui/guisys/private/gui_renderer_impl.cpp | 270 ----------------- .../gui/guisys/private/gui_renderer_impl.h | 168 ----------- .../private/gui_rendering_setup_routines.cpp | 64 ---- .../private/gui_rendering_setup_routines.h | 55 ---- .../gui/guisys/private/gui_subtree_impl.cpp | 275 ------------------ .../gui/guisys/private/gui_subtree_impl.h | 97 ------ .../recursive_directory_watcher.cpp | 39 --- .../livereload/recursive_directory_watcher.h | 31 -- .../recursive_directory_watcher_worker.cpp | 73 ----- .../recursive_directory_watcher_worker.h | 49 ---- .../guisys/private/opengl_debug_logger.cpp | 49 ---- .../gui/guisys/private/opengl_debug_logger.h | 43 --- .../private/platforms/context_extraction.h | 23 -- .../platforms/context_extraction_cocoa.mm | 59 ---- .../platforms/context_extraction_win32.cpp | 47 --- .../platforms/context_extraction_x11.cpp | 61 ---- .../gui/guisys/public/gui_application.cpp | 27 -- .../gui/guisys/public/gui_application.h | 26 -- libopenage/gui/guisys/public/gui_engine.cpp | 16 - libopenage/gui/guisys/public/gui_engine.h | 30 -- .../gui/guisys/public/gui_event_queue.cpp | 20 -- .../gui/guisys/public/gui_event_queue.h | 26 -- .../gui/guisys/public/gui_image_provider.cpp | 16 - .../gui/guisys/public/gui_image_provider.h | 22 -- libopenage/gui/guisys/public/gui_input.cpp | 19 -- libopenage/gui/guisys/public/gui_input.h | 31 -- .../gui/guisys/public/gui_property_map.cpp | 166 ----------- .../gui/guisys/public/gui_property_map.h | 87 ------ libopenage/gui/guisys/public/gui_renderer.cpp | 25 -- libopenage/gui/guisys/public/gui_renderer.h | 38 --- .../guisys/public/gui_singleton_items_info.h | 13 - libopenage/gui/guisys/public/gui_subtree.cpp | 25 -- libopenage/gui/guisys/public/gui_subtree.h | 37 --- libopenage/gui/integration/CMakeLists.txt | 2 - .../gui/integration/private/CMakeLists.txt | 9 - .../private/gui_image_provider_link.cpp | 36 --- .../private/gui_image_provider_link.h | 28 -- .../gui/integration/private/gui_log.cpp | 53 ---- libopenage/gui/integration/private/gui_log.h | 12 - .../gui_make_standalone_subtexture.cpp | 13 - .../gui/integration/public/CMakeLists.txt | 3 - .../public/gui_application_with_logger.cpp | 24 -- .../public/gui_application_with_logger.h | 19 -- libopenage/gui/registrations.cpp | 12 - libopenage/gui/registrations.h | 15 - libopenage/presenter/presenter.cpp | 1 - libopenage/renderer/gui/CMakeLists.txt | 2 - libopenage/renderer/gui/engine_link.cpp | 96 ------ libopenage/renderer/gui/engine_link.h | 81 ------ libopenage/renderer/gui/gui.cpp | 3 - libopenage/renderer/gui/gui.h | 12 - libopenage/renderer/gui/guisys/CMakeLists.txt | 9 + .../renderer/gui/guisys/link/gui_item.cpp | 20 ++ .../{ => renderer}/gui/guisys/link/gui_item.h | 187 ++++++------ .../gui/guisys/link/gui_item_link.h | 7 +- .../gui/guisys/link/gui_item_list_model.h | 34 ++- .../gui/guisys/link/gui_list_model.cpp | 17 +- .../gui/guisys/link/gui_list_model.h | 23 +- .../gui/guisys/link/gui_property_map_impl.cpp | 14 +- .../gui/guisys/link/gui_property_map_impl.h | 7 +- .../gui/guisys/link/gui_singleton_item.cpp | 15 + .../gui/guisys/link/gui_singleton_item.h | 21 ++ .../guisys/link/qtsdl_checked_static_cast.h | 16 + .../gui/guisys/private/gui_ctx_setup.cpp | 3 +- ...erred_initial_constant_property_values.cpp | 9 +- ...eferred_initial_constant_property_values.h | 8 +- .../private/livereload/gui_live_reloader.cpp | 4 +- .../private/livereload/gui_live_reloader.h | 27 +- .../gui/integration/private/CMakeLists.txt | 7 +- .../private/gui_filled_texture_handles.cpp | 30 +- .../private/gui_filled_texture_handles.h | 25 +- .../gui_make_standalone_subtexture.cpp | 14 + .../private/gui_make_standalone_subtexture.h | 6 +- .../private/gui_standalone_subtexture.cpp | 6 +- .../private/gui_standalone_subtexture.h | 6 +- .../gui/integration/private/gui_texture.cpp | 13 +- .../gui/integration/private/gui_texture.h | 6 +- .../private/gui_texture_handle.cpp | 6 +- .../integration/private/gui_texture_handle.h | 7 +- libopenage/renderer/gui/qml_info.cpp | 15 - libopenage/renderer/gui/qml_info.h | 45 --- libopenage/renderer/opengl/window.cpp | 12 +- 113 files changed, 320 insertions(+), 4272 deletions(-) delete mode 100644 libopenage/gui/CMakeLists.txt delete mode 100644 libopenage/gui/actions_list_model.cpp delete mode 100644 libopenage/gui/actions_list_model.h delete mode 100644 libopenage/gui/gui.cpp delete mode 100644 libopenage/gui/gui.h delete mode 100644 libopenage/gui/guisys/CMakeLists.txt delete mode 100644 libopenage/gui/guisys/link/gui_item.cpp delete mode 100644 libopenage/gui/guisys/link/gui_singleton_item.cpp delete mode 100644 libopenage/gui/guisys/link/gui_singleton_item.h delete mode 100644 libopenage/gui/guisys/link/qml_engine_with_singleton_items_info.cpp delete mode 100644 libopenage/gui/guisys/link/qml_engine_with_singleton_items_info.h delete mode 100644 libopenage/gui/guisys/link/qtsdl_checked_static_cast.h delete mode 100644 libopenage/gui/guisys/private/game_logic_caller.cpp delete mode 100644 libopenage/gui/guisys/private/game_logic_caller.h delete mode 100644 libopenage/gui/guisys/private/gui_application_impl.cpp delete mode 100644 libopenage/gui/guisys/private/gui_application_impl.h delete mode 100644 libopenage/gui/guisys/private/gui_callback.cpp delete mode 100644 libopenage/gui/guisys/private/gui_callback.h delete mode 100644 libopenage/gui/guisys/private/gui_ctx_setup.cpp delete mode 100644 libopenage/gui/guisys/private/gui_ctx_setup.h delete mode 100644 libopenage/gui/guisys/private/gui_dedicated_thread.cpp delete mode 100644 libopenage/gui/guisys/private/gui_dedicated_thread.h delete mode 100644 libopenage/gui/guisys/private/gui_engine_impl.cpp delete mode 100644 libopenage/gui/guisys/private/gui_engine_impl.h delete mode 100644 libopenage/gui/guisys/private/gui_event_queue_impl.cpp delete mode 100644 libopenage/gui/guisys/private/gui_event_queue_impl.h delete mode 100644 libopenage/gui/guisys/private/gui_image_provider_impl.cpp delete mode 100644 libopenage/gui/guisys/private/gui_image_provider_impl.h delete mode 100644 libopenage/gui/guisys/private/gui_input_impl.cpp delete mode 100644 libopenage/gui/guisys/private/gui_input_impl.h delete mode 100644 libopenage/gui/guisys/private/gui_renderer_impl.cpp delete mode 100644 libopenage/gui/guisys/private/gui_renderer_impl.h delete mode 100644 libopenage/gui/guisys/private/gui_rendering_setup_routines.cpp delete mode 100644 libopenage/gui/guisys/private/gui_rendering_setup_routines.h delete mode 100644 libopenage/gui/guisys/private/gui_subtree_impl.cpp delete mode 100644 libopenage/gui/guisys/private/gui_subtree_impl.h delete mode 100644 libopenage/gui/guisys/private/livereload/recursive_directory_watcher.cpp delete mode 100644 libopenage/gui/guisys/private/livereload/recursive_directory_watcher.h delete mode 100644 libopenage/gui/guisys/private/livereload/recursive_directory_watcher_worker.cpp delete mode 100644 libopenage/gui/guisys/private/livereload/recursive_directory_watcher_worker.h delete mode 100644 libopenage/gui/guisys/private/opengl_debug_logger.cpp delete mode 100644 libopenage/gui/guisys/private/opengl_debug_logger.h delete mode 100644 libopenage/gui/guisys/private/platforms/context_extraction.h delete mode 100644 libopenage/gui/guisys/private/platforms/context_extraction_cocoa.mm delete mode 100644 libopenage/gui/guisys/private/platforms/context_extraction_win32.cpp delete mode 100644 libopenage/gui/guisys/private/platforms/context_extraction_x11.cpp delete mode 100644 libopenage/gui/guisys/public/gui_application.cpp delete mode 100644 libopenage/gui/guisys/public/gui_application.h delete mode 100644 libopenage/gui/guisys/public/gui_engine.cpp delete mode 100644 libopenage/gui/guisys/public/gui_engine.h delete mode 100644 libopenage/gui/guisys/public/gui_event_queue.cpp delete mode 100644 libopenage/gui/guisys/public/gui_event_queue.h delete mode 100644 libopenage/gui/guisys/public/gui_image_provider.cpp delete mode 100644 libopenage/gui/guisys/public/gui_image_provider.h delete mode 100644 libopenage/gui/guisys/public/gui_input.cpp delete mode 100644 libopenage/gui/guisys/public/gui_input.h delete mode 100644 libopenage/gui/guisys/public/gui_property_map.cpp delete mode 100644 libopenage/gui/guisys/public/gui_property_map.h delete mode 100644 libopenage/gui/guisys/public/gui_renderer.cpp delete mode 100644 libopenage/gui/guisys/public/gui_renderer.h delete mode 100644 libopenage/gui/guisys/public/gui_singleton_items_info.h delete mode 100644 libopenage/gui/guisys/public/gui_subtree.cpp delete mode 100644 libopenage/gui/guisys/public/gui_subtree.h delete mode 100644 libopenage/gui/integration/CMakeLists.txt delete mode 100644 libopenage/gui/integration/private/CMakeLists.txt delete mode 100644 libopenage/gui/integration/private/gui_image_provider_link.cpp delete mode 100644 libopenage/gui/integration/private/gui_image_provider_link.h delete mode 100644 libopenage/gui/integration/private/gui_log.cpp delete mode 100644 libopenage/gui/integration/private/gui_log.h delete mode 100644 libopenage/gui/integration/private/gui_make_standalone_subtexture.cpp delete mode 100644 libopenage/gui/integration/public/CMakeLists.txt delete mode 100644 libopenage/gui/integration/public/gui_application_with_logger.cpp delete mode 100644 libopenage/gui/integration/public/gui_application_with_logger.h delete mode 100644 libopenage/gui/registrations.cpp delete mode 100644 libopenage/gui/registrations.h delete mode 100644 libopenage/renderer/gui/engine_link.cpp delete mode 100644 libopenage/renderer/gui/engine_link.h create mode 100644 libopenage/renderer/gui/guisys/link/gui_item.cpp rename libopenage/{ => renderer}/gui/guisys/link/gui_item.h (62%) rename libopenage/{ => renderer}/gui/guisys/link/gui_item_link.h (97%) rename libopenage/{ => renderer}/gui/guisys/link/gui_item_list_model.h (68%) rename libopenage/{ => renderer}/gui/guisys/link/gui_list_model.cpp (85%) rename libopenage/{ => renderer}/gui/guisys/link/gui_list_model.h (58%) rename libopenage/{ => renderer}/gui/guisys/link/gui_property_map_impl.cpp (66%) rename libopenage/{ => renderer}/gui/guisys/link/gui_property_map_impl.h (74%) create mode 100644 libopenage/renderer/gui/guisys/link/gui_singleton_item.cpp create mode 100644 libopenage/renderer/gui/guisys/link/gui_singleton_item.h create mode 100644 libopenage/renderer/gui/guisys/link/qtsdl_checked_static_cast.h rename libopenage/{ => renderer}/gui/guisys/private/livereload/deferred_initial_constant_property_values.cpp (84%) rename libopenage/{ => renderer}/gui/guisys/private/livereload/deferred_initial_constant_property_values.h (88%) rename libopenage/{ => renderer}/gui/guisys/private/livereload/gui_live_reloader.cpp (98%) rename libopenage/{ => renderer}/gui/guisys/private/livereload/gui_live_reloader.h (78%) rename libopenage/{ => renderer}/gui/integration/private/gui_filled_texture_handles.cpp (70%) rename libopenage/{ => renderer}/gui/integration/private/gui_filled_texture_handles.h (74%) create mode 100644 libopenage/renderer/gui/integration/private/gui_make_standalone_subtexture.cpp rename libopenage/{ => renderer}/gui/integration/private/gui_make_standalone_subtexture.h (87%) rename libopenage/{ => renderer}/gui/integration/private/gui_standalone_subtexture.cpp (91%) rename libopenage/{ => renderer}/gui/integration/private/gui_standalone_subtexture.h (89%) rename libopenage/{ => renderer}/gui/integration/private/gui_texture.cpp (84%) rename libopenage/{ => renderer}/gui/integration/private/gui_texture.h (91%) rename libopenage/{ => renderer}/gui/integration/private/gui_texture_handle.cpp (94%) rename libopenage/{ => renderer}/gui/integration/private/gui_texture_handle.h (92%) delete mode 100644 libopenage/renderer/gui/qml_info.cpp delete mode 100644 libopenage/renderer/gui/qml_info.h diff --git a/libopenage/CMakeLists.txt b/libopenage/CMakeLists.txt index 46fc15da13..12ddac7899 100644 --- a/libopenage/CMakeLists.txt +++ b/libopenage/CMakeLists.txt @@ -340,7 +340,6 @@ add_subdirectory("engine") add_subdirectory("error") add_subdirectory("event") add_subdirectory("gamestate") -add_subdirectory("gui") add_subdirectory("input") add_subdirectory("job") add_subdirectory("log") diff --git a/libopenage/gui/CMakeLists.txt b/libopenage/gui/CMakeLists.txt deleted file mode 100644 index a96c486f54..0000000000 --- a/libopenage/gui/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -add_sources(libopenage - actions_list_model.cpp - gui.cpp - registrations.cpp -) - -add_subdirectory("guisys") - -add_sources(libopenage - ${QT_SDL_SOURCES} -) - -add_subdirectory("integration") diff --git a/libopenage/gui/actions_list_model.cpp b/libopenage/gui/actions_list_model.cpp deleted file mode 100644 index a8fa45fa46..0000000000 --- a/libopenage/gui/actions_list_model.cpp +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. - -#include "actions_list_model.h" - -#include "../log/log.h" - -#include -#include - -namespace openage { -namespace gui { - -namespace { -const int registration = qmlRegisterType("yay.sfttech.openage", 1, 0, "ActionsListModel"); -} - -ActionsListModel::ActionsListModel(QObject *parent) : - QAbstractListModel{parent} { - Q_UNUSED(registration); -} - -ActionsListModel::~ActionsListModel() = default; - -QUrl ActionsListModel::get_icons_source() const { - return QUrl(this->icons_source); -} - -void ActionsListModel::set_icons_source(QUrl icons_source) { - this->icons_source = std::move(icons_source); -} - -void ActionsListModel::set_icons_source(const std::string &icons_source) { - this->icons_source = QUrl(icons_source.c_str()); - emit this->icons_source_changed(this->icons_source); -} - -QHash ActionsListModel::roleNames() const { - QHash roles; - roles[static_cast(ActionsRoles::IconRole)] = "ico"; - roles[static_cast(ActionsRoles::IconCheckedRole)] = "icoChk"; - roles[static_cast(ActionsRoles::GroupIDRole)] = "grpID"; - roles[static_cast(ActionsRoles::NameRole)] = "name"; - return roles; -} - -int ActionsListModel::rowCount(const QModelIndex &) const { - return this->buttons.size(); -} - -QVariant ActionsListModel::data(const QModelIndex &index, int role) const { - return this->buttons.at(index.row()).value(role); -} - -QMap ActionsListModel::itemData(const QModelIndex &index) const { - return this->buttons.at(index.row()); -} - -void ActionsListModel::clear_buttons() { - this->beginResetModel(); - this->buttons.clear(); - this->endResetModel(); -} - -void ActionsListModel::add_button(int ico, int ico_chk, int grp_id, const char *name) { - QMap map; - map[static_cast(ActionsRoles::IconRole)] = QVariant(ico); - map[static_cast(ActionsRoles::IconCheckedRole)] = QVariant(ico_chk); - map[static_cast(ActionsRoles::GroupIDRole)] = QVariant(grp_id); - map[static_cast(ActionsRoles::NameRole)] = QVariant(name); - this->buttons.push_back(map); -} - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/actions_list_model.h b/libopenage/gui/actions_list_model.h deleted file mode 100644 index 7ffff6a752..0000000000 --- a/libopenage/gui/actions_list_model.h +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include -#include -#include - -namespace openage { -namespace gui { - -/** - * Model used for the Action Buttons to render (e.g. for civilian units, - * military units, buildings etc.) - */ -class ActionsListModel : public QAbstractListModel { - Q_OBJECT - - Q_PROPERTY(QUrl iconsSource READ get_icons_source WRITE set_icons_source NOTIFY icons_source_changed) - -public: - ActionsListModel(QObject *parent = nullptr); - virtual ~ActionsListModel(); - - QUrl get_icons_source() const; - void set_icons_source(QUrl icons_source); - - enum class ActionsRoles { - IconRole = Qt::UserRole + 1, - IconCheckedRole, - GroupIDRole, - NameRole - }; - - enum class GroupIDs { - NoGroup, - StanceGroup - }; - -signals: - void icons_source_changed(const QUrl icons_source); - -private: - virtual QHash roleNames() const override; - virtual int rowCount(const QModelIndex &) const override; - virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - virtual QMap itemData(const QModelIndex &index) const override; - - /** - * Utility function to create a QUrl from a string and set it as iconsSource - */ - void set_icons_source(const std::string &icons_source); - - /** - * Clears all buttons - */ - void clear_buttons(); - - /** - * Shortcut to creating a QMap for a button - */ - void add_button(int ico, int ico_chk, int grp_id, const char *name); - - QUrl icons_source; - std::vector> buttons; -}; - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/gui.cpp b/libopenage/gui/gui.cpp deleted file mode 100644 index 9f2df8a983..0000000000 --- a/libopenage/gui/gui.cpp +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "gui.h" - -#include "../util/path.h" - - -namespace openage { -namespace gui { - - -GUI::GUI(/* SDL_Window *window, */ - const std::string &source, - const std::string &rootdir, - EngineQMLInfo *info) : - application{}, - render_updater{}, - renderer{/* window */}, - game_logic_updater{}, - engine{&renderer}, - subtree{ - &renderer, - &game_logic_updater, - &engine, - source, - rootdir}, - input{&renderer, &game_logic_updater} { - // info->display->register_resize_action(this); - // info->display->register_input_action(this); - // info->display->register_drawhud_action(this); -} - -GUI::~GUI() { -} - -void GUI::process_events() { - this->game_logic_updater.process_callbacks(); - this->application.processEvents(); -} - -// bool GUI::on_resize(coord::viewport_delta new_size) { -// this->renderer.resize(new_size.x, new_size.y); -// return true; -// } - -// bool GUI::on_input(SDL_Event *event) { -// return not this->input.process(event); -// } - -// namespace { -// /** -// * Restores blending function. -// */ -// class BlendPreserver { -// public: -// BlendPreserver() : -// was_on{}, -// src{}, -// dst{} { -// glGetBooleanv(GL_BLEND, &this->was_on); - -// if (this->was_on != GL_FALSE) { -// glGetIntegerv(GL_BLEND_SRC_ALPHA, &this->src); -// glGetIntegerv(GL_BLEND_DST_ALPHA, &this->dst); -// } -// } - -// ~BlendPreserver() { -// if (this->was_on != GL_FALSE) { -// glEnable(GL_BLEND); -// glBlendFunc(this->src, this->dst); -// } -// else { -// glDisable(GL_BLEND); -// } -// } - -// private: -// GLboolean was_on; -// GLint src; -// GLint dst; -// }; - -// } // namespace - -// bool GUI::on_drawhud() { -// this->render_updater.process_callbacks(); - -// BlendPreserver preserve_blend; - -// auto tex = this->renderer.render(); - -// glEnable(GL_BLEND); -// glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - -// this->textured_screen_quad_shader->use(); - -// glActiveTexture(GL_TEXTURE0); -// glBindTexture(GL_TEXTURE_2D, tex); - -// glEnableVertexAttribArray(this->textured_screen_quad_shader->pos_id); - -// glBindBuffer(GL_ARRAY_BUFFER, this->screen_quad_vbo); -// glVertexAttribPointer( -// this->textured_screen_quad_shader->pos_id, -// 2, -// GL_FLOAT, -// GL_FALSE, -// 2 * sizeof(float), -// 0); - -// glDrawArrays(GL_TRIANGLE_FAN, 0, 4); - -// glDisableVertexAttribArray(this->textured_screen_quad_shader->pos_id); -// glBindBuffer(GL_ARRAY_BUFFER, 0); - -// glBindTexture(GL_TEXTURE_2D, 0); - -// this->textured_screen_quad_shader->stopusing(); - -// return true; -// } - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/gui.h b/libopenage/gui/gui.h deleted file mode 100644 index 36273e5462..0000000000 --- a/libopenage/gui/gui.h +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -#include "guisys/public/gui_engine.h" -#include "guisys/public/gui_event_queue.h" -#include "guisys/public/gui_input.h" -#include "guisys/public/gui_renderer.h" -#include "guisys/public/gui_subtree.h" -#include "integration/public/gui_application_with_logger.h" - - -namespace qtsdl { -class GuiSingletonItemsInfo; -} // namespace qtsdl - -namespace openage { -namespace gui { - -class EngineQMLInfo; - - -/** - * Main entry point for the openage Qt-based user interface. - * - * Legacy variant for the "old" renderer. - */ -class GUI { -public: - explicit GUI(/* SDL_Window *window, */ - const std::string &source, - const std::string &rootdir, - EngineQMLInfo *info = nullptr); - virtual ~GUI(); - - void process_events(); - -private: - // virtual bool on_resize(coord::viewport_delta new_size) override; - // virtual bool on_input(SDL_Event *event) override; - // virtual bool on_drawhud() override; - - GuiApplicationWithLogger application; - qtsdl::GuiEventQueue render_updater; - qtsdl::GuiRenderer renderer; - qtsdl::GuiEventQueue game_logic_updater; - qtsdl::GuiEngine engine; - qtsdl::GuiSubtree subtree; - qtsdl::GuiInput input; -}; - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/guisys/CMakeLists.txt b/libopenage/gui/guisys/CMakeLists.txt deleted file mode 100644 index 922c3ec72c..0000000000 --- a/libopenage/gui/guisys/CMakeLists.txt +++ /dev/null @@ -1,43 +0,0 @@ -list(APPEND QT_SDL_SOURCES - ${CMAKE_CURRENT_SOURCE_DIR}/link/gui_item.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/link/gui_list_model.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/link/gui_property_map_impl.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/link/gui_singleton_item.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/link/qml_engine_with_singleton_items_info.cpp -) - -list(APPEND QT_SDL_SOURCES - ${CMAKE_CURRENT_SOURCE_DIR}/public/gui_application.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/public/gui_engine.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/public/gui_event_queue.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/public/gui_image_provider.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/public/gui_input.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/public/gui_property_map.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/public/gui_renderer.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/public/gui_subtree.cpp -) - -list(APPEND QT_SDL_SOURCES - ${CMAKE_CURRENT_SOURCE_DIR}/private/game_logic_caller.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/gui_application_impl.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/gui_callback.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/gui_ctx_setup.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/gui_dedicated_thread.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/gui_engine_impl.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/gui_event_queue_impl.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/gui_image_provider_impl.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/gui_input_impl.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/gui_renderer_impl.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/gui_rendering_setup_routines.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/gui_subtree_impl.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/opengl_debug_logger.cpp -) - -list(APPEND QT_SDL_SOURCES - ${CMAKE_CURRENT_SOURCE_DIR}/private/livereload/deferred_initial_constant_property_values.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/livereload/gui_live_reloader.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/livereload/recursive_directory_watcher.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/private/livereload/recursive_directory_watcher_worker.cpp -) - -set(QT_SDL_SOURCES ${QT_SDL_SOURCES} PARENT_SCOPE) diff --git a/libopenage/gui/guisys/link/gui_item.cpp b/libopenage/gui/guisys/link/gui_item.cpp deleted file mode 100644 index d8116501c4..0000000000 --- a/libopenage/gui/guisys/link/gui_item.cpp +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. - -#include "gui_item.h" - -namespace qtsdl { - - -QString name_tidier(const char *name) { - QString cleaner_name = QString::fromLatin1(name); - cleaner_name.remove(QRegularExpression("qtsdl|PersistentCoreHolder")); - return cleaner_name; -} - -GuiItemQObject::GuiItemQObject(QObject *parent) - : - QObject{parent}, - GuiItemBase{} { -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/link/gui_singleton_item.cpp b/libopenage/gui/guisys/link/gui_singleton_item.cpp deleted file mode 100644 index c0348a4b90..0000000000 --- a/libopenage/gui/guisys/link/gui_singleton_item.cpp +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "gui_singleton_item.h" - -namespace qtsdl { - -GuiSingletonItem::GuiSingletonItem(QObject *parent) - : - QObject{parent}, - GuiItemLink{} { -} - -GuiSingletonItem::~GuiSingletonItem() = default; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/link/gui_singleton_item.h b/libopenage/gui/guisys/link/gui_singleton_item.h deleted file mode 100644 index 73c322ba4d..0000000000 --- a/libopenage/gui/guisys/link/gui_singleton_item.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "gui_item_link.h" - -namespace qtsdl { - -class GuiSingletonItem : public QObject, public GuiItemLink { - Q_OBJECT - -public: - explicit GuiSingletonItem(QObject *parent=nullptr); - virtual ~GuiSingletonItem(); -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/link/qml_engine_with_singleton_items_info.cpp b/libopenage/gui/guisys/link/qml_engine_with_singleton_items_info.cpp deleted file mode 100644 index 8b6f403f29..0000000000 --- a/libopenage/gui/guisys/link/qml_engine_with_singleton_items_info.cpp +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#include "qml_engine_with_singleton_items_info.h" - -#include - -namespace qtsdl { - -QmlEngineWithSingletonItemsInfo::QmlEngineWithSingletonItemsInfo(std::vector> &&image_providers, GuiSingletonItemsInfo *singleton_items_info) - : - QmlEngineWithSingletonItemsInfo{image_providers, singleton_items_info} { -} - -QmlEngineWithSingletonItemsInfo::QmlEngineWithSingletonItemsInfo(std::vector> &image_providers, GuiSingletonItemsInfo *singleton_items_info) - : - QQmlEngine{}, - image_providers(image_providers.size()), - singleton_items_info{singleton_items_info} { - - std::transform(std::begin(image_providers), std::end(image_providers), std::begin(this->image_providers), [] (const std::unique_ptr &image_provider) { - return image_provider.get(); - }); - - std::for_each(std::begin(image_providers), std::end(image_providers), [this] (std::unique_ptr &image_provider) { - auto id = image_provider->get_id(); - this->addImageProvider(id, image_provider.release()); - }); -} - -QmlEngineWithSingletonItemsInfo::~QmlEngineWithSingletonItemsInfo() { - std::for_each(std::begin(this->image_providers), std::end(this->image_providers), [this] (GuiImageProviderImpl *image_provider) { - image_provider->give_up(); - this->removeImageProvider(image_provider->get_id()); - }); -} - -GuiSingletonItemsInfo* QmlEngineWithSingletonItemsInfo::get_singleton_items_info() const { - return this->singleton_items_info; -} - -std::vector QmlEngineWithSingletonItemsInfo::get_image_providers() const { - return this->image_providers; -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/link/qml_engine_with_singleton_items_info.h b/libopenage/gui/guisys/link/qml_engine_with_singleton_items_info.h deleted file mode 100644 index 2470f24cce..0000000000 --- a/libopenage/gui/guisys/link/qml_engine_with_singleton_items_info.h +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include - -#include "../private/gui_image_provider_impl.h" - -namespace qtsdl { - -class GuiSingletonItemsInfo; - -/** - * The Qml Engine used by openage. - * - * It's extended to contain the "singleton items info" and a list of image providers. - * The singleton item info is just a struct that allows to carry some variables - * in the qml-engine, namely the openage-engine. - * - * That way, the openage-engine and the qml-engine have a 1:1 relation and - * qml can access the main engine directly. - */ -class QmlEngineWithSingletonItemsInfo : public QQmlEngine { - Q_OBJECT - -public: - explicit QmlEngineWithSingletonItemsInfo(std::vector> &&image_providers, GuiSingletonItemsInfo *singleton_items_info=nullptr); - explicit QmlEngineWithSingletonItemsInfo(std::vector> &image_providers, GuiSingletonItemsInfo *singleton_items_info=nullptr); - virtual ~QmlEngineWithSingletonItemsInfo(); - - GuiSingletonItemsInfo* get_singleton_items_info() const; - std::vector get_image_providers() const; - -private: - std::vector image_providers; - GuiSingletonItemsInfo *singleton_items_info; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/link/qtsdl_checked_static_cast.h b/libopenage/gui/guisys/link/qtsdl_checked_static_cast.h deleted file mode 100644 index ecd5506984..0000000000 --- a/libopenage/gui/guisys/link/qtsdl_checked_static_cast.h +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -namespace qtsdl { - -template -T checked_static_cast(U *u) { - assert(dynamic_cast(u)); - return static_cast(u); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/game_logic_caller.cpp b/libopenage/gui/guisys/private/game_logic_caller.cpp deleted file mode 100644 index 3e277f686b..0000000000 --- a/libopenage/gui/guisys/private/game_logic_caller.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "game_logic_caller.h" - -#include "gui_callback.h" - -namespace qtsdl { - -GameLogicCaller::GameLogicCaller() - : - QObject{} { -} - -void GameLogicCaller::set_game_logic_callback(GuiCallback *game_logic_callback) { - QObject::disconnect(this, &GameLogicCaller::in_game_logic_thread, nullptr, nullptr); - QObject::disconnect(this, &GameLogicCaller::in_game_logic_thread_blocking, nullptr, nullptr); - QObject::connect(this, &GameLogicCaller::in_game_logic_thread, game_logic_callback, &GuiCallback::process); - QObject::connect(this, &GameLogicCaller::in_game_logic_thread_blocking, game_logic_callback, &GuiCallback::process_blocking, Qt::DirectConnection); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/game_logic_caller.h b/libopenage/gui/guisys/private/game_logic_caller.h deleted file mode 100644 index ef858b67b4..0000000000 --- a/libopenage/gui/guisys/private/game_logic_caller.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include - -namespace qtsdl { - -class GuiCallback; - -/** - * Attaches to the GuiCallbackImpl. - */ -class GameLogicCaller : public QObject { - Q_OBJECT - -public: - explicit GameLogicCaller(); - - /** - * Set up signal to be able to run code in the game logic thread. - */ - void set_game_logic_callback(GuiCallback *game_logic_callback); - -signals: - void in_game_logic_thread(const std::function& f) const; - void in_game_logic_thread_blocking(const std::function& f) const; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_application_impl.cpp b/libopenage/gui/guisys/private/gui_application_impl.cpp deleted file mode 100644 index bd5f6eeb78..0000000000 --- a/libopenage/gui/guisys/private/gui_application_impl.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "gui_application_impl.h" - -#include -#include - -#include -#include -#include - -namespace qtsdl { - -std::weak_ptr GuiApplicationImpl::instance; - -std::shared_ptr GuiApplicationImpl::get() { - std::shared_ptr candidate = GuiApplicationImpl::instance.lock(); - - assert(!candidate || std::this_thread::get_id() == candidate->owner); - - // Ensure that OpenGL is used and not OpenGL ES. - // This occurred in macos. See issue #1177 (PR #1179) - if (!candidate) { - QSurfaceFormat format; - format.setRenderableType(QSurfaceFormat::OpenGL); - QSurfaceFormat::setDefaultFormat(format); - } - - return candidate ? candidate : std::shared_ptr{new GuiApplicationImpl}; -} - -GuiApplicationImpl::~GuiApplicationImpl() { - assert(std::this_thread::get_id() == this->owner); -} - -void GuiApplicationImpl::processEvents() { - assert(std::this_thread::get_id() == this->owner); -#ifndef __APPLE__ - this->app.processEvents(); -#endif -} - -namespace { - int argc = 1; - char arg[] = "qtsdl"; - char *argv = &arg[0]; -} - -GuiApplicationImpl::GuiApplicationImpl() - : -#ifndef NDEBUG - owner{std::this_thread::get_id()}, -#endif - app{argc, &argv} -{ - // Set locale back to POSIX for the decimal point parsing (see qcoreapplication.html#locale-settings). - std::locale::global(std::locale().combine>(std::locale::classic())); - - qInfo() << "Compiled with Qt" << QT_VERSION_STR << "and run with Qt" << qVersion(); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_application_impl.h b/libopenage/gui/guisys/private/gui_application_impl.h deleted file mode 100644 index 14cd858691..0000000000 --- a/libopenage/gui/guisys/private/gui_application_impl.h +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -#include - -namespace qtsdl { - -/** - * Houses gui logic event queue. - * - * To launch it in a dedicated thread, use qtsdl::GuiDedicatedThread instead. - */ -class GuiApplicationImpl { -public: - static std::shared_ptr get(); - - ~GuiApplicationImpl(); - - void processEvents(); - -private: - GuiApplicationImpl(); - - GuiApplicationImpl(const GuiApplicationImpl&) = delete; - GuiApplicationImpl& operator=(const GuiApplicationImpl&) = delete; - -#ifndef NDEBUG - const std::thread::id owner; -#endif - - QGuiApplication app; - - static std::weak_ptr instance; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_callback.cpp b/libopenage/gui/guisys/private/gui_callback.cpp deleted file mode 100644 index 57c63ee01c..0000000000 --- a/libopenage/gui/guisys/private/gui_callback.cpp +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "gui_callback.h" - -#include - -namespace qtsdl { - -namespace { -const int registration = qRegisterMetaType>("function"); -} - -GuiCallback::GuiCallback() - : - QObject{} { - Q_UNUSED(registration); -} - -GuiCallback::~GuiCallback() = default; - -void GuiCallback::process(const std::function &f) { - f(); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_callback.h b/libopenage/gui/guisys/private/gui_callback.h deleted file mode 100644 index e42515b15f..0000000000 --- a/libopenage/gui/guisys/private/gui_callback.h +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include -#include - -namespace qtsdl { - -class GuiCallback : public QObject { - Q_OBJECT - -public: - GuiCallback(); - virtual ~GuiCallback(); - -signals: - void process_blocking(const std::function &f); - -public slots: - void process(const std::function &f); -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_ctx_setup.cpp b/libopenage/gui/guisys/private/gui_ctx_setup.cpp deleted file mode 100644 index 3cc34d35fe..0000000000 --- a/libopenage/gui/guisys/private/gui_ctx_setup.cpp +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. - -#include "gui_ctx_setup.h" - -#include - -#include - -#include "opengl_debug_logger.h" -#include "platforms/context_extraction.h" - -namespace qtsdl { - -CtxExtractionException::CtxExtractionException(const std::string &what_arg) : - std::runtime_error{what_arg} { -} - -QOpenGLContext *CtxExtractionMode::get_ctx() { - return &this->ctx; -} - -GuiUniqueRenderingContext::GuiUniqueRenderingContext(/* SDL_Window *window */) : - CtxExtractionMode{} { - QVariant handle; - WId id; - - // std::tie(handle, id) = extract_native_context(window); - - // if (handle.isValid()) { - // // pass the SDL opengl context so qt can use it - // this->ctx.setNativeHandle(handle); - this->ctx.create(); - assert(this->ctx.isValid()); - - // reuse the sdl window - QWindow *w = QWindow::fromWinId(id); // fails on Wayland! - w->setSurfaceType(QSurface::OpenGLSurface); - - if (this->ctx.makeCurrent(w)) { - return; - } - // } - - throw CtxExtractionException("adding GUI to the main rendering context failed"); -} - -void GuiUniqueRenderingContext::pre_render() { -} - -void GuiUniqueRenderingContext::post_render() { -} - -GuiSeparateRenderingContext::GuiSeparateRenderingContext(/* SDL_Window *window */) : - CtxExtractionMode{} { - QVariant handle; - - // std::tie(handle, this->make_current_back) = extract_native_context_and_switchback_func(window); - - // if (handle.isValid()) { - // this->main_ctx.setNativeHandle(handle); - this->main_ctx.create(); - assert(this->main_ctx.isValid()); - - auto context_debug_parameters = get_current_opengl_debug_parameters(this->main_ctx); - - this->ctx.setFormat(this->main_ctx.format()); - this->ctx.setShareContext(&this->main_ctx); - this->ctx.create(); - assert(this->ctx.isValid()); - assert(!(this->main_ctx.format().options() ^ this->ctx.format().options()).testFlag(QSurfaceFormat::DebugContext)); - - this->offscreen_surface.setFormat(this->ctx.format()); - this->offscreen_surface.create(); - - this->pre_render(); - apply_opengl_debug_parameters(context_debug_parameters, this->ctx); - this->post_render(); - // } - // else { - // throw CtxExtractionException("creating separate context for GUI failed"); - // } -} - -GuiSeparateRenderingContext::~GuiSeparateRenderingContext() { - this->pre_render(); - this->ctx_logger.reset(); - this->post_render(); -} - -void GuiSeparateRenderingContext::pre_render() { - if (!this->ctx.makeCurrent(&this->offscreen_surface)) { - assert(false); - return; - } -} - -void GuiSeparateRenderingContext::post_render() { - this->make_current_back(); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_ctx_setup.h b/libopenage/gui/guisys/private/gui_ctx_setup.h deleted file mode 100644 index 8b5d1952d9..0000000000 --- a/libopenage/gui/guisys/private/gui_ctx_setup.h +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include - -#include -#include - -QT_FORWARD_DECLARE_CLASS(QOpenGLDebugLogger) - -namespace qtsdl { - -class CtxExtractionException : public std::runtime_error { -public: - explicit CtxExtractionException(const std::string &what_arg); -}; - -/** - * Abstract base for the method of getting a Qt-usable context. - */ -class CtxExtractionMode { -public: - virtual ~CtxExtractionMode() { - } - - /** - * @return context that can be used by Qt - */ - QOpenGLContext *get_ctx(); - - /** - * Function that must be called before rendering the GUI. - */ - virtual void pre_render() = 0; - - /** - * Function that must be called after rendering the GUI. - */ - virtual void post_render() = 0; - -protected: - QOpenGLContext ctx; -}; - -/** - * Use the same context to render the GUI. - */ -class GuiUniqueRenderingContext : public CtxExtractionMode { -public: - explicit GuiUniqueRenderingContext(/* SDL_Window *window */); - - virtual void pre_render() override; - virtual void post_render() override; -}; - -/** - * Create a separate context to render the GUI, make it shared with the main context. - */ -class GuiSeparateRenderingContext : public CtxExtractionMode { -public: - explicit GuiSeparateRenderingContext(/* SDL_Window *window */); - virtual ~GuiSeparateRenderingContext(); - - virtual void pre_render() override; - virtual void post_render() override; - -private: - /** - * GL context of the game - */ - QOpenGLContext main_ctx; - - /** - * GL debug logger of the GL context of the GUI - */ - std::unique_ptr ctx_logger; - - /** - * Function to make the game context current - */ - std::function make_current_back; - - /** - * Surface that is needed to make the GUI context current - */ - QOffscreenSurface offscreen_surface; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_dedicated_thread.cpp b/libopenage/gui/guisys/private/gui_dedicated_thread.cpp deleted file mode 100644 index 5eab4a77d0..0000000000 --- a/libopenage/gui/guisys/private/gui_dedicated_thread.cpp +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#include "gui_dedicated_thread.h" - -#include -#include - -#include - -#include "gui_application_impl.h" - -namespace qtsdl { - -std::weak_ptr GuiDedicatedThread::instance; - -bool GuiDedicatedThread::exists = false; -std::mutex GuiDedicatedThread::existence_guard; -std::condition_variable GuiDedicatedThread::destroyed; - -GuiDedicatedThread::GuiDedicatedThread() - : - worker{} { - - bool gui_started = false; - std::mutex gui_started_guard; - std::unique_lock lck{gui_started_guard}; - - std::condition_variable proceed_cond; - - this->worker = std::thread{[&] { - auto app = GuiApplicationImpl::get(); - - { - std::unique_lock lckInGui{gui_started_guard}; - gui_started = true; - } - - proceed_cond.notify_one(); - - QCoreApplication::instance()->exec(); - }}; - - proceed_cond.wait(lck, [&] {return gui_started;}); -} - -GuiDedicatedThread::~GuiDedicatedThread() { - QCoreApplication::instance()->quit(); - this->worker.join(); -} - -std::shared_ptr GuiDedicatedThread::get() { - std::shared_ptr candidate; - - std::unique_lock lck{GuiDedicatedThread::existence_guard}; - - GuiDedicatedThread::destroyed.wait(lck, [&candidate] { - return (candidate = GuiDedicatedThread::instance.lock()) || !GuiDedicatedThread::exists; - }); - - if (!candidate) { - GuiDedicatedThread::instance = candidate = std::shared_ptr{new GuiDedicatedThread, [] (GuiDedicatedThread *p) { - delete p; - - if (p) { - std::unique_lock dlck{GuiDedicatedThread::existence_guard}; - GuiDedicatedThread::exists = false; - - dlck.unlock(); - GuiDedicatedThread::destroyed.notify_all(); - } - }}; - - GuiDedicatedThread::exists = true; - - lck.unlock(); - GuiDedicatedThread::destroyed.notify_all(); - } - - return candidate; -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_dedicated_thread.h b/libopenage/gui/guisys/private/gui_dedicated_thread.h deleted file mode 100644 index f96339b5c7..0000000000 --- a/libopenage/gui/guisys/private/gui_dedicated_thread.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include -#include - -namespace qtsdl { - -/** - * Runs the gui logic in separate thread. - * - * For sharing the thread with something else, use qtsdl::GuiApplicationImpl instead. - */ -class GuiDedicatedThread { -public: - static std::shared_ptr get(); - - ~GuiDedicatedThread(); - -private: - GuiDedicatedThread(); - - std::thread worker; - - static std::weak_ptr instance; - - static bool exists; - static std::mutex existence_guard; - static std::condition_variable destroyed; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_engine_impl.cpp b/libopenage/gui/guisys/private/gui_engine_impl.cpp deleted file mode 100644 index e83141fb13..0000000000 --- a/libopenage/gui/guisys/private/gui_engine_impl.cpp +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "gui_engine_impl.h" - -#include - -#include -#include -#include - -#include "../public/gui_engine.h" -#include "gui_image_provider_impl.h" -#include "gui_renderer_impl.h" - -namespace qtsdl { - -GuiEngineImpl::GuiEngineImpl(GuiRenderer *renderer, - const std::vector &image_providers, - GuiSingletonItemsInfo *singleton_items_info) - : - QObject{}, - renderer{}, - engine{GuiImageProviderImpl::take_ownership(image_providers), singleton_items_info} { - - QThread *gui_thread = QCoreApplication::instance()->thread(); - this->moveToThread(gui_thread); - this->engine.moveToThread(gui_thread); - this->watcher.moveToThread(gui_thread); - - assert(!this->engine.incubationController()); - this->attach_to(GuiRendererImpl::impl(renderer)); - - QObject::connect(this, - &GuiEngineImpl::rootDirsPathsChanged, - &this->watcher, - &RecursiveDirectoryWatcher::rootDirsPathsChanged); - - QObject::connect(&this->watcher, - &RecursiveDirectoryWatcher::changeDetected, - this, - &GuiEngineImpl::onReload); -} - -GuiEngineImpl::~GuiEngineImpl() = default; - -GuiEngineImpl* GuiEngineImpl::impl(GuiEngine *engine) { - return engine->impl.get(); -} - -void GuiEngineImpl::attach_to(GuiRendererImpl *renderer) { - this->renderer = renderer; - this->engine.setIncubationController(this->renderer->get_window()->incubationController()); -} - -QQmlEngine* GuiEngineImpl::get_qml_engine() { - return &this->engine; -} - -void GuiEngineImpl::onReload() { - qDebug("reloading GUI"); - this->engine.clearComponentCache(); - emit this->reload(); -} - -void GuiEngineImpl::add_root_dir_path(const QString &root_dir_path) { - this->root_dirs_paths.push_back(root_dir_path); - emit this->rootDirsPathsChanged(this->root_dirs_paths); -} - -void GuiEngineImpl::remove_root_dir_path(const QString &root_dir_path) { - if (this->root_dirs_paths.removeOne(root_dir_path)) - emit this->rootDirsPathsChanged(this->root_dirs_paths); - else - qWarning() << "Failed to remove path watched by ReloadableQmlEngine."; -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_engine_impl.h b/libopenage/gui/guisys/private/gui_engine_impl.h deleted file mode 100644 index 57f78569aa..0000000000 --- a/libopenage/gui/guisys/private/gui_engine_impl.h +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -#include "../link/qml_engine_with_singleton_items_info.h" -#include "livereload/recursive_directory_watcher.h" - -QT_FORWARD_DECLARE_CLASS(QQuickWindow) - -namespace qtsdl { - -class GuiRenderer; -class GuiRendererImpl; -class GuiImageProvider; -class GuiEngine; -class GuiSingletonItemsInfo; - -class GuiEngineImpl : public QObject { - Q_OBJECT - -public: - explicit GuiEngineImpl(GuiRenderer *renderer, - const std::vector &image_providers=std::vector(), - GuiSingletonItemsInfo *singleton_items_info=nullptr); - virtual ~GuiEngineImpl(); - - static GuiEngineImpl* impl(GuiEngine *engine); - - QQmlEngine* get_qml_engine(); - - void add_root_dir_path(const QString &root_dir_path); - void remove_root_dir_path(const QString &root_dir_path); - -signals: - void reload(); - void rootDirsPathsChanged(const QStringList&); - -public slots: - void attach_to(GuiRendererImpl *renderer); - void onReload(); - -private: - GuiRendererImpl *renderer; - - QmlEngineWithSingletonItemsInfo engine; - RecursiveDirectoryWatcher watcher; - - QStringList root_dirs_paths; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_event_queue_impl.cpp b/libopenage/gui/guisys/private/gui_event_queue_impl.cpp deleted file mode 100644 index bef677b91d..0000000000 --- a/libopenage/gui/guisys/private/gui_event_queue_impl.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "gui_event_queue_impl.h" - -#include - -#ifdef __APPLE__ -#include -#endif -#include - -#include "../public/gui_event_queue.h" - -namespace qtsdl { - -GuiEventQueueImpl::GuiEventQueueImpl() - : - thread{QThread::currentThread()} { -} - -GuiEventQueueImpl::~GuiEventQueueImpl() = default; - -GuiEventQueueImpl* GuiEventQueueImpl::impl(GuiEventQueue *event_queue) { - return event_queue->impl.get(); -} - -void GuiEventQueueImpl::process_callbacks() { - assert(QThread::currentThread() == this->thread); -#ifdef __APPLE__ - if (QThread::currentThread() != QCoreApplication::instance()->thread()) this->callback_processor.processEvents(); -#else - this->callback_processor.processEvents(); -#endif -} - -QThread* GuiEventQueueImpl::get_thread() { - return this->thread; -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_event_queue_impl.h b/libopenage/gui/guisys/private/gui_event_queue_impl.h deleted file mode 100644 index 5c97155f0d..0000000000 --- a/libopenage/gui/guisys/private/gui_event_queue_impl.h +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -QT_FORWARD_DECLARE_CLASS(QThread) - -namespace qtsdl { - -class GuiEventQueue; - -/** - * Provides synchronization with some game thread. - */ -class GuiEventQueueImpl { -public: - explicit GuiEventQueueImpl(); - ~GuiEventQueueImpl(); - - static GuiEventQueueImpl* impl(GuiEventQueue *event_queue); - - void process_callbacks(); - - QThread* get_thread(); - -private: - QThread * const thread; - QEventLoop callback_processor; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_image_provider_impl.cpp b/libopenage/gui/guisys/private/gui_image_provider_impl.cpp deleted file mode 100644 index 1badada7d2..0000000000 --- a/libopenage/gui/guisys/private/gui_image_provider_impl.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "gui_image_provider_impl.h" - -#include - -#include "../public/gui_image_provider.h" - -namespace qtsdl { - -GuiImageProviderImpl::GuiImageProviderImpl() - : - QQuickImageProvider{QQmlImageProviderBase::Texture, QQuickImageProvider::ForceAsynchronousImageLoading} { -} - -GuiImageProviderImpl::~GuiImageProviderImpl() = default; - -std::unique_ptr GuiImageProviderImpl::take_ownership(GuiImageProvider *image_provider) { - std::unique_ptr ptr{image_provider->impl.release()}; - image_provider->impl = decltype(image_provider->impl) {ptr.get(), [] (GuiImageProviderImpl*) {}}; - return ptr; -} - -std::vector> GuiImageProviderImpl::take_ownership(const std::vector &image_providers) { - std::vector> image_provider_owning_ptrs(image_providers.size()); - - std::transform(std::begin(image_providers), std::end(image_providers), std::begin(image_provider_owning_ptrs), static_cast(*)(GuiImageProvider*)>(GuiImageProviderImpl::take_ownership)); - - return image_provider_owning_ptrs; -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_image_provider_impl.h b/libopenage/gui/guisys/private/gui_image_provider_impl.h deleted file mode 100644 index 409fa1815e..0000000000 --- a/libopenage/gui/guisys/private/gui_image_provider_impl.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -#include - -namespace qtsdl { - -class GuiImageProvider; - -class GuiImageProviderImpl : public QQuickImageProvider { -public: - explicit GuiImageProviderImpl(); - virtual ~GuiImageProviderImpl(); - - static std::unique_ptr take_ownership(GuiImageProvider *image_provider); - static std::vector> take_ownership(const std::vector &image_providers); - - virtual const char* get_id() const = 0; - - virtual void give_up() = 0; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_input_impl.cpp b/libopenage/gui/guisys/private/gui_input_impl.cpp deleted file mode 100644 index 8675785596..0000000000 --- a/libopenage/gui/guisys/private/gui_input_impl.cpp +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "gui_input_impl.h" - -#include -#include -#include - -#include "../public/gui_event_queue.h" -#include "../public/gui_renderer.h" -#include "gui_event_queue_impl.h" -#include "gui_renderer_impl.h" - -namespace qtsdl { - -GuiInputImpl::GuiInputImpl(GuiRenderer *renderer, GuiEventQueue *game_logic_updater) : - QObject{}, - mouse_buttons_state{}, - game_logic_updater{GuiEventQueueImpl::impl(game_logic_updater)} { - const bool logic_diff_input = this->game_logic_updater->get_thread() != QThread::currentThread(); - const bool gui_diff_input = QCoreApplication::instance()->thread() != QThread::currentThread(); - const Qt::ConnectionType input_to_gui = gui_diff_input ? logic_diff_input ? Qt::BlockingQueuedConnection : Qt::QueuedConnection : Qt::DirectConnection; - - QObject::connect(this, &GuiInputImpl::input_event, GuiRendererImpl::impl(renderer)->get_window(), &EventHandlingQuickWindow::on_input_event, input_to_gui); -} - -GuiInputImpl::~GuiInputImpl() = default; - -namespace { -// static_assert(!(Qt::LeftButton & (static_cast(Qt::LeftButton) - 1)), "Qt non-one-bit mask."); -// static_assert(!(Qt::RightButton & (static_cast(Qt::RightButton) - 1)), "Qt non-one-bit mask."); -// static_assert(!(Qt::MiddleButton & (static_cast(Qt::MiddleButton) - 1)), "Qt non-one-bit mask."); -// static_assert(!(Qt::XButton1 & (static_cast(Qt::XButton1) - 1)), "Qt non-one-bit mask."); -// static_assert(!(Qt::XButton2 & (static_cast(Qt::XButton2) - 1)), "Qt non-one-bit mask."); - -// static_assert(SDL_BUTTON_LMASK == Qt::LeftButton, "SDL/Qt mouse button mask incompatibility."); -// static_assert(1 << (SDL_BUTTON_LEFT - 1) == Qt::LeftButton, "SDL/Qt mouse button mask incompatibility."); - -// // Right and middle are swapped. -// static_assert(SDL_BUTTON_RMASK == Qt::MiddleButton, "SDL/Qt mouse button mask incompatibility."); -// static_assert(1 << (SDL_BUTTON_RIGHT - 1) == Qt::MiddleButton, "SDL/Qt mouse button mask incompatibility."); - -// static_assert(SDL_BUTTON_MMASK == Qt::RightButton, "SDL/Qt mouse button mask incompatibility."); -// static_assert(1 << (SDL_BUTTON_MIDDLE - 1) == Qt::RightButton, "SDL/Qt mouse button mask incompatibility."); - -// static_assert(SDL_BUTTON_X1MASK == Qt::XButton1, "SDL/Qt mouse button mask incompatibility."); -// static_assert(1 << (SDL_BUTTON_X1 - 1) == Qt::XButton1, "SDL/Qt mouse button mask incompatibility."); - -// static_assert(SDL_BUTTON_X2MASK == Qt::XButton2, "SDL/Qt mouse button mask incompatibility."); -// static_assert(1 << (SDL_BUTTON_X2 - 1) == Qt::XButton2, "SDL/Qt mouse button mask incompatibility."); - -// static_assert(Qt::MiddleButton >> 1 == Qt::RightButton, "Qt::RightButton or Qt::MiddleButton has moved."); - -// int sdl_mouse_mask_to_qt(Uint32 state) { -// return (state & (Qt::LeftButton | Qt::XButton1 | Qt::XButton2)) | ((state & Qt::RightButton) << 1) | ((state & Qt::MiddleButton) >> 1); -// } - -// Qt::MouseButtons sdl_mouse_state_to_qt(Uint32 state) { -// return static_cast(sdl_mouse_mask_to_qt(state)); -// } - -// Qt::MouseButton sdl_mouse_btn_to_qt(Uint8 button) { -// return static_cast(sdl_mouse_mask_to_qt(1 << (button - 1))); -// } - -// int sdl_key_to_qt(SDL_Keycode sym) { -// switch (sym) { -// case SDLK_BACKSPACE: -// return Qt::Key_Backspace; -// case SDLK_DELETE: -// return Qt::Key_Delete; -// default: -// return 0; -// } -// } -} // namespace - -bool GuiInputImpl::process(/* SDL_Event *e */) { - // switch (e->type) { - // case SDL_MOUSEMOTION: { - // QMouseEvent ev{QEvent::MouseMove, QPoint{e->motion.x, e->motion.y}, Qt::MouseButton::NoButton, this->mouse_buttons_state = sdl_mouse_state_to_qt(e->motion.state), Qt::KeyboardModifier::NoModifier}; - // ev.setAccepted(false); - - // // Allow dragging stuff under the gui overlay. - // return relay_input_event(&ev, e->motion.state & (SDL_BUTTON_LMASK | SDL_BUTTON_MMASK | SDL_BUTTON_RMASK)); - // } - - // case SDL_MOUSEBUTTONDOWN: { - // auto button = sdl_mouse_btn_to_qt(e->button.button); - // QMouseEvent ev{QEvent::MouseButtonPress, QPoint{e->button.x, e->button.y}, button, this->mouse_buttons_state |= button, Qt::KeyboardModifier::NoModifier}; - // ev.setAccepted(false); - - // bool accepted = relay_input_event(&ev); - - // if (e->button.clicks == 2) { - // QMouseEvent ev_dbl{QEvent::MouseButtonDblClick, QPoint{e->button.x, e->button.y}, button, this->mouse_buttons_state, Qt::KeyboardModifier::NoModifier}; - // ev_dbl.setAccepted(false); - // accepted = relay_input_event(&ev_dbl) || accepted; - // } - - // return accepted; - // } - - // case SDL_MOUSEBUTTONUP: { - // auto button = sdl_mouse_btn_to_qt(e->button.button); - // QMouseEvent ev{QEvent::MouseButtonRelease, QPoint{e->button.x, e->button.y}, button, this->mouse_buttons_state &= ~button, Qt::KeyboardModifier::NoModifier}; - // ev.setAccepted(false); - - // // Allow dragging stuff under the gui overlay: when no item is grabbed, it probably means that initial MousButtonPress was outside gui. - // return relay_input_event(&ev, true); - // } - - // case SDL_MOUSEWHEEL: { - // QPoint pos; - // SDL_GetMouseState(&pos.rx(), &pos.ry()); - - // QWheelEvent ev{ - // pos, - // pos, - // QPoint{}, - // QPoint{e->wheel.x, e->wheel.y}, - // this->mouse_buttons_state, - // Qt::KeyboardModifier::NoModifier, // Correct states? - // Qt::ScrollPhase::NoScrollPhase, // ^ - // false, - // }; - // ev.setAccepted(false); - - // return relay_input_event(&ev); - // } - - // case SDL_KEYDOWN: { - // QKeyEvent ev{QEvent::KeyPress, sdl_key_to_qt(e->key.keysym.sym), Qt::NoModifier, QChar(static_cast(e->key.keysym.sym))}; - // ev.setAccepted(false); - // return relay_input_event(&ev); - // } - - // case SDL_KEYUP: { - // QKeyEvent ev{QEvent::KeyRelease, sdl_key_to_qt(e->key.keysym.sym), Qt::NoModifier, QChar(static_cast(e->key.keysym.sym))}; - // ev.setAccepted(false); - // return relay_input_event(&ev); - // } - - // default: - // return false; - // } - return false; -} - -bool GuiInputImpl::relay_input_event(QEvent *ev, bool only_if_grabbed) { - const bool logic_diff_input = this->game_logic_updater->get_thread() != QThread::currentThread(); - std::atomic processed{false}; - - emit this->input_event(&processed, ev, only_if_grabbed); - - // We have sent an event to the gui thread and want a response about collision. But the gui can be - // executing a getter that blocks the gui thread while doing something in the logic thread. - // So, if the logic thread is the same as the input thread, it should be running somehow. - // - // TODO: if/when the logic thread or input thread of the main game is made separate, give mutex or - // queue to the gui in order to replace this busy wait. - if (!logic_diff_input) - while (!processed) { - this->game_logic_updater->process_callbacks(); - QThread::usleep(1); - } - - return ev->isAccepted(); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_input_impl.h b/libopenage/gui/guisys/private/gui_input_impl.h deleted file mode 100644 index 4863b239e8..0000000000 --- a/libopenage/gui/guisys/private/gui_input_impl.h +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include - -namespace qtsdl { - -class GuiRenderer; -class GuiEventQueue; -class GuiEventQueueImpl; - -class GuiInputImpl : public QObject { - Q_OBJECT - -public: - explicit GuiInputImpl(GuiRenderer *renderer, GuiEventQueue *game_logic_updater); - virtual ~GuiInputImpl(); - - /** - * Returns true if the event was accepted. - */ - bool process(/* SDL_Event *e */); - -signals: - void input_event(std::atomic *processed, QEvent *ev, bool only_if_grabbed = false); - -private: - bool relay_input_event(QEvent *ev, bool only_if_grabbed = false); - - Qt::MouseButtons mouse_buttons_state; - GuiEventQueueImpl *game_logic_updater; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_renderer_impl.cpp b/libopenage/gui/guisys/private/gui_renderer_impl.cpp deleted file mode 100644 index a75974655f..0000000000 --- a/libopenage/gui/guisys/private/gui_renderer_impl.cpp +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "gui_renderer_impl.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../public/gui_renderer.h" - - -namespace qtsdl { - -namespace { -const int registration = qRegisterMetaType *>("atomic_bool_ptr"); -} - -EventHandlingQuickWindow::EventHandlingQuickWindow(QQuickRenderControl *render_control) : - QQuickWindow{render_control}, - focused_item{} { - Q_UNUSED(registration); -} - -EventHandlingQuickWindow::~EventHandlingQuickWindow() = default; - -void EventHandlingQuickWindow::on_input_event(std::atomic *processed, QEvent *event, bool only_if_grabbed) { - if (!only_if_grabbed || this->mouseGrabberItem()) { - if (this->focused_item && (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease)) { - QCoreApplication::instance()->sendEvent(this->focused_item, event); - } - else { - QCoreApplication::instance()->sendEvent(this, event); - - auto change_focus = [this](QQuickItem *item) { - if (this->focused_item != item) { - if (this->focused_item) { - QFocusEvent focus_out{QEvent::FocusOut, Qt::ActiveWindowFocusReason}; - QCoreApplication::instance()->sendEvent(this->focused_item, &focus_out); - } - - if (item) { - QFocusEvent focus_in{QEvent::FocusIn, Qt::ActiveWindowFocusReason}; - QCoreApplication::instance()->sendEvent(item, &focus_in); - } - } - - this->focused_item = item; - }; - - // Loose keyboard focus when clicked outside of gui. - if (event->type() == QEvent::MouseButtonPress && !event->isAccepted()) - change_focus(nullptr); - - // Normally, the QQuickWindow would handle keyboard focus automatically, but it can't because neither QQuickWindow nor - // its target QWindow respond to requestActivate(). Which means no focus event propagation when injecting mouse clicks. - // So, the workaround is to look specifically for TextFields and give them focus directly. - // TODO: to remove when the proper focus for the foreign (that obtained from QWindow::fromWinId()) windows is implemented (Qt 5.6). - if (this->mouseGrabberItem() && this->mouseGrabberItem()->metaObject()->superClass() && this->mouseGrabberItem()->metaObject()->superClass()->className() == QString("QQuickTextInput") && (event->type() == QEvent::MouseButtonPress)) - change_focus(this->mouseGrabberItem()); - } - } - - *processed = true; -} - -void EventHandlingQuickWindow::on_resized(const QSize &size) { - this->resize(size); -} - -GuiRendererImpl::GuiRendererImpl(/* SDL_Window *window */) : - QObject{}, - gui_rendering_setup_routines{/* window */}, - need_fbo_resize{true}, - need_sync{}, - need_render{}, - gui_locked{}, - renderer_waiting_on_cond{} { - this->moveToThread(QCoreApplication::instance()->thread()); - - QObject::connect(&this->render_control, &QQuickRenderControl::renderRequested, [&]() { - this->need_render = true; - }); - - QObject::connect(&this->render_control, &QQuickRenderControl::sceneChanged, this, &GuiRendererImpl::on_scene_changed); - - this->window = std::make_unique(&this->render_control); - this->window->moveToThread(QCoreApplication::instance()->thread()); - QObject::connect(this, &GuiRendererImpl::resized, this->window.get(), &EventHandlingQuickWindow::on_resized); - // this->window->setClearBeforeRendering(true); - this->window->setColor(QColor{0, 0, 0, 0}); - - QObject::connect(&*this->window, &QQuickWindow::sceneGraphInitialized, this, [this] { - std::tie(this->new_fbo_width, this->new_fbo_height) = std::make_tuple(this->window->width(), this->window->height()); - this->need_fbo_resize = true; - }); - - QObject::connect(&*this->window, &QQuickWindow::widthChanged, [this] { this->new_fbo_width = this->window->width(); this->need_fbo_resize = true; }); - QObject::connect(&*this->window, &QQuickWindow::heightChanged, [this] { this->new_fbo_height = this->window->height(); this->need_fbo_resize = true; }); - - GuiRenderingCtxActivator activate_render(this->gui_rendering_setup_routines); - - // TODO: Make independent from OpenGL - this->window->setGraphicsDevice( - QQuickGraphicsDevice::fromOpenGLContext(this->gui_rendering_setup_routines.get_ctx())); - this->render_control.initialize(); -} - -void GuiRendererImpl::on_scene_changed() { - this->need_sync = true; - this->need_render = true; - this->render_control.polishItems(); -} - -void GuiRendererImpl::reinit_fbo_if_needed() { - assert(QThread::currentThread() == this->gui_rendering_setup_routines.get_ctx()->thread()); - - if (this->need_fbo_resize) { - this->fbo = std::make_unique(QSize(this->new_fbo_width, this->new_fbo_height), QOpenGLFramebufferObject::CombinedDepthStencil); - - // dirty workaround; texture id from our own implementation should be passed here - QQuickRenderTarget target = QQuickRenderTarget::fromOpenGLTexture(this->fbo->texture(), this->fbo->size()); - - this->window->setRenderTarget(target); - this->need_fbo_resize = false; - } - - assert(this->fbo); -} - -GuiRendererImpl::~GuiRendererImpl() { - // TODO: MAYBE: - // the this->ctx member frees the - // gl context even though that's SDL's job. - // - // the qt doc says that a native context isn't destroyed! - // but somehow it is lost or destroyed! - // https://doc.qt.io/qt-5/qopenglcontext.html#setNativeHandle -} - -GuiRendererImpl *GuiRendererImpl::impl(GuiRenderer *renderer) { - return renderer->impl.get(); -} - -GLuint GuiRendererImpl::render() { - GuiRenderingCtxActivator activate_render(this->gui_rendering_setup_routines); - - this->reinit_fbo_if_needed(); - - this->render_control.beginFrame(); - - // QQuickRenderControl::sync() must be called from the render thread while the gui thread is stopped. - if (this->need_sync) { - if (QCoreApplication::instance()->thread() != QThread::currentThread()) { - std::unique_lock lck{this->gui_guard}; - - if (this->need_sync) { - QCoreApplication::instance()->postEvent(this, new QEvent{QEvent::User}, INT_MAX); - - this->renderer_waiting_on_cond = true; - this->gui_locked_cond.wait(lck, [this] { return this->gui_locked; }); - this->renderer_waiting_on_cond = false; - - this->render_control.sync(); - - this->need_sync = false; - this->gui_locked = false; - - lck.unlock(); - this->gui_locked_cond.notify_one(); - } - } - else { - this->render_control.sync(); - } - } - - this->render_control.render(); - this->render_control.endFrame(); - - // this->window->resetOpenGLState(); - - return this->fbo->texture(); -} - -void GuiRendererImpl::make_sure_render_thread_unlocked() { - assert(QThread::currentThread() == QCoreApplication::instance()->thread()); - - if (this->need_sync && QThread::currentThread() != this->render_control.thread()) { - std::unique_lock lck{this->gui_guard}; - - if (this->renderer_waiting_on_cond) { - this->process_freeze(std::move(lck)); - QCoreApplication::instance()->removePostedEvents(this, QEvent::User); - } - } -} - -bool GuiRendererImpl::make_sure_render_thread_wont_sync() { - assert(QThread::currentThread() == QCoreApplication::instance()->thread()); - - if (this->need_sync && QThread::currentThread() != this->render_control.thread()) { - std::unique_lock lck{this->gui_guard}; - - if (this->renderer_waiting_on_cond) { - this->process_freeze(std::move(lck)); - QCoreApplication::instance()->removePostedEvents(this, QEvent::User); - assert(!this->need_sync); - } - else { - assert(this->need_sync); - this->need_sync = false; - return true; - } - } - - return false; -} - -void GuiRendererImpl::demand_sync() { - assert(QThread::currentThread() == QCoreApplication::instance()->thread()); - this->need_sync = true; -} - -bool GuiRendererImpl::event(QEvent *e) { - if (e->type() == QEvent::User) { - std::unique_lock lck{this->gui_guard}; - this->process_freeze(std::move(lck)); - return true; - } - else { - return this->QObject::event(e); - } -} - -void GuiRendererImpl::process_freeze(std::unique_lock lck) { - this->gui_locked = true; - - lck.unlock(); - this->gui_locked_cond.notify_one(); - - lck.lock(); - this->gui_locked_cond.wait(lck, [this] { return !this->gui_locked; }); -} - -EventHandlingQuickWindow *GuiRendererImpl::get_window() { - return &*this->window; -} - -void GuiRendererImpl::resize(const QSize &size) { - emit this->resized(size); -} - -TemporaryDisableGuiRendererSync::TemporaryDisableGuiRendererSync(GuiRendererImpl &renderer) : - renderer{renderer}, - need_sync{renderer.make_sure_render_thread_wont_sync()} { -} - -TemporaryDisableGuiRendererSync::~TemporaryDisableGuiRendererSync() { - if (this->need_sync) - renderer.demand_sync(); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_renderer_impl.h b/libopenage/gui/guisys/private/gui_renderer_impl.h deleted file mode 100644 index 45186f08c3..0000000000 --- a/libopenage/gui/guisys/private/gui_renderer_impl.h +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#ifndef __APPLE__ -#ifdef _MSC_VER -#define NOMINMAX -#include -#endif //_MSC_VER -#include -#else // __APPLE__ -#include -#endif - -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "gui_rendering_setup_routines.h" - -QT_FORWARD_DECLARE_CLASS(QOpenGLFramebufferObject) - -namespace qtsdl { - -class GuiRenderer; - -class EventHandlingQuickWindow : public QQuickWindow { - Q_OBJECT - -public: - explicit EventHandlingQuickWindow(QQuickRenderControl *render_control); - virtual ~EventHandlingQuickWindow(); - -public slots: - void on_input_event(std::atomic *processed, QEvent *event, bool only_if_grabbed); - void on_resized(const QSize &size); - -private: - // TODO: to remove when the proper focus for the foreign (that obtained from QWindow::fromWinId()) windows is implemented (Qt 5.6). - QQuickItem *focused_item; -}; - -/** - * Passes the native graphic context to Qt. - */ -class GuiRendererImpl : public QObject { - Q_OBJECT - -public: - explicit GuiRendererImpl(/* SDL_Window *window */); - ~GuiRendererImpl(); - - static GuiRendererImpl *impl(GuiRenderer *renderer); - - /** - * @return texture ID where GUI was rendered - */ - GLuint render(); - - void resize(const QSize &size); - - EventHandlingQuickWindow *get_window(); - - /** - * When render thread is locked waiting for the gui thread to finish its current event and - * go to the high-priority 'freeze' event; but the gui thread can't finish the current event - * because it's going to lock the game-logic thread that will lock the render thread somehow. - * - * In this situation the gui thread should call this function to immediately process the 'freeze' - * event handler inside current event and remove the event from the gui queue. - * - * 'GuiRendererImpl::need_sync' is only set from the gui thread, so after the calling this function, - * we are fine for entire duration of the processing of the current event. - * - * If 'GuiRendererImpl::need_sync' is set, this function blocks until the render thread comes around - * to do the 'QQuickRenderControl::sync()'. If it's not good enough, it's possible to implement two - * separate functions to set 'GuiRendererImpl::need_sync' to false and then back to true. - */ - void make_sure_render_thread_unlocked(); - - /** - * Assures that the render thread won't try to stop the gui thread for syncing. - * - * Must be called from the gui thread. - * - * @return true if need_sync was set to false (you should restore it with the demand_sync()) - */ - bool make_sure_render_thread_wont_sync(); - - /** - * Sets 'GuiRendererImpl::need_sync' to true. - * - * Must be called from the gui thread. - */ - void demand_sync(); - -signals: - void resized(const QSize &size); - -private: - virtual bool event(QEvent *e) override; - - void process_freeze(std::unique_lock lck); - -private slots: - void on_scene_changed(); - -private: - /** - * If size changes, then create a new FBO for GUI rendering - */ - void reinit_fbo_if_needed(); - - /** - * Contains rendering context - * Use GuiRenderingCtxActivator to enable it - */ - GuiRenderingSetupRoutines gui_rendering_setup_routines; - - /** - * Contains scene graph of the GUI - */ - std::unique_ptr window; - - /** - * Object for sending render command to Qt - */ - QQuickRenderControl render_control; - - /** - * FBO where the GUI is rendered - */ - std::unique_ptr fbo; - - std::atomic new_fbo_width; - std::atomic new_fbo_height; - std::atomic need_fbo_resize; - - std::atomic need_sync; - std::atomic need_render; - - bool gui_locked; - std::mutex gui_guard; - std::condition_variable gui_locked_cond; - bool renderer_waiting_on_cond; -}; - -class TemporaryDisableGuiRendererSync { -public: - explicit TemporaryDisableGuiRendererSync(GuiRendererImpl &renderer); - ~TemporaryDisableGuiRendererSync(); - -private: - TemporaryDisableGuiRendererSync(const TemporaryDisableGuiRendererSync &) = delete; - TemporaryDisableGuiRendererSync &operator=(const TemporaryDisableGuiRendererSync &) = delete; - - GuiRendererImpl &renderer; - const bool need_sync; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_rendering_setup_routines.cpp b/libopenage/gui/guisys/private/gui_rendering_setup_routines.cpp deleted file mode 100644 index 059cc3f7b2..0000000000 --- a/libopenage/gui/guisys/private/gui_rendering_setup_routines.cpp +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. - -#include "gui_rendering_setup_routines.h" - -#include - -#include - -#include "gui_ctx_setup.h" - -namespace qtsdl { - -GuiRenderingSetupRoutines::GuiRenderingSetupRoutines(/* SDL_Window *window */) { - try { - this->ctx_extraction_mode = std::make_unique(/* window */); - } - catch (const CtxExtractionException &) { - qInfo() << "Falling back to separate render context for GUI"; - - try { - this->ctx_extraction_mode = std::make_unique(/* window */); - } - catch (const CtxExtractionException &) { - assert(false && "setting up context for GUI failed"); - } - } -} - -GuiRenderingSetupRoutines::~GuiRenderingSetupRoutines() = default; - -QOpenGLContext *GuiRenderingSetupRoutines::get_ctx() { - return this->ctx_extraction_mode->get_ctx(); -} - -void GuiRenderingSetupRoutines::pre_render() { - this->ctx_extraction_mode->pre_render(); -} - -void GuiRenderingSetupRoutines::post_render() { - this->ctx_extraction_mode->post_render(); -} - -GuiRenderingCtxActivator::GuiRenderingCtxActivator(GuiRenderingSetupRoutines &rendering_setup_routines) : - rendering_setup_routines{&rendering_setup_routines} { - this->rendering_setup_routines->pre_render(); -} - -GuiRenderingCtxActivator::~GuiRenderingCtxActivator() { - if (this->rendering_setup_routines) - this->rendering_setup_routines->post_render(); -} - -GuiRenderingCtxActivator::GuiRenderingCtxActivator(GuiRenderingCtxActivator &&o) : - rendering_setup_routines{o.rendering_setup_routines} { - o.rendering_setup_routines = nullptr; -} - -GuiRenderingCtxActivator &GuiRenderingCtxActivator::operator=(GuiRenderingCtxActivator &&o) { - this->rendering_setup_routines = o.rendering_setup_routines; - o.rendering_setup_routines = nullptr; - return *this; -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_rendering_setup_routines.h b/libopenage/gui/guisys/private/gui_rendering_setup_routines.h deleted file mode 100644 index a262228c25..0000000000 --- a/libopenage/gui/guisys/private/gui_rendering_setup_routines.h +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include - -QT_FORWARD_DECLARE_CLASS(QOpenGLContext) - -namespace qtsdl { - -class CtxExtractionMode; - -class GuiRenderingCtxActivator; - -/** - * Returns a GL context usable by Qt classes. - * Provides pre- and post-rendering functions to make the context usable for GUI rendering. - */ -class GuiRenderingSetupRoutines { -public: - explicit GuiRenderingSetupRoutines(/* SDL_Window *window */); - ~GuiRenderingSetupRoutines(); - - QOpenGLContext *get_ctx(); - -private: - friend class GuiRenderingCtxActivator; - void pre_render(); - void post_render(); - - std::unique_ptr ctx_extraction_mode; -}; - -/** - * Prepares the context for rendering the GUI for one frame. - * Activator must be destroyed as soon as the GUI has executed its frame render call. - */ -class GuiRenderingCtxActivator { -public: - explicit GuiRenderingCtxActivator(GuiRenderingSetupRoutines &rendering_setup_routines); - ~GuiRenderingCtxActivator(); - - GuiRenderingCtxActivator(GuiRenderingCtxActivator &&o); - GuiRenderingCtxActivator &operator=(GuiRenderingCtxActivator &&o); - -private: - GuiRenderingCtxActivator(const GuiRenderingCtxActivator &) = delete; - GuiRenderingCtxActivator &operator=(const GuiRenderingCtxActivator &) = delete; - - GuiRenderingSetupRoutines *rendering_setup_routines; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_subtree_impl.cpp b/libopenage/gui/guisys/private/gui_subtree_impl.cpp deleted file mode 100644 index 5bfcb73fa9..0000000000 --- a/libopenage/gui/guisys/private/gui_subtree_impl.cpp +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "gui_subtree_impl.h" -#include "gui_renderer_impl.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "gui_engine_impl.h" -#include "../link/gui_item.h" -#include "../public/gui_subtree.h" -#include "gui_event_queue_impl.h" - -namespace qtsdl { - - -GuiSubtreeImpl::GuiSubtreeImpl(GuiRenderer *renderer, - GuiEventQueue *game_logic_updater, - GuiEngine *engine, - const QString &source, - const QString &rootdir) - : - QObject{}, - renderer{}, - engine{}, - root{} { - - QObject::connect( - &this->game_logic_callback, - &GuiCallback::process_blocking, - this, - &GuiSubtreeImpl::on_process_game_logic_callback_blocking, - Qt::DirectConnection - ); - - QObject::connect( - this, - &GuiSubtreeImpl::process_game_logic_callback_blocking, - &this->game_logic_callback, - &GuiCallback::process, - (QCoreApplication::instance()->thread() != QThread::currentThread() - ? Qt::BlockingQueuedConnection - : Qt::DirectConnection) - ); - - this->moveToThread(QCoreApplication::instance()->thread()); - this->attach_to(GuiEventQueueImpl::impl(game_logic_updater)); - this->attach_to(GuiRendererImpl::impl(renderer)); - this->attach_to(GuiEngineImpl::impl(engine), rootdir); - - // Should now be initialized by the engine-attaching - assert(this->root_component); - - // Need to queue the loading because some underlying game logic elements - // require the loop to be running (maybe some things that are created after - // the gui). - QMetaObject::invokeMethod(this->root_component.get(), - "loadUrl", Qt::QueuedConnection, - Q_ARG(QUrl, QUrl::fromLocalFile(source))); -} - -GuiSubtreeImpl::~GuiSubtreeImpl() = default; - -void GuiSubtreeImpl::onEngineReloaded() { - const QUrl source = this->root_component->url(); - - this->destroy_root(); - - this->root_component = std::make_unique(this->engine.get_qml_engine()); - - QObject::connect( - this->root_component.get(), - &QQmlComponent::statusChanged, - this, - &GuiSubtreeImpl::component_status_changed - ); - - this->root_component->loadUrl(source); -} - -void GuiSubtreeImpl::attach_to(GuiEventQueueImpl *game_logic_updater) { - this->game_logic_callback.moveToThread(game_logic_updater->get_thread()); -} - -void GuiSubtreeImpl::attach_to(GuiRendererImpl *renderer) { - assert(renderer); - - if (this->renderer) - QObject::disconnect(this->renderer, nullptr, this, nullptr); - - this->renderer = renderer; - - QObject::connect( - this->renderer, - &GuiRendererImpl::resized, - this, - &GuiSubtreeImpl::on_resized - ); - this->reparent_root(); -} - -void GuiSubtreeImpl::attach_to(GuiEngineImpl *engine_impl, const QString &root_dir) { - if (this->engine.has_subtree()) { - this->destroy_root(); - this->engine = GuiEngineImplConnection{}; - } - - this->root_component = std::make_unique(engine_impl->get_qml_engine()); - - QObject::connect( - this->root_component.get(), - &QQmlComponent::statusChanged, - this, - &GuiSubtreeImpl::component_status_changed - ); - - // operator = && - this->engine = GuiEngineImplConnection{this, engine_impl, root_dir}; - - this->root_component->moveToThread(QCoreApplication::instance()->thread()); -} - -void GuiSubtreeImpl::component_status_changed(QQmlComponent::Status status) { - if (QQmlComponent::Error == status) { - qCritical("%s", qUtf8Printable(this->root_component->errorString())); - return; - } - - if (QQmlComponent::Ready == status) { - assert(!this->root); - - this->root = qobject_cast(this->root_component->beginCreate(this->engine.rootContext())); - assert(this->root); - - this->init_persistent_items(); - - this->root_component->completeCreate(); - - this->reparent_root(); - } -} - -void GuiSubtreeImpl::on_resized(const QSize &size) { - if (this->root) - this->root->setSize(size); -} - -void GuiSubtreeImpl::on_process_game_logic_callback_blocking(const std::function &f) { - TemporaryDisableGuiRendererSync {*this->renderer}; - emit this->process_game_logic_callback_blocking(f); -} - -void GuiSubtreeImpl::init_persistent_items() { - auto persistent = this->root->findChildren(); - - for (auto ap : persistent) - ap->get_attachee()->set_game_logic_callback(&this->game_logic_callback); - - this->reloader.init_persistent_items(persistent); -} - -void GuiSubtreeImpl::reparent_root() { - if (this->root) { - QQuickWindow *window = this->renderer->get_window(); - - this->root->setParentItem(window->contentItem()); - this->root->setSize(QSize{window->width(), window->height()}); - } -} - -void GuiSubtreeImpl::destroy_root() { - if (this->root) { - this->root->setParent(nullptr); - this->root->setParentItem(nullptr); - this->root->deleteLater(); - this->root = nullptr; - } -} - -GuiEngineImplConnection::GuiEngineImplConnection() - : - subtree{}, - engine{} { -} - -GuiEngineImplConnection::GuiEngineImplConnection(GuiSubtreeImpl *subtree, - GuiEngineImpl *engine, - QString root_dir) - : - subtree{subtree}, - engine{engine}, - root_dir{std::move(root_dir)} { - - assert(this->subtree); - assert(this->engine); - - QObject::connect( - this->engine, - &GuiEngineImpl::reload, - this->subtree, - &GuiSubtreeImpl::onEngineReloaded - ); - - // add the directory so it can be watched for changes. - this->engine->add_root_dir_path(this->root_dir); -} - -GuiEngineImplConnection::~GuiEngineImplConnection() { - this->disconnect(); -} - -void GuiEngineImplConnection::disconnect() { - if (this->has_subtree()) { - assert(this->engine); - - QObject::disconnect( - this->engine, - &GuiEngineImpl::reload, - this->subtree, - &GuiSubtreeImpl::onEngineReloaded - ); - - if (not this->root_dir.isEmpty()) { - this->engine->remove_root_dir_path(this->root_dir); - } - } -} - -GuiEngineImplConnection::GuiEngineImplConnection(GuiEngineImplConnection &&cnx) noexcept - : - subtree{cnx.subtree}, - engine{cnx.engine} { - - cnx.subtree = nullptr; - cnx.engine = nullptr; -} - -GuiEngineImplConnection& GuiEngineImplConnection::operator=(GuiEngineImplConnection &&cnx) noexcept { - this->disconnect(); - - this->subtree = cnx.subtree; - this->engine = cnx.engine; - - cnx.subtree = nullptr; - cnx.engine = nullptr; - - return *this; -} - -bool GuiEngineImplConnection::has_subtree() const { - return this->subtree != nullptr; -} - -QQmlContext* GuiEngineImplConnection::rootContext() const { - assert(this->subtree); - assert(this->engine); - return this->engine->get_qml_engine()->rootContext(); -} - -QQmlEngine* GuiEngineImplConnection::get_qml_engine() const { - assert(this->subtree); - assert(this->engine); - return this->engine->get_qml_engine(); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/gui_subtree_impl.h b/libopenage/gui/guisys/private/gui_subtree_impl.h deleted file mode 100644 index 6c737c11f1..0000000000 --- a/libopenage/gui/guisys/private/gui_subtree_impl.h +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include - -#include -#include - -#include "gui_callback.h" -#include "livereload/gui_live_reloader.h" - -QT_FORWARD_DECLARE_CLASS(QQuickItem) - -namespace qtsdl { - -class GuiRenderer; -class GuiRendererImpl; -class GuiEventQueue; -class GuiEventQueueImpl; -class GuiEngine; -class GuiEngineImpl; -class GuiSubtreeImpl; - -class GuiEngineImplConnection { -public: - GuiEngineImplConnection(); - explicit GuiEngineImplConnection(GuiSubtreeImpl *subtree, - GuiEngineImpl *engine, - QString root_dir); - ~GuiEngineImplConnection(); - - GuiEngineImplConnection(GuiEngineImplConnection &&cnx) noexcept; - GuiEngineImplConnection& operator=(GuiEngineImplConnection &&cnx) noexcept; - - bool has_subtree() const; - - QQmlContext* rootContext() const; - QQmlEngine* get_qml_engine() const; - -private: - GuiEngineImplConnection(const GuiEngineImplConnection &cnx) = delete; - GuiEngineImplConnection& operator=(const GuiEngineImplConnection &cnx) = delete; - - void disconnect(); - - GuiSubtreeImpl *subtree; - GuiEngineImpl *engine; - QString root_dir; -}; - - -class GuiSubtreeImpl : public QObject { - Q_OBJECT - -public: - explicit GuiSubtreeImpl(GuiRenderer *renderer, - GuiEventQueue *game_logic_updater, - GuiEngine *engine, - const QString &source, - const QString &rootdir); - virtual ~GuiSubtreeImpl(); - -public slots: - void onEngineReloaded(); - - void attach_to(GuiEventQueueImpl *game_logic_updater); - void attach_to(GuiRendererImpl *renderer); - void attach_to(GuiEngineImpl *engine, const QString &source); - -private slots: - void component_status_changed(QQmlComponent::Status status); - void on_resized(const QSize &size); - void on_process_game_logic_callback_blocking(const std::function &f); - -signals: - void process_game_logic_callback_blocking(const std::function &f); - -private: - void init_persistent_items(); - - void reparent_root(); - void destroy_root(); - - GuiRendererImpl *renderer; - GuiEngineImplConnection engine; - GuiLiveReloader reloader; - - GuiCallback game_logic_callback; - - std::unique_ptr root_component; - QQuickItem *root; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/livereload/recursive_directory_watcher.cpp b/libopenage/gui/guisys/private/livereload/recursive_directory_watcher.cpp deleted file mode 100644 index 6e3422dc0c..0000000000 --- a/libopenage/gui/guisys/private/livereload/recursive_directory_watcher.cpp +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#include "recursive_directory_watcher.h" - -#include -#include - -#include "recursive_directory_watcher_worker.h" - -namespace qtsdl { - -RecursiveDirectoryWatcher::RecursiveDirectoryWatcher(QObject *parent) - : - QObject{parent} { - - QSemaphore wait_worker_started; - - this->worker = std::async(std::launch::async, [this, &wait_worker_started] { - QEventLoop loop; - QObject::connect(this, &RecursiveDirectoryWatcher::quit, &loop, &QEventLoop::quit); - - RecursiveDirectoryWatcherWorker watcher; - QObject::connect(&watcher, &RecursiveDirectoryWatcherWorker::changeDetected, this, &RecursiveDirectoryWatcher::changeDetected); - QObject::connect(this, &RecursiveDirectoryWatcher::rootDirsPathsChanged, &watcher, &RecursiveDirectoryWatcherWorker::onRootDirsPathsChanged); - - wait_worker_started.release(); - - loop.exec(); - }); - - wait_worker_started.acquire(); -} - -RecursiveDirectoryWatcher::~RecursiveDirectoryWatcher() { - emit this->quit(); - this->worker.wait(); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/livereload/recursive_directory_watcher.h b/libopenage/gui/guisys/private/livereload/recursive_directory_watcher.h deleted file mode 100644 index d55e7b8f99..0000000000 --- a/libopenage/gui/guisys/private/livereload/recursive_directory_watcher.h +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include -#include - -namespace qtsdl { - -/** - * Emits a signal when anything changes in the directories. - */ -class RecursiveDirectoryWatcher : public QObject { - Q_OBJECT - -public: - explicit RecursiveDirectoryWatcher(QObject *parent = nullptr); - virtual ~RecursiveDirectoryWatcher(); - -signals: - void changeDetected(); - void rootDirsPathsChanged(const QStringList&); - void quit(); - -private: - std::future worker; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/livereload/recursive_directory_watcher_worker.cpp b/libopenage/gui/guisys/private/livereload/recursive_directory_watcher_worker.cpp deleted file mode 100644 index ff8e9f7242..0000000000 --- a/libopenage/gui/guisys/private/livereload/recursive_directory_watcher_worker.cpp +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#include "recursive_directory_watcher_worker.h" - -#include - -#include -#include - -namespace qtsdl { - -namespace { -const int batch_ms = 100; -} - -RecursiveDirectoryWatcherWorker::RecursiveDirectoryWatcherWorker() - : - QObject{} { - - this->batching_timer.setInterval(batch_ms); - this->batching_timer.setSingleShot(true); - QObject::connect(&this->batching_timer, &QTimer::timeout, this, &RecursiveDirectoryWatcherWorker::changeDetected); - QObject::connect(&this->batching_timer, &QTimer::timeout, this, &RecursiveDirectoryWatcherWorker::restartWatching); -} - -void RecursiveDirectoryWatcherWorker::onRootDirsPathsChanged(const QStringList &root_dirs_paths) { - if (this->root_dirs_paths != root_dirs_paths) { - this->root_dirs_paths = root_dirs_paths; - this->restartWatching(); - } -} - -namespace { -QStringList collect_entries_to_watch(const QStringList &root_dirs_paths) { - QStringList root_dirs_paths_no_duplicates = root_dirs_paths; - root_dirs_paths_no_duplicates.removeDuplicates(); - - QStringList entries_to_watch; - - std::for_each(std::begin(root_dirs_paths_no_duplicates), std::end(root_dirs_paths_no_duplicates), [&entries_to_watch] (const QString& root_dir_path) { - QDirIterator it{root_dir_path, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks}; - - while (it.hasNext()) { - entries_to_watch.append(it.next()); - } - }); - - return entries_to_watch; -} -} - -void RecursiveDirectoryWatcherWorker::restartWatching() { - this->restart_watching(collect_entries_to_watch(this->root_dirs_paths)); -} - -void RecursiveDirectoryWatcherWorker::restart_watching(const QStringList &entries_to_watch) { - this->watcher.reset(); - this->watcher = std::make_unique(); - - QObject::connect(&*this->watcher, &QFileSystemWatcher::directoryChanged, this, &RecursiveDirectoryWatcherWorker::onEntryChanged); - - if (entries_to_watch.empty()) - qWarning() << "RecursiveDirectoryWatcheWorker hasn't found any files to watch."; - else - this->watcher->addPaths(entries_to_watch); -} - -void RecursiveDirectoryWatcherWorker::onEntryChanged() { - if (!this->batching_timer.isActive()) - this->batching_timer.start(); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/livereload/recursive_directory_watcher_worker.h b/libopenage/gui/guisys/private/livereload/recursive_directory_watcher_worker.h deleted file mode 100644 index 88af26d97d..0000000000 --- a/libopenage/gui/guisys/private/livereload/recursive_directory_watcher_worker.h +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include -#include -#include - -namespace qtsdl { - -/** - * Emits a signal when anything changes in the directories. - */ -class RecursiveDirectoryWatcherWorker : public QObject { - Q_OBJECT - -public: - RecursiveDirectoryWatcherWorker(); - -signals: - void changeDetected(); - -public slots: - void onRootDirsPathsChanged(const QStringList &root_dirs_paths); - void restartWatching(); - -private slots: - void onEntryChanged(); - -private: - void restart_watching(const QStringList &entries_to_watch); - - /** - * Actual watcher. - * Its event processing and destruction has to be in the same separate thread. - */ - std::unique_ptr watcher; - - /** - * Waits to glue multiple changes into one event. - */ - QTimer batching_timer; - - QStringList root_dirs_paths; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/opengl_debug_logger.cpp b/libopenage/gui/guisys/private/opengl_debug_logger.cpp deleted file mode 100644 index b61bc58f20..0000000000 --- a/libopenage/gui/guisys/private/opengl_debug_logger.cpp +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. - -#include "opengl_debug_logger.h" - -#include -#include -#include - -#ifdef __APPLE__ -// from https://www.khronos.org/registry/OpenGL/api/GL/glext.h -#define GL_DEBUG_CALLBACK_FUNCTION 0x8244 -#define GL_DEBUG_OUTPUT_SYNCHRONOUS 0x8242 -#define GL_DEBUG_TYPE_ERROR 0x824C -#define GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR 0x824E -#endif - -namespace qtsdl { - -gl_debug_parameters get_current_opengl_debug_parameters(QOpenGLContext ¤t_source_context) { - gl_debug_parameters params{}; - - if (QOpenGLVersionFunctionsFactory::get(¤t_source_context)) - if ((params.is_debug = current_source_context.format().options().testFlag(QSurfaceFormat::DebugContext))) { - glGetPointerv(GL_DEBUG_CALLBACK_FUNCTION, ¶ms.callback); - params.synchronous = glIsEnabled(GL_DEBUG_OUTPUT_SYNCHRONOUS); - } - - return params; -} - -void apply_opengl_debug_parameters(gl_debug_parameters params, QOpenGLContext ¤t_dest_context) { - if (params.is_debug && params.callback) { - if (auto functions = QOpenGLVersionFunctionsFactory::get(¤t_dest_context)) { - functions->initializeOpenGLFunctions(); - - functions->glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_FALSE); - - functions->glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_ERROR, GL_DONT_CARE, 0, nullptr, GL_TRUE); - functions->glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR, GL_DONT_CARE, 0, nullptr, GL_TRUE); - - functions->glDebugMessageCallback((GLDEBUGPROC)params.callback, nullptr); - - if (params.synchronous) - glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); - } - } -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/opengl_debug_logger.h b/libopenage/gui/guisys/private/opengl_debug_logger.h deleted file mode 100644 index 7fda1cad80..0000000000 --- a/libopenage/gui/guisys/private/opengl_debug_logger.h +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -QT_FORWARD_DECLARE_CLASS(QOpenGLContext) - -namespace qtsdl { -struct gl_debug_parameters { - /** - * True if the GL context is a debug context - */ - bool is_debug; - - /** - * Function that GL context uses to report debug messages - */ - GLvoid *callback; - - /** - * True if debug callback calling method is chosen to be synchronous - */ - bool synchronous; -}; - -/** - * Get debugging settings of the current GL context - * - * @param current_source_context current GL context - * @return debugging settings - */ -gl_debug_parameters get_current_opengl_debug_parameters(QOpenGLContext ¤t_source_context); - -/** - * Create a GL logger in the current GL context - * - * @param params debugging settings - * @param current_dest_context current GL context to which parameters will be applied - */ -void apply_opengl_debug_parameters(gl_debug_parameters params, QOpenGLContext ¤t_dest_context); - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/platforms/context_extraction.h b/libopenage/gui/guisys/private/platforms/context_extraction.h deleted file mode 100644 index 7bbf515d8b..0000000000 --- a/libopenage/gui/guisys/private/platforms/context_extraction.h +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -#include -#include - -namespace qtsdl { - -/** - * @return current context (or null) and id of the window - */ -// std::tuple extract_native_context(SDL_Window *window); - -/** - * @return current context (or null) and function to get it back to the window - */ -// std::tuple> extract_native_context_and_switchback_func(SDL_Window *window); - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/platforms/context_extraction_cocoa.mm b/libopenage/gui/guisys/private/platforms/context_extraction_cocoa.mm deleted file mode 100644 index 076924d8af..0000000000 --- a/libopenage/gui/guisys/private/platforms/context_extraction_cocoa.mm +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. - -#include - -#include "context_extraction.h" - -#include - -#include "SDL_syswm.h" -#import - -namespace qtsdl { - -std::tuple extract_native_context(SDL_Window *window) { - - assert(window); - - NSOpenGLContext *current_context = [NSOpenGLContext currentContext]; - assert(current_context); - - NSView *view = nullptr; - - SDL_SysWMinfo wm_info; - SDL_VERSION(&wm_info.version); - - - if (SDL_GetWindowWMInfo(window, &wm_info)) { - NSWindow *ns_window = wm_info.info.cocoa.window; - view = [ns_window contentView]; - assert(view); - } - return std::make_tuple(QVariant::fromValue(QCocoaNativeContext(current_context)), reinterpret_cast(view)); -} - -std::tuple> extract_native_context_and_switchback_func(SDL_Window *window) { - assert(window); - - NSOpenGLContext *current_context = [NSOpenGLContext currentContext]; - assert(current_context); - - NSView *view = nullptr; - - SDL_SysWMinfo wm_info; - SDL_VERSION(&wm_info.version); - - if (SDL_GetWindowWMInfo(window, &wm_info)) { - NSWindow *ns_window = wm_info.info.cocoa.window; - view = [ns_window contentView]; - assert(view); - - return std::make_tuple(QVariant::fromValue(QCocoaNativeContext(current_context)), [current_context] { - [current_context makeCurrentContext]; - }); - } - - return std::tuple>{}; -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/platforms/context_extraction_win32.cpp b/libopenage/gui/guisys/private/platforms/context_extraction_win32.cpp deleted file mode 100644 index 1c413ff926..0000000000 --- a/libopenage/gui/guisys/private/platforms/context_extraction_win32.cpp +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include - -#include "context_extraction.h" -#include - -#include -// #include -#include - -namespace qtsdl { - -// std::tuple extract_native_context(/* SDL_Window *window */) { -// assert(window); - -// HGLRC current_context; -// SDL_SysWMinfo wm_info; -// SDL_VERSION(&wm_info.version); -// if (SDL_GetWindowWMInfo(window, &wm_info)) { -// current_context = wglGetCurrentContext(); -// assert(current_context); -// } -// QWGLNativeContext nativeContext(current_context, wm_info.info.win.window); -// return {QVariant::fromValue(nativeContext), reinterpret_cast(wm_info.info.win.window)}; -// } - -// std::tuple> extract_native_context_and_switchback_func(/* SDL_Window *window */) { -// assert(window); - -// HGLRC current_context; -// SDL_SysWMinfo wm_info; -// SDL_VERSION(&wm_info.version); -// if (SDL_GetWindowWMInfo(window, &wm_info)) { -// current_context = wglGetCurrentContext(); -// assert(current_context); - -// return std::make_tuple(QVariant::fromValue(QWGLNativeContext(current_context, wm_info.info.win.window)), [wm_info, current_context] { -// wglMakeCurrent(wm_info.info.win.hdc, current_context); -// }); -// } - -// return std::tuple>{}; -// } - - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/private/platforms/context_extraction_x11.cpp b/libopenage/gui/guisys/private/platforms/context_extraction_x11.cpp deleted file mode 100644 index 333307c1f9..0000000000 --- a/libopenage/gui/guisys/private/platforms/context_extraction_x11.cpp +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include - -#include "context_extraction.h" - -// #include -#include -// #include - -// DO NOT INCLUDE ANYTHING HERE, X11 HEADERS BREAK STUFF - -namespace qtsdl { - -// std::tuple extract_native_context(SDL_Window *window) { -// assert(window); - -// GLXContext current_context = nullptr; -// SDL_SysWMinfo wm_info; -// SDL_VERSION(&wm_info.version); - -// if (SDL_GetWindowWMInfo(window, &wm_info)) { -// assert(wm_info.info.x11.display); - -// current_context = glXGetCurrentContext(); -// assert(current_context); - -// return std::make_tuple( -// QVariant::fromValue( -// QGLXNativeContext(current_context, -// wm_info.info.x11.display, -// wm_info.info.x11.window)), -// wm_info.info.x11.window -// ); -// } - -// return std::tuple{}; -// } - -// std::tuple> extract_native_context_and_switchback_func(SDL_Window *window) { -// assert(window); - -// GLXContext current_context; -// SDL_SysWMinfo wm_info; -// SDL_VERSION(&wm_info.version); - -// if (SDL_GetWindowWMInfo(window, &wm_info)) { -// assert(wm_info.info.x11.display); - -// current_context = glXGetCurrentContext(); -// assert(current_context); - -// return std::make_tuple(QVariant::fromValue(QGLXNativeContext(current_context, wm_info.info.x11.display, wm_info.info.x11.window)), [wm_info, current_context] { -// glXMakeCurrent(wm_info.info.x11.display, wm_info.info.x11.window, current_context); -// }); -// } - -// return std::tuple>{}; -// } - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_application.cpp b/libopenage/gui/guisys/public/gui_application.cpp deleted file mode 100644 index 1b7a511095..0000000000 --- a/libopenage/gui/guisys/public/gui_application.cpp +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include - -#include "../public/gui_application.h" - -#include "../private/gui_application_impl.h" - -namespace qtsdl { - -GuiApplication::GuiApplication() - : - application{GuiApplicationImpl::get()} { -} - -GuiApplication::GuiApplication(std::shared_ptr application) - : - application{application} { -} - -GuiApplication::~GuiApplication() = default; - -void GuiApplication::processEvents() { - this->application->processEvents(); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_application.h b/libopenage/gui/guisys/public/gui_application.h deleted file mode 100644 index 6b64e6b111..0000000000 --- a/libopenage/gui/guisys/public/gui_application.h +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -namespace qtsdl { - -class GuiApplicationImpl; - -/** - * Houses gui logic event queue. - */ -class GuiApplication { -public: - GuiApplication(); - GuiApplication(std::shared_ptr application); - ~GuiApplication(); - - void processEvents(); - -private: - std::shared_ptr application; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_engine.cpp b/libopenage/gui/guisys/public/gui_engine.cpp deleted file mode 100644 index 689a4ff87f..0000000000 --- a/libopenage/gui/guisys/public/gui_engine.cpp +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "../public/gui_engine.h" - -#include "../private/gui_engine_impl.h" - -namespace qtsdl { - -GuiEngine::GuiEngine(GuiRenderer *renderer, const std::vector &image_providers, GuiSingletonItemsInfo *singleton_items_info) - : - impl{std::make_unique(renderer, image_providers, singleton_items_info)} { -} - -GuiEngine::~GuiEngine() = default; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_engine.h b/libopenage/gui/guisys/public/gui_engine.h deleted file mode 100644 index 153ceb11ab..0000000000 --- a/libopenage/gui/guisys/public/gui_engine.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -namespace qtsdl { - -class GuiRenderer; -class GuiImageProvider; -class GuiEngineImpl; -class GuiSingletonItemsInfo; - -/** - * Represents one QML execution environment. - */ -class GuiEngine { -public: - explicit GuiEngine(GuiRenderer *renderer, - const std::vector &image_providers = std::vector(), - GuiSingletonItemsInfo *singleton_items_info = nullptr); - ~GuiEngine(); - -private: - friend class GuiEngineImpl; - std::unique_ptr impl; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_event_queue.cpp b/libopenage/gui/guisys/public/gui_event_queue.cpp deleted file mode 100644 index 4b9866c1a9..0000000000 --- a/libopenage/gui/guisys/public/gui_event_queue.cpp +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "../public/gui_event_queue.h" - -#include "../private/gui_event_queue_impl.h" - -namespace qtsdl { - -GuiEventQueue::GuiEventQueue() - : - impl{std::make_unique()} { -} - -GuiEventQueue::~GuiEventQueue() = default; - -void GuiEventQueue::process_callbacks() { - this->impl->process_callbacks(); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_event_queue.h b/libopenage/gui/guisys/public/gui_event_queue.h deleted file mode 100644 index d7b6076e9a..0000000000 --- a/libopenage/gui/guisys/public/gui_event_queue.h +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -namespace qtsdl { - -class GuiEventQueueImpl; - -/** - * Provides synchronization with some game thread. - */ -class GuiEventQueue { -public: - explicit GuiEventQueue(); - ~GuiEventQueue(); - - void process_callbacks(); - -private: - friend class GuiEventQueueImpl; - std::unique_ptr impl; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_image_provider.cpp b/libopenage/gui/guisys/public/gui_image_provider.cpp deleted file mode 100644 index 961a0f3c20..0000000000 --- a/libopenage/gui/guisys/public/gui_image_provider.cpp +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "../public/gui_image_provider.h" - -#include "../private/gui_image_provider_impl.h" - -namespace qtsdl { - -GuiImageProvider::GuiImageProvider(std::unique_ptr impl) - : - impl{impl.release(), std::default_delete()} { -} - -GuiImageProvider::~GuiImageProvider() = default; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_image_provider.h b/libopenage/gui/guisys/public/gui_image_provider.h deleted file mode 100644 index 6a5e074769..0000000000 --- a/libopenage/gui/guisys/public/gui_image_provider.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -namespace qtsdl { - -class GuiImageProviderImpl; - -class GuiImageProvider { -public: - explicit GuiImageProvider(std::unique_ptr impl); - ~GuiImageProvider(); - -private: - friend class GuiImageProviderImpl; - std::unique_ptr> impl; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_input.cpp b/libopenage/gui/guisys/public/gui_input.cpp deleted file mode 100644 index 861e6a5f6e..0000000000 --- a/libopenage/gui/guisys/public/gui_input.cpp +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "../public/gui_input.h" - -#include "../private/gui_input_impl.h" - -namespace qtsdl { - -GuiInput::GuiInput(GuiRenderer *renderer, GuiEventQueue *game_logic_updater) : - impl{std::make_unique(renderer, game_logic_updater)} { -} - -GuiInput::~GuiInput() = default; - -bool GuiInput::process(/* SDL_Event *event */) { - return this->impl->process(/* event */); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_input.h b/libopenage/gui/guisys/public/gui_input.h deleted file mode 100644 index 1a46b227be..0000000000 --- a/libopenage/gui/guisys/public/gui_input.h +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -namespace qtsdl { - -class GuiRenderer; -class GuiEventQueue; -class GuiInputImpl; - -/** - * Converts SDL input events into Qt events. - */ -class GuiInput { -public: - explicit GuiInput(GuiRenderer *renderer, GuiEventQueue *game_logic_updater); - ~GuiInput(); - - /** - * Returns true if the event was accepted. - */ - bool process(/* SDL_Event *event */); - -private: - friend class GuiInputImpl; - std::unique_ptr impl; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_property_map.cpp b/libopenage/gui/guisys/public/gui_property_map.cpp deleted file mode 100644 index b52b3a31b3..0000000000 --- a/libopenage/gui/guisys/public/gui_property_map.cpp +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "gui_property_map.h" - -#include -#include -#include -#include - -#include -#include - -#include - -#include "../link/gui_property_map_impl.h" - -namespace qtsdl { - -GuiPropertyMap::GuiPropertyMap() : - impl{std::make_unique()} { - assert(QCoreApplication::instance()); -} - -GuiPropertyMap::~GuiPropertyMap() = default; - -namespace { -template -T getter(const QObject &map, const char *name) { - auto v = map.property(name); - - if (!v.isValid()) - return T(); - - using namespace std::string_literals; - - if (!v.canConvert()) - throw std::runtime_error("Can't interpret a property '"s + name + "' of type '"s + v.typeName() + "' as '"s + typeid(T).name() + "'."s); - - return v.value(); -} - -template -void setter(QObject &map, const char *name, T v) { - map.setProperty(name, QVariant::fromValue(v)); -} - -template <> -void setter(QObject &map, const char *name, const char *v) { - map.setProperty(name, v); -} - -std::vector strings_to_vector(const QStringList &strings) { - std::vector result(strings.size()); - std::transform(std::begin(strings), std::end(strings), std::begin(result), [](const QString &s) { return s.toStdString(); }); - - return result; -} - -QStringList vector_to_strings(const std::vector &v) { - QStringList strings; - std::transform(std::begin(v), std::end(v), std::back_inserter(strings), [](const std::string &s) { return QString::fromStdString(s); }); - - return strings; -} -} // namespace - -template <> -bool GuiPropertyMap::getv(const char *name) const { - return getter(*this->impl, name); -} - -template <> -void GuiPropertyMap::setv(const char *name, bool v) { - setter(*this->impl, name, v); -} - -template <> -int GuiPropertyMap::getv(const char *name) const { - return getter(*this->impl, name); -} - -template <> -void GuiPropertyMap::setv(const char *name, int v) { - setter(*this->impl, name, v); -} - -template <> -double GuiPropertyMap::getv(const char *name) const { - return getter(*this->impl, name); -} - -template <> -void GuiPropertyMap::setv(const char *name, double v) { - setter(*this->impl, name, v); -} - -template <> -std::string GuiPropertyMap::getv(const char *name) const { - return getter(*this->impl, name).toStdString(); -} - -template <> -void GuiPropertyMap::setv(const char *name, const std::string &v) { - setter(*this->impl, name, QString::fromStdString(v)); -} - -template <> -void GuiPropertyMap::setv(const char *name, const char *v) { - setter(*this->impl, name, v); -} - -template <> -std::vector GuiPropertyMap::getv>(const char *name) const { - return strings_to_vector(getter(*this->impl, name)); -} - -template <> -void GuiPropertyMap::setv &>(const char *name, const std::vector &v) { - setter(*this->impl, name, vector_to_strings(v)); -} - -template <> -QString GuiPropertyMap::getv(const char *name) const { - return getter(*this->impl, name); -} - -template <> -void GuiPropertyMap::setv(const char *name, const QString &v) { - setter(*this->impl, name, v); -} - -template <> -QStringList GuiPropertyMap::getv(const char *name) const { - return getter(*this->impl, name); -} - -template <> -void GuiPropertyMap::setv(const char *name, const QStringList &v) { - setter(*this->impl, name, v); -} - -void GuiPropertyMap::setv(const char *name, const std::string &v) { - this->setv(name, v); -} - -void GuiPropertyMap::setv(const std::string &name, const std::string &v) { - this->setv(name.c_str(), v); -} - -void GuiPropertyMap::setv(const char *name, const std::vector &v) { - this->setv &>(name, v); -} - -void GuiPropertyMap::setv(const std::string &name, const std::vector &v) { - this->setv &>(name.c_str(), v); -} - -std::vector GuiPropertyMap::get_csv(const char *name) const { - return strings_to_vector(getter(*this->impl, name).split(QRegularExpression("\\s*,\\s*"))); -} - -void GuiPropertyMap::set_csv(const char *name, const std::vector &v) { - setter(*this->impl, name, vector_to_strings(v).join(", ")); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_property_map.h b/libopenage/gui/guisys/public/gui_property_map.h deleted file mode 100644 index 7c67238027..0000000000 --- a/libopenage/gui/guisys/public/gui_property_map.h +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include - -namespace qtsdl { - -class GuiPropertyMapImpl; - -struct GuiPropertyMap { - GuiPropertyMap(); - ~GuiPropertyMap(); - - template - T getv(const char *name) const; - - template - T getv(const std::string &name) const { - return this->getv(name.c_str()); - } - - std::vector get_csv(const char *name) const; - - std::vector get_csv(const std::string &name) const { - return this->get_csv(name.c_str()); - } - - template - void setv(const char *name, T v); - - template - void setv(const std::string &name, T v) { - this->setv(name.c_str(), v); - } - - void set_csv(const char *name, const std::vector &v); - - void set_csv(const std::string &name, const std::vector &v) { - this->set_csv(name.c_str(), v); - } - - void setv(const char *name, const std::string &v); - void setv(const std::string &name, const std::string &v); - - void setv(const char *name, const std::vector &v); - void setv(const std::string &name, const std::vector &v); - - std::unique_ptr impl; -}; - -template<> -bool GuiPropertyMap::getv(const char *name) const; - -template<> -void GuiPropertyMap::setv(const char *name, bool v); - -template<> -int GuiPropertyMap::getv(const char *name) const; - -template<> -void GuiPropertyMap::setv(const char *name, int v); - -template<> -double GuiPropertyMap::getv(const char *name) const; - -template<> -void GuiPropertyMap::setv(const char *name, double v); - -template<> -std::string GuiPropertyMap::getv(const char *name) const; - -template<> -void GuiPropertyMap::setv(const char *name, const std::string &v); - -template<> -void GuiPropertyMap::setv(const char *name, const char *v); - -template<> -std::vector GuiPropertyMap::getv>(const char *name) const; - -template<> -void GuiPropertyMap::setv&>(const char *name, const std::vector &v); - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_renderer.cpp b/libopenage/gui/guisys/public/gui_renderer.cpp deleted file mode 100644 index a11063e7e5..0000000000 --- a/libopenage/gui/guisys/public/gui_renderer.cpp +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "../public/gui_renderer.h" - -#include - -#include "../private/gui_renderer_impl.h" - -namespace qtsdl { - -GuiRenderer::GuiRenderer(/* SDL_Window *window */) : - impl{std::make_unique(/* window */)} { -} - -GuiRenderer::~GuiRenderer() = default; - -GLuint GuiRenderer::render() { - return this->impl->render(); -} - -void GuiRenderer::resize(int w, int h) { - this->impl->resize(QSize{w, h}); -} - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_renderer.h b/libopenage/gui/guisys/public/gui_renderer.h deleted file mode 100644 index bc8e93dd99..0000000000 --- a/libopenage/gui/guisys/public/gui_renderer.h +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#ifndef __APPLE__ -#ifdef _MSC_VER -#define NOMINMAX -#include -#endif //_MSC_VER -#include -#else // __APPLE__ -#include -#endif - -namespace qtsdl { - -class GuiRendererImpl; - -/** - * Passes the native graphic context to Qt. - */ -class GuiRenderer { -public: - // TODO: allow FBO variant - explicit GuiRenderer(/* SDL_Window *window */); - ~GuiRenderer(); - - GLuint render(); - void resize(int w, int h); - -private: - friend class GuiRendererImpl; - std::unique_ptr impl; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_singleton_items_info.h b/libopenage/gui/guisys/public/gui_singleton_items_info.h deleted file mode 100644 index 1889b4c2c0..0000000000 --- a/libopenage/gui/guisys/public/gui_singleton_items_info.h +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. - -#pragma once - -namespace qtsdl { - -/** - * Accessible during creation of any singleton QML item. - */ -class GuiSingletonItemsInfo { -}; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_subtree.cpp b/libopenage/gui/guisys/public/gui_subtree.cpp deleted file mode 100644 index 00abcedab1..0000000000 --- a/libopenage/gui/guisys/public/gui_subtree.cpp +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "gui_subtree.h" - -#include "../private/gui_subtree_impl.h" - -namespace qtsdl { - -GuiSubtree::GuiSubtree(GuiRenderer *renderer, - GuiEventQueue *game_logic_updater, - GuiEngine *engine, - const std::string &source, - const std::string &rootdir) - : - impl{std::make_unique( - renderer, - game_logic_updater, - engine, - QString::fromStdString(source), - QString::fromStdString(rootdir) - )} {} - -GuiSubtree::~GuiSubtree() = default; - -} // namespace qtsdl diff --git a/libopenage/gui/guisys/public/gui_subtree.h b/libopenage/gui/guisys/public/gui_subtree.h deleted file mode 100644 index ffd4141b2a..0000000000 --- a/libopenage/gui/guisys/public/gui_subtree.h +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - - -namespace qtsdl { - -class GuiRenderer; -class GuiEventQueue; -class GuiEngine; -class GuiSubtreeImpl; - - -/** - * A root item that loads its code from source url. - * - * rootdir is the qml file root folder which is watched for changes. - */ -class GuiSubtree { -public: - explicit GuiSubtree(GuiRenderer *renderer, - GuiEventQueue *game_logic_updater, - GuiEngine *engine, - const std::string &source, - const std::string &rootdir); - - ~GuiSubtree(); - -private: - friend class GuiSubtreeImpl; - std::unique_ptr impl; -}; - -} // namespace qtsdl diff --git a/libopenage/gui/integration/CMakeLists.txt b/libopenage/gui/integration/CMakeLists.txt deleted file mode 100644 index 0bb272b5c5..0000000000 --- a/libopenage/gui/integration/CMakeLists.txt +++ /dev/null @@ -1,2 +0,0 @@ -add_subdirectory("private") -add_subdirectory("public") diff --git a/libopenage/gui/integration/private/CMakeLists.txt b/libopenage/gui/integration/private/CMakeLists.txt deleted file mode 100644 index 88be5e9db6..0000000000 --- a/libopenage/gui/integration/private/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -add_sources(libopenage - gui_filled_texture_handles.cpp - gui_image_provider_link.cpp - gui_log.cpp - gui_make_standalone_subtexture.cpp - gui_standalone_subtexture.cpp - gui_texture.cpp - gui_texture_handle.cpp -) diff --git a/libopenage/gui/integration/private/gui_image_provider_link.cpp b/libopenage/gui/integration/private/gui_image_provider_link.cpp deleted file mode 100644 index a770c0f552..0000000000 --- a/libopenage/gui/integration/private/gui_image_provider_link.cpp +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "gui_image_provider_link.h" - -#include - -#include - -#include "../../../error/error.h" - -#include "../../guisys/link/qml_engine_with_singleton_items_info.h" -#include "../../guisys/link/qtsdl_checked_static_cast.h" - -namespace openage::gui { - -GuiImageProviderLink::GuiImageProviderLink(QObject *parent) : - QObject{parent} { -} - -GuiImageProviderLink::~GuiImageProviderLink() = default; - -QObject *GuiImageProviderLink::provider(QQmlEngine *engine, const char *id) { - qtsdl::QmlEngineWithSingletonItemsInfo *engine_with_singleton_items_info = qtsdl::checked_static_cast(engine); - auto image_providers = engine_with_singleton_items_info->get_image_providers(); - - auto found_it = std::find_if(std::begin(image_providers), std::end(image_providers), [id](qtsdl::GuiImageProviderImpl *image_provider) { - return image_provider->get_id() == id; - }); - - ENSURE(found_it != std::end(image_providers), "The image provider '" << id << "' wasn't created or wasn't passed to the QML engine creation function."); - - // owned by the QML engine - return nullptr; -} - -} // namespace openage::gui diff --git a/libopenage/gui/integration/private/gui_image_provider_link.h b/libopenage/gui/integration/private/gui_image_provider_link.h deleted file mode 100644 index 5090edcf2c..0000000000 --- a/libopenage/gui/integration/private/gui_image_provider_link.h +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include -#include - -QT_FORWARD_DECLARE_CLASS(QQmlEngine) -QT_FORWARD_DECLARE_CLASS(QJSEngine) - -namespace openage { - -namespace gui { - -class GuiImageProviderLink : public QObject { - Q_OBJECT - -public: - explicit GuiImageProviderLink(QObject *parent); - virtual ~GuiImageProviderLink(); - - static QObject *provider(QQmlEngine *, const char *id); -}; - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/integration/private/gui_log.cpp b/libopenage/gui/integration/private/gui_log.cpp deleted file mode 100644 index 481374f9de..0000000000 --- a/libopenage/gui/integration/private/gui_log.cpp +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "../../integration/private/gui_log.h" - -#include - -#include "../../../log/log.h" -#include "log/message.h" - - -namespace openage { -namespace gui { - -void gui_log(QtMsgType type, const QMessageLogContext &context, const QString &msg) { - log::level msg_lvl; - - switch (type) { - case QtDebugMsg: - msg_lvl = log::level::dbg; - break; - case QtInfoMsg: - msg_lvl = log::level::info; - break; - case QtWarningMsg: - msg_lvl = log::level::warn; - break; - case QtCriticalMsg: - msg_lvl = log::level::err; - break; - case QtFatalMsg: - msg_lvl = log::level::crit; - break; - default: - msg_lvl = log::level::warn; - break; - } - - log::MessageBuilder builder{ - context.file != nullptr ? context.file : "", - static_cast(context.line), - context.function != nullptr ? context.function : "", - msg_lvl}; - - // TODO: maybe it's not UTF-8 - // TODO: Qt should become a LogSource - log::log(builder << msg.toUtf8().data()); - - if (type == QtFatalMsg) - abort(); -} - -} // namespace gui -} // namespace openage diff --git a/libopenage/gui/integration/private/gui_log.h b/libopenage/gui/integration/private/gui_log.h deleted file mode 100644 index 4966f5e182..0000000000 --- a/libopenage/gui/integration/private/gui_log.h +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -namespace openage { -namespace gui { - -void gui_log(QtMsgType type, const QMessageLogContext &context, const QString &msg); - -}} // namespace openage::gui diff --git a/libopenage/gui/integration/private/gui_make_standalone_subtexture.cpp b/libopenage/gui/integration/private/gui_make_standalone_subtexture.cpp deleted file mode 100644 index 854f5a65bc..0000000000 --- a/libopenage/gui/integration/private/gui_make_standalone_subtexture.cpp +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#include "gui_make_standalone_subtexture.h" -#include "gui_standalone_subtexture.h" - -namespace openage { -namespace gui { - -std::unique_ptr make_standalone_subtexture(GLuint id, const QSize &size) { - return std::make_unique(id, size); -} - -}} // namespace openage::gui diff --git a/libopenage/gui/integration/public/CMakeLists.txt b/libopenage/gui/integration/public/CMakeLists.txt deleted file mode 100644 index 4acda98465..0000000000 --- a/libopenage/gui/integration/public/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -add_sources(libopenage - gui_application_with_logger.cpp -) diff --git a/libopenage/gui/integration/public/gui_application_with_logger.cpp b/libopenage/gui/integration/public/gui_application_with_logger.cpp deleted file mode 100644 index 0b845f42fc..0000000000 --- a/libopenage/gui/integration/public/gui_application_with_logger.cpp +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. - -#include "gui_application_with_logger.h" - -#include "../../guisys/private/gui_application_impl.h" -#include "../private/gui_log.h" - -namespace openage::gui { - -namespace { -std::shared_ptr create() { - qInstallMessageHandler(gui_log); - return qtsdl::GuiApplicationImpl::get(); -} -} - -GuiApplicationWithLogger::GuiApplicationWithLogger() - : - GuiApplication{create()} { -} - -GuiApplicationWithLogger::~GuiApplicationWithLogger() = default; - -} // namespace openage::gui diff --git a/libopenage/gui/integration/public/gui_application_with_logger.h b/libopenage/gui/integration/public/gui_application_with_logger.h deleted file mode 100644 index 69a074a196..0000000000 --- a/libopenage/gui/integration/public/gui_application_with_logger.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. - -#pragma once - -#include "../../guisys/public/gui_application.h" - -namespace openage { -namespace gui { - -/** - * Houses gui logic event queue and attaches to game logger. - */ -class GuiApplicationWithLogger : public qtsdl::GuiApplication { -public: - GuiApplicationWithLogger(); - ~GuiApplicationWithLogger(); -}; - -}} // namespace openage::gui diff --git a/libopenage/gui/registrations.cpp b/libopenage/gui/registrations.cpp deleted file mode 100644 index d3325f9416..0000000000 --- a/libopenage/gui/registrations.cpp +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2017-2017 the openage authors. See copying.md for legal info. - -#include "registrations.h" - -#include - -namespace openage { -namespace gui { - -const int reg_path = qRegisterMetaType("util::Path"); - -}} // openage::gui diff --git a/libopenage/gui/registrations.h b/libopenage/gui/registrations.h deleted file mode 100644 index 2bc595ceb9..0000000000 --- a/libopenage/gui/registrations.h +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2017-2017 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "../util/path.h" - -namespace openage { -namespace gui { - - -}} // openage::gui - -Q_DECLARE_METATYPE(openage::util::Path) diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index 925d2d650d..06498e51c6 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -18,7 +18,6 @@ #include "renderer/camera/camera.h" #include "renderer/gui/gui.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" -#include "renderer/gui/qml_info.h" #include "renderer/render_factory.h" #include "renderer/resources/assets/asset_manager.h" #include "renderer/resources/shader_source.h" diff --git a/libopenage/renderer/gui/CMakeLists.txt b/libopenage/renderer/gui/CMakeLists.txt index a0a61a9b62..fdddb92d06 100644 --- a/libopenage/renderer/gui/CMakeLists.txt +++ b/libopenage/renderer/gui/CMakeLists.txt @@ -1,7 +1,5 @@ add_sources(libopenage - engine_link.cpp gui.cpp - qml_info.cpp ) add_subdirectory("guisys") diff --git a/libopenage/renderer/gui/engine_link.cpp b/libopenage/renderer/gui/engine_link.cpp deleted file mode 100644 index b4045a583c..0000000000 --- a/libopenage/renderer/gui/engine_link.cpp +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "engine_link.h" - -#include - -#include "error/error.h" - -#include "gamestate/simulation.h" - -#include "gui/guisys/link/qml_engine_with_singleton_items_info.h" -#include "gui/guisys/link/qtsdl_checked_static_cast.h" -#include "renderer/gui/qml_info.h" - -namespace openage::renderer::gui { - -namespace { -// this pushes the EngineLink in the QML engine. -// a qml engine calls the static provider() to obtain a handle. -const int registration = qmlRegisterSingletonType("yay.sfttech.openage", 1, 0, "Engine", &EngineLink::provider); -} // namespace - - -EngineLink::EngineLink(QObject *parent, gamestate::GameSimulation *engine) : - GuiSingletonItem{parent}, - core{engine} { - Q_UNUSED(registration); - - // ENSURE(!unwrap(this)->gui_link, "Sharing singletons between QML engines is not supported for now."); - - // when the engine announces that the global key bindings - // changed, update the display. - // QObject::connect( - // &unwrap(this)->gui_signals, - // &EngineSignals::global_binds_changed, - // this, - // &EngineLink::on_global_binds_changed); - - // trigger the engine signal, - // which then triggers this->on_global_binds_changed. - // unwrap(this)->announce_global_binds(); -} - -EngineLink::~EngineLink() { - // unwrap(this)->gui_link = nullptr; -} - -// a qml engine requests a handle to the engine link with that static -// method we do this by extracting the per-qmlengine singleton from the -// engine (the qmlenginewithsingletoninfo), then just return the new link -// instance -QObject *EngineLink::provider(QQmlEngine *engine, QJSEngine *) { - // cast the engine to our specialization - qtsdl::QmlEngineWithSingletonItemsInfo *engine_with_singleton_items_info = qtsdl::checked_static_cast(engine); - - // get the singleton container out of the custom qml engine - auto info = static_cast( - engine_with_singleton_items_info->get_singleton_items_info()); - ENSURE(info, "qml-globals were lost or not passed to the gui subsystem"); - - // owned by the QML engine - // this handle contains the pointer to the openage engine, - // obtained through the qmlengine - return new EngineLink{nullptr, info->engine}; -} - -QStringList EngineLink::get_global_binds() const { - return this->global_binds; -} - -void EngineLink::stop() { - this->core->stop(); -} - -void EngineLink::on_global_binds_changed(const std::vector &global_binds) { - QStringList new_global_binds; - - // create the qstring list from the std string list - // which is then displayed in the ui - std::transform( - std::begin(global_binds), - std::end(global_binds), - std::back_inserter(new_global_binds), - [](const std::string &s) { - return QString::fromStdString(s); - }); - - new_global_binds.sort(); - - if (this->global_binds != new_global_binds) { - this->global_binds = new_global_binds; - emit this->global_binds_changed(); - } -} - -} // namespace openage::renderer::gui diff --git a/libopenage/renderer/gui/engine_link.h b/libopenage/renderer/gui/engine_link.h deleted file mode 100644 index 9b0b7089cf..0000000000 --- a/libopenage/renderer/gui/engine_link.h +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include - -#include "gui/guisys/link/gui_singleton_item.h" -#include "util/path.h" - -QT_FORWARD_DECLARE_CLASS(QQmlEngine) -QT_FORWARD_DECLARE_CLASS(QJSEngine) - -namespace openage { - -namespace gamestate { -class GameSimulation; -} - -namespace renderer::gui { -class EngineLink; -} // namespace renderer::gui -} // namespace openage - -namespace qtsdl { -template <> -struct Wrap { - using Type = openage::renderer::gui::EngineLink; -}; - -template <> -struct Unwrap { - using Type = openage::gamestate::GameSimulation; -}; - -} // namespace qtsdl - -namespace openage { -namespace renderer { -namespace gui { - -class EngineLink : public qtsdl::GuiSingletonItem { - Q_OBJECT - - /** - * The text list of global key bindings. - * displayed so one can see what keys are active. - */ - Q_PROPERTY(QStringList globalBinds - READ get_global_binds - NOTIFY global_binds_changed) - -public: - explicit EngineLink(QObject *parent, gamestate::GameSimulation *engine); - virtual ~EngineLink(); - - static QObject *provider(QQmlEngine *, QJSEngine *); - - template - U *get() const { - return core; - } - - QStringList get_global_binds() const; - - Q_INVOKABLE void stop(); - -signals: - void global_binds_changed(); - -private slots: - void on_global_binds_changed(const std::vector &global_binds); - -private: - gamestate::GameSimulation *core; - - QStringList global_binds; -}; - -} // namespace gui -} // namespace renderer -} // namespace openage diff --git a/libopenage/renderer/gui/gui.cpp b/libopenage/renderer/gui/gui.cpp index c877c4a537..f51320c24c 100644 --- a/libopenage/renderer/gui/gui.cpp +++ b/libopenage/renderer/gui/gui.cpp @@ -6,7 +6,6 @@ #include "renderer/gui/guisys/public/gui_input.h" #include "renderer/gui/guisys/public/gui_renderer.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" -#include "renderer/gui/qml_info.h" #include "renderer/opengl/context.h" #include "renderer/renderer.h" #include "renderer/resources/shader_source.h" @@ -24,8 +23,6 @@ GUI::GUI(std::shared_ptr app, const util::Path &assetdir, const std::shared_ptr &renderer) : application{app}, - render_updater{}, - game_logic_updater{}, gui_renderer{std::make_shared(window)}, gui_input{std::make_shared(gui_renderer)}, engine{std::make_shared(gui_renderer)}, diff --git a/libopenage/renderer/gui/gui.h b/libopenage/renderer/gui/gui.h index dc8b0fcfdc..1cecfb7bd6 100644 --- a/libopenage/renderer/gui/gui.h +++ b/libopenage/renderer/gui/gui.h @@ -6,7 +6,6 @@ #include #include -#include "gui/guisys/public/gui_event_queue.h" #include "renderer/gui/guisys/public/gui_subtree.h" namespace qtgui { @@ -30,8 +29,6 @@ class UniformInput; namespace gui { -class QMLInfo; - /** * Interface (= HUD) of the game. * @@ -99,15 +96,6 @@ class GUI { */ std::shared_ptr application; - /** - * TODO - */ - qtsdl::GuiEventQueue render_updater; - /** - * TODO - */ - qtsdl::GuiEventQueue game_logic_updater; - /** * Qt-based renderer for the GUI texture. Draws into * \p gui_texture. diff --git a/libopenage/renderer/gui/guisys/CMakeLists.txt b/libopenage/renderer/gui/guisys/CMakeLists.txt index d4f5349e29..3b454bfec5 100644 --- a/libopenage/renderer/gui/guisys/CMakeLists.txt +++ b/libopenage/renderer/gui/guisys/CMakeLists.txt @@ -1,4 +1,13 @@ +list(APPEND QT_SDL_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/link/gui_item.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/link/gui_list_model.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/link/gui_property_map_impl.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/link/gui_singleton_item.cpp +) + list(APPEND QTGUI_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/private/livereload/deferred_initial_constant_property_values.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/private/livereload/gui_live_reloader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/private/livereload/recursive_directory_watcher_worker.cpp ${CMAKE_CURRENT_SOURCE_DIR}/private/livereload/recursive_directory_watcher.cpp ) diff --git a/libopenage/renderer/gui/guisys/link/gui_item.cpp b/libopenage/renderer/gui/guisys/link/gui_item.cpp new file mode 100644 index 0000000000..20bbb7c8c0 --- /dev/null +++ b/libopenage/renderer/gui/guisys/link/gui_item.cpp @@ -0,0 +1,20 @@ +// Copyright 2015-2023 the openage authors. See copying.md for legal info. + +#include "gui_item.h" + + +namespace qtgui { + + +QString name_tidier(const char *name) { + QString cleaner_name = QString::fromLatin1(name); + cleaner_name.remove(QRegularExpression("qtgui|PersistentCoreHolder")); + return cleaner_name; +} + +GuiItemQObject::GuiItemQObject(QObject *parent) : + QObject{parent}, + GuiItemBase{} { +} + +} // namespace qtgui diff --git a/libopenage/gui/guisys/link/gui_item.h b/libopenage/renderer/gui/guisys/link/gui_item.h similarity index 62% rename from libopenage/gui/guisys/link/gui_item.h rename to libopenage/renderer/gui/guisys/link/gui_item.h index 00d252dba3..450d3dc796 100644 --- a/libopenage/gui/guisys/link/gui_item.h +++ b/libopenage/renderer/gui/guisys/link/gui_item.h @@ -1,26 +1,25 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once -#include #include #include +#include #include #include #include -#include +#include #include +#include #include -#include -#include "../private/game_logic_caller.h" -#include "../private/livereload/deferred_initial_constant_property_values.h" -#include "qtsdl_checked_static_cast.h" -#include "gui_item_link.h" +#include "renderer/gui/guisys/link/gui_item_link.h" +#include "renderer/gui/guisys/link/qtsdl_checked_static_cast.h" +#include "renderer/gui/guisys/private/livereload/deferred_initial_constant_property_values.h" -namespace qtsdl { +namespace qtgui { /** * Cleans a text from unneeded content like "qtsdl". @@ -43,17 +42,15 @@ class PersistentCoreHolderBase { virtual void adopt_shell(GuiItemLink *link) = 0; }; -template +template class PersistentCoreHolder : public PersistentCoreHolderBase { public: - PersistentCoreHolder(std::unique_ptr core) - : + PersistentCoreHolder(std::unique_ptr core) : PersistentCoreHolderBase{}, core{std::move(core)} { } - explicit PersistentCoreHolder() - : + explicit PersistentCoreHolder() : PersistentCoreHolderBase{} { } @@ -74,13 +71,13 @@ class GuiItemBase : public DeferredInitialConstantPropertyValues { friend class GuiLiveReloader; friend class GuiSubtreeImpl; - template + template friend class GuiItemMethods; - template + template friend class GuiItemCoreInstantiator; - template + template friend class GuiItemListModel; /** @@ -109,26 +106,18 @@ class GuiItemBase : public DeferredInitialConstantPropertyValues { virtual void on_core_adopted() { } - /** - * Set up signal to be able to run code in the game logic thread. - */ - void set_game_logic_callback(GuiCallback *game_logic_callback) { - this->game_logic_caller.set_game_logic_callback(game_logic_callback); - } - protected: - GameLogicCaller game_logic_caller; std::function()> instantiate_core_func; - std::function do_adopt_core_func; + std::function do_adopt_core_func; std::function on_core_adopted_func; }; -template +template class GuiItemOrigin { - template + template friend class GuiItemMethods; - template + template friend class GuiItemCoreInstantiator; /** @@ -140,10 +129,9 @@ class GuiItemOrigin { /** * Member function of the GuiPersistentItem */ -template +template class GuiItemMethods { public: - #ifndef NDEBUG virtual ~GuiItemMethods() { } @@ -152,12 +140,13 @@ class GuiItemMethods { /** * Get core. */ - template - U* get() const { - if (checked_static_cast(this)->holder->core) { - assert(checked_static_cast(checked_static_cast(this)->holder->core.get())); - return checked_static_cast(checked_static_cast(this)->holder->core.get()); - } else { + template + U *get() const { + if (checked_static_cast(this)->holder->core) { + assert(checked_static_cast(checked_static_cast(this)->holder->core.get())); + return checked_static_cast(checked_static_cast(this)->holder->core.get()); + } + else { return nullptr; } } @@ -165,12 +154,13 @@ class GuiItemMethods { /** * Get core. */ - template - U* get() { - if (checked_static_cast(this)->holder->core) { - assert(checked_static_cast(checked_static_cast(this)->holder->core.get())); - return checked_static_cast(checked_static_cast(this)->holder->core.get()); - } else { + template + U *get() { + if (checked_static_cast(this)->holder->core) { + assert(checked_static_cast(checked_static_cast(this)->holder->core.get())); + return checked_static_cast(checked_static_cast(this)->holder->core.get()); + } + else { return nullptr; } } @@ -178,21 +168,16 @@ class GuiItemMethods { /** * Invoke a function in the logic thread (the thread of the core of this object). */ - template - void i(F f, Args&& ... args) { - GuiItemBase *base = checked_static_cast(checked_static_cast(this)); - - emit base->game_logic_caller.in_game_logic_thread([=, this] { - static_assert_about_unwrapping(this))), decltype(unwrap_if_can(args))...>(); - f(unwrap(checked_static_cast(this)), unwrap_if_can(args)...); - }); + template + void i(F f, Args &&...args) { + GuiItemBase *base = checked_static_cast(checked_static_cast(this)); } protected: /** * Set property. */ - template + template void s(F f, P &p, A &&arg) { if (p != arg) { p = std::forward(arg); @@ -203,7 +188,7 @@ class GuiItemMethods { /** * Set property even if it's the same. */ - template + template void sf(F f, P &p, A &&arg) { p = std::forward(arg); this->assign_while_catching_constant_properies_inits(f, p); @@ -213,38 +198,32 @@ class GuiItemMethods { /** * Static QML properties assignments during the init phase are stored for a batch assignment at the end of the init phase. */ - template + template void assign_while_catching_constant_properies_inits(F f, A &&arg) { - GuiItemBase *base = checked_static_cast(checked_static_cast(this)); + GuiItemBase *base = checked_static_cast(checked_static_cast(this)); - static_assert_about_unwrapping(this))), decltype(unwrap_if_can(arg))>(); - - if (base->init_over) - emit base->game_logic_caller.in_game_logic_thread([=, this] {f(unwrap(checked_static_cast(this)), unwrap_if_can(arg));}); - else - base->static_properties_assignments.push_back([=, this] { - emit base->game_logic_caller.in_game_logic_thread([=, this] {f(unwrap(checked_static_cast(this)), unwrap_if_can(arg));}); - }); + static_assert_about_unwrapping(this))), decltype(unwrap_if_can(arg))>(); } }; -class GuiItemQObject : public QObject, public GuiItemBase, public GuiItemLink { +class GuiItemQObject : public QObject + , public GuiItemBase + , public GuiItemLink { Q_OBJECT public: - explicit GuiItemQObject(QObject *parent=nullptr); + explicit GuiItemQObject(QObject *parent = nullptr); }; -template +template class GuiItemCoreInstantiator : public GuiItemMethods { public: /** * Sets up a factory for the type T. */ - explicit GuiItemCoreInstantiator(GuiItemBase *item_base) - : - GuiItemMethods{} { - + explicit GuiItemCoreInstantiator(GuiItemBase *item_base) : + GuiItemMethods {} + { using namespace std::placeholders; item_base->instantiate_core_func = std::bind(&GuiItemCoreInstantiator::instantiate_core, this); @@ -256,11 +235,12 @@ class GuiItemCoreInstantiator : public GuiItemMethods { * Creates and returns a core object of type Unwrap::Type inside a holder. */ std::unique_ptr instantiate_core() { - T *origin = checked_static_cast(this); + T *origin = checked_static_cast(this); if (origin->holder) { return std::unique_ptr(); - } else { + } + else { auto core = std::make_unique::Type>(origin); return std::make_uniqueholder)>::type>(std::move(core)); } @@ -272,11 +252,12 @@ class GuiItemCoreInstantiator : public GuiItemMethods { void do_adopt_core(PersistentCoreHolderBase *holder, const QString &tag) { assert(holder); - T *origin = checked_static_cast(this); + T *origin = checked_static_cast(this); if (origin->holder) { qFatal("Error in QML code: GuiLiveReloader was asked to use same tag '%s' for multiple objects.", qUtf8Printable(tag)); - } else { + } + else { if (typeid(decltype(*origin->holder)) != typeid(*holder)) { qFatal( "Error in QML code: GuiLiveReloader was asked " @@ -284,17 +265,17 @@ class GuiItemCoreInstantiator : public GuiItemMethods { "using tag '%s'.", qUtf8Printable(name_tidier(typeid(decltype(*origin->holder)).name())), qUtf8Printable(name_tidier(typeid(*holder).name())), - qUtf8Printable(tag) - ); - } else { + qUtf8Printable(tag)); + } + else { origin->holder = checked_static_castholder)>(holder); origin->holder->adopt_shell(origin); } } } - GuiItemCoreInstantiator(const GuiItemCoreInstantiator&) = delete; - GuiItemCoreInstantiator& operator=(const GuiItemCoreInstantiator&) = delete; + GuiItemCoreInstantiator(const GuiItemCoreInstantiator &) = delete; + GuiItemCoreInstantiator &operator=(const GuiItemCoreInstantiator &) = delete; }; /** @@ -308,26 +289,30 @@ class GuiItemCoreInstantiator : public GuiItemMethods { * * @tparam T type of the concrete shell class (pass the descendant class) */ -template -class GuiItem : public GuiItemOrigin, public GuiItemCoreInstantiator { +template +class GuiItem : public GuiItemOrigin + , public GuiItemCoreInstantiator { public: /** * Creates an empty QObject shell for a core that is wrappable into a type *T. */ - explicit GuiItem(GuiItemBase *item_base) - : + explicit GuiItem(GuiItemBase *item_base) : GuiItemOrigin{}, - GuiItemCoreInstantiator{item_base} { + GuiItemCoreInstantiator { + item_base + } + { } }; -template -class GuiItemInterface : public GuiItemOrigin, public GuiItemMethods { +template +class GuiItemInterface : public GuiItemOrigin + , public GuiItemMethods { public: - explicit GuiItemInterface() - : + explicit GuiItemInterface() : GuiItemOrigin{}, - GuiItemMethods{} { + GuiItemMethods {} + { } }; @@ -335,16 +320,15 @@ namespace { class NullClass { NullClass() = delete; }; -} +} // namespace /** * Shadows inherited member functions. */ -template -class Shadow: public T { +template +class Shadow : public T { public: - Shadow(QObject *parent=nullptr) - : + Shadow(QObject *parent = nullptr) : T{parent} { } @@ -357,8 +341,9 @@ class Shadow: public T { /** * Switches the factory to production of the instances of the derived class. */ -template -class Inherits: public Shadow, public GuiItemCoreInstantiator

{ +template +class Inherits : public Shadow + , public GuiItemCoreInstantiator

{ public: using Shadow::get; using GuiItemCoreInstantiator

::get; @@ -369,14 +354,16 @@ class Inherits: public Shadow, public GuiItemCoreInstantiator

{ using Shadow::sf; using GuiItemCoreInstantiator

::sf; - Inherits(QObject *parent=nullptr) - : + Inherits(QObject *parent = nullptr) : Shadow{parent}, - GuiItemCoreInstantiator

{this} { + GuiItemCoreInstantiator

{ + this + } + { } virtual ~Inherits() { } }; -} // namespace qtsdl +} // namespace qtgui diff --git a/libopenage/gui/guisys/link/gui_item_link.h b/libopenage/renderer/gui/guisys/link/gui_item_link.h similarity index 97% rename from libopenage/gui/guisys/link/gui_item_link.h rename to libopenage/renderer/gui/guisys/link/gui_item_link.h index 60cc7e5572..0bd7b8d61f 100644 --- a/libopenage/gui/guisys/link/gui_item_link.h +++ b/libopenage/renderer/gui/guisys/link/gui_item_link.h @@ -5,9 +5,10 @@ #include #include -#include "qtsdl_checked_static_cast.h" +#include "renderer/gui/guisys/link/qtsdl_checked_static_cast.h" -namespace qtsdl { + +namespace qtgui { /** * Base for QObject wrapper of the domain-specific classes. @@ -119,4 +120,4 @@ constexpr void static_assert_about_unwrapping() { static_assert(can_call{}, "One of possible causes: if you're passing SomethingLink*, then don't forget to #include \"something_link.h\"."); } -} // namespace qtsdl +} // namespace qtgui diff --git a/libopenage/gui/guisys/link/gui_item_list_model.h b/libopenage/renderer/gui/guisys/link/gui_item_list_model.h similarity index 68% rename from libopenage/gui/guisys/link/gui_item_list_model.h rename to libopenage/renderer/gui/guisys/link/gui_item_list_model.h index 38e902d4d3..7158247ccd 100644 --- a/libopenage/gui/guisys/link/gui_item_list_model.h +++ b/libopenage/renderer/gui/guisys/link/gui_item_list_model.h @@ -1,27 +1,29 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once #include -#include "gui_item.h" -#include "gui_property_map_impl.h" -#include "gui_list_model.h" +#include "renderer/gui/guisys/link/gui_item.h" +#include "renderer/gui/guisys/link/gui_list_model.h" +#include "renderer/gui/guisys/link/gui_property_map_impl.h" -namespace qtsdl { + +namespace qtgui { /** * A shell object fir cores inherited from GuiPropertyMap * * Core can use a key-value interface while in QML it looks like a ListModel. */ -template +template class GuiItemListModel : public GuiItem { public: - explicit GuiItemListModel(GuiItemBase *item_base) - : - GuiItem{item_base} { - + explicit GuiItemListModel(GuiItemBase *item_base) : + GuiItem { + item_base + } + { item_base->on_core_adopted_func = std::bind(&GuiItemListModel::on_core_adopted, this); } @@ -33,9 +35,9 @@ class GuiItemListModel : public GuiItem { } void establish_from_gui_propagation() { - auto _this = checked_static_cast(this); + auto _this = checked_static_cast(this); - QObject::connect(_this, &GuiListModel::changed_from_gui, [_this] (const QByteArray &name, const QVariant &value) { + QObject::connect(_this, &GuiListModel::changed_from_gui, [_this](const QByteArray &name, const QVariant &value) { emit _this->game_logic_caller.in_game_logic_thread_blocking([&] { unwrap(_this)->impl->setProperty(name, value); }); @@ -43,14 +45,14 @@ class GuiItemListModel : public GuiItem { } void establish_to_gui_propagation() { - auto _this = checked_static_cast(this); + auto _this = checked_static_cast(this); auto properties = unwrap(_this)->impl.get(); QObject::connect(properties, &GuiPropertyMapImpl::property_changed, _this, &GuiListModel::on_property_changed); } void do_initial_to_gui_propagation() { - auto _this = checked_static_cast(this); + auto _this = checked_static_cast(this); auto properties = unwrap(_this)->impl.get(); auto property_names = properties->dynamicPropertyNames(); @@ -58,7 +60,7 @@ class GuiItemListModel : public GuiItem { std::vector> values; values.reserve(property_names.size()); - std::transform(std::begin(property_names), std::end(property_names), std::back_inserter(values), [properties] (const QByteArray &name) { + std::transform(std::begin(property_names), std::end(property_names), std::back_inserter(values), [properties](const QByteArray &name) { return std::make_tuple(name, properties->property(name)); }); @@ -66,4 +68,4 @@ class GuiItemListModel : public GuiItem { } }; -} // namespace qtsdl +} // namespace qtgui diff --git a/libopenage/gui/guisys/link/gui_list_model.cpp b/libopenage/renderer/gui/guisys/link/gui_list_model.cpp similarity index 85% rename from libopenage/gui/guisys/link/gui_list_model.cpp rename to libopenage/renderer/gui/guisys/link/gui_list_model.cpp index 72bbf20663..467468020b 100644 --- a/libopenage/gui/guisys/link/gui_list_model.cpp +++ b/libopenage/renderer/gui/guisys/link/gui_list_model.cpp @@ -1,13 +1,13 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "gui_list_model.h" #include -namespace qtsdl { -GuiListModel::GuiListModel(QObject *parent) - : +namespace qtgui { + +GuiListModel::GuiListModel(QObject *parent) : QAbstractListModel{parent} { } @@ -20,7 +20,7 @@ void GuiListModel::set(const std::vector> &valu } void GuiListModel::on_property_changed(const QByteArray &name, const QVariant &value) { - auto foundIt = std::find_if(std::begin(this->values), std::end(this->values), [&name] (std::tuple& p) { + auto foundIt = std::find_if(std::begin(this->values), std::end(this->values), [&name](std::tuple &p) { return std::get(p) == name; }); @@ -33,14 +33,15 @@ void GuiListModel::on_property_changed(const QByteArray &name, const QVariant &v auto i = this->index(std::distance(std::begin(this->values), foundIt)); emit this->dataChanged(i, i, {Qt::EditRole}); } - } else { + } + else { this->beginInsertRows(QModelIndex(), this->values.size(), this->values.size()); this->values.emplace(std::end(this->values), name, value); this->endInsertRows(); } } -int GuiListModel::rowCount(const QModelIndex&) const { +int GuiListModel::rowCount(const QModelIndex &) const { return values.size(); } @@ -71,4 +72,4 @@ bool GuiListModel::setData(const QModelIndex &index, const QVariant &value, int return false; } -} // namespace qtsdl +} // namespace qtgui diff --git a/libopenage/gui/guisys/link/gui_list_model.h b/libopenage/renderer/gui/guisys/link/gui_list_model.h similarity index 58% rename from libopenage/gui/guisys/link/gui_list_model.h rename to libopenage/renderer/gui/guisys/link/gui_list_model.h index 6f93ce48b2..3a3b85c52e 100644 --- a/libopenage/gui/guisys/link/gui_list_model.h +++ b/libopenage/renderer/gui/guisys/link/gui_list_model.h @@ -1,24 +1,27 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once -#include #include +#include #include -#include "gui_item.h" +#include "renderer/gui/guisys/link/gui_item.h" + -namespace qtsdl { +namespace qtgui { /** * Adapts core that uses a property map to QAbstractListModel interface. */ -class GuiListModel : public QAbstractListModel, public GuiItemBase, public GuiItemLink { +class GuiListModel : public QAbstractListModel + , public GuiItemBase + , public GuiItemLink { Q_OBJECT public: - GuiListModel(QObject *parent=nullptr); + GuiListModel(QObject *parent = nullptr); virtual ~GuiListModel(); void set(const std::vector> &values); @@ -30,12 +33,12 @@ public slots: void changed_from_gui(const char *name, const QVariant &value); private: - virtual int rowCount(const QModelIndex&) const override; + virtual int rowCount(const QModelIndex &) const override; virtual Qt::ItemFlags flags(const QModelIndex &index) const override; - virtual QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override; - virtual bool setData(const QModelIndex &index, const QVariant &value, int role=Qt::EditRole) override; + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; std::vector> values; }; -} // namespace qtsdl +} // namespace qtgui diff --git a/libopenage/gui/guisys/link/gui_property_map_impl.cpp b/libopenage/renderer/gui/guisys/link/gui_property_map_impl.cpp similarity index 66% rename from libopenage/gui/guisys/link/gui_property_map_impl.cpp rename to libopenage/renderer/gui/guisys/link/gui_property_map_impl.cpp index 885589c38b..1a8e8b7db3 100644 --- a/libopenage/gui/guisys/link/gui_property_map_impl.cpp +++ b/libopenage/renderer/gui/guisys/link/gui_property_map_impl.cpp @@ -1,16 +1,16 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "gui_property_map_impl.h" #include #include -#include "qtsdl_checked_static_cast.h" +#include "renderer/gui/guisys/link/qtsdl_checked_static_cast.h" -namespace qtsdl { -GuiPropertyMapImpl::GuiPropertyMapImpl() - : +namespace qtgui { + +GuiPropertyMapImpl::GuiPropertyMapImpl() : QObject{} { } @@ -18,7 +18,7 @@ GuiPropertyMapImpl::~GuiPropertyMapImpl() = default; bool GuiPropertyMapImpl::event(QEvent *e) { if (e->type() == QEvent::DynamicPropertyChange) { - auto property_name = checked_static_cast(e)->propertyName(); + auto property_name = checked_static_cast(e)->propertyName(); emit this->property_changed(property_name, this->property(property_name)); return true; } @@ -26,4 +26,4 @@ bool GuiPropertyMapImpl::event(QEvent *e) { return this->QObject::event(e); } -} // namespace qtsdl +} // namespace qtgui diff --git a/libopenage/gui/guisys/link/gui_property_map_impl.h b/libopenage/renderer/gui/guisys/link/gui_property_map_impl.h similarity index 74% rename from libopenage/gui/guisys/link/gui_property_map_impl.h rename to libopenage/renderer/gui/guisys/link/gui_property_map_impl.h index a7054345be..d0118f4c58 100644 --- a/libopenage/gui/guisys/link/gui_property_map_impl.h +++ b/libopenage/renderer/gui/guisys/link/gui_property_map_impl.h @@ -1,10 +1,11 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once #include -namespace qtsdl { + +namespace qtgui { class GuiPropertyMapImpl : public QObject { Q_OBJECT @@ -20,4 +21,4 @@ class GuiPropertyMapImpl : public QObject { virtual bool event(QEvent *e) override; }; -} // namespace qtsdl +} // namespace qtgui diff --git a/libopenage/renderer/gui/guisys/link/gui_singleton_item.cpp b/libopenage/renderer/gui/guisys/link/gui_singleton_item.cpp new file mode 100644 index 0000000000..dc01d27e4e --- /dev/null +++ b/libopenage/renderer/gui/guisys/link/gui_singleton_item.cpp @@ -0,0 +1,15 @@ +// Copyright 2015-2023 the openage authors. See copying.md for legal info. + +#include "gui_singleton_item.h" + + +namespace qtgui { + +GuiSingletonItem::GuiSingletonItem(QObject *parent) : + QObject{parent}, + GuiItemLink{} { +} + +GuiSingletonItem::~GuiSingletonItem() = default; + +} // namespace qtgui diff --git a/libopenage/renderer/gui/guisys/link/gui_singleton_item.h b/libopenage/renderer/gui/guisys/link/gui_singleton_item.h new file mode 100644 index 0000000000..8024ecf032 --- /dev/null +++ b/libopenage/renderer/gui/guisys/link/gui_singleton_item.h @@ -0,0 +1,21 @@ +// Copyright 2015-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include "renderer/gui/guisys/link/gui_item_link.h" + + +namespace qtgui { + +class GuiSingletonItem : public QObject + , public GuiItemLink { + Q_OBJECT + +public: + explicit GuiSingletonItem(QObject *parent = nullptr); + virtual ~GuiSingletonItem(); +}; + +} // namespace qtgui diff --git a/libopenage/renderer/gui/guisys/link/qtsdl_checked_static_cast.h b/libopenage/renderer/gui/guisys/link/qtsdl_checked_static_cast.h new file mode 100644 index 0000000000..7b51a89e03 --- /dev/null +++ b/libopenage/renderer/gui/guisys/link/qtsdl_checked_static_cast.h @@ -0,0 +1,16 @@ +// Copyright 2015-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include + + +namespace qtgui { + +template +T checked_static_cast(U *u) { + assert(dynamic_cast(u)); + return static_cast(u); +} + +} // namespace qtgui diff --git a/libopenage/renderer/gui/guisys/private/gui_ctx_setup.cpp b/libopenage/renderer/gui/guisys/private/gui_ctx_setup.cpp index d5c32b56e4..49b05af242 100644 --- a/libopenage/renderer/gui/guisys/private/gui_ctx_setup.cpp +++ b/libopenage/renderer/gui/guisys/private/gui_ctx_setup.cpp @@ -4,10 +4,11 @@ #include +#include #include #include +#include -#include "gui/guisys/private/platforms/context_extraction.h" #include "renderer/gui/guisys/private/opengl_debug_logger.h" #include "renderer/opengl/context.h" #include "renderer/opengl/window.h" diff --git a/libopenage/gui/guisys/private/livereload/deferred_initial_constant_property_values.cpp b/libopenage/renderer/gui/guisys/private/livereload/deferred_initial_constant_property_values.cpp similarity index 84% rename from libopenage/gui/guisys/private/livereload/deferred_initial_constant_property_values.cpp rename to libopenage/renderer/gui/guisys/private/livereload/deferred_initial_constant_property_values.cpp index 932ca20791..f17f7f2a0e 100644 --- a/libopenage/gui/guisys/private/livereload/deferred_initial_constant_property_values.cpp +++ b/libopenage/renderer/gui/guisys/private/livereload/deferred_initial_constant_property_values.cpp @@ -1,11 +1,10 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "deferred_initial_constant_property_values.h" -namespace qtsdl { +namespace qtgui { -DeferredInitialConstantPropertyValues::DeferredInitialConstantPropertyValues() - : +DeferredInitialConstantPropertyValues::DeferredInitialConstantPropertyValues() : init_over{} { } @@ -27,4 +26,4 @@ bool DeferredInitialConstantPropertyValues::is_init_over() const { return this->init_over; } -} // namespace qtsdl +} // namespace qtgui diff --git a/libopenage/gui/guisys/private/livereload/deferred_initial_constant_property_values.h b/libopenage/renderer/gui/guisys/private/livereload/deferred_initial_constant_property_values.h similarity index 88% rename from libopenage/gui/guisys/private/livereload/deferred_initial_constant_property_values.h rename to libopenage/renderer/gui/guisys/private/livereload/deferred_initial_constant_property_values.h index 699c9ed64c..1add8b41e1 100644 --- a/libopenage/gui/guisys/private/livereload/deferred_initial_constant_property_values.h +++ b/libopenage/renderer/gui/guisys/private/livereload/deferred_initial_constant_property_values.h @@ -1,11 +1,11 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once -#include #include +#include -namespace qtsdl { +namespace qtgui { /** * Stores static properties during initialization to be able to assign them after all 'liveReloadTag' properties are set. @@ -36,4 +36,4 @@ class DeferredInitialConstantPropertyValues { std::vector> static_properties_assignments; }; -} // namespace qtsdl +} // namespace qtgui diff --git a/libopenage/gui/guisys/private/livereload/gui_live_reloader.cpp b/libopenage/renderer/gui/guisys/private/livereload/gui_live_reloader.cpp similarity index 98% rename from libopenage/gui/guisys/private/livereload/gui_live_reloader.cpp rename to libopenage/renderer/gui/guisys/private/livereload/gui_live_reloader.cpp index 474de95dc9..142ec7892e 100644 --- a/libopenage/gui/guisys/private/livereload/gui_live_reloader.cpp +++ b/libopenage/renderer/gui/guisys/private/livereload/gui_live_reloader.cpp @@ -8,7 +8,7 @@ #include "../../link/gui_item.h" #include "deferred_initial_constant_property_values.h" -namespace qtsdl { +namespace qtgui { namespace { const int registration = qmlRegisterUncreatableType("yay.sfttech.livereload", 1, 0, "LR", "LR is non-instantiable. It provides the 'LR.tag' attached property."); @@ -88,4 +88,4 @@ void GuiLiveReloader::init_persistent_items(const QList -#include #include +#include +#include -#include #include #include +#include #include #include // since qt 5.14, the std::hash of q* types are included in qt #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) namespace std { -template<> +template <> struct hash { - size_t operator()(const QString& val) const noexcept { + size_t operator()(const QString &val) const noexcept { return qHash(val); } }; -} +} // namespace std #endif -namespace qtsdl { +namespace qtgui { class GuiItemBase; @@ -39,7 +39,7 @@ class GuiLiveReloaderAttachedProperty : public QObject { public: explicit GuiLiveReloaderAttachedProperty(QObject *object); - GuiItemBase* get_attachee() const; + GuiItemBase *get_attachee() const; QString get_tag() const; void set_tag(const QString &tag); @@ -60,7 +60,7 @@ class GuiLiveReloaderAttachedPropertyProvider : public QObject { Q_OBJECT public: - static GuiLiveReloaderAttachedProperty* qmlAttachedProperties(QObject*); + static GuiLiveReloaderAttachedProperty *qmlAttachedProperties(QObject *); }; class PersistentCoreHolderBase; @@ -69,15 +69,14 @@ class PersistentCoreHolderBase; * Stores objects that need to be kept alive across GUI reloads. */ class GuiLiveReloader { - public: - void init_persistent_items(const QList &items); + void init_persistent_items(const QList &items); private: using TagToPreservableMap = std::unordered_map>; TagToPreservableMap preservable; }; -} // namespace qtsdl +} // namespace qtgui -QML_DECLARE_TYPEINFO(qtsdl::GuiLiveReloaderAttachedPropertyProvider, QML_HAS_ATTACHED_PROPERTIES) +QML_DECLARE_TYPEINFO(qtgui::GuiLiveReloaderAttachedPropertyProvider, QML_HAS_ATTACHED_PROPERTIES) diff --git a/libopenage/renderer/gui/integration/private/CMakeLists.txt b/libopenage/renderer/gui/integration/private/CMakeLists.txt index 1ef1a3946a..3457b62e3a 100644 --- a/libopenage/renderer/gui/integration/private/CMakeLists.txt +++ b/libopenage/renderer/gui/integration/private/CMakeLists.txt @@ -1,3 +1,8 @@ add_sources(libopenage - gui_log.cpp + gui_filled_texture_handles.cpp + gui_log.cpp + gui_make_standalone_subtexture.cpp + gui_standalone_subtexture.cpp + gui_texture.cpp + gui_texture_handle.cpp ) diff --git a/libopenage/gui/integration/private/gui_filled_texture_handles.cpp b/libopenage/renderer/gui/integration/private/gui_filled_texture_handles.cpp similarity index 70% rename from libopenage/gui/integration/private/gui_filled_texture_handles.cpp rename to libopenage/renderer/gui/integration/private/gui_filled_texture_handles.cpp index 20df907316..db664516b4 100644 --- a/libopenage/gui/integration/private/gui_filled_texture_handles.cpp +++ b/libopenage/renderer/gui/integration/private/gui_filled_texture_handles.cpp @@ -1,12 +1,13 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "gui_filled_texture_handles.h" #include -#include "gui_texture_handle.h" +#include "renderer/gui/integration/private/gui_texture_handle.h" -namespace openage::gui { + +namespace openage::renderer::gui { GuiFilledTextureHandles::GuiFilledTextureHandles() = default; @@ -19,39 +20,38 @@ void GuiFilledTextureHandles::add_texture_handle(const QString &id, const QSize void GuiFilledTextureHandles::free_texture_handle(SizedTextureHandle *filled_handle) { std::unique_lock lck{this->handles_mutex}; - this->handles.erase(std::remove_if(std::begin(this->handles), std::end(this->handles), [filled_handle] (const std::tuple& handle) { - return std::get(handle) == filled_handle; - }), std::end(this->handles)); + this->handles.erase(std::remove_if(std::begin(this->handles), std::end(this->handles), [filled_handle](const std::tuple &handle) { + return std::get(handle) == filled_handle; + }), + std::end(this->handles)); } void GuiFilledTextureHandles::fill_all_handles_with_texture(const TextureHandle &texture) { std::unique_lock lck{this->handles_mutex}; - std::for_each(std::begin(this->handles), std::end(this->handles), [&texture] (std::tuple& handle) { - auto filled_handle = std::get(handle); + std::for_each(std::begin(this->handles), std::end(this->handles), [&texture](std::tuple &handle) { + auto filled_handle = std::get(handle); *filled_handle = {texture, textureSize(*filled_handle)}; }); } -void GuiFilledTextureHandles::refresh_all_handles_with_texture(const std::function& refresher) { +void GuiFilledTextureHandles::refresh_all_handles_with_texture(const std::function &refresher) { std::unique_lock lck{this->handles_mutex}; std::vector refreshed_handles(this->handles.size()); - std::transform(std::begin(this->handles), std::end(this->handles), std::begin(refreshed_handles), [refresher] (const std::tuple& handle) { + std::transform(std::begin(this->handles), std::end(this->handles), std::begin(refreshed_handles), [refresher](const std::tuple &handle) { SizedTextureHandle refreshed_handles; refresher(std::get(handle), std::get(handle), &refreshed_handles); return refreshed_handles; }); for (std::size_t i = 0, e = refreshed_handles.size(); i != e; ++i) - *std::get(this->handles[i]) = refreshed_handles[i]; + *std::get(this->handles[i]) = refreshed_handles[i]; } -GuiFilledTextureHandleUser::GuiFilledTextureHandleUser(std::shared_ptr texture_handles, const QString &id, const QSize &requested_size, SizedTextureHandle *filled_handle) - : +GuiFilledTextureHandleUser::GuiFilledTextureHandleUser(std::shared_ptr texture_handles, const QString &id, const QSize &requested_size, SizedTextureHandle *filled_handle) : texture_handles{std::move(texture_handles)}, filled_handle{filled_handle} { - this->texture_handles->add_texture_handle(id, requested_size, filled_handle); } @@ -60,4 +60,4 @@ GuiFilledTextureHandleUser::~GuiFilledTextureHandleUser() { texture_handles->free_texture_handle(filled_handle); } -} // namespace openage::gui +} // namespace openage::renderer::gui diff --git a/libopenage/gui/integration/private/gui_filled_texture_handles.h b/libopenage/renderer/gui/integration/private/gui_filled_texture_handles.h similarity index 74% rename from libopenage/gui/integration/private/gui_filled_texture_handles.h rename to libopenage/renderer/gui/integration/private/gui_filled_texture_handles.h index 8d88a7d688..afca15e1c2 100644 --- a/libopenage/gui/integration/private/gui_filled_texture_handles.h +++ b/libopenage/renderer/gui/integration/private/gui_filled_texture_handles.h @@ -1,17 +1,16 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once -#include -#include -#include #include +#include +#include +#include -#include #include +#include -namespace openage { -namespace gui { +namespace openage::renderer::gui { class TextureHandle; class SizedTextureHandle; @@ -31,7 +30,7 @@ class GuiFilledTextureHandles { void free_texture_handle(SizedTextureHandle *filled_handle); void fill_all_handles_with_texture(const TextureHandle &texture); - void refresh_all_handles_with_texture(const std::function& refresher); + void refresh_all_handles_with_texture(const std::function &refresher); private: /** @@ -40,7 +39,7 @@ class GuiFilledTextureHandles { * It's not a proper Qt usage, so the live reload of the game assets for the gui may break in future Qt releases. * When it breaks, this feature should be implemented via the recreation of the qml engine. */ - std::vector> handles; + std::vector> handles; std::mutex handles_mutex; }; @@ -49,14 +48,14 @@ class GuiFilledTextureHandleUser { GuiFilledTextureHandleUser(std::shared_ptr texture_handles, const QString &id, const QSize &requested_size, SizedTextureHandle *filled_handle); ~GuiFilledTextureHandleUser(); - GuiFilledTextureHandleUser(GuiFilledTextureHandleUser&&) noexcept = default; + GuiFilledTextureHandleUser(GuiFilledTextureHandleUser &&) noexcept = default; private: - GuiFilledTextureHandleUser(const GuiFilledTextureHandleUser&) = delete; - GuiFilledTextureHandleUser& operator=(const GuiFilledTextureHandleUser&) = delete; + GuiFilledTextureHandleUser(const GuiFilledTextureHandleUser &) = delete; + GuiFilledTextureHandleUser &operator=(const GuiFilledTextureHandleUser &) = delete; std::shared_ptr texture_handles; SizedTextureHandle *filled_handle; }; -}} // namespace openage::gui +} // namespace openage::renderer::gui diff --git a/libopenage/renderer/gui/integration/private/gui_make_standalone_subtexture.cpp b/libopenage/renderer/gui/integration/private/gui_make_standalone_subtexture.cpp new file mode 100644 index 0000000000..5151a3934a --- /dev/null +++ b/libopenage/renderer/gui/integration/private/gui_make_standalone_subtexture.cpp @@ -0,0 +1,14 @@ +// Copyright 2015-2023 the openage authors. See copying.md for legal info. + +#include "gui_make_standalone_subtexture.h" + +#include "renderer/gui/integration/private/gui_standalone_subtexture.h" + + +namespace openage::renderer::gui { + +std::unique_ptr make_standalone_subtexture(GLuint id, const QSize &size) { + return std::make_unique(id, size); +} + +} // namespace openage::renderer::gui diff --git a/libopenage/gui/integration/private/gui_make_standalone_subtexture.h b/libopenage/renderer/gui/integration/private/gui_make_standalone_subtexture.h similarity index 87% rename from libopenage/gui/integration/private/gui_make_standalone_subtexture.h rename to libopenage/renderer/gui/integration/private/gui_make_standalone_subtexture.h index e971f478c1..1a74659582 100644 --- a/libopenage/gui/integration/private/gui_make_standalone_subtexture.h +++ b/libopenage/renderer/gui/integration/private/gui_make_standalone_subtexture.h @@ -8,8 +8,7 @@ #include -namespace openage { -namespace gui { +namespace openage::renderer::gui { /* * Reason for this is to resolve epoxy header clash that occur @@ -20,5 +19,4 @@ namespace gui { */ std::unique_ptr make_standalone_subtexture(GLuint id, const QSize &size); -} // namespace gui -} // namespace openage +} // namespace openage::renderer::gui diff --git a/libopenage/gui/integration/private/gui_standalone_subtexture.cpp b/libopenage/renderer/gui/integration/private/gui_standalone_subtexture.cpp similarity index 91% rename from libopenage/gui/integration/private/gui_standalone_subtexture.cpp rename to libopenage/renderer/gui/integration/private/gui_standalone_subtexture.cpp index 3f55d77bc3..63ca492169 100644 --- a/libopenage/gui/integration/private/gui_standalone_subtexture.cpp +++ b/libopenage/renderer/gui/integration/private/gui_standalone_subtexture.cpp @@ -2,8 +2,7 @@ #include "gui_standalone_subtexture.h" -namespace openage { -namespace gui { +namespace openage::renderer::gui { GuiStandaloneSubtexture::GuiStandaloneSubtexture(GLuint id, const QSize &size) : id(id), @@ -43,5 +42,4 @@ QSize GuiStandaloneSubtexture::textureSize() const { return this->size; } -} // namespace gui -} // namespace openage +} // namespace openage::renderer::gui diff --git a/libopenage/gui/integration/private/gui_standalone_subtexture.h b/libopenage/renderer/gui/integration/private/gui_standalone_subtexture.h similarity index 89% rename from libopenage/gui/integration/private/gui_standalone_subtexture.h rename to libopenage/renderer/gui/integration/private/gui_standalone_subtexture.h index 3512d2d066..462e82f370 100644 --- a/libopenage/gui/integration/private/gui_standalone_subtexture.h +++ b/libopenage/renderer/gui/integration/private/gui_standalone_subtexture.h @@ -5,8 +5,7 @@ #include #include -namespace openage { -namespace gui { +namespace openage::renderer::gui { class GuiStandaloneSubtexture : public QSGTexture { Q_OBJECT @@ -30,5 +29,4 @@ class GuiStandaloneSubtexture : public QSGTexture { const QSize size; }; -} // namespace gui -} // namespace openage +} // namespace openage::renderer::gui diff --git a/libopenage/gui/integration/private/gui_texture.cpp b/libopenage/renderer/gui/integration/private/gui_texture.cpp similarity index 84% rename from libopenage/gui/integration/private/gui_texture.cpp rename to libopenage/renderer/gui/integration/private/gui_texture.cpp index 8c6de0746a..c61b1ffe1b 100644 --- a/libopenage/gui/integration/private/gui_texture.cpp +++ b/libopenage/renderer/gui/integration/private/gui_texture.cpp @@ -4,10 +4,11 @@ #include -#include "gui_make_standalone_subtexture.h" -#include "gui_texture.h" +#include "renderer/gui/integration/private/gui_make_standalone_subtexture.h" +#include "renderer/gui/integration/private/gui_texture.h" -namespace openage::gui { + +namespace openage::renderer::gui { GuiTexture::GuiTexture(const SizedTextureHandle &texture_handle) : QSGTexture{}, @@ -35,7 +36,7 @@ bool GuiTexture::hasMipmaps() const { } bool GuiTexture::isAtlasTexture() const { - return openage::gui::isAtlasTexture(this->texture_handle); + return openage::renderer::gui::isAtlasTexture(this->texture_handle); } namespace { @@ -82,7 +83,7 @@ int GuiTexture::textureId() const { } QSize GuiTexture::textureSize() const { - return openage::gui::textureSize(this->texture_handle); + return openage::renderer::gui::textureSize(this->texture_handle); } -} // namespace openage::gui +} // namespace openage::renderer::gui diff --git a/libopenage/gui/integration/private/gui_texture.h b/libopenage/renderer/gui/integration/private/gui_texture.h similarity index 91% rename from libopenage/gui/integration/private/gui_texture.h rename to libopenage/renderer/gui/integration/private/gui_texture.h index aa84a0c10e..1972822763 100644 --- a/libopenage/gui/integration/private/gui_texture.h +++ b/libopenage/renderer/gui/integration/private/gui_texture.h @@ -8,8 +8,7 @@ #include "gui_texture_handle.h" -namespace openage { -namespace gui { +namespace openage::renderer::gui { class GuiTexture : public QSGTexture { Q_OBJECT @@ -35,5 +34,4 @@ class GuiTexture : public QSGTexture { mutable std::unique_ptr standalone; }; -} // namespace gui -} // namespace openage +} // namespace openage::renderer::gui diff --git a/libopenage/gui/integration/private/gui_texture_handle.cpp b/libopenage/renderer/gui/integration/private/gui_texture_handle.cpp similarity index 94% rename from libopenage/gui/integration/private/gui_texture_handle.cpp rename to libopenage/renderer/gui/integration/private/gui_texture_handle.cpp index efe9da7fb8..07a840dbf1 100644 --- a/libopenage/gui/integration/private/gui_texture_handle.cpp +++ b/libopenage/renderer/gui/integration/private/gui_texture_handle.cpp @@ -5,8 +5,7 @@ #include -namespace openage { -namespace gui { +namespace openage::renderer::gui { SizedTextureHandle::SizedTextureHandle() : TextureHandle{0}, @@ -44,5 +43,4 @@ QSize aspect_fit_size(const TextureHandle &texture_handle, const QSize &requeste } } -} // namespace gui -} // namespace openage +} // namespace openage::renderer::gui diff --git a/libopenage/gui/integration/private/gui_texture_handle.h b/libopenage/renderer/gui/integration/private/gui_texture_handle.h similarity index 92% rename from libopenage/gui/integration/private/gui_texture_handle.h rename to libopenage/renderer/gui/integration/private/gui_texture_handle.h index 5d943a1358..f9cdf59686 100644 --- a/libopenage/gui/integration/private/gui_texture_handle.h +++ b/libopenage/renderer/gui/integration/private/gui_texture_handle.h @@ -4,9 +4,7 @@ #include -namespace openage { - -namespace gui { +namespace openage::renderer::gui { class TextureHandle { public: @@ -33,5 +31,4 @@ QSize native_size(const TextureHandle &texture_handle); */ QSize aspect_fit_size(const TextureHandle &texture_handle, const QSize &requested_size); -} // namespace gui -} // namespace openage +} // namespace openage::renderer::gui diff --git a/libopenage/renderer/gui/qml_info.cpp b/libopenage/renderer/gui/qml_info.cpp deleted file mode 100644 index 16e03f333b..0000000000 --- a/libopenage/renderer/gui/qml_info.cpp +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. - -#include "qml_info.h" - -namespace openage { -namespace renderer { -namespace gui { - -QMLInfo::QMLInfo(gamestate::GameSimulation *engine, const util::Path &asset_dir) : - engine{engine}, - asset_dir{asset_dir} {} - -} // namespace gui -} // namespace renderer -} // namespace openage diff --git a/libopenage/renderer/gui/qml_info.h b/libopenage/renderer/gui/qml_info.h deleted file mode 100644 index a5a668a086..0000000000 --- a/libopenage/renderer/gui/qml_info.h +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include "gui/guisys/public/gui_singleton_items_info.h" -#include "util/path.h" - -namespace openage { - -namespace gamestate { -class GameSimulation; -} - -namespace presenter { -class Presenter; -} - -namespace renderer { -namespace gui { - -class QMLInfo : public qtsdl::GuiSingletonItemsInfo { -public: - QMLInfo(gamestate::GameSimulation *engine, const util::Path &asset_dir); - - /** - * The openage engine, so it can be "used" in QML as a "QML Singleton". - * With this pointer, all of QML can find back to the engine. - */ - gamestate::GameSimulation *engine; - - /** - * The openage display. - */ - presenter::Presenter *display; - - /** - * Search path for finding assets n stuff. - */ - util::Path asset_dir; -}; - - -} // namespace gui -} // namespace renderer -} // namespace openage diff --git a/libopenage/renderer/opengl/window.cpp b/libopenage/renderer/opengl/window.cpp index daedbf7537..68ac012ed1 100644 --- a/libopenage/renderer/opengl/window.cpp +++ b/libopenage/renderer/opengl/window.cpp @@ -2,18 +2,18 @@ #include "window.h" +#include +#include +#include +#include + #include "error/error.h" -#include "gui/guisys/public/gui_application.h" #include "log/log.h" + #include "renderer/opengl/context.h" #include "renderer/opengl/renderer.h" #include "renderer/window_event_handler.h" -#include -#include -#include -#include - namespace openage::renderer::opengl { From eaeb1f89b6edccc2e9a0a59ac20e0aa4bebeb358 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 5 Nov 2023 02:21:11 +0100 Subject: [PATCH 056/771] renderer: Organize terrain into chunks. --- .../renderer/stages/terrain/CMakeLists.txt | 1 + .../renderer/stages/terrain/terrain_chunk.cpp | 122 ++++++++++++++++++ .../renderer/stages/terrain/terrain_chunk.h | 106 +++++++++++++++ .../renderer/stages/terrain/terrain_model.cpp | 111 +++------------- .../renderer/stages/terrain/terrain_model.h | 29 ++--- .../stages/terrain/terrain_renderer.cpp | 43 +++--- 6 files changed, 277 insertions(+), 135 deletions(-) create mode 100644 libopenage/renderer/stages/terrain/terrain_chunk.cpp create mode 100644 libopenage/renderer/stages/terrain/terrain_chunk.h diff --git a/libopenage/renderer/stages/terrain/CMakeLists.txt b/libopenage/renderer/stages/terrain/CMakeLists.txt index 6da55d3490..f3d10e1354 100644 --- a/libopenage/renderer/stages/terrain/CMakeLists.txt +++ b/libopenage/renderer/stages/terrain/CMakeLists.txt @@ -1,4 +1,5 @@ add_sources(libopenage + terrain_chunk.cpp terrain_mesh.cpp terrain_model.cpp terrain_render_entity.cpp diff --git a/libopenage/renderer/stages/terrain/terrain_chunk.cpp b/libopenage/renderer/stages/terrain/terrain_chunk.cpp new file mode 100644 index 0000000000..73109cd2c4 --- /dev/null +++ b/libopenage/renderer/stages/terrain/terrain_chunk.cpp @@ -0,0 +1,122 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "terrain_chunk.h" + +#include "renderer/resources/assets/asset_manager.h" +#include "renderer/resources/mesh_data.h" +#include "renderer/stages/terrain/terrain_mesh.h" +#include "renderer/stages/terrain/terrain_render_entity.h" + + +namespace openage::renderer::terrain { + +TerrainChunk::TerrainChunk(const std::shared_ptr &asset_manager, + const util::Vector2s size, + const util::Vector2s offset) : + size{size}, + offset{offset}, + asset_manager{asset_manager} {} + +void TerrainChunk::set_render_entity(const std::shared_ptr &entity) { + this->render_entity = entity; +} + +void TerrainChunk::fetch_updates(const time::time_t &time) { + // TODO: Don't create model if render entity is not set + if (not this->render_entity) { + return; + } + + // Check render entity for updates + if (not this->render_entity->is_changed()) { + return; + } + // TODO: Change mesh instead of recreating it + // TODO: Multiple meshes + auto new_mesh = this->create_mesh(); + this->meshes.clear(); + this->meshes.push_back(new_mesh); + + // Indicate to the render entity that its updates have been processed. + this->render_entity->clear_changed_flag(); +} + +void TerrainChunk::update_uniforms(const time::time_t &time) { + for (auto &mesh : this->meshes) { + mesh->update_uniforms(time); + } +} + +const std::vector> &TerrainChunk::get_meshes() const { + return this->meshes; +} + +std::shared_ptr TerrainChunk::create_mesh() { + // Update mesh + auto size = this->render_entity->get_size(); + auto src_verts = this->render_entity->get_vertices(); + + // dst_verts places vertices in order + // (left to right, bottom to top) + std::vector dst_verts{}; + dst_verts.reserve(src_verts.size() * 5); + for (auto v : src_verts) { + // Transform to scene coords + auto v_vec = v.to_world_space(); + dst_verts.push_back(v_vec[0]); + dst_verts.push_back(v_vec[1]); + dst_verts.push_back(v_vec[2]); + // TODO: Texture scaling + dst_verts.push_back((v.ne / 10).to_float()); + dst_verts.push_back((v.se / 10).to_float()); + } + + // split the grid into triangles using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we split each tile into two triangles + // with counter-clockwise vertex order + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + } + } + + resources::VertexInputInfo info{ + {resources::vertex_input_t::V3F32, resources::vertex_input_t::V2F32}, + resources::vertex_layout_t::AOS, + resources::vertex_primitive_t::TRIANGLES, + resources::index_t::U16}; + + auto const vert_data_size = dst_verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), reinterpret_cast(dst_verts.data()), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), reinterpret_cast(idxs.data()), idx_data_size); + + resources::MeshData meshdata{std::move(vert_data), std::move(idx_data), info}; + + // Update textures + auto tex_manager = this->asset_manager->get_texture_manager(); + + // TODO: Support multiple textures per terrain + + auto terrain_mesh = std::make_shared( + this->asset_manager, + this->render_entity->get_terrain_path(), + std::move(meshdata)); + + return terrain_mesh; +} + +} // namespace openage::renderer::terrain diff --git a/libopenage/renderer/stages/terrain/terrain_chunk.h b/libopenage/renderer/stages/terrain/terrain_chunk.h new file mode 100644 index 0000000000..7158ea1a90 --- /dev/null +++ b/libopenage/renderer/stages/terrain/terrain_chunk.h @@ -0,0 +1,106 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include + +#include "time/time.h" +#include "util/vector.h" + + +namespace openage::renderer { + +namespace resources { +class AssetManager; +} + +namespace terrain { +class TerrainRenderMesh; +class TerrainRenderEntity; + +const size_t MAX_CHUNK_WIDTH = 16; +const size_t MAX_CHUNK_HEIGHT = 16; + + +class TerrainChunk { +public: + /** + * Create a new terrain chunk. + * + * @param asset_manager Asset manager for loading textures. + */ + TerrainChunk(const std::shared_ptr &asset_manager, + const util::Vector2s size, + const util::Vector2s offset); + + ~TerrainChunk() = default; + + /** + * Set the terrain render entity for vertex updates of this mesh. + * + * @param entity New terrain render entity. + */ + void set_render_entity(const std::shared_ptr &entity); + + /** + * Fetch updates from the render entity. + * + * @param time Current simulation time. + */ + void fetch_updates(const time::time_t &time = 0.0); + + /** + * Update the uniforms of the meshes. + * + * @param time Current simulation time. + */ + void update_uniforms(const time::time_t &time = 0.0); + + /** + * Get the meshes composing the terrain. + * + * @return Vector of terrain meshes. + */ + const std::vector> &get_meshes() const; + +private: + /** + * Create a terrain mesh from the data provided by the render entity. + * + * @return New terrain mesh. + */ + std::shared_ptr create_mesh(); + + /** + * Size of the chunk in tiles (width x height). + */ + util::Vector2s size; + + /** + * Offset of the chunk from origin in tiles (x, y). + */ + util::Vector2s offset; + + /** + * Meshes composing the terrain. Each mesh represents a drawable vertex surface + * and a texture. + */ + std::vector> meshes; + + /** + * Asset manager for central accessing and loading textures. + */ + std::shared_ptr asset_manager; + + /** + * Source for ingame terrain coordinates. These coordinates are translated into + * our render vertex mesh when \p update() is called. + */ + std::shared_ptr render_entity; +}; + + +} // namespace terrain +} // namespace openage::renderer diff --git a/libopenage/renderer/stages/terrain/terrain_model.cpp b/libopenage/renderer/stages/terrain/terrain_model.cpp index 4b73b556c9..f557934850 100644 --- a/libopenage/renderer/stages/terrain/terrain_model.cpp +++ b/libopenage/renderer/stages/terrain/terrain_model.cpp @@ -12,7 +12,7 @@ #include "coord/scene.h" #include "renderer/resources/assets/asset_manager.h" #include "renderer/resources/mesh_data.h" -#include "renderer/stages/terrain/terrain_mesh.h" +#include "renderer/stages/terrain/terrain_chunk.h" #include "renderer/stages/terrain/terrain_render_entity.h" #include "util/fixed_point.h" #include "util/vector.h" @@ -21,117 +21,38 @@ namespace openage::renderer::terrain { TerrainRenderModel::TerrainRenderModel(const std::shared_ptr &asset_manager) : - meshes{}, + chunks{}, camera{nullptr}, - asset_manager{asset_manager}, - render_entity{nullptr} { + asset_manager{asset_manager} { } void TerrainRenderModel::set_render_entity(const std::shared_ptr &entity) { - this->render_entity = entity; - this->fetch_updates(); + // ASDF: Set chunk size and offset from parameters + auto chunk = std::make_shared(this->asset_manager, util::Vector2s{10, 10}, util::Vector2s{0, 0}); + chunk->set_render_entity(entity); + chunk->fetch_updates(); + + this->chunks.push_back(chunk); } void TerrainRenderModel::set_camera(const std::shared_ptr &camera) { this->camera = camera; } -void TerrainRenderModel::fetch_updates() { - // TODO: Don't create model if render entity is not set - if (not this->render_entity) { - return; - } - - // Check render entity for updates - if (not this->render_entity->is_changed()) { - return; +void TerrainRenderModel::fetch_updates(const time::time_t &time) { + for (auto &chunk : this->chunks) { + chunk->fetch_updates(time); } - // TODO: Change mesh instead of recreating it - // TODO: Multiple meshes - auto new_mesh = this->create_mesh(); - this->meshes.clear(); - this->meshes.push_back(new_mesh); - - // Indicate to the render entity that its updates have been processed. - this->render_entity->clear_changed_flag(); } void TerrainRenderModel::update_uniforms(const time::time_t &time) { - for (auto &mesh : this->meshes) { - mesh->update_uniforms(time); + for (auto &chunk : this->chunks) { + chunk->update_uniforms(time); } } -const std::vector> &TerrainRenderModel::get_meshes() const { - return this->meshes; -} - -std::shared_ptr TerrainRenderModel::create_mesh() { - // Update mesh - auto size = this->render_entity->get_size(); - auto src_verts = this->render_entity->get_vertices(); - - // dst_verts places vertices in order - // (left to right, bottom to top) - std::vector dst_verts{}; - dst_verts.reserve(src_verts.size() * 5); - for (auto v : src_verts) { - // Transform to scene coords - auto v_vec = v.to_world_space(); - dst_verts.push_back(v_vec[0]); - dst_verts.push_back(v_vec[1]); - dst_verts.push_back(v_vec[2]); - // TODO: Texture scaling - dst_verts.push_back((v.ne / 10).to_float()); - dst_verts.push_back((v.se / 10).to_float()); - } - - // split the grid into triangles using an index array - std::vector idxs; - idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); - // iterate over all tiles in the grid by columns, i.e. starting - // from the left corner to the bottom corner if you imagine it from - // the camera's point of view - for (size_t i = 0; i < size[0] - 1; ++i) { - for (size_t j = 0; j < size[1] - 1; ++j) { - // since we are working on tiles, we split each tile into two triangles - // with counter-clockwise vertex order - idxs.push_back(j + i * size[1]); // bottom left - idxs.push_back(j + 1 + i * size[1]); // bottom right - idxs.push_back(j + size[1] + i * size[1]); // top left - idxs.push_back(j + 1 + i * size[1]); // bottom right - idxs.push_back(j + size[1] + 1 + i * size[1]); // top right - idxs.push_back(j + size[1] + i * size[1]); // top left - } - } - - resources::VertexInputInfo info{ - {resources::vertex_input_t::V3F32, resources::vertex_input_t::V2F32}, - resources::vertex_layout_t::AOS, - resources::vertex_primitive_t::TRIANGLES, - resources::index_t::U16}; - - auto const vert_data_size = dst_verts.size() * sizeof(float); - std::vector vert_data(vert_data_size); - std::memcpy(vert_data.data(), reinterpret_cast(dst_verts.data()), vert_data_size); - - auto const idx_data_size = idxs.size() * sizeof(uint16_t); - std::vector idx_data(idx_data_size); - std::memcpy(idx_data.data(), reinterpret_cast(idxs.data()), idx_data_size); - - resources::MeshData meshdata{std::move(vert_data), std::move(idx_data), info}; - - // Update textures - auto tex_manager = this->asset_manager->get_texture_manager(); - - // TODO: Support multiple textures per terrain - - auto terrain_mesh = std::make_shared( - this->asset_manager, - this->render_entity->get_terrain_path(), - std::move(meshdata)); - - return terrain_mesh; +const std::vector> &TerrainRenderModel::get_chunks() const { + return this->chunks; } } // namespace openage::renderer::terrain diff --git a/libopenage/renderer/stages/terrain/terrain_model.h b/libopenage/renderer/stages/terrain/terrain_model.h index 4a1ec15aed..9b94213a12 100644 --- a/libopenage/renderer/stages/terrain/terrain_model.h +++ b/libopenage/renderer/stages/terrain/terrain_model.h @@ -21,6 +21,7 @@ class AssetManager; namespace terrain { class TerrainRenderEntity; class TerrainRenderMesh; +class TerrainChunk; /** * 3D model of the whole terrain. Combines the individual meshes @@ -47,8 +48,10 @@ class TerrainRenderModel { /** * Fetch updates from the render entity. + * + * @param time Current simulation time. */ - void fetch_updates(); + void fetch_updates(const time::time_t &time = 0.0); /** * Update the uniforms of the renderable associated with this object. @@ -58,25 +61,17 @@ class TerrainRenderModel { void update_uniforms(const time::time_t &time = 0.0); /** - * Get the meshes composing the terrain. + * Get the chunks composing the terrain. * - * @return Vector of terrain meshes. + * @return Chunks of the terrain. */ - const std::vector> &get_meshes() const; + const std::vector> &get_chunks() const; private: /** - * Create a terrain mesh from the data provided by the render entity. - * - * @return New terrain mesh. - */ - std::shared_ptr create_mesh(); - - /** - * Meshes composing the terrain. Each mesh represents a drawable vertex surface - * and a texture. + * Chunks composing the terrain. */ - std::vector> meshes; + std::vector> chunks; /** * Camera for view and projection uniforms. @@ -87,12 +82,6 @@ class TerrainRenderModel { * Asset manager for central accessing and loading textures. */ std::shared_ptr asset_manager; - - /** - * Source for ingame terrain coordinates. These coordinates are translated into - * our render vertex mesh when \p update() is called. - */ - std::shared_ptr render_entity; }; } // namespace terrain diff --git a/libopenage/renderer/stages/terrain/terrain_renderer.cpp b/libopenage/renderer/stages/terrain/terrain_renderer.cpp index 62b7de8207..008600c19e 100644 --- a/libopenage/renderer/stages/terrain/terrain_renderer.cpp +++ b/libopenage/renderer/stages/terrain/terrain_renderer.cpp @@ -8,6 +8,7 @@ #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" #include "renderer/shader_program.h" +#include "renderer/stages/terrain/terrain_chunk.h" #include "renderer/stages/terrain/terrain_mesh.h" #include "renderer/stages/terrain/terrain_model.h" #include "renderer/window.h" @@ -56,26 +57,28 @@ void TerrainRenderer::set_render_entity(const std::shared_ptrmodel->fetch_updates(); auto current_time = this->clock->get_real_time(); - for (auto &mesh : this->model->get_meshes()) { - if (mesh->requires_renderable()) [[unlikely]] { /*probably doesn't happen that often?*/ - // TODO: Update uniforms and geometry individually, depending on what changed - // TODO: Update existing renderable instead of recreating it - auto geometry = this->renderer->add_mesh_geometry(mesh->get_mesh()); - auto transform_unifs = this->display_shader->create_empty_input(); - - Renderable display_obj{ - transform_unifs, - geometry, - true, - true, // it's a 3D object, so we need depth testing - }; - - // TODO: Remove old renderable instead of clearing everything - this->render_pass->clear_renderables(); - this->render_pass->add_renderables(display_obj); - mesh->clear_requires_renderable(); - - mesh->set_uniforms(transform_unifs); + for (auto &chunk : this->model->get_chunks()) { + for (auto &mesh : chunk->get_meshes()) { + if (mesh->requires_renderable()) [[unlikely]] { /*probably doesn't happen that often?*/ + // TODO: Update uniforms and geometry individually, depending on what changed + // TODO: Update existing renderable instead of recreating it + auto geometry = this->renderer->add_mesh_geometry(mesh->get_mesh()); + auto transform_unifs = this->display_shader->create_empty_input(); + + Renderable display_obj{ + transform_unifs, + geometry, + true, + true, // it's a 3D object, so we need depth testing + }; + + // TODO: Remove old renderable instead of clearing everything + this->render_pass->clear_renderables(); + this->render_pass->add_renderables(display_obj); + mesh->clear_requires_renderable(); + + mesh->set_uniforms(transform_unifs); + } } } this->model->update_uniforms(current_time); From 330f40e6c4a4b42ce47a33cd0eb6afa14b17cb25 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 5 Nov 2023 15:43:56 +0100 Subject: [PATCH 057/771] gamestate: Terrain chunks. --- libopenage/gamestate/terrain.cpp | 17 +++++- libopenage/gamestate/terrain.h | 24 +++++++-- libopenage/gamestate/terrain_chunk.cpp | 36 ++++++++++++- libopenage/gamestate/terrain_chunk.h | 53 +++++++++++++++++++ .../renderer/stages/terrain/terrain_chunk.h | 4 -- 5 files changed, 121 insertions(+), 13 deletions(-) diff --git a/libopenage/gamestate/terrain.cpp b/libopenage/gamestate/terrain.cpp index 247b457251..09a9d7cf6b 100644 --- a/libopenage/gamestate/terrain.cpp +++ b/libopenage/gamestate/terrain.cpp @@ -6,6 +6,7 @@ #include #include +#include "gamestate/terrain_chunk.h" #include "renderer/stages/terrain/terrain_render_entity.h" namespace openage::gamestate { @@ -25,7 +26,7 @@ Terrain::Terrain(const std::string &texture_path) : } } -void Terrain::push_to_render() { +void Terrain::render_update() { if (this->render_entity != nullptr) { this->render_entity->update(this->size, this->height_map, @@ -36,7 +37,19 @@ void Terrain::push_to_render() { void Terrain::set_render_entity(const std::shared_ptr &entity) { this->render_entity = entity; - this->push_to_render(); + this->render_update(); +} + +const std::vector> &Terrain::get_chunks() const { + return this->chunks; +} + +void Terrain::generate() { + auto chunk = std::make_shared(util::Vector2s{10, 10}, util::Vector2s{0, 0}); + chunk->set_render_entity(this->render_entity); + chunk->render_update(time::time_t::zero(), this->texture_path); + + this->chunks.push_back(chunk); } } // namespace openage::gamestate diff --git a/libopenage/gamestate/terrain.h b/libopenage/gamestate/terrain.h index 1e1aef9846..c1df66d415 100644 --- a/libopenage/gamestate/terrain.h +++ b/libopenage/gamestate/terrain.h @@ -14,6 +14,7 @@ class TerrainRenderEntity; } namespace gamestate { +class TerrainChunk; /** * Entity for managing the map terrain of a game. @@ -30,19 +31,32 @@ class Terrain { */ void set_render_entity(const std::shared_ptr &entity); + const std::vector> &get_chunks() const; + + // TODO: This should be an event + void generate(); + private: // test connection to renderer - void push_to_render(); + void render_update(); - // size of the map - // origin is the left corner - // x = top left edge; y = top right edge + /** + * Total size of the map + * origin is the left corner + * x = top left edge; y = top right edge + */ util::Vector2s size; + + /** + * Subdivision of the main terrain entity. + */ + std::vector> chunks; + + // ASDF: Move these members into terrain chunk // Heights of the terrain grid std::vector height_map; // path to a texture std::string texture_path; - // render entity for pushing updates to std::shared_ptr render_entity; }; diff --git a/libopenage/gamestate/terrain_chunk.cpp b/libopenage/gamestate/terrain_chunk.cpp index d434a7a921..1d5ef1f76f 100644 --- a/libopenage/gamestate/terrain_chunk.cpp +++ b/libopenage/gamestate/terrain_chunk.cpp @@ -1,8 +1,40 @@ -// Copyright 2018-2018 the openage authors. See copying.md for legal info. +// Copyright 2018-2023 the openage authors. See copying.md for legal info. #include "terrain_chunk.h" namespace openage::gamestate { -} // openage::gamestate +TerrainChunk::TerrainChunk(const util::Vector2s size, + const util::Vector2s offset) : + size{size}, + offset{offset}, + height_map{} { + if (this->size[0] > MAX_CHUNK_WIDTH || this->size[1] > MAX_CHUNK_HEIGHT) { + throw Error(MSG(err) << "Terrain chunk size exceeds maximum size: " + << this->size[0] << "x" << this->size[1] << " > " + << MAX_CHUNK_WIDTH << "x" << MAX_CHUNK_HEIGHT); + } + + // fill the terrain grid with height values + this->height_map.reserve(this->size[0] * this->size[1]); + for (size_t i = 0; i < this->size[0] * this->size[1]; ++i) { + this->height_map.push_back(0.0f); + } +} + +void TerrainChunk::set_render_entity(const std::shared_ptr &entity) { + this->render_entity = entity; +} + +void TerrainChunk::render_update(const time::time_t &time, + const std::string &terrain_path) { + if (this->render_entity != nullptr) { + this->render_entity->update(this->size, + this->height_map, + terrain_path, + time); + } +} + +} // namespace openage::gamestate diff --git a/libopenage/gamestate/terrain_chunk.h b/libopenage/gamestate/terrain_chunk.h index 784aec7c9e..b4b2f2d48f 100644 --- a/libopenage/gamestate/terrain_chunk.h +++ b/libopenage/gamestate/terrain_chunk.h @@ -2,13 +2,66 @@ #pragma once +#include + +#include "renderer/stages/terrain/terrain_render_entity.h" +#include "time/time.h" +#include "util/vector.h" + namespace openage::gamestate { +const size_t MAX_CHUNK_WIDTH = 16; +const size_t MAX_CHUNK_HEIGHT = 16; + + /** * Subdivision of the main terrain entity. */ class TerrainChunk { +public: + TerrainChunk(const util::Vector2s size, + const util::Vector2s offset); + ~TerrainChunk() = default; + + /** + * Set the current render entity of the terrain. + * + * @param entity New render entity. + */ + void set_render_entity(const std::shared_ptr &entity); + + /** + * Update the render entity. + * + * @param time Simulation time of the update. + * @param terrain_path Path to the terrain definition used at \p time. + */ + void render_update(const time::time_t &time, + const std::string &terrain_path); + +private: + /** + * Size of the terrain chunk. + * Origin is the left corner. + * x = top left edge; y = top right edge. + */ + util::Vector2s size; + + /** + * Offset of the terrain chunk to the origin. + */ + util::Vector2s offset; + + /** + * Height map of the terrain chunk. + */ + std::vector height_map; + + /** + * Render entity for pushing updates to the renderer. Can be \p nullptr. + */ + std::shared_ptr render_entity; }; } // namespace openage::gamestate diff --git a/libopenage/renderer/stages/terrain/terrain_chunk.h b/libopenage/renderer/stages/terrain/terrain_chunk.h index 7158ea1a90..88e2d70bdc 100644 --- a/libopenage/renderer/stages/terrain/terrain_chunk.h +++ b/libopenage/renderer/stages/terrain/terrain_chunk.h @@ -20,10 +20,6 @@ namespace terrain { class TerrainRenderMesh; class TerrainRenderEntity; -const size_t MAX_CHUNK_WIDTH = 16; -const size_t MAX_CHUNK_HEIGHT = 16; - - class TerrainChunk { public: /** From 542ec29ed43e82f94f5efa059d58207fbd6a21a0 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 5 Nov 2023 16:47:38 +0100 Subject: [PATCH 058/771] gamestate: Move terrain render updates to terrain chunk. --- libopenage/gamestate/CMakeLists.txt | 1 + libopenage/gamestate/game.cpp | 19 +++++- libopenage/gamestate/game.h | 13 +++- libopenage/gamestate/game_entity.h | 2 +- libopenage/gamestate/game_state.cpp | 12 +++- libopenage/gamestate/game_state.h | 24 ++++++- libopenage/gamestate/simulation.cpp | 7 ++- libopenage/gamestate/simulation.h | 6 ++ libopenage/gamestate/terrain.cpp | 44 +++++-------- libopenage/gamestate/terrain.h | 53 +++++++++------- libopenage/gamestate/terrain_factory.cpp | 44 +++++++++++++ libopenage/gamestate/terrain_factory.h | 79 ++++++++++++++++++++++++ libopenage/gamestate/universe.cpp | 7 --- libopenage/gamestate/universe.h | 2 + 14 files changed, 245 insertions(+), 68 deletions(-) create mode 100644 libopenage/gamestate/terrain_factory.cpp create mode 100644 libopenage/gamestate/terrain_factory.h diff --git a/libopenage/gamestate/CMakeLists.txt b/libopenage/gamestate/CMakeLists.txt index dddf843136..c7cdaebee2 100644 --- a/libopenage/gamestate/CMakeLists.txt +++ b/libopenage/gamestate/CMakeLists.txt @@ -8,6 +8,7 @@ add_sources(libopenage player.cpp simulation.cpp terrain_chunk.cpp + terrain_factory.cpp terrain.cpp types.cpp world.cpp diff --git a/libopenage/gamestate/game.cpp b/libopenage/gamestate/game.cpp index d31310945d..dae9f02528 100644 --- a/libopenage/gamestate/game.cpp +++ b/libopenage/gamestate/game.cpp @@ -13,6 +13,8 @@ #include "assets/modpack.h" #include "gamestate/entity_factory.h" #include "gamestate/game_state.h" +#include "gamestate/terrain.h" +#include "gamestate/terrain_factory.h" #include "gamestate/universe.h" #include "util/path.h" #include "util/strings.h" @@ -22,7 +24,8 @@ namespace openage::gamestate { Game::Game(const std::shared_ptr &event_loop, const std::shared_ptr &mod_manager, - const std::shared_ptr &entity_factory) : + const std::shared_ptr &entity_factory, + const std::shared_ptr &terrain_factory) : db{nyan::Database::create()}, state{std::make_shared(this->db, event_loop)}, universe{std::make_shared(state)} { @@ -39,6 +42,8 @@ Game::Game(const std::shared_ptr &event_loop, // This can be removed when we spawn based on game logic rather than // hardcoded entity types. this->state->set_mod_manager(mod_manager); + + this->generate_terrain(terrain_factory); } const std::shared_ptr &Game::get_state() const { @@ -47,6 +52,7 @@ const std::shared_ptr &Game::get_state() const { void Game::attach_renderer(const std::shared_ptr &render_factory) { this->universe->attach_renderer(render_factory); + this->state->get_terrain()->attach_renderer(render_factory); } void Game::load_data(const std::shared_ptr &mod_manager) { @@ -119,4 +125,15 @@ void Game::load_path(const util::Path &base_dir, } } +void Game::generate_terrain(const std::shared_ptr &terrain_factory) { + auto terrain = terrain_factory->add_terrain(); + + auto chunk0 = terrain_factory->add_chunk(util::Vector2s{10, 10}, util::Vector2s{0, 0}); + auto chunk1 = terrain_factory->add_chunk(util::Vector2s{10, 10}, util::Vector2s{10, 0}); + terrain->add_chunk(chunk0); + terrain->add_chunk(chunk1); + + this->state->set_terrain(terrain); +} + } // namespace openage::gamestate diff --git a/libopenage/gamestate/game.h b/libopenage/gamestate/game.h index aecfd3bd72..139c4e87fa 100644 --- a/libopenage/gamestate/game.h +++ b/libopenage/gamestate/game.h @@ -30,6 +30,7 @@ class Path; namespace gamestate { class GameState; class EntityFactory; +class TerrainFactory; class Universe; /** @@ -53,7 +54,8 @@ class Game { */ Game(const std::shared_ptr &event_loop, const std::shared_ptr &mod_manager, - const std::shared_ptr &entity_factory); + const std::shared_ptr &entity_factory, + const std::shared_ptr &terrain_factory); ~Game() = default; /** @@ -93,6 +95,15 @@ class Game { const std::string &search, bool recursive = false); + /** + * Generate the terrain for the current game. + * + * TODO: Use a real map generator. + * + * @param terrain_factory Factory for creating terrain objects. + */ + void generate_terrain(const std::shared_ptr &terrain_factory); + /** * Nyan game data database. */ diff --git a/libopenage/gamestate/game_entity.h b/libopenage/gamestate/game_entity.h index a79f167e7f..d4b61202e9 100644 --- a/libopenage/gamestate/game_entity.h +++ b/libopenage/gamestate/game_entity.h @@ -103,7 +103,7 @@ class GameEntity { * Update the render entity. * * @param time Simulation time of the update. - * @param animation_path Animation path used at \p time. + * @param animation_path Path to the animation definition used at \p time. */ void render_update(const time::time_t &time, const std::string &animation_path); diff --git a/libopenage/gamestate/game_state.cpp b/libopenage/gamestate/game_state.cpp index 9cda363379..fba3826779 100644 --- a/libopenage/gamestate/game_state.cpp +++ b/libopenage/gamestate/game_state.cpp @@ -23,20 +23,24 @@ const std::shared_ptr &GameState::get_db_view() { return this->db_view; } -void GameState::add_game_entity(const std::shared_ptr &entity) { +void GameState::add_game_entity(const std::shared_ptr entity) { if (this->game_entities.contains(entity->get_id())) [[unlikely]] { throw Error(MSG(err) << "Game entity with ID " << entity->get_id() << " already exists"); } this->game_entities[entity->get_id()] = entity; } -void GameState::add_player(const std::shared_ptr &player) { +void GameState::add_player(const std::shared_ptr player) { if (this->players.contains(player->get_id())) [[unlikely]] { throw Error(MSG(err) << "Player with ID " << player->get_id() << " already exists"); } this->players[player->get_id()] = player; } +void GameState::set_terrain(const std::shared_ptr terrain) { + this->terrain = terrain; +} + const std::shared_ptr &GameState::get_game_entity(entity_id_t id) const { if (!this->game_entities.contains(id)) [[unlikely]] { throw Error(MSG(err) << "Game entity with ID " << id << " does not exist"); @@ -55,6 +59,10 @@ const std::shared_ptr &GameState::get_player(player_id_t id) const { return this->players.at(id); } +const std::shared_ptr &GameState::get_terrain() const { + return this->terrain; +} + 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 d252acaadc..6cce1b09b1 100644 --- a/libopenage/gamestate/game_state.h +++ b/libopenage/gamestate/game_state.h @@ -27,6 +27,7 @@ class EventLoop; namespace gamestate { class GameEntity; class Player; +class Terrain; /** * State of the game. @@ -59,14 +60,21 @@ class GameState : public openage::event::State { * * @param entity New game entity. */ - void add_game_entity(const std::shared_ptr &entity); + void add_game_entity(const std::shared_ptr entity); /** * Add a new player to the index. * * @param player New player. */ - void add_player(const std::shared_ptr &player); + void add_player(const std::shared_ptr player); + + /** + * Set the terrain of the current game. + * + * @param terrain Terrain object. + */ + void set_terrain(const std::shared_ptr terrain); /** * Get a game entity by its ID. @@ -93,6 +101,13 @@ class GameState : public openage::event::State { */ const std::shared_ptr &get_player(player_id_t id) const; + /** + * Get the terrain of the current game. + * + * @return Terrain object. + */ + const std::shared_ptr &get_terrain() const; + /** * TODO: Only for testing. */ @@ -115,6 +130,11 @@ class GameState : public openage::event::State { */ std::unordered_map> players; + /** + * Terrain of the current game. + */ + std::shared_ptr terrain; + /** * TODO: Only for testing */ diff --git a/libopenage/gamestate/simulation.cpp b/libopenage/gamestate/simulation.cpp index c925888815..4af77d2815 100644 --- a/libopenage/gamestate/simulation.cpp +++ b/libopenage/gamestate/simulation.cpp @@ -9,6 +9,7 @@ #include "gamestate/event/send_command.h" #include "gamestate/event/spawn_entity.h" #include "gamestate/event/wait.h" +#include "gamestate/terrain_factory.h" #include "time/clock.h" #include "time/time_loop.h" @@ -27,6 +28,7 @@ GameSimulation::GameSimulation(const util::Path &root_dir, time_loop{time_loop}, event_loop{std::make_shared()}, entity_factory{std::make_shared()}, + terrain_factory{std::make_shared()}, mod_manager{std::make_shared(this->root_dir / "assets" / "converted")}, spawner{std::make_shared(this->event_loop)}, commander{std::make_shared(this->event_loop)} { @@ -54,9 +56,11 @@ void GameSimulation::start() { this->init_event_handlers(); + // TODO: wait for presenter to initialize before starting? this->game = std::make_shared(event_loop, this->mod_manager, - this->entity_factory); + this->entity_factory, + this->terrain_factory); this->running = true; @@ -114,6 +118,7 @@ void GameSimulation::attach_renderer(const std::shared_ptrgame->attach_renderer(render_factory); this->entity_factory->attach_renderer(render_factory); + this->terrain_factory->attach_renderer(render_factory); } void GameSimulation::set_modpacks(const std::vector &modpacks) { diff --git a/libopenage/gamestate/simulation.h b/libopenage/gamestate/simulation.h index aab81c524e..fe2de8257c 100644 --- a/libopenage/gamestate/simulation.h +++ b/libopenage/gamestate/simulation.h @@ -31,6 +31,7 @@ class TimeLoop; namespace gamestate { class EntityFactory; class Game; +class TerrainFactory; namespace event { class Commander; @@ -178,6 +179,11 @@ class GameSimulation final { */ std::shared_ptr entity_factory; + /** + * Factory for creating terrain. + */ + std::shared_ptr terrain_factory; + /** * Mod manager. */ diff --git a/libopenage/gamestate/terrain.cpp b/libopenage/gamestate/terrain.cpp index 09a9d7cf6b..2633d1a710 100644 --- a/libopenage/gamestate/terrain.cpp +++ b/libopenage/gamestate/terrain.cpp @@ -7,49 +7,33 @@ #include #include "gamestate/terrain_chunk.h" -#include "renderer/stages/terrain/terrain_render_entity.h" +#include "renderer/render_factory.h" + namespace openage::gamestate { -Terrain::Terrain(const std::string &texture_path) : +Terrain::Terrain() : size{0, 0}, - height_map{}, - texture_path{texture_path}, - render_entity{nullptr} { - // TODO: Actual terrain generation code - this->size = util::Vector2s{10, 10}; - - // fill the terrain grid with height values - this->height_map.reserve(this->size[0] * this->size[1]); - for (size_t i = 0; i < this->size[0] * this->size[1]; ++i) { - this->height_map.push_back(0.0f); - } -} - -void Terrain::render_update() { - if (this->render_entity != nullptr) { - this->render_entity->update(this->size, - this->height_map, - this->texture_path); - } + chunks{} { + // TODO: Get actual size of terrain. } -void Terrain::set_render_entity(const std::shared_ptr &entity) { - this->render_entity = entity; - - this->render_update(); +void Terrain::add_chunk(const std::shared_ptr chunk) { + this->chunks.push_back(chunk); } const std::vector> &Terrain::get_chunks() const { return this->chunks; } -void Terrain::generate() { - auto chunk = std::make_shared(util::Vector2s{10, 10}, util::Vector2s{0, 0}); - chunk->set_render_entity(this->render_entity); - chunk->render_update(time::time_t::zero(), this->texture_path); +void Terrain::attach_renderer(const std::shared_ptr &render_factory) { + for (auto &chunk : this->get_chunks()) { + auto render_entity = render_factory->add_terrain_render_entity(); + chunk->set_render_entity(render_entity); - this->chunks.push_back(chunk); + chunk->render_update(time::time_t::zero(), + "../test/textures/test_terrain.terrain"); + } } } // namespace openage::gamestate diff --git a/libopenage/gamestate/terrain.h b/libopenage/gamestate/terrain.h index c1df66d415..71f0bd63d7 100644 --- a/libopenage/gamestate/terrain.h +++ b/libopenage/gamestate/terrain.h @@ -9,9 +9,9 @@ #include "util/vector.h" namespace openage { -namespace renderer::terrain { -class TerrainRenderEntity; -} +namespace renderer { +class RenderFactory; +} // namespace renderer namespace gamestate { class TerrainChunk; @@ -21,27 +21,42 @@ class TerrainChunk; */ class Terrain { public: - Terrain(const std::string &texture_path); + /** + * Create a new terrain. + */ + Terrain(); ~Terrain() = default; /** - * Set the current render entity of the terrain. - * - * @param entity New render entity. - */ - void set_render_entity(const std::shared_ptr &entity); + * Add a chunk to the terrain. + * + * @param chunk New chunk. + */ + void add_chunk(const std::shared_ptr chunk); + /** + * Get the chunks of the terrain. + * + * @return Terrain chunks. + */ const std::vector> &get_chunks() const; - // TODO: This should be an event - void generate(); + /** + * Attach a renderer which enables graphical display. + * + * TODO: We currently have to do attach this here too in addition to the terrain + * factory because the renderer gets attached AFTER the terrain is + * already created. In the future, the game should wait for the renderer + * before creating the terrain. + * + * @param render_factory Factory for creating connector objects for gamestate->renderer + * communication. + */ + void attach_renderer(const std::shared_ptr &render_factory); private: - // test connection to renderer - void render_update(); - /** - * Total size of the map + * Total size of the map * origin is the left corner * x = top left edge; y = top right edge */ @@ -51,14 +66,6 @@ class Terrain { * Subdivision of the main terrain entity. */ std::vector> chunks; - - // ASDF: Move these members into terrain chunk - // Heights of the terrain grid - std::vector height_map; - // path to a texture - std::string texture_path; - // render entity for pushing updates to - std::shared_ptr render_entity; }; } // namespace gamestate diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp new file mode 100644 index 0000000000..e7a0d284a0 --- /dev/null +++ b/libopenage/gamestate/terrain_factory.cpp @@ -0,0 +1,44 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "terrain_factory.h" + +#include + +#include "gamestate/terrain.h" +#include "gamestate/terrain_chunk.h" +#include "renderer/render_factory.h" +#include "renderer/stages/terrain/terrain_render_entity.h" +#include "time/time.h" + + +namespace openage::gamestate { + +std::shared_ptr TerrainFactory::add_terrain() { + // TODO: Replace this with a proper terrain generator. + auto terrain = std::make_shared(); + + return terrain; +} + +std::shared_ptr TerrainFactory::add_chunk(const util::Vector2s size, + const util::Vector2s offset) { + auto chunk = std::make_shared(size, offset); + + if (this->render_factory) { + auto render_entity = this->render_factory->add_terrain_render_entity(); + chunk->set_render_entity(render_entity); + + chunk->render_update(time::time_t::zero(), + "../test/textures/test_terrain.terrain"); + } + + return chunk; +} + +void TerrainFactory::attach_renderer(const std::shared_ptr &render_factory) { + std::unique_lock lock{this->mutex}; + + this->render_factory = render_factory; +} + +} // namespace openage::gamestate diff --git a/libopenage/gamestate/terrain_factory.h b/libopenage/gamestate/terrain_factory.h new file mode 100644 index 0000000000..a60456819b --- /dev/null +++ b/libopenage/gamestate/terrain_factory.h @@ -0,0 +1,79 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "util/vector.h" + + +namespace openage { + +namespace renderer { +class RenderFactory; +} + +namespace gamestate { +class GameState; +class Terrain; +class TerrainChunk; + +/** + * Creates terrain data (tiles, chunks, etc.) to generate a map. + */ +class TerrainFactory { +public: + /** + * Create a new terrain factory. + */ + TerrainFactory() = default; + ~TerrainFactory() = default; + + /** + * Create a new empty terrain object. + * + * @return New terrain object. + */ + std::shared_ptr add_terrain(); + + /** + * Create a new empty terrain chunk. + * + * @param size Size of the chunk. + * @param offset Offset of the chunk. + * + * @return New terrain chunk. + */ + std::shared_ptr add_chunk(const util::Vector2s size, + const util::Vector2s offset); + + // TODO: Add tiles + // std::shared_ptr add_tile(const std::shared_ptr &loop, + // const std::shared_ptr &state, + // const nyan::fqon_t &nyan_entity); + + /** + * Attach a render factory for graphical display. + * + * This enables rendering for all created terrain chunks. + * + * @param render_factory Factory for creating connector objects for gamestate->renderer + * communication. + */ + void attach_renderer(const std::shared_ptr &render_factory); + +private: + /** + * Factory for creating connector objects to the renderer which make game entities displayable. + */ + std::shared_ptr render_factory; + + /** + * Mutex for thread safety. + */ + std::shared_mutex mutex; +}; + +} // namespace gamestate +} // namespace openage diff --git a/libopenage/gamestate/universe.cpp b/libopenage/gamestate/universe.cpp index 6299296734..cf44782e7d 100644 --- a/libopenage/gamestate/universe.cpp +++ b/libopenage/gamestate/universe.cpp @@ -10,9 +10,6 @@ namespace openage::gamestate { Universe::Universe(const std::shared_ptr &state) : world{std::make_shared(state)} { - // TODO - auto texpath = "../test/textures/test_terrain.terrain"; - this->terrain = std::make_shared(texpath); } std::shared_ptr Universe::get_world() { @@ -26,10 +23,6 @@ std::shared_ptr Universe::get_terrain() { void Universe::attach_renderer(const std::shared_ptr &render_factory) { this->render_factory = render_factory; - // TODO: Notify entities somwhere else? - auto terrain_render_entity = this->render_factory->add_terrain_render_entity(); - this->terrain->set_render_entity(terrain_render_entity); - this->world->attach_renderer(render_factory); } diff --git a/libopenage/gamestate/universe.h b/libopenage/gamestate/universe.h index ed77f84095..7e33a83765 100644 --- a/libopenage/gamestate/universe.h +++ b/libopenage/gamestate/universe.h @@ -18,6 +18,8 @@ class World; /** * Entity for managing the "physical" game world entities (units, buildings, etc.) as well as * conceptual entities (players, resources, ...). + * + * TODO: Remove Universe and other subclasses. */ class Universe { public: From c1e73d0f8bc094050c7721108c172f38305b4c92 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 5 Nov 2023 17:45:35 +0100 Subject: [PATCH 059/771] renderer: Render individual terrain chunks. --- libopenage/gamestate/terrain.cpp | 3 ++- libopenage/gamestate/terrain_chunk.cpp | 8 ++++++++ libopenage/gamestate/terrain_chunk.h | 14 ++++++++++++++ libopenage/gamestate/terrain_factory.cpp | 2 +- libopenage/renderer/demo/demo_3.cpp | 7 ++++--- libopenage/renderer/demo/stresstest_0.cpp | 7 ++++--- libopenage/renderer/render_factory.cpp | 5 +++-- libopenage/renderer/render_factory.h | 12 +++++++++++- .../renderer/stages/terrain/terrain_chunk.cpp | 11 ++++++++++- .../renderer/stages/terrain/terrain_chunk.h | 16 ++++++++++++++++ .../renderer/stages/terrain/terrain_mesh.cpp | 9 +++++---- .../renderer/stages/terrain/terrain_mesh.h | 14 ++++++++++---- .../renderer/stages/terrain/terrain_model.cpp | 7 ++++--- .../renderer/stages/terrain/terrain_model.h | 11 ++++++++--- .../renderer/stages/terrain/terrain_renderer.cpp | 7 ++++--- .../renderer/stages/terrain/terrain_renderer.h | 10 ++++++++-- 16 files changed, 112 insertions(+), 31 deletions(-) diff --git a/libopenage/gamestate/terrain.cpp b/libopenage/gamestate/terrain.cpp index 2633d1a710..f2b790e0a7 100644 --- a/libopenage/gamestate/terrain.cpp +++ b/libopenage/gamestate/terrain.cpp @@ -28,7 +28,8 @@ const std::vector> &Terrain::get_chunks() const { void Terrain::attach_renderer(const std::shared_ptr &render_factory) { for (auto &chunk : this->get_chunks()) { - auto render_entity = render_factory->add_terrain_render_entity(); + auto render_entity = render_factory->add_terrain_render_entity(chunk->get_size(), + chunk->get_offset()); chunk->set_render_entity(render_entity); chunk->render_update(time::time_t::zero(), diff --git a/libopenage/gamestate/terrain_chunk.cpp b/libopenage/gamestate/terrain_chunk.cpp index 1d5ef1f76f..8053d7e324 100644 --- a/libopenage/gamestate/terrain_chunk.cpp +++ b/libopenage/gamestate/terrain_chunk.cpp @@ -37,4 +37,12 @@ void TerrainChunk::render_update(const time::time_t &time, } } +const util::Vector2s &TerrainChunk::get_size() const { + return this->size; +} + +const util::Vector2s &TerrainChunk::get_offset() const { + return this->offset; +} + } // namespace openage::gamestate diff --git a/libopenage/gamestate/terrain_chunk.h b/libopenage/gamestate/terrain_chunk.h index b4b2f2d48f..6bc9c7ba64 100644 --- a/libopenage/gamestate/terrain_chunk.h +++ b/libopenage/gamestate/terrain_chunk.h @@ -40,6 +40,20 @@ class TerrainChunk { void render_update(const time::time_t &time, const std::string &terrain_path); + /** + * Get the size of this terrain chunk. + * + * @return Size of the terrain chunk (in tiles). + */ + const util::Vector2s &get_size() const; + + /** + * Get the offset of this terrain chunk to the terrain origin. + * + * @return Offset of the terrain chunk (in tiles). + */ + const util::Vector2s &get_offset() const; + private: /** * Size of the terrain chunk. diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp index e7a0d284a0..178bf6c541 100644 --- a/libopenage/gamestate/terrain_factory.cpp +++ b/libopenage/gamestate/terrain_factory.cpp @@ -25,7 +25,7 @@ std::shared_ptr TerrainFactory::add_chunk(const util::Vector2s siz auto chunk = std::make_shared(size, offset); if (this->render_factory) { - auto render_entity = this->render_factory->add_terrain_render_entity(); + auto render_entity = this->render_factory->add_terrain_render_entity(size, offset); chunk->set_render_entity(render_entity); chunk->render_update(time::time_t::zero(), diff --git a/libopenage/renderer/demo/demo_3.cpp b/libopenage/renderer/demo/demo_3.cpp index 6e0dc1f5ca..94aac2ff36 100644 --- a/libopenage/renderer/demo/demo_3.cpp +++ b/libopenage/renderer/demo/demo_3.cpp @@ -113,9 +113,6 @@ void renderer_demo_3(const util::Path &path) { // Create some entities to populate the scene auto render_factory = std::make_shared(terrain_renderer, world_renderer); - // Terrain - auto terrain0 = render_factory->add_terrain_render_entity(); - // Fill a 10x10 terrain grid with height values auto terrain_size = util::Vector2s{10, 10}; std::vector height_map{}; @@ -124,6 +121,10 @@ void renderer_demo_3(const util::Path &path) { height_map.push_back(0.0f); } + // Create entity for terrain rendering + auto terrain0 = render_factory->add_terrain_render_entity(terrain_size, + util::Vector2s{0, 0}); + // Create "test bumps" in the terrain to check if rendering works height_map[11] = 1.0f; height_map[23] = 2.3f; diff --git a/libopenage/renderer/demo/stresstest_0.cpp b/libopenage/renderer/demo/stresstest_0.cpp index 77b8f0f33b..b9f6c02a96 100644 --- a/libopenage/renderer/demo/stresstest_0.cpp +++ b/libopenage/renderer/demo/stresstest_0.cpp @@ -116,9 +116,6 @@ void renderer_stresstest_0(const util::Path &path) { // Create some entities to populate the scene auto render_factory = std::make_shared(terrain_renderer, world_renderer); - // Terrain - auto terrain0 = render_factory->add_terrain_render_entity(); - // Fill a 10x10 terrain grid with height values auto terrain_size = util::Vector2s{10, 10}; std::vector height_map{}; @@ -127,6 +124,10 @@ void renderer_stresstest_0(const util::Path &path) { height_map.push_back(0.0f); } + // Create entity for terrain rendering + auto terrain0 = render_factory->add_terrain_render_entity(terrain_size, + util::Vector2s{0, 0}); + // send the terrain data to the terrain renderer terrain0->update(terrain_size, height_map, diff --git a/libopenage/renderer/render_factory.cpp b/libopenage/renderer/render_factory.cpp index 463cb09d4a..6f42af5a67 100644 --- a/libopenage/renderer/render_factory.cpp +++ b/libopenage/renderer/render_factory.cpp @@ -14,9 +14,10 @@ RenderFactory::RenderFactory(const std::shared_ptr ter world_renderer{world_renderer} { } -std::shared_ptr RenderFactory::add_terrain_render_entity() { +std::shared_ptr RenderFactory::add_terrain_render_entity(const util::Vector2s chunk_size, + const util::Vector2s chunk_offset) { auto entity = std::make_shared(); - this->terrain_renderer->set_render_entity(entity); + this->terrain_renderer->add_render_entity(entity, chunk_size, chunk_offset); return entity; } diff --git a/libopenage/renderer/render_factory.h b/libopenage/renderer/render_factory.h index 84f2b6eb39..c90704bf5a 100644 --- a/libopenage/renderer/render_factory.h +++ b/libopenage/renderer/render_factory.h @@ -4,6 +4,9 @@ #include +#include "util/vector.h" + + namespace openage::renderer { namespace terrain { class TerrainRenderer; @@ -35,9 +38,16 @@ class RenderFactory { /** * Create a new terrain render entity and register it at the terrain renderer. * + * Render entities for terrain are associated with chunks, so a new render entity + * will result in the creation of a new chunk in the renderer. + * + * Size/offset of the chunk in the game simulation should match size/offset + * in the renderer. + * * @return Render entity for pushing terrain updates. */ - std::shared_ptr add_terrain_render_entity(); + std::shared_ptr add_terrain_render_entity(const util::Vector2s chunk_size, + const util::Vector2s chunk_offset); /** * Create a new world render entity and register it at the world renderer. diff --git a/libopenage/renderer/stages/terrain/terrain_chunk.cpp b/libopenage/renderer/stages/terrain/terrain_chunk.cpp index 73109cd2c4..95b48b4f64 100644 --- a/libopenage/renderer/stages/terrain/terrain_chunk.cpp +++ b/libopenage/renderer/stages/terrain/terrain_chunk.cpp @@ -34,6 +34,7 @@ void TerrainChunk::fetch_updates(const time::time_t &time) { // TODO: Change mesh instead of recreating it // TODO: Multiple meshes auto new_mesh = this->create_mesh(); + new_mesh->create_model_matrix(this->offset); this->meshes.clear(); this->meshes.push_back(new_mesh); @@ -109,7 +110,7 @@ std::shared_ptr TerrainChunk::create_mesh() { // Update textures auto tex_manager = this->asset_manager->get_texture_manager(); - // TODO: Support multiple textures per terrain + // TODO: Support multiple textures per chunk auto terrain_mesh = std::make_shared( this->asset_manager, @@ -119,4 +120,12 @@ std::shared_ptr TerrainChunk::create_mesh() { return terrain_mesh; } +util::Vector2s &TerrainChunk::get_size() { + return this->size; +} + +util::Vector2s &TerrainChunk::get_offset() { + return this->offset; +} + } // namespace openage::renderer::terrain diff --git a/libopenage/renderer/stages/terrain/terrain_chunk.h b/libopenage/renderer/stages/terrain/terrain_chunk.h index 88e2d70bdc..f9764615ba 100644 --- a/libopenage/renderer/stages/terrain/terrain_chunk.h +++ b/libopenage/renderer/stages/terrain/terrain_chunk.h @@ -37,6 +37,8 @@ class TerrainChunk { * Set the terrain render entity for vertex updates of this mesh. * * @param entity New terrain render entity. + * @param size Size of the chunk in tiles. + * @param offset Offset of the chunk from origin in tiles. */ void set_render_entity(const std::shared_ptr &entity); @@ -61,6 +63,20 @@ class TerrainChunk { */ const std::vector> &get_meshes() const; + /** + * Get the size of the chunk in tiles. + * + * @return Size of the chunk (in tiles). + */ + util::Vector2s &get_size(); + + /** + * Get the offset of the chunk from origin in tiles. + * + * @return Offset of the chunk (in tiles). + */ + util::Vector2s &get_offset(); + private: /** * Create a terrain mesh from the data provided by the render entity. diff --git a/libopenage/renderer/stages/terrain/terrain_mesh.cpp b/libopenage/renderer/stages/terrain/terrain_mesh.cpp index 1085e315d4..6d05836518 100644 --- a/libopenage/renderer/stages/terrain/terrain_mesh.cpp +++ b/libopenage/renderer/stages/terrain/terrain_mesh.cpp @@ -70,7 +70,7 @@ void TerrainRenderMesh::update_uniforms(const time::time_t &time) { } // local space -> world space - this->uniforms->update("model", this->get_model_matrix()); + this->uniforms->update("model", this->model_matrix); auto tex_info = this->terrain_info.get(time)->get_texture(0); auto tex_manager = this->asset_manager->get_texture_manager(); @@ -97,10 +97,11 @@ const std::shared_ptr &TerrainRenderMesh::get_uniforms() return this->uniforms; } -Eigen::Matrix4f TerrainRenderMesh::get_model_matrix() { +void TerrainRenderMesh::create_model_matrix(util::Vector2s &offset) { // TODO: Needs input from engine - auto transform = Eigen::Affine3f::Identity(); - return transform.matrix(); + auto model = Eigen::Affine3f::Identity(); + model.translate(Eigen::Vector3f{offset[0], offset[1], 0.0f}); + this->model_matrix = model.matrix(); } bool TerrainRenderMesh::is_changed() { diff --git a/libopenage/renderer/stages/terrain/terrain_mesh.h b/libopenage/renderer/stages/terrain/terrain_mesh.h index a571007204..f337853a10 100644 --- a/libopenage/renderer/stages/terrain/terrain_mesh.h +++ b/libopenage/renderer/stages/terrain/terrain_mesh.h @@ -10,6 +10,7 @@ #include "curve/discrete.h" #include "renderer/resources/mesh_data.h" #include "time/time.h" +#include "util/vector.h" namespace openage::renderer { @@ -110,11 +111,11 @@ class TerrainRenderMesh { void clear_requires_renderable(); /** - * Get the model transformation matrix for rendering. - * - * @return Model matrix. + * Create the model transformation matrix for rendering. + * + * @param offset Offset of the terrain mesh to the scene origin. */ - Eigen::Matrix4f get_model_matrix(); + void create_model_matrix(util::Vector2s &offset); /** * Check whether the mesh or texture were changed. @@ -159,6 +160,11 @@ class TerrainRenderMesh { * Pre-transformation vertices for the terrain model. */ renderer::resources::MeshData mesh; + + /** + * Transformation matrix for the terrain model. + */ + Eigen::Matrix4f model_matrix; }; } // namespace terrain } // namespace openage::renderer diff --git a/libopenage/renderer/stages/terrain/terrain_model.cpp b/libopenage/renderer/stages/terrain/terrain_model.cpp index f557934850..18f6fa31f8 100644 --- a/libopenage/renderer/stages/terrain/terrain_model.cpp +++ b/libopenage/renderer/stages/terrain/terrain_model.cpp @@ -26,9 +26,10 @@ TerrainRenderModel::TerrainRenderModel(const std::shared_ptr &entity) { - // ASDF: Set chunk size and offset from parameters - auto chunk = std::make_shared(this->asset_manager, util::Vector2s{10, 10}, util::Vector2s{0, 0}); +void TerrainRenderModel::add_chunk(const std::shared_ptr &entity, + const util::Vector2s size, + const util::Vector2s offset) { + auto chunk = std::make_shared(this->asset_manager, size, offset); chunk->set_render_entity(entity); chunk->fetch_updates(); diff --git a/libopenage/renderer/stages/terrain/terrain_model.h b/libopenage/renderer/stages/terrain/terrain_model.h index 9b94213a12..b2575c725d 100644 --- a/libopenage/renderer/stages/terrain/terrain_model.h +++ b/libopenage/renderer/stages/terrain/terrain_model.h @@ -6,6 +6,7 @@ #include #include "time/time.h" +#include "util/vector.h" namespace openage::renderer { @@ -33,11 +34,15 @@ class TerrainRenderModel { ~TerrainRenderModel() = default; /** - * Set the terrain render entity for vertex updates of this mesh. + * Add a new chunk to the terrain model. * - * @param entity New terrain render entity. + * @param entity Render entity of the chunk. + * @param chunk_size Size of the chunk in tiles. + * @param chunk_offset Offset of the chunk from origin in tiles. */ - void set_render_entity(const std::shared_ptr &entity); + void add_chunk(const std::shared_ptr &entity, + const util::Vector2s chunk_size, + const util::Vector2s chunk_offset); /** * Set the current camera of the scene. diff --git a/libopenage/renderer/stages/terrain/terrain_renderer.cpp b/libopenage/renderer/stages/terrain/terrain_renderer.cpp index 008600c19e..adf1c43496 100644 --- a/libopenage/renderer/stages/terrain/terrain_renderer.cpp +++ b/libopenage/renderer/stages/terrain/terrain_renderer.cpp @@ -46,11 +46,13 @@ std::shared_ptr TerrainRenderer::get_render_pass() { return this->render_pass; } -void TerrainRenderer::set_render_entity(const std::shared_ptr entity) { +void TerrainRenderer::add_render_entity(const std::shared_ptr entity, + const util::Vector2s chunk_size, + const util::Vector2s chunk_offset) { std::unique_lock lock{this->mutex}; this->render_entity = entity; - this->model->set_render_entity(this->render_entity); + this->model->add_chunk(this->render_entity, chunk_size, chunk_offset); this->update(); } @@ -73,7 +75,6 @@ void TerrainRenderer::update() { }; // TODO: Remove old renderable instead of clearing everything - this->render_pass->clear_renderables(); this->render_pass->add_renderables(display_obj); mesh->clear_requires_renderable(); diff --git a/libopenage/renderer/stages/terrain/terrain_renderer.h b/libopenage/renderer/stages/terrain/terrain_renderer.h index 286567c34a..94bfdc0c05 100644 --- a/libopenage/renderer/stages/terrain/terrain_renderer.h +++ b/libopenage/renderer/stages/terrain/terrain_renderer.h @@ -6,6 +6,8 @@ #include #include "util/path.h" +#include "util/vector.h" + namespace openage { @@ -54,11 +56,15 @@ class TerrainRenderer { std::shared_ptr get_render_pass(); /** - * Set the current render entity of the terrain renderer. + * Add a new render entity to the terrain renderer. + * + * This creates a new terrain chunk and add it to the model. * * @param render_entity New render entity. */ - void set_render_entity(const std::shared_ptr entity); + void add_render_entity(const std::shared_ptr entity, + const util::Vector2s chunk_size, + const util::Vector2s chunk_offset); /** * Update the terrain mesh and texture information. From ac89eac1dada1fc3bb04d49733ce9eed1f438b11 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 5 Nov 2023 18:24:10 +0100 Subject: [PATCH 060/771] terrain: Change chunk offsets to scene coordinate types. --- libopenage/coord/tile.cpp | 8 ++++++++ libopenage/coord/tile.h | 4 ++++ libopenage/gamestate/game.cpp | 5 +++-- libopenage/gamestate/terrain_chunk.cpp | 4 ++-- libopenage/gamestate/terrain_chunk.h | 7 ++++--- libopenage/gamestate/terrain_factory.cpp | 2 +- libopenage/gamestate/terrain_factory.h | 3 ++- libopenage/renderer/demo/demo_3.cpp | 4 +++- libopenage/renderer/demo/stresstest_0.cpp | 3 ++- libopenage/renderer/render_factory.cpp | 5 +++-- libopenage/renderer/render_factory.h | 3 ++- libopenage/renderer/stages/terrain/terrain_chunk.cpp | 4 ++-- libopenage/renderer/stages/terrain/terrain_chunk.h | 7 ++++--- libopenage/renderer/stages/terrain/terrain_mesh.cpp | 4 ++-- libopenage/renderer/stages/terrain/terrain_mesh.h | 3 ++- libopenage/renderer/stages/terrain/terrain_model.cpp | 2 +- libopenage/renderer/stages/terrain/terrain_model.h | 3 ++- .../renderer/stages/terrain/terrain_render_entity.h | 2 +- libopenage/renderer/stages/terrain/terrain_renderer.cpp | 2 +- libopenage/renderer/stages/terrain/terrain_renderer.h | 3 ++- 20 files changed, 51 insertions(+), 27 deletions(-) diff --git a/libopenage/coord/tile.cpp b/libopenage/coord/tile.cpp index b89f1e9578..2ccbcf0ce1 100644 --- a/libopenage/coord/tile.cpp +++ b/libopenage/coord/tile.cpp @@ -49,4 +49,12 @@ phys3 tile3::to_phys3() const { } +phys2_delta tile_delta::to_phys2() const { + return phys2_delta(this->ne, this->se); +} + +phys3_delta tile_delta::to_phys3(tile_t up) const { + return phys3_delta(this->ne, this->se, up); +} + } // namespace openage::coord diff --git a/libopenage/coord/tile.h b/libopenage/coord/tile.h index df08045e21..5cb3f2475d 100644 --- a/libopenage/coord/tile.h +++ b/libopenage/coord/tile.h @@ -22,6 +22,9 @@ namespace coord { struct tile_delta : CoordNeSeRelative { using CoordNeSeRelative::CoordNeSeRelative; + + phys2_delta to_phys2() const; + phys3_delta to_phys3(tile_t up = 0) const; }; struct tile : CoordNeSeAbsolute { @@ -48,6 +51,7 @@ struct tile3_delta : CoordNeSeUpRelative { constexpr tile_delta to_tile() const { return tile_delta{this->ne, this->se}; } + phys3_delta to_phys3() const; }; struct tile3 : CoordNeSeUpAbsolute { diff --git a/libopenage/gamestate/game.cpp b/libopenage/gamestate/game.cpp index dae9f02528..ae85a67171 100644 --- a/libopenage/gamestate/game.cpp +++ b/libopenage/gamestate/game.cpp @@ -19,6 +19,7 @@ #include "util/path.h" #include "util/strings.h" +#include "coord/tile.h" namespace openage::gamestate { @@ -128,8 +129,8 @@ void Game::load_path(const util::Path &base_dir, void Game::generate_terrain(const std::shared_ptr &terrain_factory) { auto terrain = terrain_factory->add_terrain(); - auto chunk0 = terrain_factory->add_chunk(util::Vector2s{10, 10}, util::Vector2s{0, 0}); - auto chunk1 = terrain_factory->add_chunk(util::Vector2s{10, 10}, util::Vector2s{10, 0}); + auto chunk0 = terrain_factory->add_chunk(util::Vector2s{10, 10}, coord::tile_delta{0, 0}); + auto chunk1 = terrain_factory->add_chunk(util::Vector2s{10, 10}, coord::tile_delta{10, 0}); terrain->add_chunk(chunk0); terrain->add_chunk(chunk1); diff --git a/libopenage/gamestate/terrain_chunk.cpp b/libopenage/gamestate/terrain_chunk.cpp index 8053d7e324..6ba3338d7b 100644 --- a/libopenage/gamestate/terrain_chunk.cpp +++ b/libopenage/gamestate/terrain_chunk.cpp @@ -6,7 +6,7 @@ namespace openage::gamestate { TerrainChunk::TerrainChunk(const util::Vector2s size, - const util::Vector2s offset) : + const coord::tile_delta offset) : size{size}, offset{offset}, height_map{} { @@ -41,7 +41,7 @@ const util::Vector2s &TerrainChunk::get_size() const { return this->size; } -const util::Vector2s &TerrainChunk::get_offset() const { +const coord::tile_delta &TerrainChunk::get_offset() const { return this->offset; } diff --git a/libopenage/gamestate/terrain_chunk.h b/libopenage/gamestate/terrain_chunk.h index 6bc9c7ba64..191ee8f185 100644 --- a/libopenage/gamestate/terrain_chunk.h +++ b/libopenage/gamestate/terrain_chunk.h @@ -4,6 +4,7 @@ #include +#include "coord/tile.h" #include "renderer/stages/terrain/terrain_render_entity.h" #include "time/time.h" #include "util/vector.h" @@ -21,7 +22,7 @@ const size_t MAX_CHUNK_HEIGHT = 16; class TerrainChunk { public: TerrainChunk(const util::Vector2s size, - const util::Vector2s offset); + const coord::tile_delta offset); ~TerrainChunk() = default; /** @@ -52,7 +53,7 @@ class TerrainChunk { * * @return Offset of the terrain chunk (in tiles). */ - const util::Vector2s &get_offset() const; + const coord::tile_delta &get_offset() const; private: /** @@ -65,7 +66,7 @@ class TerrainChunk { /** * Offset of the terrain chunk to the origin. */ - util::Vector2s offset; + coord::tile_delta offset; /** * Height map of the terrain chunk. diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp index 178bf6c541..57c2fe121c 100644 --- a/libopenage/gamestate/terrain_factory.cpp +++ b/libopenage/gamestate/terrain_factory.cpp @@ -21,7 +21,7 @@ std::shared_ptr TerrainFactory::add_terrain() { } std::shared_ptr TerrainFactory::add_chunk(const util::Vector2s size, - const util::Vector2s offset) { + const coord::tile_delta offset) { auto chunk = std::make_shared(size, offset); if (this->render_factory) { diff --git a/libopenage/gamestate/terrain_factory.h b/libopenage/gamestate/terrain_factory.h index a60456819b..88a86c4fe0 100644 --- a/libopenage/gamestate/terrain_factory.h +++ b/libopenage/gamestate/terrain_factory.h @@ -5,6 +5,7 @@ #include #include +#include "coord/tile.h" #include "util/vector.h" @@ -46,7 +47,7 @@ class TerrainFactory { * @return New terrain chunk. */ std::shared_ptr add_chunk(const util::Vector2s size, - const util::Vector2s offset); + const coord::tile_delta offset); // TODO: Add tiles // std::shared_ptr add_tile(const std::shared_ptr &loop, diff --git a/libopenage/renderer/demo/demo_3.cpp b/libopenage/renderer/demo/demo_3.cpp index 94aac2ff36..1cfec155b8 100644 --- a/libopenage/renderer/demo/demo_3.cpp +++ b/libopenage/renderer/demo/demo_3.cpp @@ -5,6 +5,7 @@ #include #include +#include "coord/tile.h" #include "renderer/camera/camera.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" @@ -21,6 +22,7 @@ #include "renderer/uniform_buffer.h" #include "time/clock.h" + namespace openage::renderer::tests { void renderer_demo_3(const util::Path &path) { @@ -123,7 +125,7 @@ void renderer_demo_3(const util::Path &path) { // Create entity for terrain rendering auto terrain0 = render_factory->add_terrain_render_entity(terrain_size, - util::Vector2s{0, 0}); + coord::tile_delta{0, 0}); // Create "test bumps" in the terrain to check if rendering works height_map[11] = 1.0f; diff --git a/libopenage/renderer/demo/stresstest_0.cpp b/libopenage/renderer/demo/stresstest_0.cpp index b9f6c02a96..39b0ba0931 100644 --- a/libopenage/renderer/demo/stresstest_0.cpp +++ b/libopenage/renderer/demo/stresstest_0.cpp @@ -4,6 +4,7 @@ #include +#include "coord/tile.h" #include "renderer/camera/camera.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" @@ -126,7 +127,7 @@ void renderer_stresstest_0(const util::Path &path) { // Create entity for terrain rendering auto terrain0 = render_factory->add_terrain_render_entity(terrain_size, - util::Vector2s{0, 0}); + coord::tile_delta{0, 0}); // send the terrain data to the terrain renderer terrain0->update(terrain_size, diff --git a/libopenage/renderer/render_factory.cpp b/libopenage/renderer/render_factory.cpp index 6f42af5a67..c40cc46508 100644 --- a/libopenage/renderer/render_factory.cpp +++ b/libopenage/renderer/render_factory.cpp @@ -2,6 +2,7 @@ #include "render_factory.h" +#include "coord/phys.h" #include "renderer/stages/terrain/terrain_render_entity.h" #include "renderer/stages/terrain/terrain_renderer.h" #include "renderer/stages/world/world_render_entity.h" @@ -15,9 +16,9 @@ RenderFactory::RenderFactory(const std::shared_ptr ter } std::shared_ptr RenderFactory::add_terrain_render_entity(const util::Vector2s chunk_size, - const util::Vector2s chunk_offset) { + const coord::tile_delta chunk_offset) { auto entity = std::make_shared(); - this->terrain_renderer->add_render_entity(entity, chunk_size, chunk_offset); + this->terrain_renderer->add_render_entity(entity, chunk_size, chunk_offset.to_phys2().to_scene2()); return entity; } diff --git a/libopenage/renderer/render_factory.h b/libopenage/renderer/render_factory.h index c90704bf5a..5a2a36de59 100644 --- a/libopenage/renderer/render_factory.h +++ b/libopenage/renderer/render_factory.h @@ -4,6 +4,7 @@ #include +#include "coord/tile.h" #include "util/vector.h" @@ -47,7 +48,7 @@ class RenderFactory { * @return Render entity for pushing terrain updates. */ std::shared_ptr add_terrain_render_entity(const util::Vector2s chunk_size, - const util::Vector2s chunk_offset); + const coord::tile_delta chunk_offset); /** * Create a new world render entity and register it at the world renderer. diff --git a/libopenage/renderer/stages/terrain/terrain_chunk.cpp b/libopenage/renderer/stages/terrain/terrain_chunk.cpp index 95b48b4f64..8ca1b8970a 100644 --- a/libopenage/renderer/stages/terrain/terrain_chunk.cpp +++ b/libopenage/renderer/stages/terrain/terrain_chunk.cpp @@ -12,7 +12,7 @@ namespace openage::renderer::terrain { TerrainChunk::TerrainChunk(const std::shared_ptr &asset_manager, const util::Vector2s size, - const util::Vector2s offset) : + const coord::scene2_delta offset) : size{size}, offset{offset}, asset_manager{asset_manager} {} @@ -124,7 +124,7 @@ util::Vector2s &TerrainChunk::get_size() { return this->size; } -util::Vector2s &TerrainChunk::get_offset() { +coord::scene2_delta &TerrainChunk::get_offset() { return this->offset; } diff --git a/libopenage/renderer/stages/terrain/terrain_chunk.h b/libopenage/renderer/stages/terrain/terrain_chunk.h index f9764615ba..4e96619865 100644 --- a/libopenage/renderer/stages/terrain/terrain_chunk.h +++ b/libopenage/renderer/stages/terrain/terrain_chunk.h @@ -6,6 +6,7 @@ #include #include +#include "coord/scene.h" #include "time/time.h" #include "util/vector.h" @@ -29,7 +30,7 @@ class TerrainChunk { */ TerrainChunk(const std::shared_ptr &asset_manager, const util::Vector2s size, - const util::Vector2s offset); + const coord::scene2_delta offset); ~TerrainChunk() = default; @@ -75,7 +76,7 @@ class TerrainChunk { * * @return Offset of the chunk (in tiles). */ - util::Vector2s &get_offset(); + coord::scene2_delta &get_offset(); private: /** @@ -93,7 +94,7 @@ class TerrainChunk { /** * Offset of the chunk from origin in tiles (x, y). */ - util::Vector2s offset; + coord::scene2_delta offset; /** * Meshes composing the terrain. Each mesh represents a drawable vertex surface diff --git a/libopenage/renderer/stages/terrain/terrain_mesh.cpp b/libopenage/renderer/stages/terrain/terrain_mesh.cpp index 6d05836518..88b471c22f 100644 --- a/libopenage/renderer/stages/terrain/terrain_mesh.cpp +++ b/libopenage/renderer/stages/terrain/terrain_mesh.cpp @@ -97,10 +97,10 @@ const std::shared_ptr &TerrainRenderMesh::get_uniforms() return this->uniforms; } -void TerrainRenderMesh::create_model_matrix(util::Vector2s &offset) { +void TerrainRenderMesh::create_model_matrix(const coord::scene2_delta &offset) { // TODO: Needs input from engine auto model = Eigen::Affine3f::Identity(); - model.translate(Eigen::Vector3f{offset[0], offset[1], 0.0f}); + model.translate(Eigen::Vector3f{offset.ne.to_float(), offset.se.to_float(), 0.0f}); this->model_matrix = model.matrix(); } diff --git a/libopenage/renderer/stages/terrain/terrain_mesh.h b/libopenage/renderer/stages/terrain/terrain_mesh.h index f337853a10..e2c7545ff8 100644 --- a/libopenage/renderer/stages/terrain/terrain_mesh.h +++ b/libopenage/renderer/stages/terrain/terrain_mesh.h @@ -7,6 +7,7 @@ #include +#include "coord/scene.h" #include "curve/discrete.h" #include "renderer/resources/mesh_data.h" #include "time/time.h" @@ -115,7 +116,7 @@ class TerrainRenderMesh { * * @param offset Offset of the terrain mesh to the scene origin. */ - void create_model_matrix(util::Vector2s &offset); + void create_model_matrix(const coord::scene2_delta &offset); /** * Check whether the mesh or texture were changed. diff --git a/libopenage/renderer/stages/terrain/terrain_model.cpp b/libopenage/renderer/stages/terrain/terrain_model.cpp index 18f6fa31f8..329968f858 100644 --- a/libopenage/renderer/stages/terrain/terrain_model.cpp +++ b/libopenage/renderer/stages/terrain/terrain_model.cpp @@ -28,7 +28,7 @@ TerrainRenderModel::TerrainRenderModel(const std::shared_ptr &entity, const util::Vector2s size, - const util::Vector2s offset) { + const coord::scene2_delta offset) { auto chunk = std::make_shared(this->asset_manager, size, offset); chunk->set_render_entity(entity); chunk->fetch_updates(); diff --git a/libopenage/renderer/stages/terrain/terrain_model.h b/libopenage/renderer/stages/terrain/terrain_model.h index b2575c725d..6677f3c4c5 100644 --- a/libopenage/renderer/stages/terrain/terrain_model.h +++ b/libopenage/renderer/stages/terrain/terrain_model.h @@ -5,6 +5,7 @@ #include #include +#include "coord/scene.h" #include "time/time.h" #include "util/vector.h" @@ -42,7 +43,7 @@ class TerrainRenderModel { */ void add_chunk(const std::shared_ptr &entity, const util::Vector2s chunk_size, - const util::Vector2s chunk_offset); + const coord::scene2_delta chunk_offset); /** * Set the current camera of the scene. diff --git a/libopenage/renderer/stages/terrain/terrain_render_entity.h b/libopenage/renderer/stages/terrain/terrain_render_entity.h index 2d074e2590..5e95896a11 100644 --- a/libopenage/renderer/stages/terrain/terrain_render_entity.h +++ b/libopenage/renderer/stages/terrain/terrain_render_entity.h @@ -81,7 +81,7 @@ class TerrainRenderEntity { bool changed; /** - * Terrain dimensions (width x height). + * Chunk dimensions (width x height). */ util::Vector2s size; diff --git a/libopenage/renderer/stages/terrain/terrain_renderer.cpp b/libopenage/renderer/stages/terrain/terrain_renderer.cpp index adf1c43496..92b5933d4f 100644 --- a/libopenage/renderer/stages/terrain/terrain_renderer.cpp +++ b/libopenage/renderer/stages/terrain/terrain_renderer.cpp @@ -48,7 +48,7 @@ std::shared_ptr TerrainRenderer::get_render_pass() { void TerrainRenderer::add_render_entity(const std::shared_ptr entity, const util::Vector2s chunk_size, - const util::Vector2s chunk_offset) { + const coord::scene2_delta chunk_offset) { std::unique_lock lock{this->mutex}; this->render_entity = entity; diff --git a/libopenage/renderer/stages/terrain/terrain_renderer.h b/libopenage/renderer/stages/terrain/terrain_renderer.h index 94bfdc0c05..4986509c95 100644 --- a/libopenage/renderer/stages/terrain/terrain_renderer.h +++ b/libopenage/renderer/stages/terrain/terrain_renderer.h @@ -5,6 +5,7 @@ #include #include +#include "coord/scene.h" #include "util/path.h" #include "util/vector.h" @@ -64,7 +65,7 @@ class TerrainRenderer { */ void add_render_entity(const std::shared_ptr entity, const util::Vector2s chunk_size, - const util::Vector2s chunk_offset); + const coord::scene2_delta chunk_offset); /** * Update the terrain mesh and texture information. From bd554c8f31fa83891eec173e7babb3bb3639ab74 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 5 Nov 2023 23:05:14 +0100 Subject: [PATCH 061/771] gamestate: Use terrain texture from modpacks is available. --- libopenage/gamestate/api/CMakeLists.txt | 1 + libopenage/gamestate/api/terrain.cpp | 22 +++++ libopenage/gamestate/api/terrain.h | 35 ++++++++ libopenage/gamestate/event/spawn_entity.cpp | 93 ++++++++++---------- libopenage/gamestate/game.cpp | 8 +- libopenage/gamestate/terrain_factory.cpp | 95 ++++++++++++++++++++- libopenage/gamestate/terrain_factory.h | 3 +- 7 files changed, 209 insertions(+), 48 deletions(-) create mode 100644 libopenage/gamestate/api/terrain.cpp create mode 100644 libopenage/gamestate/api/terrain.h diff --git a/libopenage/gamestate/api/CMakeLists.txt b/libopenage/gamestate/api/CMakeLists.txt index e235b05137..f079f11be3 100644 --- a/libopenage/gamestate/api/CMakeLists.txt +++ b/libopenage/gamestate/api/CMakeLists.txt @@ -6,6 +6,7 @@ add_sources(libopenage player_setup.cpp property.cpp sound.cpp + terrain.cpp types.cpp util.cpp ) diff --git a/libopenage/gamestate/api/terrain.cpp b/libopenage/gamestate/api/terrain.cpp new file mode 100644 index 0000000000..bfedb59e4c --- /dev/null +++ b/libopenage/gamestate/api/terrain.cpp @@ -0,0 +1,22 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "terrain.h" + +#include + + +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"; +} + +const std::string APITerrain::get_terrain_path(const nyan::Object &terrain) { + nyan::Object terrain_texture_obj = terrain.get_object("Terrain.terrain_graphic"); + std::string sprite_path = terrain_texture_obj.get_text("TerrainTexture.sprite"); + + return sprite_path; +} + +} // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/terrain.h b/libopenage/gamestate/api/terrain.h new file mode 100644 index 0000000000..ef4ca357a8 --- /dev/null +++ b/libopenage/gamestate/api/terrain.h @@ -0,0 +1,35 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include + + +namespace openage::gamestate::api { + +class APITerrain { +public: + /** + * Check if a nyan object is a terrain (type == \p engine.util.terrain.Terrain). + * + * @param obj nyan object handle. + * + * @return true if the object is a terrain, else false. + */ + static bool is_terrain(const nyan::Object &obj); + + /** + * Get the terrain path of a terrain. + * + * The path is relative to the directory the modpack is mounted in. + * + * @param terrain \p Terrain nyan object (type == \p engine.util.terrain.Terrain). + * + * @return Relative path to the terrain file. + */ + static const std::string get_terrain_path(const nyan::Object &terrain); +}; + +} // namespace openage::gamestate::api diff --git a/libopenage/gamestate/event/spawn_entity.cpp b/libopenage/gamestate/event/spawn_entity.cpp index 5cc1cf27df..0cea3cf631 100644 --- a/libopenage/gamestate/event/spawn_entity.cpp +++ b/libopenage/gamestate/event/spawn_entity.cpp @@ -76,6 +76,51 @@ static const std::vector trial_test_entities = { "trial_base.data.game_entity.generic.barracks.barracks.Barracks", }; +// TODO: Remove hardcoded test entity references +static std::vector test_entities; // declared static so we only have to do this once + + +void build_test_entities(const std::shared_ptr &gstate) { + auto modpack_ids = gstate->get_mod_manager()->get_load_order(); + for (auto &modpack_id : modpack_ids) { + if (modpack_id == "aoe1_base") { + test_entities.insert(test_entities.end(), + aoe1_test_entities.begin(), + aoe1_test_entities.end()); + } + else if (modpack_id == "de1_base") { + test_entities.insert(test_entities.end(), + de1_test_entities.begin(), + de1_test_entities.end()); + } + else if (modpack_id == "aoe2_base") { + test_entities.insert(test_entities.end(), + aoe2_test_entities.begin(), + aoe2_test_entities.end()); + } + else if (modpack_id == "de2_base") { + test_entities.insert(test_entities.end(), + de2_test_entities.begin(), + de2_test_entities.end()); + } + else if (modpack_id == "hd_base") { + test_entities.insert(test_entities.end(), + hd_test_entities.begin(), + hd_test_entities.end()); + } + else if (modpack_id == "swgb_base") { + test_entities.insert(test_entities.end(), + swgb_test_entities.begin(), + swgb_test_entities.end()); + } + else if (modpack_id == "trial_base") { + test_entities.insert(test_entities.end(), + trial_test_entities.begin(), + trial_test_entities.end()); + } + } +} + Spawner::Spawner(const std::shared_ptr &loop) : EventEntity(loop) { @@ -113,52 +158,14 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, auto game_entities = nyan_db->get_obj_children_all("engine.util.game_entity.GameEntity"); - // TODO: Remove hardcoded test entity references - static std::vector test_entities; // declared static so we only have to do this once if (test_entities.empty()) { - auto modpack_ids = gstate->get_mod_manager()->get_load_order(); - for (auto &modpack_id : modpack_ids) { - if (modpack_id == "aoe1_base") { - test_entities.insert(test_entities.end(), - aoe1_test_entities.begin(), - aoe1_test_entities.end()); - } - else if (modpack_id == "de1_base") { - test_entities.insert(test_entities.end(), - de1_test_entities.begin(), - de1_test_entities.end()); - } - else if (modpack_id == "aoe2_base") { - test_entities.insert(test_entities.end(), - aoe2_test_entities.begin(), - aoe2_test_entities.end()); - } - else if (modpack_id == "de2_base") { - test_entities.insert(test_entities.end(), - de2_test_entities.begin(), - de2_test_entities.end()); - } - else if (modpack_id == "hd_base") { - test_entities.insert(test_entities.end(), - hd_test_entities.begin(), - hd_test_entities.end()); - } - else if (modpack_id == "swgb_base") { - test_entities.insert(test_entities.end(), - swgb_test_entities.begin(), - swgb_test_entities.end()); - } - else if (modpack_id == "trial_base") { - test_entities.insert(test_entities.end(), - trial_test_entities.begin(), - trial_test_entities.end()); - } + build_test_entities(gstate); + + // Do nothing if there are no test entities + if (test_entities.empty()) { + return; } } - if (test_entities.empty()) { - // Do nothing because we don't have anything to spawn - return; - } static uint8_t index = 0; nyan::fqon_t nyan_entity = test_entities.at(index); diff --git a/libopenage/gamestate/game.cpp b/libopenage/gamestate/game.cpp index ae85a67171..dfe25b10b5 100644 --- a/libopenage/gamestate/game.cpp +++ b/libopenage/gamestate/game.cpp @@ -129,8 +129,12 @@ void Game::load_path(const util::Path &base_dir, void Game::generate_terrain(const std::shared_ptr &terrain_factory) { auto terrain = terrain_factory->add_terrain(); - auto chunk0 = terrain_factory->add_chunk(util::Vector2s{10, 10}, coord::tile_delta{0, 0}); - auto chunk1 = terrain_factory->add_chunk(util::Vector2s{10, 10}, coord::tile_delta{10, 0}); + auto chunk0 = terrain_factory->add_chunk(this->state, + util::Vector2s{10, 10}, + coord::tile_delta{0, 0}); + auto chunk1 = terrain_factory->add_chunk(this->state, + util::Vector2s{10, 10}, + coord::tile_delta{10, 0}); terrain->add_chunk(chunk0); terrain->add_chunk(chunk1); diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp index 57c2fe121c..2c78d7da83 100644 --- a/libopenage/gamestate/terrain_factory.cpp +++ b/libopenage/gamestate/terrain_factory.cpp @@ -4,15 +4,41 @@ #include +#include + +#include "gamestate/api/terrain.h" #include "gamestate/terrain.h" #include "gamestate/terrain_chunk.h" #include "renderer/render_factory.h" #include "renderer/stages/terrain/terrain_render_entity.h" #include "time/time.h" +#include "assets/mod_manager.h" +#include "gamestate/game_state.h" + namespace openage::gamestate { +static const std::vector aoe1_test_terrain = {}; +static const std::vector de1_test_terrain = {}; +static const std::vector aoe2_test_terrain = { + "aoe2_base.data.terrain.foundation.Foundation", + "aoe2_base.data.terrain.grass.Grass", + "aoe2_base.data.terrain.dirt.Dirt", +}; +static const std::vector de2_test_terrain = {}; +static const std::vector hd_test_terrain = { + "hd_base.data.terrain.foundation.Foundation", + "hd_base.data.terrain.grass.Grass", + "hd_base.data.terrain.dirt.Dirt", +}; +static const std::vector swgb_test_terrain = { + "swgb_base.data.terrain.desert0.Desert0", + "swgb_base.data.terrain.grass2.Grass2", + "swgb_base.data.terrain.foundation.Foundation", +}; +static const std::vector trial_test_terrain = {}; + std::shared_ptr TerrainFactory::add_terrain() { // TODO: Replace this with a proper terrain generator. auto terrain = std::make_shared(); @@ -20,7 +46,53 @@ std::shared_ptr TerrainFactory::add_terrain() { return terrain; } -std::shared_ptr TerrainFactory::add_chunk(const util::Vector2s size, +// TODO: Remove hardcoded test texture references +static std::vector test_terrains; // declare static so we only have to do this once + +void build_test_terrains(const std::shared_ptr &gstate) { + auto modpack_ids = gstate->get_mod_manager()->get_load_order(); + for (auto &modpack_id : modpack_ids) { + if (modpack_id == "aoe1_base") { + test_terrains.insert(test_terrains.end(), + aoe1_test_terrain.begin(), + aoe1_test_terrain.end()); + } + else if (modpack_id == "de1_base") { + test_terrains.insert(test_terrains.end(), + de1_test_terrain.begin(), + de1_test_terrain.end()); + } + else if (modpack_id == "aoe2_base") { + test_terrains.insert(test_terrains.end(), + aoe2_test_terrain.begin(), + aoe2_test_terrain.end()); + } + else if (modpack_id == "de2_base") { + test_terrains.insert(test_terrains.end(), + de2_test_terrain.begin(), + de2_test_terrain.end()); + } + else if (modpack_id == "hd_base") { + test_terrains.insert(test_terrains.end(), + hd_test_terrain.begin(), + hd_test_terrain.end()); + } + else if (modpack_id == "swgb_base") { + test_terrains.insert(test_terrains.end(), + swgb_test_terrain.begin(), + swgb_test_terrain.end()); + } + else if (modpack_id == "trial_base") { + test_terrains.insert(test_terrains.end(), + trial_test_terrain.begin(), + trial_test_terrain.end()); + } + } +} + + +std::shared_ptr TerrainFactory::add_chunk(const std::shared_ptr &gstate, + const util::Vector2s size, const coord::tile_delta offset) { auto chunk = std::make_shared(size, offset); @@ -28,8 +100,27 @@ std::shared_ptr TerrainFactory::add_chunk(const util::Vector2s siz auto render_entity = this->render_factory->add_terrain_render_entity(size, offset); chunk->set_render_entity(render_entity); + std::string test_texture_path = "../test/textures/test_terrain.terrain"; + + // TODO: Remove test texture references + static size_t test_terrain_index = 0; + if (test_terrains.empty()) { + build_test_terrains(gstate); + + // use one of the modpack terrain textures + if (not test_terrains.empty()) { + if (test_terrain_index >= test_terrains.size()) { + test_terrain_index = 0; + } + auto terrain_obj = gstate->get_db_view()->get_object(test_terrains[test_terrain_index]); + test_texture_path = api::APITerrain::get_terrain_path(terrain_obj); + + test_terrain_index += 1; + } + } + chunk->render_update(time::time_t::zero(), - "../test/textures/test_terrain.terrain"); + test_texture_path); } return chunk; diff --git a/libopenage/gamestate/terrain_factory.h b/libopenage/gamestate/terrain_factory.h index 88a86c4fe0..32f804257f 100644 --- a/libopenage/gamestate/terrain_factory.h +++ b/libopenage/gamestate/terrain_factory.h @@ -46,7 +46,8 @@ class TerrainFactory { * * @return New terrain chunk. */ - std::shared_ptr add_chunk(const util::Vector2s size, + std::shared_ptr add_chunk(const std::shared_ptr &gstate, + const util::Vector2s size, const coord::tile_delta offset); // TODO: Add tiles From 045c59355b7535e7d26a5b374d62435f39bef808 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 6 Nov 2023 01:11:48 +0100 Subject: [PATCH 062/771] assets: Use float values for scalefactor. --- assets/test/textures/test_animation.sprite | 2 +- assets/test/textures/test_missing.sprite | 2 +- assets/test/textures/test_terrain.terrain | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/test/textures/test_animation.sprite b/assets/test/textures/test_animation.sprite index 26481ff896..9f96f91b65 100644 --- a/assets/test/textures/test_animation.sprite +++ b/assets/test/textures/test_animation.sprite @@ -5,7 +5,7 @@ version 2 texture 0 "test_texture.texture" -scalefactor 1 +scalefactor 1.0 layer 0 mode=loop position=20 time_per_frame=0.125 diff --git a/assets/test/textures/test_missing.sprite b/assets/test/textures/test_missing.sprite index 00a166e27f..84b7b66676 100644 --- a/assets/test/textures/test_missing.sprite +++ b/assets/test/textures/test_missing.sprite @@ -5,7 +5,7 @@ version 2 texture 0 "test_missing.texture" -scalefactor 1 +scalefactor 1.0 layer 0 mode=once diff --git a/assets/test/textures/test_terrain.terrain b/assets/test/textures/test_terrain.terrain index bbcc8dcd7c..7588a69194 100644 --- a/assets/test/textures/test_terrain.terrain +++ b/assets/test/textures/test_terrain.terrain @@ -5,7 +5,7 @@ version 2 texture 0 "test_terrain.texture" -scalefactor 1 +scalefactor 1.0 layer 0 From 7fd1a6edf1806c7e9bc5a940573d07c72ca0abbd Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 6 Nov 2023 01:12:16 +0100 Subject: [PATCH 063/771] convert: Export .terrain definitions. --- .../export/formats/terrain_metadata.py | 15 ++- .../entity_object/export/metadata_export.py | 97 ++++++++++++++++++- .../conversion/aoc/media_subprocessor.py | 48 ++++++++- .../conversion/hd/media_subprocessor.py | 51 +++++++++- 4 files changed, 195 insertions(+), 16 deletions(-) diff --git a/openage/convert/entity_object/export/formats/terrain_metadata.py b/openage/convert/entity_object/export/formats/terrain_metadata.py index 956b44d11e..46ba96b259 100644 --- a/openage/convert/entity_object/export/formats/terrain_metadata.py +++ b/openage/convert/entity_object/export/formats/terrain_metadata.py @@ -158,7 +158,9 @@ def dump(self) -> str: output_str += "\n" # blendtable reference - output_str += f"blendtable {self.blendtable['table_id']} {self.blendtable['filename']}\n\n" + if self.blendtable: + output_str += (f"blendtable {self.blendtable['table_id']} " + "{self.blendtable['filename']}\n\n") # scale factor output_str += f"scalefactor {self.scalefactor}\n\n" @@ -185,7 +187,16 @@ def dump(self) -> str: # frame definitions for frame in self.frames: - output_str += f'frame {" ".join(str(param) for param in frame.values())}\n' + frame_attributes = list(frame.values()) + output_str += f'frame {" ".join(str(param) for param in frame_attributes[:4])}' + + if frame["priority"]: + output_str += f" priority={frame['priority']}" + + if frame["blend_mode"]: + output_str += f" blend_mode={frame['blend_mode']}" + + output_str += "\n" return output_str diff --git a/openage/convert/entity_object/export/metadata_export.py b/openage/convert/entity_object/export/metadata_export.py index a4d2fd35ed..b07b8da2e3 100644 --- a/openage/convert/entity_object/export/metadata_export.py +++ b/openage/convert/entity_object/export/metadata_export.py @@ -11,10 +11,12 @@ from ....util.observer import Observer from .formats.sprite_metadata import SpriteMetadata from .formats.texture_metadata import TextureMetadata +from .formats.terrain_metadata import TerrainMetadata if typing.TYPE_CHECKING: from openage.util.observer import Observable - from openage.convert.entity_object.export.formats.sprite_metadata import LayerMode + from openage.convert.entity_object.export.formats.sprite_metadata import LayerMode as SpriteLayerMode + from openage.convert.entity_object.export.formats.terrain_metadata import LayerMode as TerrainLayerMode class MetadataExport(Observer): @@ -50,7 +52,7 @@ def add_graphics_metadata( self, img_filename: str, tex_filename: str, - layer_mode: LayerMode, + layer_mode: SpriteLayerMode, layer_pos: int, frame_rate: float, replay_delay: float, @@ -62,12 +64,28 @@ def add_graphics_metadata( """ Add metadata from the GenieGraphic object. + :param img_filename: Filename of the exported PNG file. :param tex_filename: Filename of the .texture file. + :param layer_mode: Animation mode (off, once, loop). + :param layer_pos: Layer position. + :param frame_rate: Time spent on each frame. + :param replay_delay: Time delay before replaying the animation. + :param frame_count: Number of frames per angle in the animation. + :param angle_count: Number of angles in the animation. + :param mirror_mode: Mirroring mode (0, 1). If 1, angles above 180 degrees are mirrored. :param start_angle: Angle used for the first frame in the .texture file. """ - self.graphics_metadata[img_filename] = (tex_filename, layer_mode, layer_pos, frame_rate, - replay_delay, frame_count, angle_count, mirror_mode, - start_angle) + self.graphics_metadata[img_filename] = ( + tex_filename, + layer_mode, + layer_pos, + frame_rate, + replay_delay, + frame_count, + angle_count, + mirror_mode, + start_angle + ) def dump(self) -> str: """ @@ -191,3 +209,72 @@ def update(self, observable: Observable, message: dict = None): texture_metadata = message[self.imagefile] self.size = texture_metadata["size"] self.subtex_metadata = texture_metadata["subtex_metadata"] + + +class TerrainMetadataExport(MetadataExport): + """ + Export requests for texture definition files. + """ + + def __init__(self, targetdir, target_filename): + super().__init__(targetdir, target_filename) + + self.graphics_metadata: dict[int, tuple] = {} + self.subtex_count: dict[str, int] = {} + + def add_graphics_metadata( + self, + img_filename: str, + tex_filename: str, + layer_mode: TerrainLayerMode, + layer_pos: int, + frame_rate: float, + replay_delay: float, + frame_count: int, + ): + """ + Add metadata from the GenieGraphic object. + + :param img_filename: Filename of the exported PNG file. + :param tex_filename: Filename of the .texture file. + :param layer_mode: Animation mode (off, loop). + :param layer_pos: Layer position. + :param frame_rate: Time spent on each frame. + :param replay_delay: Time delay before replaying the animation. + :param frame_count: Number of frames in the animation. + """ + self.graphics_metadata[img_filename] = ( + tex_filename, + layer_mode, + layer_pos, + frame_rate, + replay_delay, + frame_count + ) + + def dump(self) -> str: + """ + Creates a human-readable string that can be written to a file. + """ + terrain_file = TerrainMetadata(self.targetdir, self.filename) + + tex_index = 0 + for _, metadata in self.graphics_metadata.items(): + tex_filename = metadata[0] + terrain_file.add_texture(tex_index, tex_filename) + terrain_file.add_layer(tex_index, *metadata[1:5]) + + frame_count = metadata[5] + + for frame_idx in range(frame_count): + subtex_index = frame_idx + terrain_file.add_frame( + frame_idx, + tex_index, + tex_index, + subtex_index + ) + + tex_index += 1 + + return terrain_file.dump() diff --git a/openage/convert/processor/conversion/aoc/media_subprocessor.py b/openage/convert/processor/conversion/aoc/media_subprocessor.py index 360a13b4f7..bb2326e367 100644 --- a/openage/convert/processor/conversion/aoc/media_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/media_subprocessor.py @@ -10,10 +10,12 @@ from openage.convert.value_object.read.media_types import MediaType -from ....entity_object.export.formats.sprite_metadata import LayerMode +from ....entity_object.export.formats.sprite_metadata import LayerMode as SpriteLayerMode +from ....entity_object.export.formats.terrain_metadata import LayerMode as TerrainLayerMode from ....entity_object.export.media_export_request import MediaExportRequest from ....entity_object.export.metadata_export import SpriteMetadataExport from ....entity_object.export.metadata_export import TextureMetadataExport +from ....entity_object.export.metadata_export import TerrainMetadataExport from ....value_object.read.media_types import MediaType if typing.TYPE_CHECKING: @@ -82,13 +84,13 @@ def create_graphics_requests(full_data_set: GenieObjectContainer) -> None: # Add metadata from graphics to animation metadata sequence_type = graphic["sequence_type"].value if sequence_type == 0x00: - layer_mode = LayerMode.OFF + layer_mode = SpriteLayerMode.OFF elif sequence_type & 0x08: - layer_mode = LayerMode.ONCE + layer_mode = SpriteLayerMode.ONCE else: - layer_mode = LayerMode.LOOP + layer_mode = SpriteLayerMode.LOOP layer_pos = graphic["layer"].value frame_rate = round(graphic["frame_rate"].value, ndigits=6) @@ -132,6 +134,44 @@ def create_graphics_requests(full_data_set: GenieObjectContainer) -> None: target_filename) full_data_set.graphics_exports.update({slp_id: export_request}) + texture_meta_filename = f"{texture.get_filename()}.texture" + texture_meta_export = TextureMetadataExport(targetdir, + texture_meta_filename) + full_data_set.metadata_exports.append(texture_meta_export) + + # Add texture image filename to texture metadata + texture_meta_export.add_imagefile(target_filename) + texture_meta_export.update( + None, + { + f"{target_filename}": { + "size": (481, 481), # TODO: Get actual size = sqrt(slp_frame_count) + "subtex_metadata": [ + { + "x": 0, + "y": 0, + "w": 481, + "h": 481, + "cx": 0, + "cy": 0, + } + ] + }} + ) + + terrain_meta_filename = f"{texture.get_filename()}.terrain" + terrain_meta_export = TerrainMetadataExport(targetdir, + terrain_meta_filename) + full_data_set.metadata_exports.append(terrain_meta_export) + + terrain_meta_export.add_graphics_metadata(target_filename, + texture_meta_filename, + TerrainLayerMode.OFF, + 0, + 0.0, + 0.0, + 1) + @staticmethod def create_blend_requests(full_data_set: GenieObjectContainer) -> None: """ diff --git a/openage/convert/processor/conversion/hd/media_subprocessor.py b/openage/convert/processor/conversion/hd/media_subprocessor.py index 935643efa0..74d4c5350d 100644 --- a/openage/convert/processor/conversion/hd/media_subprocessor.py +++ b/openage/convert/processor/conversion/hd/media_subprocessor.py @@ -9,9 +9,12 @@ from __future__ import annotations import typing -from ....entity_object.export.formats.sprite_metadata import LayerMode +from ....entity_object.export.formats.sprite_metadata import LayerMode as SpriteLayerMode +from ....entity_object.export.formats.terrain_metadata import LayerMode as TerrainLayerMode from ....entity_object.export.media_export_request import MediaExportRequest -from ....entity_object.export.metadata_export import SpriteMetadataExport, TextureMetadataExport +from ....entity_object.export.metadata_export import SpriteMetadataExport +from ....entity_object.export.metadata_export import TextureMetadataExport +from ....entity_object.export.metadata_export import TerrainMetadataExport from ....value_object.read.media_types import MediaType if typing.TYPE_CHECKING: @@ -77,13 +80,13 @@ def create_graphics_requests(full_data_set: GenieObjectContainer) -> None: # Add metadata from graphics to animation metadata sequence_type = graphic["sequence_type"].value if sequence_type == 0x00: - layer_mode = LayerMode.OFF + layer_mode = SpriteLayerMode.OFF elif sequence_type & 0x08: - layer_mode = LayerMode.ONCE + layer_mode = SpriteLayerMode.ONCE else: - layer_mode = LayerMode.LOOP + layer_mode = SpriteLayerMode.LOOP layer_pos = graphic["layer"].value frame_rate = round(graphic["frame_rate"].value, ndigits=6) @@ -128,6 +131,44 @@ def create_graphics_requests(full_data_set: GenieObjectContainer) -> None: target_filename) full_data_set.graphics_exports.update({slp_id: export_request}) + texture_meta_filename = f"{texture.get_filename()}.texture" + texture_meta_export = TextureMetadataExport(targetdir, + texture_meta_filename) + full_data_set.metadata_exports.append(texture_meta_export) + + # Add texture image filename to texture metadata + texture_meta_export.add_imagefile(target_filename) + texture_meta_export.update( + None, + { + f"{target_filename}": { + "size": (512, 512), + "subtex_metadata": [ + { + "x": 0, + "y": 0, + "w": 512, + "h": 512, + "cx": 0, + "cy": 0, + } + ] + }} + ) + + terrain_meta_filename = f"{texture.get_filename()}.terrain" + terrain_meta_export = TerrainMetadataExport(targetdir, + terrain_meta_filename) + full_data_set.metadata_exports.append(terrain_meta_export) + + terrain_meta_export.add_graphics_metadata(target_filename, + texture_meta_filename, + TerrainLayerMode.OFF, + 0, + 0.0, + 0.0, + 1) + @staticmethod def create_sound_requests(full_data_set: GenieObjectContainer) -> None: """ From 46acfce7e31d3099ba0deaa0a352810855e8a237 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 6 Nov 2023 01:16:45 +0100 Subject: [PATCH 064/771] gamestate: Fix getting terrain path from modpack. --- libopenage/gamestate/api/terrain.cpp | 6 ++- libopenage/gamestate/game.cpp | 2 +- libopenage/gamestate/terrain.cpp | 3 +- libopenage/gamestate/terrain_chunk.cpp | 13 ++++++ libopenage/gamestate/terrain_chunk.h | 13 ++++++ libopenage/gamestate/terrain_factory.cpp | 56 ++++++++++++------------ 6 files changed, 61 insertions(+), 32 deletions(-) diff --git a/libopenage/gamestate/api/terrain.cpp b/libopenage/gamestate/api/terrain.cpp index bfedb59e4c..e69edee5fc 100644 --- a/libopenage/gamestate/api/terrain.cpp +++ b/libopenage/gamestate/api/terrain.cpp @@ -4,6 +4,8 @@ #include +#include "gamestate/api/util.h" + namespace openage::gamestate::api { @@ -14,9 +16,9 @@ bool APITerrain::is_terrain(const nyan::Object &obj) { const std::string APITerrain::get_terrain_path(const nyan::Object &terrain) { nyan::Object terrain_texture_obj = terrain.get_object("Terrain.terrain_graphic"); - std::string sprite_path = terrain_texture_obj.get_text("TerrainTexture.sprite"); + std::string terrain_path = terrain_texture_obj.get_file("Terrain.sprite"); - return sprite_path; + return resolve_file_path(terrain, terrain_path); } } // namespace openage::gamestate::api diff --git a/libopenage/gamestate/game.cpp b/libopenage/gamestate/game.cpp index dfe25b10b5..edad807607 100644 --- a/libopenage/gamestate/game.cpp +++ b/libopenage/gamestate/game.cpp @@ -105,7 +105,7 @@ void Game::load_path(const util::Path &base_dir, }; // file loading - if (search_path.is_file() && search_path.get_suffix() == ".nyan") { + if (search_path.is_file() and search_path.get_suffix() == ".nyan") { auto loc = mod_dir + "/" + search; this->db->load(loc, fileload_func); return; diff --git a/libopenage/gamestate/terrain.cpp b/libopenage/gamestate/terrain.cpp index f2b790e0a7..6a733a86d7 100644 --- a/libopenage/gamestate/terrain.cpp +++ b/libopenage/gamestate/terrain.cpp @@ -32,8 +32,7 @@ void Terrain::attach_renderer(const std::shared_ptr &re chunk->get_offset()); chunk->set_render_entity(render_entity); - chunk->render_update(time::time_t::zero(), - "../test/textures/test_terrain.terrain"); + chunk->render_update(time::time_t::zero()); } } diff --git a/libopenage/gamestate/terrain_chunk.cpp b/libopenage/gamestate/terrain_chunk.cpp index 6ba3338d7b..d8adb2ccd0 100644 --- a/libopenage/gamestate/terrain_chunk.cpp +++ b/libopenage/gamestate/terrain_chunk.cpp @@ -45,4 +45,17 @@ const coord::tile_delta &TerrainChunk::get_offset() const { return this->offset; } +void TerrainChunk::set_terrain_path(const std::string &terrain_path) { + this->terrain_path = terrain_path; +} + +void TerrainChunk::render_update(const time::time_t &time) { + if (this->render_entity != nullptr) { + this->render_entity->update(this->size, + this->height_map, + this->terrain_path, + time); + } +} + } // namespace openage::gamestate diff --git a/libopenage/gamestate/terrain_chunk.h b/libopenage/gamestate/terrain_chunk.h index 191ee8f185..4b51fa6d6d 100644 --- a/libopenage/gamestate/terrain_chunk.h +++ b/libopenage/gamestate/terrain_chunk.h @@ -55,6 +55,16 @@ class TerrainChunk { */ const coord::tile_delta &get_offset() const; + // TODO: Remove test texture references + + // Set the terrain path of this terrain chunk. + // TODO: Remove later + void set_terrain_path(const std::string &terrain_path); + + // Send the current texture to the renderer. + // TODO: Replace later with render_update(time, terrain_path) + void render_update(const time::time_t &time); + private: /** * Size of the terrain chunk. @@ -77,6 +87,9 @@ class TerrainChunk { * Render entity for pushing updates to the renderer. Can be \p nullptr. */ std::shared_ptr render_entity; + + // TODO: Remove test texture references + std::string terrain_path; }; } // namespace openage::gamestate diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp index 2c78d7da83..5801ed1892 100644 --- a/libopenage/gamestate/terrain_factory.cpp +++ b/libopenage/gamestate/terrain_factory.cpp @@ -22,20 +22,20 @@ namespace openage::gamestate { static const std::vector aoe1_test_terrain = {}; static const std::vector de1_test_terrain = {}; static const std::vector aoe2_test_terrain = { - "aoe2_base.data.terrain.foundation.Foundation", - "aoe2_base.data.terrain.grass.Grass", - "aoe2_base.data.terrain.dirt.Dirt", + "aoe2_base.data.terrain.foundation.foundation.Foundation", + "aoe2_base.data.terrain.grass.grass.Grass", + "aoe2_base.data.terrain.dirt.dirt.Dirt", }; static const std::vector de2_test_terrain = {}; static const std::vector hd_test_terrain = { - "hd_base.data.terrain.foundation.Foundation", - "hd_base.data.terrain.grass.Grass", - "hd_base.data.terrain.dirt.Dirt", + "hd_base.data.terrain.foundation.foundation.Foundation", + "hd_base.data.terrain.grass.grass.Grass", + "hd_base.data.terrain.dirt.dirt.Dirt", }; static const std::vector swgb_test_terrain = { - "swgb_base.data.terrain.desert0.Desert0", - "swgb_base.data.terrain.grass2.Grass2", - "swgb_base.data.terrain.foundation.Foundation", + "swgb_base.data.terrain.desert0.desert0.Desert0", + "swgb_base.data.terrain.grass2.grass2.Grass2", + "swgb_base.data.terrain.foundation.foundation.Foundation", }; static const std::vector trial_test_terrain = {}; @@ -96,33 +96,35 @@ std::shared_ptr TerrainFactory::add_chunk(const std::shared_ptr(size, offset); + // TODO: Remove test texture references + std::string test_texture_path = "../test/textures/test_terrain.terrain"; + if (this->render_factory) { auto render_entity = this->render_factory->add_terrain_render_entity(size, offset); chunk->set_render_entity(render_entity); - std::string test_texture_path = "../test/textures/test_terrain.terrain"; - - // TODO: Remove test texture references - static size_t test_terrain_index = 0; - if (test_terrains.empty()) { - build_test_terrains(gstate); - - // use one of the modpack terrain textures - if (not test_terrains.empty()) { - if (test_terrain_index >= test_terrains.size()) { - test_terrain_index = 0; - } - auto terrain_obj = gstate->get_db_view()->get_object(test_terrains[test_terrain_index]); - test_texture_path = api::APITerrain::get_terrain_path(terrain_obj); + chunk->render_update(time::time_t::zero(), + test_texture_path); + } - test_terrain_index += 1; - } + // TODO: Remove test texture references + if (test_terrains.empty()) { + build_test_terrains(gstate); + } + static size_t test_terrain_index = 0; + if (not test_terrains.empty()) { + // use one of the modpack terrain textures + if (test_terrain_index >= test_terrains.size()) { + test_terrain_index = 0; } + auto terrain_obj = gstate->get_db_view()->get_object(test_terrains[test_terrain_index]); + test_texture_path = api::APITerrain::get_terrain_path(terrain_obj); - chunk->render_update(time::time_t::zero(), - test_texture_path); + test_terrain_index += 1; } + chunk->set_terrain_path(test_texture_path); + return chunk; } From cb669af4d8286d4884e8bf9cba28127af3d4ba4d Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 6 Nov 2023 22:35:58 +0100 Subject: [PATCH 065/771] input: Fix error when selection callback is missing. --- libopenage/gamestate/event/spawn_entity.cpp | 2 +- libopenage/input/controller/game/binding.h | 3 ++- libopenage/input/controller/game/controller.cpp | 16 ++++++++-------- libopenage/input/controller/game/controller.h | 3 ++- libopenage/input/input_manager.cpp | 6 +++--- libopenage/input/input_manager.h | 8 ++++---- 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/libopenage/gamestate/event/spawn_entity.cpp b/libopenage/gamestate/event/spawn_entity.cpp index 0cea3cf631..9558cba2a3 100644 --- a/libopenage/gamestate/event/spawn_entity.cpp +++ b/libopenage/gamestate/event/spawn_entity.cpp @@ -197,7 +197,7 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, // TODO: Select the unit when it's created // very dumb but it gets the job done - auto select_cb = params.get("select_cb", std::function{}); + auto select_cb = params.get("select_cb", std::function{[](entity_id_t /* id */) {}}); select_cb(entity->get_id()); gstate->add_game_entity(entity); diff --git a/libopenage/input/controller/game/binding.h b/libopenage/input/controller/game/binding.h index abddb2385f..8b94dd8875 100644 --- a/libopenage/input/controller/game/binding.h +++ b/libopenage/input/controller/game/binding.h @@ -3,6 +3,7 @@ #pragma once #include +#include #include #include "event/event.h" @@ -14,7 +15,7 @@ class Controller; using binding_flags_t = std::unordered_map; using binding_func_t = std::function(const event_arguments &, - const Controller &)>; + const std::shared_ptr)>; /** diff --git a/libopenage/input/controller/game/controller.cpp b/libopenage/input/controller/game/controller.cpp index d12f7e1135..7a17de186a 100644 --- a/libopenage/input/controller/game/controller.cpp +++ b/libopenage/input/controller/game/controller.cpp @@ -60,7 +60,8 @@ bool Controller::process(const event_arguments &ev_args, const std::shared_ptrlookup(ev_args.e); - auto game_event = bind.transform(ev_args, *this); + auto controller = this->shared_from_this(); + auto game_event = bind.transform(ev_args, controller); switch (bind.action_type) { case forward_action_t::SEND: @@ -90,15 +91,14 @@ void setup_defaults(const std::shared_ptr &ctx, const std::shared_ptr &simulation, const std::shared_ptr &camera) { binding_func_t create_entity_event{[&](const event_arguments &args, - const Controller &controller) { + const std::shared_ptr controller) { auto mouse_pos = args.mouse.to_phys3(camera); event::EventHandler::param_map::map_t params{ {"position", mouse_pos}, - {"owner", controller.get_controlled()}, + {"owner", controller->get_controlled()}, // TODO: Remove - {"select_cb", std::function{[&controller](gamestate::entity_id_t id) { - auto &mut_controller = const_cast(controller); - mut_controller.set_selected({id}); + {"select_cb", std::function{[controller](gamestate::entity_id_t id) { + controller->set_selected({id}); }}}, }; @@ -117,12 +117,12 @@ void setup_defaults(const std::shared_ptr &ctx, ctx->bind(ev_mouse_lmb, create_entity_action); binding_func_t move_entity{[&](const event_arguments &args, - const Controller &controller) { + 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()}, + {"entity_ids", controller->get_selected()}, }; auto event = simulation->get_event_loop()->create_event( diff --git a/libopenage/input/controller/game/controller.h b/libopenage/input/controller/game/controller.h index 2ef4439087..847a5999f5 100644 --- a/libopenage/input/controller/game/controller.h +++ b/libopenage/input/controller/game/controller.h @@ -2,6 +2,7 @@ #pragma once +#include #include #include @@ -32,7 +33,7 @@ class BindingContext; * * TODO: Connection to engine */ -class Controller { +class Controller : public std::enable_shared_from_this { public: Controller(const std::unordered_set &controlled_factions, size_t active_faction_id); diff --git a/libopenage/input/input_manager.cpp b/libopenage/input/input_manager.cpp index 47ff5c1834..f9d729b9ba 100644 --- a/libopenage/input/input_manager.cpp +++ b/libopenage/input/input_manager.cpp @@ -19,15 +19,15 @@ InputManager::InputManager() : gui_input{nullptr} { } -void InputManager::set_gui(const std::shared_ptr &gui_input) { +void InputManager::set_gui(const std::shared_ptr gui_input) { this->gui_input = gui_input; } -void InputManager::set_camera_controller(const std::shared_ptr &controller) { +void InputManager::set_camera_controller(const std::shared_ptr controller) { this->camera_controller = controller; } -void InputManager::set_engine_controller(const std::shared_ptr &controller) { +void InputManager::set_engine_controller(const std::shared_ptr controller) { this->engine_controller = controller; } diff --git a/libopenage/input/input_manager.h b/libopenage/input/input_manager.h index 7b6403eeaa..17e66a28be 100644 --- a/libopenage/input/input_manager.h +++ b/libopenage/input/input_manager.h @@ -24,7 +24,7 @@ class Controller; namespace game { class Controller; -} // namespace engine +} // namespace game class InputContext; @@ -43,21 +43,21 @@ class InputManager { * * @param gui_input GUI input handler. */ - void set_gui(const std::shared_ptr &gui_input); + void set_gui(const std::shared_ptr gui_input); /** * Set the controller for the camera. * * @param controller Camera controller. */ - void set_camera_controller(const std::shared_ptr &controller); + void set_camera_controller(const std::shared_ptr controller); /** * Set the controller for the engine. * * @param controller Engine controller. */ - void set_engine_controller(const std::shared_ptr &controller); + void set_engine_controller(const std::shared_ptr controller); /** * returns the global keybind context. From 83b16daa262782265ffe7f9aa186bb2ba5d63b4f Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 6 Nov 2023 22:42:53 +0100 Subject: [PATCH 066/771] gui: Remove remaining mentions of SDL. --- doc/code/gui.md | 12 +++++------ .../renderer/gui/guisys/link/gui_item.h | 4 ++-- .../renderer/gui/guisys/link/gui_item_link.h | 2 +- .../gui/guisys/link/gui_property_map_impl.cpp | 2 +- ...tic_cast.h => qtgui_checked_static_cast.h} | 0 .../guisys/private/gui_application_impl.cpp | 20 +++++++++---------- .../gui/guisys/private/gui_application_impl.h | 8 ++++---- .../gui/guisys/private/opengl_debug_logger.h | 2 +- .../gui/guisys/public/gui_application.cpp | 8 +++----- 9 files changed, 27 insertions(+), 31 deletions(-) rename libopenage/renderer/gui/guisys/link/{qtsdl_checked_static_cast.h => qtgui_checked_static_cast.h} (100%) diff --git a/doc/code/gui.md b/doc/code/gui.md index 6c548c2b65..3f1a4afb17 100644 --- a/doc/code/gui.md +++ b/doc/code/gui.md @@ -51,7 +51,7 @@ qmlRegisterType("yay.sfttech.openage", 1, 0, "ResourceAmount 2. Specializations `struct Wrap` and `struct Unwrap` must be defined: ```cpp -namespace qtsdl { +namespace qtgui { template<> struct Wrap { using Type = ResourceAmountLink; @@ -61,13 +61,13 @@ template<> struct Unwrap { using Type = ResourceAmount; }; -} // namespace qtsdl +} // namespace qtgui ``` 3. Also ResourceAmount needs a public member to be added: ```cpp public: - qtsdl::GuiItemLink *gui_link + qtgui::GuiItemLink *gui_link ``` 4. Declare and implement needed properties and signals in the `ResourceAmountLink` using Qt property syntax. @@ -78,7 +78,7 @@ There is a class `GeneratorParameters` in `libopenage/` directory. It has a big list of parameters of different types like `generation_seed`, `player_radius`, `player_names`, etc. So, we're not going to write a Qt property for each one: -1. `GeneratorParameters` must derive from the `qtsdl::GuiPropertyMap`. +1. `GeneratorParameters` must derive from the `qtgui::GuiPropertyMap`. 2. `GeneratorParameters` should set its initial values like so: ```cpp @@ -96,7 +96,7 @@ qmlRegisterType("yay.sfttech.openage", 1, 0, "Generator 4. Specializations `struct Wrap` and `struct Unwrap` must be defined: ```cpp -namespace qtsdl { +namespace qtgui { template<> struct Wrap { using Type = GeneratorParametersLink; @@ -106,7 +106,7 @@ template<> struct Unwrap { using Type = GeneratorParameters; }; -} // namespace qtsdl +} // namespace qtgui ``` That results into a `ListModel`-like QML type with `display` and `edit` roles. diff --git a/libopenage/renderer/gui/guisys/link/gui_item.h b/libopenage/renderer/gui/guisys/link/gui_item.h index 450d3dc796..9f8a5bd59f 100644 --- a/libopenage/renderer/gui/guisys/link/gui_item.h +++ b/libopenage/renderer/gui/guisys/link/gui_item.h @@ -15,14 +15,14 @@ #include #include "renderer/gui/guisys/link/gui_item_link.h" -#include "renderer/gui/guisys/link/qtsdl_checked_static_cast.h" +#include "renderer/gui/guisys/link/qtgui_checked_static_cast.h" #include "renderer/gui/guisys/private/livereload/deferred_initial_constant_property_values.h" namespace qtgui { /** - * Cleans a text from unneeded content like "qtsdl". + * Cleans a text from unneeded content like "qtgui". */ QString name_tidier(const char *name); diff --git a/libopenage/renderer/gui/guisys/link/gui_item_link.h b/libopenage/renderer/gui/guisys/link/gui_item_link.h index 0bd7b8d61f..0965aa2b4d 100644 --- a/libopenage/renderer/gui/guisys/link/gui_item_link.h +++ b/libopenage/renderer/gui/guisys/link/gui_item_link.h @@ -5,7 +5,7 @@ #include #include -#include "renderer/gui/guisys/link/qtsdl_checked_static_cast.h" +#include "renderer/gui/guisys/link/qtgui_checked_static_cast.h" namespace qtgui { diff --git a/libopenage/renderer/gui/guisys/link/gui_property_map_impl.cpp b/libopenage/renderer/gui/guisys/link/gui_property_map_impl.cpp index 1a8e8b7db3..d6204ae719 100644 --- a/libopenage/renderer/gui/guisys/link/gui_property_map_impl.cpp +++ b/libopenage/renderer/gui/guisys/link/gui_property_map_impl.cpp @@ -5,7 +5,7 @@ #include #include -#include "renderer/gui/guisys/link/qtsdl_checked_static_cast.h" +#include "renderer/gui/guisys/link/qtgui_checked_static_cast.h" namespace qtgui { diff --git a/libopenage/renderer/gui/guisys/link/qtsdl_checked_static_cast.h b/libopenage/renderer/gui/guisys/link/qtgui_checked_static_cast.h similarity index 100% rename from libopenage/renderer/gui/guisys/link/qtsdl_checked_static_cast.h rename to libopenage/renderer/gui/guisys/link/qtgui_checked_static_cast.h diff --git a/libopenage/renderer/gui/guisys/private/gui_application_impl.cpp b/libopenage/renderer/gui/guisys/private/gui_application_impl.cpp index c0ef0dd1b7..fb8674b69e 100644 --- a/libopenage/renderer/gui/guisys/private/gui_application_impl.cpp +++ b/libopenage/renderer/gui/guisys/private/gui_application_impl.cpp @@ -2,12 +2,12 @@ #include "gui_application_impl.h" -#include #include +#include #include -#include #include +#include namespace qtgui { @@ -41,22 +41,20 @@ void GuiApplicationImpl::processEvents() { } namespace { - int argc = 1; - char arg[] = "qtsdl"; - char *argv = &arg[0]; -} +int argc = 1; +char arg[] = "qtgui"; +char *argv = &arg[0]; +} // namespace -GuiApplicationImpl::GuiApplicationImpl() - : +GuiApplicationImpl::GuiApplicationImpl() : #ifndef NDEBUG owner{std::this_thread::get_id()}, #endif - app{argc, &argv} -{ + app{argc, &argv} { // Set locale back to POSIX for the decimal point parsing (see qcoreapplication.html#locale-settings). std::locale::global(std::locale().combine>(std::locale::classic())); qInfo() << "Compiled with Qt" << QT_VERSION_STR << "and run with Qt" << qVersion(); } -} // namespace qtsdl +} // namespace qtgui diff --git a/libopenage/renderer/gui/guisys/private/gui_application_impl.h b/libopenage/renderer/gui/guisys/private/gui_application_impl.h index 7294cf8827..f772c77a0f 100644 --- a/libopenage/renderer/gui/guisys/private/gui_application_impl.h +++ b/libopenage/renderer/gui/guisys/private/gui_application_impl.h @@ -2,8 +2,8 @@ #pragma once -#include #include +#include #include @@ -12,7 +12,7 @@ namespace qtgui { /** * Houses gui logic event queue. * - * To launch it in a dedicated thread, use qtsdl::GuiDedicatedThread instead. + * To launch it in a dedicated thread, use qtgui::GuiDedicatedThread instead. */ class GuiApplicationImpl { public: @@ -25,8 +25,8 @@ class GuiApplicationImpl { private: GuiApplicationImpl(); - GuiApplicationImpl(const GuiApplicationImpl&) = delete; - GuiApplicationImpl& operator=(const GuiApplicationImpl&) = delete; + GuiApplicationImpl(const GuiApplicationImpl &) = delete; + GuiApplicationImpl &operator=(const GuiApplicationImpl &) = delete; #ifndef NDEBUG const std::thread::id owner; diff --git a/libopenage/renderer/gui/guisys/private/opengl_debug_logger.h b/libopenage/renderer/gui/guisys/private/opengl_debug_logger.h index f2763c96e7..3c297e197f 100644 --- a/libopenage/renderer/gui/guisys/private/opengl_debug_logger.h +++ b/libopenage/renderer/gui/guisys/private/opengl_debug_logger.h @@ -40,4 +40,4 @@ gl_debug_parameters get_current_opengl_debug_parameters(QOpenGLContext ¤t_ */ void apply_opengl_debug_parameters(gl_debug_parameters params, QOpenGLContext ¤t_dest_context); -} // namespace qtsdl +} // namespace qtgui diff --git a/libopenage/renderer/gui/guisys/public/gui_application.cpp b/libopenage/renderer/gui/guisys/public/gui_application.cpp index 08a93aa42d..80cba97bd2 100644 --- a/libopenage/renderer/gui/guisys/public/gui_application.cpp +++ b/libopenage/renderer/gui/guisys/public/gui_application.cpp @@ -8,13 +8,11 @@ namespace qtgui { -GuiApplication::GuiApplication() - : +GuiApplication::GuiApplication() : application{GuiApplicationImpl::get()} { } -GuiApplication::GuiApplication(std::shared_ptr application) - : +GuiApplication::GuiApplication(std::shared_ptr application) : application{application} { } @@ -24,4 +22,4 @@ void GuiApplication::process_events() { this->application->processEvents(); } -} // namespace qtsdl +} // namespace qtgui From 9d6a038b8f200428955eb7991114df937fb7f06f Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 7 Nov 2023 01:46:16 +0100 Subject: [PATCH 067/771] engine: Fix capture of argument in thread function. --- libopenage/engine/engine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/engine/engine.cpp b/libopenage/engine/engine.cpp index 9af6d4fe89..fa2c251b4d 100644 --- a/libopenage/engine/engine.cpp +++ b/libopenage/engine/engine.cpp @@ -55,7 +55,7 @@ Engine::Engine(mode mode, // if presenter is used, run it in a separate thread if (this->run_mode == mode::FULL) { - this->threads.emplace_back([&]() { + this->threads.emplace_back([&, debug_graphics]() { this->presenter->run(debug_graphics); // Make sure that the presenter gets destructed in the same thread From eb84b4c78e98de93935e201374931d208999ce53 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 10 Nov 2023 22:37:01 +0100 Subject: [PATCH 068/771] renderer: Use correct world space transformation for terrain model. --- libopenage/gamestate/game.cpp | 8 ++++++++ libopenage/renderer/stages/terrain/terrain_mesh.cpp | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/libopenage/gamestate/game.cpp b/libopenage/gamestate/game.cpp index edad807607..121bef4f56 100644 --- a/libopenage/gamestate/game.cpp +++ b/libopenage/gamestate/game.cpp @@ -135,8 +135,16 @@ void Game::generate_terrain(const std::shared_ptr &terrain_facto auto chunk1 = terrain_factory->add_chunk(this->state, util::Vector2s{10, 10}, coord::tile_delta{10, 0}); + auto chunk2 = terrain_factory->add_chunk(this->state, + util::Vector2s{10, 10}, + coord::tile_delta{0, 10}); + auto chunk3 = terrain_factory->add_chunk(this->state, + util::Vector2s{10, 10}, + coord::tile_delta{10, 10}); terrain->add_chunk(chunk0); terrain->add_chunk(chunk1); + terrain->add_chunk(chunk2); + terrain->add_chunk(chunk3); this->state->set_terrain(terrain); } diff --git a/libopenage/renderer/stages/terrain/terrain_mesh.cpp b/libopenage/renderer/stages/terrain/terrain_mesh.cpp index 88b471c22f..b02f867050 100644 --- a/libopenage/renderer/stages/terrain/terrain_mesh.cpp +++ b/libopenage/renderer/stages/terrain/terrain_mesh.cpp @@ -100,7 +100,7 @@ const std::shared_ptr &TerrainRenderMesh::get_uniforms() void TerrainRenderMesh::create_model_matrix(const coord::scene2_delta &offset) { // TODO: Needs input from engine auto model = Eigen::Affine3f::Identity(); - model.translate(Eigen::Vector3f{offset.ne.to_float(), offset.se.to_float(), 0.0f}); + model.translate(offset.to_world_space()); this->model_matrix = model.matrix(); } From fd7f28a4c4b56e4acde4bdccaf81f64d99f1af7f Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 10 Nov 2023 22:40:27 +0100 Subject: [PATCH 069/771] doc: Update run instructions. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 00d3715d85..db9b9a4a82 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Quickstart ``` * **I compiled everything. Now how do I run it?** - * Execute `./bin/run`. + * Execute `cd bin && ./run main`. * [The convert script](/doc/media_convert.md) will transform original assets into openage formats, which are a lot saner and more moddable. * Use your brain and react to the things you'll see. From e1eee5e2211af49219b4255f3ae3f0ed4469bec2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 11 Nov 2023 20:33:54 +0100 Subject: [PATCH 070/771] gamestate: Terrain tile definition. --- libopenage/gamestate/CMakeLists.txt | 1 + libopenage/gamestate/terrain_tile.cpp | 7 ++++++ libopenage/gamestate/terrain_tile.h | 31 +++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 libopenage/gamestate/terrain_tile.cpp create mode 100644 libopenage/gamestate/terrain_tile.h diff --git a/libopenage/gamestate/CMakeLists.txt b/libopenage/gamestate/CMakeLists.txt index c7cdaebee2..a94cd93514 100644 --- a/libopenage/gamestate/CMakeLists.txt +++ b/libopenage/gamestate/CMakeLists.txt @@ -9,6 +9,7 @@ add_sources(libopenage simulation.cpp terrain_chunk.cpp terrain_factory.cpp + terrain_tile.cpp terrain.cpp types.cpp world.cpp diff --git a/libopenage/gamestate/terrain_tile.cpp b/libopenage/gamestate/terrain_tile.cpp new file mode 100644 index 0000000000..945c848d73 --- /dev/null +++ b/libopenage/gamestate/terrain_tile.cpp @@ -0,0 +1,7 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "terrain_tile.h" + + +namespace openage::gamestate { +} diff --git a/libopenage/gamestate/terrain_tile.h b/libopenage/gamestate/terrain_tile.h new file mode 100644 index 0000000000..b2b8e2dcc3 --- /dev/null +++ b/libopenage/gamestate/terrain_tile.h @@ -0,0 +1,31 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include + +#include "util/fixed_point.h" + + +namespace openage::gamestate { + +using terrain_elevation_t = util::FixedPoint; + +/** + * A single terrain tile. + */ +struct TerrainTile { + /** + * Terrain definition used by this tile. + */ + nyan::Object terrain; + + /** + * Height of this tile on the terrain. + */ + terrain_elevation_t elevation; +}; + +} // namespace openage::gamestate From 1f7ea144536fe817c1b6b7e430f1fd744daca290 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 11 Nov 2023 21:57:52 +0100 Subject: [PATCH 071/771] gamestate: Replace terrain height map with tiles. --- libopenage/gamestate/terrain_chunk.cpp | 31 +++++++------ libopenage/gamestate/terrain_chunk.h | 6 ++- libopenage/gamestate/terrain_factory.cpp | 35 +++++++++------ libopenage/gamestate/terrain_tile.cpp | 5 ++- libopenage/gamestate/terrain_tile.h | 12 +++++- libopenage/renderer/demo/demo_3.cpp | 34 +++++++-------- libopenage/renderer/demo/stresstest_0.cpp | 10 ++--- .../stages/terrain/terrain_render_entity.cpp | 43 +++++++++++++++---- .../stages/terrain/terrain_render_entity.h | 34 +++++++++++---- 9 files changed, 140 insertions(+), 70 deletions(-) diff --git a/libopenage/gamestate/terrain_chunk.cpp b/libopenage/gamestate/terrain_chunk.cpp index d8adb2ccd0..a6d10251b1 100644 --- a/libopenage/gamestate/terrain_chunk.cpp +++ b/libopenage/gamestate/terrain_chunk.cpp @@ -6,21 +6,16 @@ namespace openage::gamestate { TerrainChunk::TerrainChunk(const util::Vector2s size, - const coord::tile_delta offset) : + const coord::tile_delta offset, + const std::vector &&tiles) : size{size}, offset{offset}, - height_map{} { + tiles{tiles} { if (this->size[0] > MAX_CHUNK_WIDTH || this->size[1] > MAX_CHUNK_HEIGHT) { throw Error(MSG(err) << "Terrain chunk size exceeds maximum size: " << this->size[0] << "x" << this->size[1] << " > " << MAX_CHUNK_WIDTH << "x" << MAX_CHUNK_HEIGHT); } - - // fill the terrain grid with height values - this->height_map.reserve(this->size[0] * this->size[1]); - for (size_t i = 0; i < this->size[0] * this->size[1]; ++i) { - this->height_map.push_back(0.0f); - } } void TerrainChunk::set_render_entity(const std::shared_ptr &entity) { @@ -30,9 +25,15 @@ void TerrainChunk::set_render_entity(const std::shared_ptrrender_entity != nullptr) { + // TODO: Update individual tiles instead of the whole chunk + std::vector> tiles; + tiles.reserve(this->tiles.size()); + for (const auto &tile : this->tiles) { + tiles.emplace_back(tile.elevation, terrain_path); + } + this->render_entity->update(this->size, - this->height_map, - terrain_path, + tiles, time); } } @@ -51,9 +52,15 @@ void TerrainChunk::set_terrain_path(const std::string &terrain_path) { void TerrainChunk::render_update(const time::time_t &time) { if (this->render_entity != nullptr) { + // TODO: Update individual tiles instead of the whole chunk + std::vector> tiles; + tiles.reserve(this->tiles.size()); + for (const auto &tile : this->tiles) { + tiles.emplace_back(tile.elevation, terrain_path); + } + this->render_entity->update(this->size, - this->height_map, - this->terrain_path, + tiles, time); } } diff --git a/libopenage/gamestate/terrain_chunk.h b/libopenage/gamestate/terrain_chunk.h index 4b51fa6d6d..df9091a321 100644 --- a/libopenage/gamestate/terrain_chunk.h +++ b/libopenage/gamestate/terrain_chunk.h @@ -5,6 +5,7 @@ #include #include "coord/tile.h" +#include "gamestate/terrain_tile.h" #include "renderer/stages/terrain/terrain_render_entity.h" #include "time/time.h" #include "util/vector.h" @@ -22,7 +23,8 @@ const size_t MAX_CHUNK_HEIGHT = 16; class TerrainChunk { public: TerrainChunk(const util::Vector2s size, - const coord::tile_delta offset); + const coord::tile_delta offset, + const std::vector &&tiles); ~TerrainChunk() = default; /** @@ -81,7 +83,7 @@ class TerrainChunk { /** * Height map of the terrain chunk. */ - std::vector height_map; + std::vector tiles; /** * Render entity for pushing updates to the renderer. Can be \p nullptr. diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp index 5801ed1892..a3abd0cb42 100644 --- a/libopenage/gamestate/terrain_factory.cpp +++ b/libopenage/gamestate/terrain_factory.cpp @@ -94,34 +94,45 @@ void build_test_terrains(const std::shared_ptr &gstate) { std::shared_ptr TerrainFactory::add_chunk(const std::shared_ptr &gstate, const util::Vector2s size, const coord::tile_delta offset) { - auto chunk = std::make_shared(size, offset); - // TODO: Remove test texture references std::string test_texture_path = "../test/textures/test_terrain.terrain"; - if (this->render_factory) { - auto render_entity = this->render_factory->add_terrain_render_entity(size, offset); - chunk->set_render_entity(render_entity); - - chunk->render_update(time::time_t::zero(), - test_texture_path); - } - // TODO: Remove test texture references + // ========== + std::optional terrain_obj; if (test_terrains.empty()) { build_test_terrains(gstate); } + static size_t test_terrain_index = 0; if (not test_terrains.empty()) { // use one of the modpack terrain textures if (test_terrain_index >= test_terrains.size()) { test_terrain_index = 0; } - auto terrain_obj = gstate->get_db_view()->get_object(test_terrains[test_terrain_index]); - test_texture_path = api::APITerrain::get_terrain_path(terrain_obj); + terrain_obj = gstate->get_db_view()->get_object(test_terrains[test_terrain_index]); + test_texture_path = api::APITerrain::get_terrain_path(terrain_obj.value()); test_terrain_index += 1; } + // ========== + + // fill the chunk with tiles + std::vector tiles{}; + tiles.reserve(size[0] * size[1]); + for (size_t i = 0; i < size[0] * size[1]; ++i) { + tiles.emplace_back(terrain_obj, test_texture_path, 0.0f); + } + + auto chunk = std::make_shared(size, offset, std::move(tiles)); + + if (this->render_factory) { + auto render_entity = this->render_factory->add_terrain_render_entity(size, offset); + chunk->set_render_entity(render_entity); + + chunk->render_update(time::time_t::zero(), + test_texture_path); + } chunk->set_terrain_path(test_texture_path); diff --git a/libopenage/gamestate/terrain_tile.cpp b/libopenage/gamestate/terrain_tile.cpp index 945c848d73..0fcb157d06 100644 --- a/libopenage/gamestate/terrain_tile.cpp +++ b/libopenage/gamestate/terrain_tile.cpp @@ -4,4 +4,7 @@ namespace openage::gamestate { -} + +// This file is intentionally empty. + +} // namespace openage::gamestate diff --git a/libopenage/gamestate/terrain_tile.h b/libopenage/gamestate/terrain_tile.h index b2b8e2dcc3..e2a046bb07 100644 --- a/libopenage/gamestate/terrain_tile.h +++ b/libopenage/gamestate/terrain_tile.h @@ -3,6 +3,7 @@ #pragma once #include +#include #include @@ -19,8 +20,17 @@ using terrain_elevation_t = util::FixedPoint; struct TerrainTile { /** * Terrain definition used by this tile. + * + * TODO: Make this non-optional once all modpacks support terrain graphics. */ - nyan::Object terrain; + std::optional terrain; + + /** + * Path to the terrain asset used by this tile. + * + * TODO: Remove this and fetch the asset path from the terrain definition. + */ + std::string terrain_asset_path; /** * Height of this tile on the terrain. diff --git a/libopenage/renderer/demo/demo_3.cpp b/libopenage/renderer/demo/demo_3.cpp index 1cfec155b8..9dbaa9b93e 100644 --- a/libopenage/renderer/demo/demo_3.cpp +++ b/libopenage/renderer/demo/demo_3.cpp @@ -117,10 +117,10 @@ void renderer_demo_3(const util::Path &path) { // Fill a 10x10 terrain grid with height values auto terrain_size = util::Vector2s{10, 10}; - std::vector height_map{}; - height_map.reserve(terrain_size[0] * terrain_size[1]); + std::vector> tiles{}; + tiles.reserve(terrain_size[0] * terrain_size[1]); for (size_t i = 0; i < terrain_size[0] * terrain_size[1]; ++i) { - height_map.push_back(0.0f); + tiles.emplace_back(0.0f, "./textures/test_terrain.terrain"); } // Create entity for terrain rendering @@ -128,25 +128,23 @@ void renderer_demo_3(const util::Path &path) { coord::tile_delta{0, 0}); // Create "test bumps" in the terrain to check if rendering works - height_map[11] = 1.0f; - height_map[23] = 2.3f; - height_map[42] = 4.2f; - height_map[69] = 6.9f; // nice + tiles[11].first = 1.0f; + tiles[23].first = 2.3f; + tiles[42].first = 4.2f; + tiles[69].first = 6.9f; // nice // A hill - height_map[55] = 3.0f; // center - height_map[45] = 2.0f; // bottom left slope - height_map[35] = 1.0f; - height_map[56] = 1.0f; // bottom right slope (little steeper) - height_map[65] = 2.0f; // top right slope - height_map[75] = 1.0f; - height_map[54] = 2.0f; // top left slope - height_map[53] = 1.0f; + tiles[55].first = 3.0f; // center + tiles[45].first = 2.0f; // bottom left slope + tiles[35].first = 1.0f; + tiles[56].first = 1.0f; // bottom right slope (little steeper) + tiles[65].first = 2.0f; // top right slope + tiles[75].first = 1.0f; + tiles[54].first = 2.0f; // top left slope + tiles[53].first = 1.0f; // send the terrain data to the terrain renderer - terrain0->update(terrain_size, - height_map, - "./textures/test_terrain.terrain"); + terrain0->update(terrain_size, tiles); // World entities auto world0 = render_factory->add_world_render_entity(); diff --git a/libopenage/renderer/demo/stresstest_0.cpp b/libopenage/renderer/demo/stresstest_0.cpp index 39b0ba0931..b33abb9685 100644 --- a/libopenage/renderer/demo/stresstest_0.cpp +++ b/libopenage/renderer/demo/stresstest_0.cpp @@ -119,10 +119,10 @@ void renderer_stresstest_0(const util::Path &path) { // Fill a 10x10 terrain grid with height values auto terrain_size = util::Vector2s{10, 10}; - std::vector height_map{}; - height_map.reserve(terrain_size[0] * terrain_size[1]); + std::vector> tiles{}; + tiles.reserve(terrain_size[0] * terrain_size[1]); for (size_t i = 0; i < terrain_size[0] * terrain_size[1]; ++i) { - height_map.push_back(0.0f); + tiles.emplace_back(0.0f, "./textures/test_terrain.terrain"); } // Create entity for terrain rendering @@ -130,9 +130,7 @@ void renderer_stresstest_0(const util::Path &path) { coord::tile_delta{0, 0}); // send the terrain data to the terrain renderer - terrain0->update(terrain_size, - height_map, - "./textures/test_terrain.terrain"); + terrain0->update(terrain_size, tiles); // World entities std::vector> render_entities{}; diff --git a/libopenage/renderer/stages/terrain/terrain_render_entity.cpp b/libopenage/renderer/stages/terrain/terrain_render_entity.cpp index a142461cb0..2d83b018f7 100644 --- a/libopenage/renderer/stages/terrain/terrain_render_entity.cpp +++ b/libopenage/renderer/stages/terrain/terrain_render_entity.cpp @@ -16,13 +16,38 @@ TerrainRenderEntity::TerrainRenderEntity() : terrain_path{nullptr, 0} { } -void TerrainRenderEntity::update(util::Vector2s size, - std::vector height_map, - const std::string terrain_path, +void TerrainRenderEntity::update_tile(const util::Vector2s size, + const coord::tile &pos, + const terrain_elevation_t elevation, + const std::string terrain_path, + const time::time_t time) { + std::unique_lock lock{this->mutex}; + + if (this->vertices.empty()) { + throw Error(MSG(err) << "Cannot update tile: Vertices have not been initialized yet."); + } + + // find the postion of the tile in the vertex array + auto left_corner = pos.ne * size[0] + pos.se; + + // update the 4 vertices of the tile + this->vertices[left_corner].up = elevation.to_float(); + this->vertices[left_corner + 1].up = elevation.to_float(); // bottom corner + this->vertices[left_corner + (size[0] + 1)].up = elevation.to_float(); // top corner + this->vertices[left_corner + (size[0] + 2)].up = elevation.to_float(); // right corner + + // set texture path + this->terrain_path.set_last(time, terrain_path); + + this->changed = true; +} + +void TerrainRenderEntity::update(const util::Vector2s size, + const tiles_t tiles, const time::time_t time) { std::unique_lock lock{this->mutex}; - // increase by 1 in every dimension because height_map + // increase by 1 in every dimension because tiles // size is number of tiles, but we want number of vertices util::Vector2i tile_size{size[0], size[1]}; this->size = util::Vector2s{size[0] + 1, size[1] + 1}; @@ -36,16 +61,16 @@ void TerrainRenderEntity::update(util::Vector2s size, // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - surround.push_back(height_map[(i - 1) * size[1] + j - 1]); + surround.push_back(tiles[(i - 1) * size[1] + j - 1].first.to_float()); } if (j < tile_size[1] and i - 1 >= 0) { - surround.push_back(height_map[(i - 1) * size[1] + j]); + surround.push_back(tiles[(i - 1) * size[1] + j].first.to_float()); } if (j < tile_size[1] and i < tile_size[0]) { - surround.push_back(height_map[i * size[1] + j]); + surround.push_back(tiles[i * size[1] + j].first.to_float()); } if (j - 1 >= 0 and i < tile_size[0]) { - surround.push_back(height_map[i * size[1] + j - 1]); + surround.push_back(tiles[i * size[1] + j - 1].first.to_float()); } // select the height of the highest surrounding tile auto max_height = *std::max_element(surround.begin(), surround.end()); @@ -59,7 +84,7 @@ void TerrainRenderEntity::update(util::Vector2s size, } // set texture path - this->terrain_path.set_last(time, terrain_path); + this->terrain_path.set_last(time, tiles[0].second); this->changed = true; } diff --git a/libopenage/renderer/stages/terrain/terrain_render_entity.h b/libopenage/renderer/stages/terrain/terrain_render_entity.h index 5e95896a11..a7b044b874 100644 --- a/libopenage/renderer/stages/terrain/terrain_render_entity.h +++ b/libopenage/renderer/stages/terrain/terrain_render_entity.h @@ -8,32 +8,49 @@ #include #include "coord/scene.h" +#include "coord/tile.h" #include "curve/discrete.h" #include "time/time.h" #include "util/vector.h" -namespace openage::renderer { +namespace openage::renderer::terrain { -namespace terrain { class TerrainRenderEntity { public: TerrainRenderEntity(); ~TerrainRenderEntity() = default; + using terrain_elevation_t = util::FixedPoint; + using tiles_t = std::vector>; + /** - * Update the render entity with information from the + * Update a single tile of the displayed terrain (chunk) with information from the * gamestate. * * @param size Size of the terrain in tiles (width x length) - * @param height_map Height of terrain tiles. + * @param pos Position of the tile in the chunk. + * @param elevation Height of terrain tile. * @param terrain_path Path to the terrain definition. * @param time Simulation time of the update. */ - void update(util::Vector2s size, - std::vector height_map, - const std::string terrain_path, + void update_tile(const util::Vector2s size, + const coord::tile &pos, + const terrain_elevation_t elevation, + const std::string terrain_path, + const time::time_t time = 0.0); + + /** + * Update the full grid of the displayed terrain (chunk) with information from the + * gamestate. + * + * @param size Size of the terrain in tiles (width x length) + * @param tiles Animation data for each tile (elevation, terrain path). + * @param time Simulation time of the update. + */ + void update(const util::Vector2s size, + const tiles_t tiles, const time::time_t time = 0.0); /** @@ -102,5 +119,4 @@ class TerrainRenderEntity { */ std::shared_mutex mutex; }; -} // namespace terrain -} // namespace openage::renderer +} // namespace openage::renderer::terrain From 520ca99600a5ff08febc3b9d0d8ea84cc2a6f417 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 11 Nov 2023 22:03:19 +0100 Subject: [PATCH 072/771] terrain: Remove old terrain code. --- libopenage/CMakeLists.txt | 1 - libopenage/coord/phys.cpp | 1 - libopenage/coord/tile.cpp | 4 +- libopenage/pathfinding/a_star.cpp | 1 - libopenage/pathfinding/path.cpp | 1 - libopenage/terrain/CMakeLists.txt | 4 - libopenage/terrain/terrain.cpp | 640 --------------------------- libopenage/terrain/terrain.h | 372 ---------------- libopenage/terrain/terrain_chunk.cpp | 186 -------- libopenage/terrain/terrain_chunk.h | 107 ----- 10 files changed, 3 insertions(+), 1314 deletions(-) delete mode 100644 libopenage/terrain/CMakeLists.txt delete mode 100644 libopenage/terrain/terrain.cpp delete mode 100644 libopenage/terrain/terrain.h delete mode 100644 libopenage/terrain/terrain_chunk.cpp delete mode 100644 libopenage/terrain/terrain_chunk.h diff --git a/libopenage/CMakeLists.txt b/libopenage/CMakeLists.txt index 12ddac7899..eb54fd8bf2 100644 --- a/libopenage/CMakeLists.txt +++ b/libopenage/CMakeLists.txt @@ -349,7 +349,6 @@ add_subdirectory("presenter") add_subdirectory("pyinterface") add_subdirectory("renderer") add_subdirectory("rng") -add_subdirectory("terrain") add_subdirectory("testing") add_subdirectory("time") add_subdirectory("util") diff --git a/libopenage/coord/phys.cpp b/libopenage/coord/phys.cpp index 2daf15ab20..250709e2b2 100644 --- a/libopenage/coord/phys.cpp +++ b/libopenage/coord/phys.cpp @@ -5,7 +5,6 @@ #include "coord/pixel.h" #include "coord/scene.h" #include "coord/tile.h" -#include "terrain/terrain.h" #include "util/math.h" #include "util/math_constants.h" diff --git a/libopenage/coord/tile.cpp b/libopenage/coord/tile.cpp index 2ccbcf0ce1..e20868ff98 100644 --- a/libopenage/coord/tile.cpp +++ b/libopenage/coord/tile.cpp @@ -2,7 +2,9 @@ #include "tile.h" -#include "../terrain/terrain.h" +#include "chunk.h" +#include "phys.h" + namespace openage::coord { diff --git a/libopenage/pathfinding/a_star.cpp b/libopenage/pathfinding/a_star.cpp index 7d9d9275cb..b53aed88f4 100644 --- a/libopenage/pathfinding/a_star.cpp +++ b/libopenage/pathfinding/a_star.cpp @@ -16,7 +16,6 @@ #include "../datastructure/pairing_heap.h" #include "../log/log.h" -#include "../terrain/terrain.h" #include "../util/strings.h" #include "heuristics.h" #include "path.h" diff --git a/libopenage/pathfinding/path.cpp b/libopenage/pathfinding/path.cpp index c3f5e91eff..f1da51af78 100644 --- a/libopenage/pathfinding/path.cpp +++ b/libopenage/pathfinding/path.cpp @@ -2,7 +2,6 @@ #include -#include "../terrain/terrain.h" #include "path.h" namespace openage::path { diff --git a/libopenage/terrain/CMakeLists.txt b/libopenage/terrain/CMakeLists.txt deleted file mode 100644 index 753cf4249a..0000000000 --- a/libopenage/terrain/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -add_sources(libopenage - terrain.cpp - terrain_chunk.cpp -) diff --git a/libopenage/terrain/terrain.cpp b/libopenage/terrain/terrain.cpp deleted file mode 100644 index 7ad26a0899..0000000000 --- a/libopenage/terrain/terrain.cpp +++ /dev/null @@ -1,640 +0,0 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. - -#include "terrain.h" - -#include -#include -#include -#include - -#include "../coord/chunk.h" -#include "../coord/pixel.h" -#include "../coord/tile.h" -#include "../error/error.h" -#include "../log/log.h" -#include "../util/misc.h" -#include "../util/strings.h" - -#include "terrain_chunk.h" - -namespace openage { - -TileContent::TileContent() : - terrain_id{0} { -} - -TileContent::~TileContent() = default; - -Terrain::Terrain(terrain_meta *meta, bool is_infinite) : - infinite{is_infinite}, - meta{meta} { - // TODO: - //this->limit_positive = - //this->limit_negative = - - // maps chunk position to chunks - this->chunks = std::unordered_map{}; -} - -Terrain::~Terrain() { - log::log(MSG(dbg) << "Cleanup terrain"); - - for (auto &chunk : this->chunks) { - // this chunk was autogenerated, so clean it up - if (chunk.second->manually_created == false) { - delete chunk.second; - } - } -} - -std::vector Terrain::used_chunks() const { - std::vector result; - for (auto &c : chunks) { - result.push_back(c.first); - } - return result; -} - -bool Terrain::fill(const int *data, const coord::tile_delta &size) { - bool was_cut = false; - - coord::tile pos = {0, 0}; - for (; pos.ne < size.ne; pos.ne++) { - for (pos.se = 0; pos.se < size.se; pos.se++) { - if (this->check_tile(pos) == tile_state::invalid) { - was_cut = true; - continue; - } - int terrain_id = data[pos.ne * size.ne + pos.se]; - TerrainChunk *chunk = this->get_create_chunk(pos); - chunk->get_data(pos)->terrain_id = terrain_id; - } - } - return was_cut; -} - -void Terrain::attach_chunk(TerrainChunk *new_chunk, - const coord::chunk &position, - bool manually_created) { - new_chunk->set_terrain(this); - new_chunk->manually_created = manually_created; - log::log(MSG(dbg) << "Inserting new chunk at (" << position.ne << "," << position.se << ")"); - this->chunks[position] = new_chunk; - - struct chunk_neighbors neigh = this->get_chunk_neighbors(position); - for (int i = 0; i < 8; i++) { - TerrainChunk *neighbor = neigh.neighbor[i]; - if (neighbor != nullptr) { - //set the new chunks neighbor to the neighbor chunk - new_chunk->neighbors.neighbor[i] = neighbor; - - //set the neighbors neighbor on the opposite direction - //to the new chunk - neighbor->neighbors.neighbor[(i + 4) % 8] = new_chunk; - - log::log(MSG(dbg) << "Neighbor " << i << " gets notified of new neighbor."); - } - else { - log::log(MSG(dbg) << "Neighbor " << i << " not found."); - } - } -} - -TerrainChunk *Terrain::get_chunk(const coord::chunk &position) { - auto iter = this->chunks.find(position); - - if (iter == this->chunks.end()) { - return nullptr; - } - else { - return iter->second; - } -} - -TerrainChunk *Terrain::get_chunk(const coord::tile &position) { - return this->get_chunk(position.to_chunk()); -} - -TerrainChunk *Terrain::get_create_chunk(const coord::chunk &position) { - TerrainChunk *res = this->get_chunk(position); - if (res == nullptr) { - res = new TerrainChunk(); - this->attach_chunk(res, position, false); - } - return res; -} - -TerrainChunk *Terrain::get_create_chunk(const coord::tile &position) { - return this->get_create_chunk(position.to_chunk()); -} - -TileContent *Terrain::get_data(const coord::tile &position) { - TerrainChunk *c = this->get_chunk(position.to_chunk()); - if (c == nullptr) { - return nullptr; - } - else { - return c->get_data(position.get_pos_on_chunk()); - } -} - -bool Terrain::validate_terrain(terrain_t terrain_id) { - if (terrain_id >= static_cast(this->meta->terrain_id_count)) { - throw Error(MSG(err) << "Requested terrain_id is out of range: " << terrain_id); - } - else { - return true; - } -} - -bool Terrain::validate_mask(ssize_t mask_id) { - if (mask_id >= static_cast(this->meta->blendmode_count)) { - throw Error(MSG(err) << "Requested mask_id is out of range: " << mask_id); - } - else { - return true; - } -} - -int Terrain::priority(terrain_t terrain_id) { - this->validate_terrain(terrain_id); - return this->meta->terrain_id_priority_map[terrain_id]; -} - -int Terrain::blendmode(terrain_t terrain_id) { - this->validate_terrain(terrain_id); - return this->meta->terrain_id_blendmode_map[terrain_id]; -} - -unsigned Terrain::get_subtexture_id(const coord::tile &pos, unsigned atlas_size) { - unsigned result = 0; - - result += util::mod(pos.se, atlas_size); - result *= atlas_size; - result += util::mod(pos.ne, atlas_size); - - return result; -} - -struct chunk_neighbors Terrain::get_chunk_neighbors(const coord::chunk &position) { - struct chunk_neighbors ret; - - for (int i = 0; i < 8; i++) { - coord::chunk tmp{ - position.ne + (coord::chunk_t)neigh_offsets[i].ne, - position.se + (coord::chunk_t)neigh_offsets[i].se}; - ret.neighbor[i] = this->get_chunk(tmp); - } - - return ret; -} - -int Terrain::get_blending_mode(terrain_t base_id, terrain_t neighbor_id) { - /* - * this function may require much more code, but this simple - * magnitude comparison seems to do the job. - * feel free to confirm or fix the behavior. - * - * my guess is that the blending mode encodes another information - * not publicly noticed yet: the overlay priority. - * the higher the blendmode id, the higher the mode priority. - * this may also be the reason why there are mask duplicates - * in blendomatic.dat - * - * funny enough, just using the modes in the dat file lead - * to a totally wrong render. the convert script reassigns the - * blending modes with a simple key=>val mapping, - * and after that, it looks perfect. - */ - - int base_mode = this->blendmode(base_id); - int neighbor_mode = this->blendmode(neighbor_id); - - if (neighbor_mode > base_mode) { - return neighbor_mode; - } - else { - return base_mode; - } -} - -tile_state Terrain::check_tile(const coord::tile &position) { - if (!this->check_tile_position(position)) { - return tile_state::invalid; - } - else { - TerrainChunk *chunk = this->get_chunk(position); - if (chunk == nullptr) { - return tile_state::creatable; - } - else { - return tile_state::existing; - } - } -} - -bool Terrain::check_tile_position(const coord::tile & /*pos*/) { - if (this->infinite) { - return true; - } - else { - throw Error(ERR << "non-infinite terrains are not supported yet"); - } -} - - -struct terrain_render_data Terrain::create_draw_advice(const coord::tile &ab, - const coord::tile &cd, - const coord::tile &ef, - const coord::tile &gh, - bool blending_enabled) { - /* - * The passed parameters define the screen corners. - * - * ne, se coordinates - * o = screen corner, where the tile coordinates can be queried. - * x = corner of the rhombus that will be drawn, calculated by all o. - * - * cb - * x - * . . - * . . - * ab o===========o cd - * . = visible = . - * gb x = screen = x cf - * . = = . - * gh o===========o ef - * . . - * . . - * x - * gf - * - * The rendering area may be optimized further in the future, - * to exactly fit the visible screen. - * For now, we are drawing the big rhombus. - */ - - // procedure: find all the tiles to be drawn - // and store them to a tile drawing instruction structure - struct terrain_render_data data; - - // vector of tiles to be drawn - std::vector *tiles = &data.tiles; - - // ordered set of objects on the terrain (buildings.) - // it's ordered by the visibility layers. - // auto objects = &data.objects; - - coord::tile gb = {gh.ne, ab.se}; - coord::tile cf = {cd.ne, ef.se}; - - // hint the vector about the number of tiles it will contain - size_t tiles_count = std::abs(cf.ne - gb.ne) * std::abs(cf.se - gb.se); - tiles->reserve(tiles_count); - - // sweep the whole rhombus area - for (coord::tile tilepos = gb; tilepos.ne <= cf.ne; tilepos.ne++) { - for (tilepos.se = gb.se; tilepos.se <= cf.se; tilepos.se++) { - // get the terrain tile drawing data - auto tile = this->create_tile_advice(tilepos, blending_enabled); - tiles->push_back(tile); - - // get the object standing on the tile - // TODO: make the terrain independent of objects standing on it. - TileContent *tile_content = this->get_data(tilepos); - if (tile_content != nullptr) { - // for (auto obj_item : tile_content->obj) { - // objects->insert(obj_item); - // } - } - } - } - - return data; -} - - -struct tile_draw_data Terrain::create_tile_advice(coord::tile position, bool blending_enabled) { - // this struct will be filled with all tiles and overlays to draw. - struct tile_draw_data tile; - tile.count = 0; - - TileContent *base_tile_content = this->get_data(position); - - // chunk of this tile does not exist - if (base_tile_content == nullptr) { - return tile; - } - - struct tile_data base_tile_data; - - // the base terrain id of the tile - base_tile_data.terrain_id = base_tile_content->terrain_id; - - // the base terrain is not existant. - if (base_tile_data.terrain_id < 0) { - return tile; - } - - this->validate_terrain(base_tile_data.terrain_id); - - base_tile_data.state = tile_state::existing; - base_tile_data.pos = position; - base_tile_data.priority = this->priority(base_tile_data.terrain_id); - base_tile_data.subtexture_id = 0; - base_tile_data.blend_mode = -1; - base_tile_data.mask_id = -1; - - tile.data[tile.count] = base_tile_data; - tile.count += 1; - - // blendomatic!!111 - // see doc/media/blendomatic for the idea behind this. - if (blending_enabled) { - // the neighbors of the base tile - struct neighbor_tile neigh_data[8]; - - // get all neighbor tiles around position, reset the influence directions. - this->get_neighbors(position, neigh_data, this->meta->influences_buf.get()); - - // create influence list (direction, priority) - // strip and order influences, get the final influence data structure - struct influence_group influence_group = this->calculate_influences( - &base_tile_data, - neigh_data, - this->meta->influences_buf.get()); - - // create the draw_masks from the calculated influences - this->calculate_masks(position, &tile, &influence_group); - } - - return tile; -} - -void Terrain::get_neighbors(coord::tile basepos, - neighbor_tile *neigh_data, - influence *influences_by_terrain_id) { - // walk over all given neighbor tiles and store them to the influence list, - // group them by terrain id. - - for (int neigh_id = 0; neigh_id < 8; neigh_id++) { - // the current neighbor - auto neighbor = &neigh_data[neigh_id]; - - // calculate the pos of the neighbor tile - coord::tile neigh_pos = basepos + neigh_offsets[neigh_id]; - - // get the neighbor data - TileContent *neigh_content = this->get_data(neigh_pos); - - // chunk for neighbor or single tile is not existant - if (neigh_content == nullptr || neigh_content->terrain_id < 0) { - neighbor->state = tile_state::missing; - } - else { - neighbor->terrain_id = neigh_content->terrain_id; - neighbor->state = tile_state::existing; - neighbor->priority = this->priority(neighbor->terrain_id); - - // reset influence directions for this tile - influences_by_terrain_id[neighbor->terrain_id].direction = 0; - } - } -} - -struct influence_group Terrain::calculate_influences(struct tile_data *base_tile, - struct neighbor_tile *neigh_data, - struct influence *influences_by_terrain_id) { - // influences to actually draw (-> maximum 8) - struct influence_group influences {}; - influences.count = 0; - - // process adjacent neighbors first, - // then add diagonal influences, if no adjacent influence was found - constexpr int neigh_id_lookup[] = {1, 3, 5, 7, 0, 2, 4, 6}; - - for (int i = 0; i < 8; i++) { - // diagonal neighbors: (neigh_id % 2) == 0 - // adjacent neighbors: (neigh_id % 2) == 1 - - int neigh_id = neigh_id_lookup[i]; - bool is_adjacent_neighbor = neigh_id % 2 == 1; - bool is_diagonal_neighbor = not is_adjacent_neighbor; - - // the current neighbor_tile. - auto neighbor = &neigh_data[neigh_id]; - - // neighbor is nonexistant - if (neighbor->state == tile_state::missing) { - continue; - } - - // neighbor only interesting if it's a different terrain than the base. - // if it is the same id, the priorities are equal. - // neighbor draws over the base if it's priority is greater. - if (neighbor->priority > base_tile->priority) { - // get influence storage for the neighbor terrain id - // to group influences by id - auto influence = &influences_by_terrain_id[neighbor->terrain_id]; - - // check if diagonal influence is valid - if (is_diagonal_neighbor) { - // get the adjacent neighbors to the current diagonal - // influence - // (a & 0x07) == (a % 8) - uint8_t adj_neigh_0 = (neigh_id - 1) & 0x07; - uint8_t adj_neigh_1 = (neigh_id + 1) & 0x07; - - uint8_t neigh_mask = (1 << adj_neigh_0) | (1 << adj_neigh_1); - - // the adjacent neigbors are already influencing - // the current tile, therefore don't apply the diagonal mask - if ((influence->direction & neigh_mask) != 0) { - continue; - } - } - - // this terrain id hasn't had influence so far: - // add it to the list of influences. - if (influence->direction == 0) { - influences.terrain_ids[influences.count] = neighbor->terrain_id; - influences.count += 1; - } - - // as tile i has influence for this priority - // => bit i is set to 1 by 2^i - influence->direction |= 1 << neigh_id; - influence->priority = neighbor->priority; - influence->terrain_id = neighbor->terrain_id; - } - } - - // influences_by_terrain_id will be merged in the following, - // unused terrain ids will be dropped now. - - // shrink the big influence buffer that had entries for all terrains - // by copying the possible (max 8) influences to a separate buffer. - for (int k = 0; k < influences.count; k++) { - int relevant_id = influences.terrain_ids[k]; - influences.data[k] = influences_by_terrain_id[relevant_id]; - } - - // order the influences by their priority - for (int k = 1; k < influences.count; k++) { - struct influence tmp_influence = influences.data[k]; - - int l = k - 1; - while (l >= 0 && influences.data[l].priority > tmp_influence.priority) { - influences.data[l + 1] = influences.data[l]; - l -= 1; - } - - influences.data[l + 1] = tmp_influence; - } - - return influences; -} - - -void Terrain::calculate_masks(coord::tile position, - struct tile_draw_data *tile_data, - struct influence_group *influences) { - // influences are grouped by terrain id. - // the direction member has each bit set to 1 that is an influence from that direction. - // create a mask for this direction combination. - - // the base tile is stored at position 0 of the draw_mask - terrain_t base_terrain_id = tile_data->data[0].terrain_id; - - // iterate over all neighbors (with different terrain_ids) that have influence - for (ssize_t i = 0; i < influences->count; i++) { - // neighbor id of the current influence - char direction_bits = influences->data[i].direction; - - // all bits are 0 -> no influence directions stored. - // => no influence can be ignored. - if (direction_bits == 0) { - continue; - } - - terrain_t neighbor_terrain_id = influences->data[i].terrain_id; - int adjacent_mask_id = -1; - - /* neighbor ids: - 0 - 7 1 => 8 neighbors that can have influence on - 6 @ 2 the mask id selection. - 5 3 - 4 - */ - - // filter adjacent and diagonal influences neighbor_id: 76543210 - uint8_t direction_bits_adjacent = direction_bits & 0xAA; //0b10101010 - uint8_t direction_bits_diagonal = direction_bits & 0x55; //0b01010101 - - switch (direction_bits_adjacent) { - case 0x08: //0b00001000 - adjacent_mask_id = 0; //0..3 - break; - case 0x02: //0b00000010 - adjacent_mask_id = 4; //4..7 - break; - case 0x20: //0b00100000 - adjacent_mask_id = 8; //8..11 - break; - case 0x80: //0b10000000 - adjacent_mask_id = 12; //12..15 - break; - case 0x22: //0b00100010 - adjacent_mask_id = 20; - break; - case 0x88: //0b10001000 - adjacent_mask_id = 21; - break; - case 0xA0: //0b10100000 - adjacent_mask_id = 22; - break; - case 0x82: //0b10000010 - adjacent_mask_id = 23; - break; - case 0x28: //0b00101000 - adjacent_mask_id = 24; - break; - case 0x0A: //0b00001010 - adjacent_mask_id = 25; - break; - case 0x2A: //0b00101010 - adjacent_mask_id = 26; - break; - case 0xA8: //0b10101000 - adjacent_mask_id = 27; - break; - case 0xA2: //0b10100010 - adjacent_mask_id = 28; - break; - case 0x8A: //0b10001010 - adjacent_mask_id = 29; - break; - case 0xAA: //0b10101010 - adjacent_mask_id = 30; - break; - } - - // if it's the linear adjacent mask, cycle the 4 possible masks. - // e.g. long shorelines don't look the same then. - // maskid == 0x08 0x02 0x80 0x20 for that. - if (adjacent_mask_id <= 12 && adjacent_mask_id % 4 == 0) { - //we have 4 = 2^2 anti redundancy masks, so keep the last 2 bits - uint8_t anti_redundancy_offset = (position.ne + position.se) & 0x03; - adjacent_mask_id += anti_redundancy_offset; - } - - // get the blending mode (the mask selection) for this transition - // the mode is dependent on the two meeting terrain types - int blend_mode = this->get_blending_mode(base_terrain_id, neighbor_terrain_id); - - // append the mask for the adjacent blending - if (adjacent_mask_id >= 0) { - struct tile_data *overlay = &tile_data->data[tile_data->count]; - overlay->pos = position; - overlay->mask_id = adjacent_mask_id; - overlay->blend_mode = blend_mode; - overlay->terrain_id = neighbor_terrain_id; - overlay->subtexture_id = 0; - overlay->state = tile_state::existing; - - tile_data->count += 1; - } - - // append the mask for the diagonal blending - if (direction_bits_diagonal > 0) { - for (int l = 0; l < 4; l++) { - // generate one mask for each influencing diagonal neighbor id. - // even if they all have the same terrain_id, - // because we don't have combined diagonal influence masks. - - // l == 0: pos = 0b000000001, mask = 18 - // l == 1: pos = 0b000000100, mask = 16 - // l == 2: pos = 0b000010000, mask = 17 - // l == 3: pos = 0b001000000, mask = 19 - - int current_direction_bit = 1 << (l * 2); - constexpr int diag_mask_id_map[4] = {18, 16, 17, 19}; - - if (direction_bits_diagonal & current_direction_bit) { - struct tile_data *overlay = &tile_data->data[tile_data->count]; - overlay->pos = position; - overlay->mask_id = diag_mask_id_map[l]; - overlay->blend_mode = blend_mode; - overlay->terrain_id = neighbor_terrain_id; - overlay->subtexture_id = 0; - overlay->state = tile_state::existing; - - tile_data->count += 1; - } - } - } - } -} - -} // namespace openage diff --git a/libopenage/terrain/terrain.h b/libopenage/terrain/terrain.h deleted file mode 100644 index 02ceba1cae..0000000000 --- a/libopenage/terrain/terrain.h +++ /dev/null @@ -1,372 +0,0 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "../coord/chunk.h" -#include "../coord/phys.h" -#include "../coord/pixel.h" -#include "../coord/tile.h" -#include "../util/misc.h" - -namespace openage { - -class RenderOptions; -class TerrainChunk; - -/** - * type that for terrain ids. - * it's signed so that -1 can indicate a missing tile. - * TODO: get rid of the signedness. - */ -using terrain_t = int; - -/** - * hashing for chunk coordinates. - * - * this allows storage of chunk coords as keys in an unordered map. - */ -struct coord_chunk_hash { - size_t operator()(const coord::chunk &input) const { - constexpr int half_size_t_bits = sizeof(size_t) * 4; - - return ((size_t)input.ne << half_size_t_bits) | input.se; - } -}; - - -/** - * describes the properties of one terrain tile. - * - * this includes the terrain_id (ice, water, grass, ...) - * and the list of objects which have a bounding box overlapping the tile - */ -class TileContent { -public: - TileContent(); - ~TileContent(); - terrain_t terrain_id; -}; - - -/** - * coordinate offsets for getting tile neighbors by their id. - */ -constexpr coord::tile_delta const neigh_offsets[] = { - {1, -1}, - {1, 0}, - {1, 1}, - {0, 1}, - {-1, 1}, - {-1, 0}, - {-1, -1}, - {0, -1}}; - - -/** - * describes the state of a terrain tile. - */ -enum class tile_state { - missing, //!< tile is not created yet - existing, //!< tile is already existing - creatable, //!< tile does not exist but can be created - invalid, //!< tile does not exist and can not be created -}; - - -/** - * storage for influences by neighbor tiles. - */ -struct influence { - uint8_t direction; //!< bitmask for influence directions, bit 0 = neighbor 0, etc. - int priority; //!< the blending priority for this influence - terrain_t terrain_id; //!< the terrain id of the influence -}; - -/** - * influences for one tile. - * as a tile has 8 adjacent and diagonal neighbors, - * the maximum number of influences is 8. - */ -struct influence_group { - int count; - terrain_t terrain_ids[8]; - struct influence data[8]; -}; - -/** - * one influence on another tile. - */ -struct neighbor_tile { - terrain_t terrain_id; - tile_state state; - int priority; -}; - -/** - * storage data for a single terrain tile. - */ -struct tile_data { - terrain_t terrain_id; - coord::tile pos{0, 0}; - int subtexture_id; - int priority; - int mask_id; - int blend_mode; - tile_state state; -}; - -/** - * collection of drawing data for a single tile. - * because of influences, a maximum of 8+1 draws - * could be requested. - */ -struct tile_draw_data { - ssize_t count; - struct tile_data data[9]; -}; - -/** - * the complete render instruction collection for the terrain. - * this is passed to the renderer and will be drawn on screen. - */ -struct terrain_render_data { - std::vector tiles; -}; - -/** - * specification for all available - * tile types and blending data - */ -struct terrain_meta { - size_t terrain_id_count; - size_t blendmode_count; - - std::unique_ptr terrain_id_priority_map; - std::unique_ptr terrain_id_blendmode_map; - - std::unique_ptr influences_buf; -}; - -/** - * the terrain class is the main top-management interface - * for dealing with cost-benefit analysis to maximize company profits. - * - * actually this is just the entrypoint and container for the terrain chunks. - */ -class Terrain { -public: - Terrain(terrain_meta *meta, bool is_infinite); - ~Terrain(); - - bool infinite; //!< chunks are automagically created as soon as they are referenced - - // TODO: finite terrain limits - // TODO: non-square shaped terrain bounds - - /** - * returns a list of all referenced chunks - */ - std::vector used_chunks() const; - - /** - * fill the terrain with given terrain_id values. - * @returns whether the data filled on the terrain was cut because of - * the terrains size limit. - */ - bool fill(const int *data, const coord::tile_delta &size); - - /** - * Attach a chunk to the terrain, to a given position. - * - * @param new_chunk The chunk to be attached - * @param position The chunk position where the chunk will be placed - * @param manually_created Was this chunk created manually? If true, it will not be free'd automatically - */ - void attach_chunk(TerrainChunk *new_chunk, const coord::chunk &position, bool manual = true); - - /** - * get a terrain chunk by a given chunk position. - * - * @return the chunk if exists, nullptr else - */ - TerrainChunk *get_chunk(const coord::chunk &position); - - /** - * get a terrain chunk by a given tile position. - * - * @return the chunk it exists, nullptr else - */ - TerrainChunk *get_chunk(const coord::tile &position); - - /** - * get or create a terrain chunk for a given chunk position. - * - * @return the (maybe newly created) chunk - */ - TerrainChunk *get_create_chunk(const coord::chunk &position); - - /** - * get or create a terrain chunk for a given tile position. - * - * @return the (maybe newly created) chunk - */ - TerrainChunk *get_create_chunk(const coord::tile &position); - - /** - * return tile data for the given position. - * - * the only reason the chunks exist, is because of this data. - */ - TileContent *get_data(const coord::tile &position); - - /** - * get the neighbor chunks of a given chunk. - * - * - * chunk neighbor ids: - * 0 / <- ne - * 7 1 - * 6 @ 2 - * 5 3 - * 4 \ <- se - * - * ne se - * 0: 1 -1 - * 1: 1 0 - * 2: 1 1 - * 3: 0 1 - * 4: -1 1 - * 5: -1 0 - * 6: -1 -1 - * 7: 0 -1 - * - * @param position: the position of the center chunk. - */ - struct chunk_neighbors get_chunk_neighbors(const coord::chunk &position); - - /** - * return the subtexture offset id for a given tile position. - * the maximum offset is determined by the atlas size. - * - * this function returns always the right value, so that neighbor tiles - * of the same terrain (like grass-grass) are matching (without blendomatic). - * -> e.g. grass only map. - */ - unsigned get_subtexture_id(const coord::tile &pos, unsigned atlas_size); - - /** - * checks the creation state and premissions of a given tile position. - */ - tile_state check_tile(const coord::tile &position); - - /** - * checks whether the given tile position is allowed to exist on this terrain. - */ - // TODO: rename to is_tile_position_valid - bool check_tile_position(const coord::tile &position); - - /** - * validate whether the given terrain id is available. - */ - bool validate_terrain(terrain_t terrain_id); - - /** - * validate whether the given mask id is available. - */ - bool validate_mask(ssize_t mask_id); - - /** - * return the blending priority for a given terrain id. - */ - int priority(terrain_t terrain_id); - - /** - * return the blending mode/blendomatic mask set for a given terrain id. - */ - int blendmode(terrain_t terrain_id); - - /** - * return the blending mode id for two given neighbor ids. - */ - int get_blending_mode(terrain_t base_id, terrain_t neighbor_id); - - /** - * create the drawing instruction data. - * - * created draw data according to the given tile boundaries. - * - * - * @param ab: upper left tile - * @param cd: upper right tile - * @param ef: lower right tile - * @param gh: lower left tile - * - * @returns a drawing instruction struct that contains all information for rendering - */ - struct terrain_render_data create_draw_advice(const coord::tile &ab, - const coord::tile &cd, - const coord::tile &ef, - const coord::tile &gh, - bool blending_enabled); - - /** - * create rendering and blending information for a single tile on the terrain. - */ - struct tile_draw_data create_tile_advice(coord::tile position, bool blending_enabled); - - /** - * gather neighbors of a given base tile. - * - * @param basepos: the base position, around which the neighbors will be fetched - * @param neigh_tiles: the destination buffer where the neighbors will be stored - * @param influences_by_terrain_id: influence buffer that is reset in the same step - */ - void get_neighbors(coord::tile basepos, - struct neighbor_tile *neigh_tiles, - struct influence *influences_by_terrain_id); - - /** - * look at neighbor tiles around the base_tile, and store the influence bits. - * - * @param base_tile: the base tile for which influences are calculated - * @param neigh_tiles: the neigbors of base_tile - * @param influences_by_terrain_id: influences will be stored to this buffer, as bitmasks - * @returns an influence group that describes the maximum 8 possible influences on the base_tile - */ - struct influence_group calculate_influences(struct tile_data *base_tile, - struct neighbor_tile *neigh_tiles, - struct influence *influences_by_terrain_id); - - /** - * calculate blending masks for a given tile position. - * - * @param position: the base tile position, for which the masks are calculated - * @param tile_data: the buffer where the created drawing layers will be stored in - * @param influences: the buffer where calculated influences were stored to - * - * @see calculate_influences - */ - void calculate_masks(coord::tile position, - struct tile_draw_data *tile_data, - struct influence_group *influences); - -private: - /** - * terrain meta data - */ - terrain_meta *meta; - - /** - * maps chunk coordinates to chunks. - */ - std::unordered_map chunks; -}; - -} // namespace openage diff --git a/libopenage/terrain/terrain_chunk.cpp b/libopenage/terrain/terrain_chunk.cpp deleted file mode 100644 index 0fd77923dc..0000000000 --- a/libopenage/terrain/terrain_chunk.cpp +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. - -#include "terrain_chunk.h" - -#include - -#include "../coord/phys.h" -#include "../coord/pixel.h" -#include "../coord/tile.h" -#include "../error/error.h" -#include "../log/log.h" -#include "../util/misc.h" - -#include "terrain.h" - -namespace openage { - - -TerrainChunk::TerrainChunk() : - manually_created{true} { - this->tile_count = std::pow(chunk_size, 2); - - // the data array for this chunk. - // each element describes the tile data. - this->data = new TileContent[this->tile_count]; - - // initialize all neighbors as nonexistant - for (int i = 0; i < 8; i++) { - this->neighbors.neighbor[i] = nullptr; - } - - log::log(MSG(dbg) << "Terrain chunk created: " - << "size=" << chunk_size << ", " - << "tiles=" << this->tile_count); -} - - -TerrainChunk::~TerrainChunk() { - delete[] this->data; -} - -TileContent *TerrainChunk::get_data(coord::tile abspos) { - return this->get_data(abspos.get_pos_on_chunk()); -} - -TileContent *TerrainChunk::get_data(coord::tile_delta pos) { - return this->get_data(this->tile_position(pos)); -} - -TileContent *TerrainChunk::get_data(size_t pos) { - return &this->data[pos]; -} - -TileContent *TerrainChunk::get_data_neigh(coord::tile_delta pos) { - // determine the neighbor id by the given position - int neighbor_id = this->neighbor_id_by_pos(pos); - - // if the location is not on the current chunk, the neighbor id is != -1 - if (neighbor_id != -1) { - // get the chunk where the requested neighbor tile lies on. - TerrainChunk *neigh_chunk = this->neighbors.neighbor[neighbor_id]; - - // this neighbor does not exist, so the tile does not exist. - if (neigh_chunk == nullptr) { - return nullptr; - } - - // get position of tile on neighbor - size_t pos_on_neighbor = this->tile_position_neigh(pos); - - return neigh_chunk->get_data(pos_on_neighbor); - } - // the position lies on the current chunk. - else { - return this->get_data(pos); - } -} - -/* - * get the chunk neighbor id by a given position not lying on this chunk. - * - * neighbor ids: - * - * ne - * -- - * /| - * 0 - * 7 1 - * 6 @ 2 - * 5 3 - * 4 - * \| - * -- - * se - */ -int TerrainChunk::neighbor_id_by_pos(coord::tile_delta pos) { - int neigh_id = -1; - - if (pos.ne < 0) { - if (pos.se < 0) { - neigh_id = 6; - } - else if (pos.se >= (ssize_t)chunk_size) { - neigh_id = 4; - } - else { - neigh_id = 5; - } - } - else if (pos.ne >= (ssize_t)chunk_size) { - if (pos.se < 0) { - neigh_id = 0; - } - else if (pos.se >= (ssize_t)chunk_size) { - neigh_id = 2; - } - else { - neigh_id = 1; - } - } - else { - if (pos.se < 0) { - neigh_id = 7; - } - else if (pos.se >= (ssize_t)chunk_size) { - neigh_id = 3; - } - else { - neigh_id = -1; - } - } - return neigh_id; -} - -/* - * calculates the memory position of a given tile location. - * - * give this function isometric coordinates, it returns the tile index. - * - * # is a single terrain tile: - * - * 3 - * 2 # - * 1 # # - * ne= 0 # * # - * # # # # - * se= 0 # # # - * 1 # # - * 2 # - * 3 - * - * for example, * is at position (2, 1) - * the returned index would be 6 (count for each ne row, starting at se=0) - */ -size_t TerrainChunk::tile_position(coord::tile_delta pos) { - if (this->neighbor_id_by_pos(pos) != -1) { - throw Error(MSG(err) << "Tile " - "(" - << pos.ne << ", " << pos.se << ") " - "has been requested, but is not part of this chunk."); - } - - return pos.se * chunk_size + pos.ne; -} - -size_t TerrainChunk::tile_position_neigh(coord::tile_delta pos) { - // get position of tile on neighbor - pos.ne = util::mod(pos.ne); - pos.se = util::mod(pos.se); - - return pos.se * chunk_size + pos.ne; -} - -size_t TerrainChunk::get_tile_count() { - return this->tile_count; -} - -size_t TerrainChunk::get_size() { - return chunk_size; -} - -void TerrainChunk::set_terrain(Terrain *parent) { - this->terrain = parent; -} - -} // namespace openage diff --git a/libopenage/terrain/terrain_chunk.h b/libopenage/terrain/terrain_chunk.h deleted file mode 100644 index 683d46c0fd..0000000000 --- a/libopenage/terrain/terrain_chunk.h +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include - -#include "../coord/pixel.h" -#include "../coord/tile.h" -#include "../util/file.h" - -namespace openage { - -class Terrain; -class TerrainChunk; -class TileContent; - - -/** -the number of tiles per direction on a chunk -*/ -constexpr size_t chunk_size = 16; - -/** -adjacent neighbors of a chunk. - -neighbor ids: - 0 - 7 1 - 6 @ 2 - 5 3 - 4 -*/ -struct chunk_neighbors { - TerrainChunk *neighbor[8]; -}; - -/** -terrain chunk class represents one chunk of the the drawn terrain. -*/ -class TerrainChunk { -public: - TerrainChunk(); - ~TerrainChunk(); - - /** - * stores the length for one chunk side. - */ - size_t size; - - /** - * number of tiles on that chunk (this->size^2) - */ - size_t tile_count; - - /** - * stores the chunk data, one tile_content struct for each tile. - */ - TileContent *data; - - /** - * the terrain to which this chunk belongs to. - */ - Terrain *terrain; - - /** - * the 8 neighbors this chunk has. - */ - chunk_neighbors neighbors; - - /** - * get tile data by absolute coordinates. - */ - TileContent *get_data(coord::tile abspos); - - /** - * get tile data by coordinates that are releative to this chunk. - */ - TileContent *get_data(coord::tile_delta pos); - - /** - * get tile data by memory position. - */ - TileContent *get_data(size_t pos); - - /** - * get the tile data a given tile position relative to this chunk. - * - * also queries neighbors if the position is not on this chunk. - */ - TileContent *get_data_neigh(coord::tile_delta pos); - - int neighbor_id_by_pos(coord::tile_delta pos); - - size_t tile_position(coord::tile_delta pos); - size_t tile_position_neigh(coord::tile_delta pos); - size_t get_tile_count(); - - size_t tiles_in_row(unsigned int row); - size_t get_size(); - - void set_terrain(Terrain *parent); - - bool manually_created; -}; - -} // namespace openage From 1718b672ce14c880aa2221ab1af77e6958c2fb83 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 11 Nov 2023 22:09:48 +0100 Subject: [PATCH 073/771] util: Remove CSV reader. --- libopenage/util/CMakeLists.txt | 1 - libopenage/util/csv.cpp | 69 ----------- libopenage/util/csv.h | 216 --------------------------------- 3 files changed, 286 deletions(-) delete mode 100644 libopenage/util/csv.cpp delete mode 100644 libopenage/util/csv.h diff --git a/libopenage/util/CMakeLists.txt b/libopenage/util/CMakeLists.txt index 841a00757f..1a2bee51e7 100644 --- a/libopenage/util/CMakeLists.txt +++ b/libopenage/util/CMakeLists.txt @@ -2,7 +2,6 @@ add_sources(libopenage color.cpp compiler.cpp constinit_vector.cpp - csv.cpp enum.cpp enum_test.cpp externalprofiler.cpp diff --git a/libopenage/util/csv.cpp b/libopenage/util/csv.cpp deleted file mode 100644 index e0bf825c3b..0000000000 --- a/libopenage/util/csv.cpp +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. - -#include "csv.h" - -#include - -#include "file.h" -#include "../error/error.h" -#include "../log/log.h" - - -namespace openage { -namespace util { - - -CSVCollection::CSVCollection(const Path &entryfile_path) { - - auto file = entryfile_path.open(); - log::log(DBG << "Loading csv collection: " << file); - - // The file format is defined in: - // openage/convert/dataformat/data_definition.py - // example: - // - // ### some/folder/and/filename.docx - // # irrelevant - // # comments - // data,stuff,moar,bla - - // store the currently read file name - std::string current_file; - - for (auto &line : file.get_lines()) { - // a new file starts: - if (line[0] == '#' and - line[1] == '#' and - line[2] == '#' and - line[3] == ' ') { - - // remove the "### " - current_file = (line.erase(0, 4)); - - // create a vector to put lines in - this->data.emplace(current_file, std::vector{}); - } - else { - if (line.empty() or line[0] == '#') { - continue; - } - - if (current_file.size() > 0) [[likely]] { - // add line to the current file linelist - this->data.at(current_file).push_back(line); - } - else { - throw Error{ - ERR << "csv collection content encountered " - << "without known target in " << entryfile_path - << ", linedata: " << line - }; - } - } - } - - log::log(INFO << "Loaded multi-csv file: " - << this->data.size() << " sub-files"); -} - -}} // openage::util diff --git a/libopenage/util/csv.h b/libopenage/util/csv.h deleted file mode 100644 index 88d5ea0322..0000000000 --- a/libopenage/util/csv.h +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright 2013-2017 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "../error/error.h" -#include "compiler.h" -#include "fslike/native.h" -#include "path.h" - - -namespace openage { -namespace util { - - -/** - * Collection of multiple csv files. - * Read from a packed csv that contains all the data. - * - * Then, data can be read recursively. - */ -class CSVCollection { -public: - - /** - * Type for storing csv data: - * {filename: [line, ...]}. - */ - using csv_file_map_t = std::unordered_map>; - - /** - * Initialize the collection by reading the given file. - * This file must contain the data that this collection is made up of. - */ - explicit CSVCollection(const Path &entryfile); - virtual ~CSVCollection() = default; - - - /** - * This function is the entry point to load the whole file tree recursively. - * - * Should be called again from the .recurse() method of the struct. - * - * The internal flow is as follows: - * * read entries of the given files - * (call to the generated field parsers (the fill function)) - * * then, recurse into referenced subdata entries - * (this implementation is generated) - * * from there, reach this function again to read each subdata entry. - * - */ - template - std::vector read(const std::string &filename) const { - - // first read the content from the data - auto result = this->get_data(filename); - - std::string dir = dirname(filename); - - size_t line_count = 0; - - // then recurse into each subdata entry. - for (auto &entry : result) { - line_count += 1; - - if (not entry.recurse(*this, dir)) { - throw Error{ - MSG(err) - << "Failed to read follow-up files for " - << filename << ":" << line_count - }; - } - } - - return result; - } - - - /** - * Parse the data from one file in the map. - */ - template - std::vector get_data(const std::string &filename) const { - - size_t line_count = 0; - - std::vector ret; - - // locate the data in the collection - auto it = this->data.find(filename); - - if (it != std::end(this->data)) { - const std::vector &lines = it->second; - - for (auto &line : lines) { - line_count += 1; - lineformat current_line_data; - - // use the line copy to fill the current line struct. - int error_column = current_line_data.fill(line); - if (error_column != -1) { - throw Error{ - ERR - << "Failed to read CSV file: " - << filename << ":" << line_count << ":" << error_column - << ": error parsing " << line - }; - } - - ret.push_back(current_line_data); - } - } - else { - throw Error{ - ERR << "File was not found in csv cache: '" - << filename << "'" - }; - } - - return ret; - } - -protected: - csv_file_map_t data; -}; - - -/** - * Referenced file tree structure. - * - * Used for storing information for subtype members - * that need to be recursed. - */ -template -struct csv_subdata { - /** - * File where to read subdata from. - * This name is relative to the file that defined the subdata! - */ - std::string filename; - - /** - * Data that was read from the file with this->filename. - */ - std::vector data; - - /** - * Read the data of the lineformat from the collection. - * Can descend recursively into dependencies. - */ - bool read(const CSVCollection &collection, const std::string &basedir) { - std::string next_file = basedir; - if (basedir.size() > 0) { - next_file += fslike::PATHSEP; - } - next_file += this->filename; - - this->data = collection.read(next_file); - return true; - } - - /** - * Convenience operator to access data - */ - const lineformat &operator [](size_t idx) const { - return this->data[idx]; - } -}; - - -/** - * read a single csv file. - * call the destination struct .fill() method for actually storing line data - */ -template -std::vector read_csv_file(const Path &path) { - - File csv = path.open(); - - std::vector ret; - size_t line_count = 0; - - for (auto &line : csv.get_lines()) { - line_count += 1; - - // ignore comments and empty lines - if (line.empty() || line[0] == '#') { - continue; - } - - lineformat current_line_data; - - // use the line copy to fill the current line struct. - int error_column = current_line_data.fill(line); - if (error_column != -1) { - throw Error{ - ERR - << "Failed to read CSV file: " - << csv << ":" << line_count << ":" << error_column - << ": error parsing " << line - }; - } - - ret.push_back(current_line_data); - } - - return ret; -} - -}} // openage::util From 040ead866b682780d1b18816a75cdfd280f14b8a Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 11 Nov 2023 22:13:42 +0100 Subject: [PATCH 074/771] renderer: Remove unused animation class. --- libopenage/renderer/CMakeLists.txt | 1 - libopenage/renderer/animation.cpp | 14 ------------- libopenage/renderer/animation.h | 33 ------------------------------ 3 files changed, 48 deletions(-) delete mode 100644 libopenage/renderer/animation.cpp delete mode 100644 libopenage/renderer/animation.h diff --git a/libopenage/renderer/CMakeLists.txt b/libopenage/renderer/CMakeLists.txt index b6578cd128..756e9e4d98 100644 --- a/libopenage/renderer/CMakeLists.txt +++ b/libopenage/renderer/CMakeLists.txt @@ -1,5 +1,4 @@ add_sources(libopenage - animation.cpp color.cpp definitions.cpp geometry.cpp diff --git a/libopenage/renderer/animation.cpp b/libopenage/renderer/animation.cpp deleted file mode 100644 index a9b0a54e37..0000000000 --- a/libopenage/renderer/animation.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2021-2021 the openage authors. See copying.md for legal info. - -#include "animation.h" - -namespace openage::renderer { - -Animation2d::Animation2d(const resources::Animation2dInfo &info) : - info{info} {} - -const resources::Animation2dInfo &Animation2d::get_info() const { - return this->info; -} - -} // namespace openage::renderer diff --git a/libopenage/renderer/animation.h b/libopenage/renderer/animation.h deleted file mode 100644 index aa5e27f8cf..0000000000 --- a/libopenage/renderer/animation.h +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2021-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include "renderer/resources/animation/animation_info.h" - -namespace openage::renderer { - -class Animation2d { -public: - Animation2d() = default; - ~Animation2d() = default; - - /** - * Get the animation information. - * - * @return Animation information. - */ - const resources::Animation2dInfo &get_info() const; - -protected: - /** - * Creates a 2D animation. - */ - Animation2d(const resources::Animation2dInfo &info); - - /** - * Information about the animation layers, angles and frames. - */ - resources::Animation2dInfo info; -}; - -} // namespace openage::renderer From 0462af27a5e11125ced2bcd8825db51e064d2212 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 11 Nov 2023 22:19:18 +0100 Subject: [PATCH 075/771] Fix CI complaints. --- .../renderer/gui/guisys/private/gui_application_impl.h | 2 +- libopenage/renderer/gui/guisys/public/gui_application.cpp | 2 +- openage/convert/entity_object/export/metadata_export.py | 6 ++++-- .../convert/processor/conversion/aoc/media_subprocessor.py | 2 +- .../convert/processor/conversion/hd/media_subprocessor.py | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/libopenage/renderer/gui/guisys/private/gui_application_impl.h b/libopenage/renderer/gui/guisys/private/gui_application_impl.h index f772c77a0f..7af12c0fa4 100644 --- a/libopenage/renderer/gui/guisys/private/gui_application_impl.h +++ b/libopenage/renderer/gui/guisys/private/gui_application_impl.h @@ -1,4 +1,4 @@ -// Copyright 2015-2022 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once diff --git a/libopenage/renderer/gui/guisys/public/gui_application.cpp b/libopenage/renderer/gui/guisys/public/gui_application.cpp index 80cba97bd2..cd07289f87 100644 --- a/libopenage/renderer/gui/guisys/public/gui_application.cpp +++ b/libopenage/renderer/gui/guisys/public/gui_application.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2022 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #include diff --git a/openage/convert/entity_object/export/metadata_export.py b/openage/convert/entity_object/export/metadata_export.py index b07b8da2e3..d01e1d53f5 100644 --- a/openage/convert/entity_object/export/metadata_export.py +++ b/openage/convert/entity_object/export/metadata_export.py @@ -15,8 +15,10 @@ if typing.TYPE_CHECKING: from openage.util.observer import Observable - from openage.convert.entity_object.export.formats.sprite_metadata import LayerMode as SpriteLayerMode - from openage.convert.entity_object.export.formats.terrain_metadata import LayerMode as TerrainLayerMode + from openage.convert.entity_object.export.formats.sprite_metadata import LayerMode\ + as SpriteLayerMode + from openage.convert.entity_object.export.formats.terrain_metadata import LayerMode\ + as TerrainLayerMode class MetadataExport(Observer): diff --git a/openage/convert/processor/conversion/aoc/media_subprocessor.py b/openage/convert/processor/conversion/aoc/media_subprocessor.py index bb2326e367..90af7ad297 100644 --- a/openage/convert/processor/conversion/aoc/media_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/media_subprocessor.py @@ -1,6 +1,6 @@ # Copyright 2019-2023 the openage authors. See copying.md for legal info. # -# pylint: disable=too-many-locals,too-few-public-methods +# pylint: disable=too-many-locals,too-few-public-methods,too-many-statements """ Convert media information to metadata definitions and export requests. Subroutine of the main AoC processor. diff --git a/openage/convert/processor/conversion/hd/media_subprocessor.py b/openage/convert/processor/conversion/hd/media_subprocessor.py index 74d4c5350d..62dc1dd719 100644 --- a/openage/convert/processor/conversion/hd/media_subprocessor.py +++ b/openage/convert/processor/conversion/hd/media_subprocessor.py @@ -1,6 +1,6 @@ # Copyright 2021-2023 the openage authors. See copying.md for legal info. # -# pylint: disable=too-many-locals +# pylint: disable=too-many-locals,too-many-statements """ Convert media information to metadata definitions and export From 547a5f270e419942c2ed18ea0f418a02d16a25ae Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 11 Nov 2023 22:31:48 +0100 Subject: [PATCH 076/771] Fix clang complaints. --- libopenage/curve/map_filter_iterator.h | 2 -- libopenage/gamestate/game.cpp | 2 +- libopenage/gamestate/terrain_factory.cpp | 3 +- .../gui/integration/private/gui_texture.cpp | 36 +++++++++---------- .../private/gui_texture_handle.cpp | 2 +- libopenage/renderer/opengl/window.cpp | 2 +- 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/libopenage/curve/map_filter_iterator.h b/libopenage/curve/map_filter_iterator.h index e514330f67..5e71c0789f 100644 --- a/libopenage/curve/map_filter_iterator.h +++ b/libopenage/curve/map_filter_iterator.h @@ -32,8 +32,6 @@ class MapFilterIterator : public CurveIterator { const time::time_t &to) : CurveIterator(base, container, from, to) {} - MapFilterIterator(const MapFilterIterator &) = default; - using CurveIterator::operator=; virtual bool valid() const override { diff --git a/libopenage/gamestate/game.cpp b/libopenage/gamestate/game.cpp index 121bef4f56..8e3497bfbc 100644 --- a/libopenage/gamestate/game.cpp +++ b/libopenage/gamestate/game.cpp @@ -96,7 +96,7 @@ void Game::load_path(const util::Path &base_dir, auto base_path = base_dir.resolve_native_path(); auto search_path = base_dir / mod_dir / search; - auto fileload_func = [&base_path, &mod_dir](const std::string &filename) { + auto fileload_func = [&base_path](const std::string &filename) { // nyan wants a string filepath, so we have to construct it from the // path and subpath parameters log::log(INFO << "Loading .nyan file: " << filename); diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp index a3abd0cb42..3a46773429 100644 --- a/libopenage/gamestate/terrain_factory.cpp +++ b/libopenage/gamestate/terrain_factory.cpp @@ -9,6 +9,7 @@ #include "gamestate/api/terrain.h" #include "gamestate/terrain.h" #include "gamestate/terrain_chunk.h" +#include "gamestate/terrain_tile.h" #include "renderer/render_factory.h" #include "renderer/stages/terrain/terrain_render_entity.h" #include "time/time.h" @@ -121,7 +122,7 @@ std::shared_ptr TerrainFactory::add_chunk(const std::shared_ptr tiles{}; tiles.reserve(size[0] * size[1]); for (size_t i = 0; i < size[0] * size[1]; ++i) { - tiles.emplace_back(terrain_obj, test_texture_path, 0.0f); + tiles.push_back({terrain_obj, test_texture_path, terrain_elevation_t::zero()}); } auto chunk = std::make_shared(size, offset, std::move(tiles)); diff --git a/libopenage/renderer/gui/integration/private/gui_texture.cpp b/libopenage/renderer/gui/integration/private/gui_texture.cpp index c61b1ffe1b..d1c568eae6 100644 --- a/libopenage/renderer/gui/integration/private/gui_texture.cpp +++ b/libopenage/renderer/gui/integration/private/gui_texture.cpp @@ -40,33 +40,33 @@ bool GuiTexture::isAtlasTexture() const { } namespace { -GLuint create_compatible_texture(GLuint texture_id, GLsizei w, GLsizei h) { - glBindTexture(GL_TEXTURE_2D, texture_id); +// GLuint create_compatible_texture(GLuint texture_id, GLsizei w, GLsizei h) { +// glBindTexture(GL_TEXTURE_2D, texture_id); - GLint min_filter; - GLint mag_filter; - GLint iformat; +// GLint min_filter; +// GLint mag_filter; +// GLint iformat; - glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, &min_filter); - glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, &mag_filter); - glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &iformat); +// glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, &min_filter); +// glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, &mag_filter); +// glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &iformat); - GLuint new_texture_id; - glGenTextures(1, &new_texture_id); - glBindTexture(GL_TEXTURE_2D, new_texture_id); +// GLuint new_texture_id; +// glGenTextures(1, &new_texture_id); +// glBindTexture(GL_TEXTURE_2D, new_texture_id); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter); +// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter); +// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter); - glTexImage2D(GL_TEXTURE_2D, 0, iformat, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); +// glTexImage2D(GL_TEXTURE_2D, 0, iformat, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); - glBindTexture(GL_TEXTURE_2D, 0); +// glBindTexture(GL_TEXTURE_2D, 0); - return new_texture_id; -} +// return new_texture_id; +// } } // namespace -QSGTexture *GuiTexture::removedFromAtlas(QRhiResourceUpdateBatch *resourceUpdates /* = nullptr */) const { +QSGTexture *GuiTexture::removedFromAtlas(QRhiResourceUpdateBatch * /* resourceUpdates */ /* = nullptr */) const { if (this->isAtlasTexture()) { return this->standalone.get(); } diff --git a/libopenage/renderer/gui/integration/private/gui_texture_handle.cpp b/libopenage/renderer/gui/integration/private/gui_texture_handle.cpp index 07a840dbf1..1174d933b3 100644 --- a/libopenage/renderer/gui/integration/private/gui_texture_handle.cpp +++ b/libopenage/renderer/gui/integration/private/gui_texture_handle.cpp @@ -25,7 +25,7 @@ QSize textureSize(const SizedTextureHandle &texture_handle) { return texture_handle.size; } -QSize native_size(const TextureHandle &texture_handle) { +QSize native_size(const TextureHandle & /* texture_handle */) { return QSize(0, 0); } diff --git a/libopenage/renderer/opengl/window.cpp b/libopenage/renderer/opengl/window.cpp index 68ac012ed1..d169bff00b 100644 --- a/libopenage/renderer/opengl/window.cpp +++ b/libopenage/renderer/opengl/window.cpp @@ -146,7 +146,7 @@ std::shared_ptr GlWindow::make_renderer() { auto renderer = std::make_shared(this->get_context(), this->size * this->scale_dpr); - this->add_resize_callback([this, renderer](size_t w, size_t h, double scale) { + this->add_resize_callback([renderer](size_t w, size_t h, double scale) { // this up-scales all the default framebuffer to the "bigger" highdpi window. renderer->resize_display_target(w * scale, h * scale); }); From 5f0865e2fbd0152e853336ba2c9ad4fc26377e7a Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 11 Nov 2023 22:45:58 +0100 Subject: [PATCH 077/771] util: Make type hint Python 3.9 compliant. --- openage/util/fslike/path.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openage/util/fslike/path.py b/openage/util/fslike/path.py index 422b9f176b..ccdf8cc2b8 100644 --- a/openage/util/fslike/path.py +++ b/openage/util/fslike/path.py @@ -4,7 +4,7 @@ Provides Path, which is analogous to pathlib.Path, and the type of FSLikeObject.root. """ -from typing import NoReturn +from typing import NoReturn, Union from io import UnsupportedOperation, TextIOWrapper import os @@ -35,7 +35,7 @@ class Path: # lower. # pylint: disable=too-many-public-methods - def __init__(self, fsobj, parts: str | bytes | bytearray | list | tuple = None): + def __init__(self, fsobj, parts: Union[str, bytes, bytearray, list, tuple] = None): if isinstance(parts, str): parts = parts.encode() From 252076e787d72df8d982b5720a5bdc731d3487fd Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 13 Dec 2023 01:56:04 +0100 Subject: [PATCH 078/771] console: Fix comment formatting. --- libopenage/console/console.cpp | 208 +++++++++++++++++---------------- libopenage/console/draw.cpp | 136 ++++++++++----------- 2 files changed, 175 insertions(+), 169 deletions(-) diff --git a/libopenage/console/console.cpp b/libopenage/console/console.cpp index 274cbc087e..a07d42b898 100644 --- a/libopenage/console/console.cpp +++ b/libopenage/console/console.cpp @@ -44,79 +44,83 @@ Console::Console(/* presenter::LegacyDisplay *display */) : Console::~Console() {} -// void Console::load_colors(std::vector &colortable) { -// for (auto &c : colortable) { -// this->termcolors.emplace_back(c); -// } +/* +void Console::load_colors(std::vector &colortable) { + for (auto &c : colortable) { + this->termcolors.emplace_back(c); + } -// if (termcolors.size() != 256) { -// throw Error(MSG(err) << "Exactly 256 terminal colors are required."); -// } -// } + if (termcolors.size() != 256) { + throw Error(MSG(err) << "Exactly 256 terminal colors are required."); + } +} +*/ void Console::register_to_engine() { // TODO: Use new renderer + /* + this->display->register_input_action(this); + this->display->register_tick_action(this); + this->display->register_drawhud_action(this); + this->display->register_resize_action(this); + + Bind the console toggle key globally + auto &action = this->display->get_action_manager(); + auto &global = this->display->get_input_manager().get_global_context(); + + global.bind(action.get("TOGGLE_CONSOLE"), [this](const input::legacy::action_arg_t &) { + this->set_visible(!this->visible); + }); + - // this->display->register_input_action(this); - // this->display->register_tick_action(this); - // this->display->register_drawhud_action(this); - // this->display->register_resize_action(this); - - // Bind the console toggle key globally - // auto &action = this->display->get_action_manager(); - // auto &global = this->display->get_input_manager().get_global_context(); - - // global.bind(action.get("TOGGLE_CONSOLE"), [this](const input::legacy::action_arg_t &) { - // this->set_visible(!this->visible); - // }); - - - // TODO: bind any needed input to InputContext - - // toggle console will take highest priority - // this->input_context.bind(action.get("TOGGLE_CONSOLE"), [this](const input::legacy::action_arg_t &) { - // this->set_visible(false); - // }); - // this->input_context.bind(input::legacy::event_class::UTF8, [this](const input::legacy::action_arg_t &arg) { - // // a single char typed into the console - // std::string utf8 = arg.e.as_utf8(); - // this->buf.write(utf8.c_str()); - // command += utf8; - // return true; - // }); - // this->input_context.bind(input::legacy::event_class::NONPRINT, [this](const input::legacy::action_arg_t &arg) { - // switch (arg.e.as_char()) { - // case 8: // remove a single UTF-8 character - // if (this->command.size() > 0) { - // util::utf8_pop_back(this->command); - // this->buf.pop_last_char(); - // } - // return true; - - // case 13: // interpret command - // this->buf.write('\n'); - // this->interpret(this->command); - // this->command = ""; - // return true; - - // default: - // return false; - // } - // }); - // this->input_context.utf8_mode = true; + TODO: bind any needed input to InputContext + + toggle console will take highest priority + this->input_context.bind(action.get("TOGGLE_CONSOLE"), [this](const input::legacy::action_arg_t &) { + this->set_visible(false); + }); + this->input_context.bind(input::legacy::event_class::UTF8, [this](const input::legacy::action_arg_t &arg) { + // a single char typed into the console + std::string utf8 = arg.e.as_utf8(); + this->buf.write(utf8.c_str()); + command += utf8; + return true; + }); + this->input_context.bind(input::legacy::event_class::NONPRINT, [this](const input::legacy::action_arg_t &arg) { + switch (arg.e.as_char()) { + case 8: // remove a single UTF-8 character + if (this->command.size() > 0) { + util::utf8_pop_back(this->command); + this->buf.pop_last_char(); + } + return true; + + case 13: // interpret command + this->buf.write('\n'); + this->interpret(this->command); + this->command = ""; + return true; + + default: + return false; + } + }); + this->input_context.utf8_mode = true; + */ } void Console::set_visible(bool /* make_visible */) { // TODO: Use new renderer - - // if (make_visible) { - // this->display->get_input_manager().push_context(&this->input_context); - // this->visible = true; - // } - // else { - // this->display->get_input_manager().remove_context(&this->input_context); - // this->visible = false; - // } + /* + if (make_visible) { + this->display->get_input_manager().push_context(&this->input_context); + this->visible = true; + } + else { + this->display->get_input_manager().remove_context(&this->input_context); + this->visible = false; + } + */ } void Console::write(const char *text) { @@ -159,55 +163,55 @@ void Console::interpret(const std::string &command) { } } } +/* +bool Console::on_tick() { + if (!this->visible) { + return true; + } -// bool Console::on_tick() { -// if (!this->visible) { -// return true; -// } - -// // TODO: handle stuff such as cursor blinking, -// // repeating held-down keys -// return true; -// } - -// bool Console::on_drawhud() { -// if (!this->visible) { -// return true; -// } + // TODO: handle stuff such as cursor blinking, + // repeating held-down keys + return true; +} -// // TODO: Use new renderer +bool Console::on_drawhud() { + if (!this->visible) { + return true; + } -// // draw::to_opengl(this->display, this); + // TODO: Use new renderer -// return true; -// } + // draw::to_opengl(this->display, this); -// bool Console::on_input(SDL_Event *e) { -// // only handle inputs if the console is visible -// if (!this->visible) { -// return true; -// } + return true; +} -// switch (e->type) { -// case SDL_KEYDOWN: -// //TODO handle key inputs +bool Console::on_input(SDL_Event *e) { + // only handle inputs if the console is visible + if (!this->visible) { + return true; + } -// //do not allow anyone else to handle this input -// return false; -// } + switch (e->type) { + case SDL_KEYDOWN: + //TODO handle key inputs -// return true; -// } + //do not allow anyone else to handle this input + return false; + } -// bool Console::on_resize(coord::viewport_delta new_size) { -// coord::pixel_t w = this->buf.get_dims().x * this->charsize.x; -// coord::pixel_t h = this->buf.get_dims().y * this->charsize.y; + return true; +} -// this->bottomleft = {(new_size.x - w) / 2, (new_size.y - h) / 2}; -// this->topright = {this->bottomleft.x + w, this->bottomleft.y - h}; +bool Console::on_resize(coord::viewport_delta new_size) { + coord::pixel_t w = this->buf.get_dims().x * this->charsize.x; + coord::pixel_t h = this->buf.get_dims().y * this->charsize.y; -// return true; -// } + this->bottomleft = {(new_size.x - w) / 2, (new_size.y - h) / 2}; + this->topright = {this->bottomleft.x + w, this->bottomleft.y - h}; + return true; +} +*/ } // namespace console } // namespace openage diff --git a/libopenage/console/draw.cpp b/libopenage/console/draw.cpp index c8835d5bcc..ba5b4588c9 100644 --- a/libopenage/console/draw.cpp +++ b/libopenage/console/draw.cpp @@ -17,73 +17,75 @@ namespace openage { namespace console { namespace draw { -// void to_opengl(presenter::LegacyDisplay *engine, Console *console) { -// coord::camhud topleft{ -// console->bottomleft.x, -// // TODO This should probably just be console->topright.y -// console->bottomleft.y + console->charsize.y * console->buf.dims.y}; -// coord::pixel_t ascender = static_cast(console->font.get_ascender()); - -// renderer::TextRenderer *text_renderer = engine->get_text_renderer(); -// text_renderer->set_font(&console->font); - -// int64_t monotime = timing::get_monotonic_time(); - -// bool fastblinking_visible = (monotime % 600000000 < 300000000); -// bool slowblinking_visible = (monotime % 300000000 < 150000000); - -// for (coord::term_t x = 0; x < console->buf.dims.x; x++) { -// coord::camhud chartopleft{topleft.x + console->charsize.x * x, 0}; - -// for (coord::term_t y = 0; y < console->buf.dims.y; y++) { -// chartopleft.y = topleft.y - console->charsize.y * y; -// buf_char p = *(console->buf.chrdataptr({x, y - console->buf.scrollback_pos})); - -// int fgcolid, bgcolid; - -// bool cursor_visible_at_current_pos = (console->buf.cursorpos == coord::term{x, y - console->buf.scrollback_pos}); - -// cursor_visible_at_current_pos &= console->buf.cursor_visible; - -// if (((p.flags & CHR_NEGATIVE) != 0) xor cursor_visible_at_current_pos) { -// bgcolid = p.fgcol; -// fgcolid = p.bgcol; -// } -// else { -// bgcolid = p.bgcol; -// fgcolid = p.fgcol; -// } - -// if ((p.flags & CHR_INVISIBLE) -// or (p.flags & CHR_BLINKING and not slowblinking_visible) -// or (p.flags & CHR_BLINKINGFAST and not fastblinking_visible)) { -// fgcolid = bgcolid; -// } - -// console->termcolors[bgcolid].use(0.8); - -// glBegin(GL_QUADS); -// { -// glVertex3f(chartopleft.x, chartopleft.y, 0); -// glVertex3f(chartopleft.x, chartopleft.y - console->charsize.y, 0); -// glVertex3f(chartopleft.x + console->charsize.x, chartopleft.y - console->charsize.y, 0); -// glVertex3f(chartopleft.x + console->charsize.x, chartopleft.y, 0); -// } -// glEnd(); - -// console->termcolors[fgcolid].use(1); - -// char utf8buf[5]; -// if (util::utf8_encode(p.cp, utf8buf) == 0) { -// //unrepresentable character (question mark in black rhombus) -// text_renderer->draw(chartopleft.x, chartopleft.y - ascender, "\uFFFD"); -// } -// else { -// text_renderer->draw(chartopleft.x, chartopleft.y - ascender, utf8buf); -// } -// } -// } -// } +/* +void to_opengl(presenter::LegacyDisplay *engine, Console *console) { + coord::camhud topleft{ + console->bottomleft.x, + // TODO This should probably just be console->topright.y + console->bottomleft.y + console->charsize.y * console->buf.dims.y}; + coord::pixel_t ascender = static_cast(console->font.get_ascender()); + + renderer::TextRenderer *text_renderer = engine->get_text_renderer(); + text_renderer->set_font(&console->font); + + int64_t monotime = timing::get_monotonic_time(); + + bool fastblinking_visible = (monotime % 600000000 < 300000000); + bool slowblinking_visible = (monotime % 300000000 < 150000000); + + for (coord::term_t x = 0; x < console->buf.dims.x; x++) { + coord::camhud chartopleft{topleft.x + console->charsize.x * x, 0}; + + for (coord::term_t y = 0; y < console->buf.dims.y; y++) { + chartopleft.y = topleft.y - console->charsize.y * y; + buf_char p = *(console->buf.chrdataptr({x, y - console->buf.scrollback_pos})); + + int fgcolid, bgcolid; + + bool cursor_visible_at_current_pos = (console->buf.cursorpos == coord::term{x, y - console->buf.scrollback_pos}); + + cursor_visible_at_current_pos &= console->buf.cursor_visible; + + if (((p.flags & CHR_NEGATIVE) != 0) xor cursor_visible_at_current_pos) { + bgcolid = p.fgcol; + fgcolid = p.bgcol; + } + else { + bgcolid = p.bgcol; + fgcolid = p.fgcol; + } + + if ((p.flags & CHR_INVISIBLE) + or (p.flags & CHR_BLINKING and not slowblinking_visible) + or (p.flags & CHR_BLINKINGFAST and not fastblinking_visible)) { + fgcolid = bgcolid; + } + + console->termcolors[bgcolid].use(0.8); + + glBegin(GL_QUADS); + { + glVertex3f(chartopleft.x, chartopleft.y, 0); + glVertex3f(chartopleft.x, chartopleft.y - console->charsize.y, 0); + glVertex3f(chartopleft.x + console->charsize.x, chartopleft.y - console->charsize.y, 0); + glVertex3f(chartopleft.x + console->charsize.x, chartopleft.y, 0); + } + glEnd(); + + console->termcolors[fgcolid].use(1); + + char utf8buf[5]; + if (util::utf8_encode(p.cp, utf8buf) == 0) { + //unrepresentable character (question mark in black rhombus) + text_renderer->draw(chartopleft.x, chartopleft.y - ascender, "\uFFFD"); + } + else { + text_renderer->draw(chartopleft.x, chartopleft.y - ascender, utf8buf); + } + } + } +} +*/ void to_terminal(Buf *buf, util::FD *fd, bool clear) { //move cursor, draw top left corner From 7b2b1fe2411fbf2f33d5a28bc8dfe5021a6ae455 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 13 Dec 2023 01:56:23 +0100 Subject: [PATCH 079/771] buildsys: Remove obsolte comment. --- libopenage/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/libopenage/CMakeLists.txt b/libopenage/CMakeLists.txt index eb54fd8bf2..030231baf7 100644 --- a/libopenage/CMakeLists.txt +++ b/libopenage/CMakeLists.txt @@ -63,7 +63,6 @@ find_package(Threads REQUIRED) set(QT_VERSION_REQ "6.2") find_package(Qt6 ${QT_VERSION_REQ} REQUIRED COMPONENTS Core Quick Multimedia) -# find_package(Qt6Multimedia ${QT_VERSION_REQ} REQUIRED) if(WANT_BACKTRACE) find_package(GCCBacktrace) From be76f5439dc999a98bb5de4a35a95dcaeed0006b Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 13 Dec 2023 02:04:20 +0100 Subject: [PATCH 080/771] gamestate: Fix comment about static test entities. --- libopenage/gamestate/event/spawn_entity.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libopenage/gamestate/event/spawn_entity.cpp b/libopenage/gamestate/event/spawn_entity.cpp index 9558cba2a3..c2934ed58c 100644 --- a/libopenage/gamestate/event/spawn_entity.cpp +++ b/libopenage/gamestate/event/spawn_entity.cpp @@ -77,7 +77,8 @@ static const std::vector trial_test_entities = { }; // TODO: Remove hardcoded test entity references -static std::vector test_entities; // declared static so we only have to do this once +// declared static so we only have to init the vector once +static std::vector test_entities; void build_test_entities(const std::shared_ptr &gstate) { From 7f4706e9d4ef80458d3b64b973028d0b7840ff80 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 13 Dec 2023 02:26:52 +0100 Subject: [PATCH 081/771] gamestate: Pass game entity ptr by reference. --- libopenage/gamestate/event/spawn_entity.cpp | 2 +- libopenage/gamestate/game_state.cpp | 6 +++--- libopenage/gamestate/game_state.h | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/libopenage/gamestate/event/spawn_entity.cpp b/libopenage/gamestate/event/spawn_entity.cpp index c2934ed58c..155441445f 100644 --- a/libopenage/gamestate/event/spawn_entity.cpp +++ b/libopenage/gamestate/event/spawn_entity.cpp @@ -77,7 +77,7 @@ static const std::vector trial_test_entities = { }; // TODO: Remove hardcoded test entity references -// declared static so we only have to init the vector once +// declared static so we only have to build the vector once static std::vector test_entities; diff --git a/libopenage/gamestate/game_state.cpp b/libopenage/gamestate/game_state.cpp index fba3826779..9de7c69bff 100644 --- a/libopenage/gamestate/game_state.cpp +++ b/libopenage/gamestate/game_state.cpp @@ -23,21 +23,21 @@ const std::shared_ptr &GameState::get_db_view() { return this->db_view; } -void GameState::add_game_entity(const std::shared_ptr entity) { +void GameState::add_game_entity(const std::shared_ptr &entity) { if (this->game_entities.contains(entity->get_id())) [[unlikely]] { throw Error(MSG(err) << "Game entity with ID " << entity->get_id() << " already exists"); } this->game_entities[entity->get_id()] = entity; } -void GameState::add_player(const std::shared_ptr player) { +void GameState::add_player(const std::shared_ptr &player) { if (this->players.contains(player->get_id())) [[unlikely]] { throw Error(MSG(err) << "Player with ID " << player->get_id() << " already exists"); } this->players[player->get_id()] = player; } -void GameState::set_terrain(const std::shared_ptr terrain) { +void GameState::set_terrain(const std::shared_ptr &terrain) { this->terrain = terrain; } diff --git a/libopenage/gamestate/game_state.h b/libopenage/gamestate/game_state.h index 6cce1b09b1..401070780c 100644 --- a/libopenage/gamestate/game_state.h +++ b/libopenage/gamestate/game_state.h @@ -60,21 +60,21 @@ class GameState : public openage::event::State { * * @param entity New game entity. */ - void add_game_entity(const std::shared_ptr entity); + void add_game_entity(const std::shared_ptr &entity); /** * Add a new player to the index. * * @param player New player. */ - void add_player(const std::shared_ptr player); + void add_player(const std::shared_ptr &player); /** * Set the terrain of the current game. * * @param terrain Terrain object. */ - void set_terrain(const std::shared_ptr terrain); + void set_terrain(const std::shared_ptr &terrain); /** * Get a game entity by its ID. From 892ceb757d4b37ec45923c4fab3d2fa45d78a05a Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 13 Dec 2023 02:29:11 +0100 Subject: [PATCH 082/771] gamestate: Make 'Player' moveable. --- libopenage/gamestate/player.cpp | 11 ++++++++++ libopenage/gamestate/player.h | 37 ++++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/libopenage/gamestate/player.cpp b/libopenage/gamestate/player.cpp index e57c4d8a35..cb7aeed3ea 100644 --- a/libopenage/gamestate/player.cpp +++ b/libopenage/gamestate/player.cpp @@ -13,6 +13,13 @@ Player::Player(player_id_t id, db_view{db_view} { } +std::shared_ptr Player::copy(entity_id_t id) { + auto copy = std::shared_ptr(new Player(*this)); + copy->set_id(id); + + return copy; +} + player_id_t Player::get_id() const { return this->id; } @@ -21,4 +28,8 @@ const std::shared_ptr &Player::get_db_view() const { return this->db_view; } +void Player::set_id(entity_id_t id) { + this->id = id; +} + } // namespace openage::gamestate diff --git a/libopenage/gamestate/player.h b/libopenage/gamestate/player.h index d9bdbe3bd2..a7eb5bac4d 100644 --- a/libopenage/gamestate/player.h +++ b/libopenage/gamestate/player.h @@ -27,15 +27,20 @@ class Player { Player(player_id_t id, const std::shared_ptr &db_view); - // players can't be copied to prevent duplicate IDs - Player(const Player &) = delete; - Player(Player &&) = delete; - - Player &operator=(const Player &) = delete; - Player &operator=(Player &&) = delete; + Player(Player &&) = default; + Player &operator=(Player &&) = default; ~Player() = default; + /** + * Copy this player. + * + * @param id Unique identifier. + * + * @return Copy of this player. + */ + std::shared_ptr copy(entity_id_t id); + /** * Get the unique ID of the player. * @@ -50,11 +55,29 @@ class Player { */ const std::shared_ptr &get_db_view() const; +protected: + /** + * A player cannot be default copied because of their unique ID. + * + * \p copy() must be used instead. + */ + Player(const Player &) = default; + Player &operator=(const Player &) = default; + private: + /** + * Set the unique identifier of this player. + * + * Only called by \p copy(). + * + * @param id New ID. + */ + void set_id(entity_id_t id); + /** * Player ID. Must be unique. */ - const player_id_t id; + player_id_t id; /** * Player view of the nyan game data database. From 9d4c3ba6786d0dc973369e10ac67d30a25859d46 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 13 Dec 2023 02:38:28 +0100 Subject: [PATCH 083/771] gamestate: Pass terrain chunk by reference. --- libopenage/gamestate/terrain.cpp | 2 +- libopenage/gamestate/terrain.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/gamestate/terrain.cpp b/libopenage/gamestate/terrain.cpp index 6a733a86d7..f31fc648b8 100644 --- a/libopenage/gamestate/terrain.cpp +++ b/libopenage/gamestate/terrain.cpp @@ -18,7 +18,7 @@ Terrain::Terrain() : // TODO: Get actual size of terrain. } -void Terrain::add_chunk(const std::shared_ptr chunk) { +void Terrain::add_chunk(const std::shared_ptr &chunk) { this->chunks.push_back(chunk); } diff --git a/libopenage/gamestate/terrain.h b/libopenage/gamestate/terrain.h index 71f0bd63d7..d91a432a02 100644 --- a/libopenage/gamestate/terrain.h +++ b/libopenage/gamestate/terrain.h @@ -32,7 +32,7 @@ class Terrain { * * @param chunk New chunk. */ - void add_chunk(const std::shared_ptr chunk); + void add_chunk(const std::shared_ptr &chunk); /** * Get the chunks of the terrain. From d63518944bba935cec6fb209870ae5952f7aec3b Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 13 Dec 2023 02:41:32 +0100 Subject: [PATCH 084/771] gamestate: Fix missing std::move. --- libopenage/gamestate/terrain_chunk.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/gamestate/terrain_chunk.cpp b/libopenage/gamestate/terrain_chunk.cpp index a6d10251b1..9fc5422b65 100644 --- a/libopenage/gamestate/terrain_chunk.cpp +++ b/libopenage/gamestate/terrain_chunk.cpp @@ -10,7 +10,7 @@ TerrainChunk::TerrainChunk(const util::Vector2s size, const std::vector &&tiles) : size{size}, offset{offset}, - tiles{tiles} { + tiles{std::move(tiles)} { if (this->size[0] > MAX_CHUNK_WIDTH || this->size[1] > MAX_CHUNK_HEIGHT) { throw Error(MSG(err) << "Terrain chunk size exceeds maximum size: " << this->size[0] << "x" << this->size[1] << " > " From 8d779ed9ca500357af365d69b9c3bfe1f2f82ba2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 13 Dec 2023 02:45:17 +0100 Subject: [PATCH 085/771] input: Pass controllers by reference. --- libopenage/input/input_manager.cpp | 6 +++--- libopenage/input/input_manager.h | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libopenage/input/input_manager.cpp b/libopenage/input/input_manager.cpp index f9d729b9ba..47ff5c1834 100644 --- a/libopenage/input/input_manager.cpp +++ b/libopenage/input/input_manager.cpp @@ -19,15 +19,15 @@ InputManager::InputManager() : gui_input{nullptr} { } -void InputManager::set_gui(const std::shared_ptr gui_input) { +void InputManager::set_gui(const std::shared_ptr &gui_input) { this->gui_input = gui_input; } -void InputManager::set_camera_controller(const std::shared_ptr controller) { +void InputManager::set_camera_controller(const std::shared_ptr &controller) { this->camera_controller = controller; } -void InputManager::set_engine_controller(const std::shared_ptr controller) { +void InputManager::set_engine_controller(const std::shared_ptr &controller) { this->engine_controller = controller; } diff --git a/libopenage/input/input_manager.h b/libopenage/input/input_manager.h index 17e66a28be..f81a5a5bc4 100644 --- a/libopenage/input/input_manager.h +++ b/libopenage/input/input_manager.h @@ -43,21 +43,21 @@ class InputManager { * * @param gui_input GUI input handler. */ - void set_gui(const std::shared_ptr gui_input); + void set_gui(const std::shared_ptr &gui_input); /** * Set the controller for the camera. * * @param controller Camera controller. */ - void set_camera_controller(const std::shared_ptr controller); + void set_camera_controller(const std::shared_ptr &controller); /** * Set the controller for the engine. * * @param controller Engine controller. */ - void set_engine_controller(const std::shared_ptr controller); + void set_engine_controller(const std::shared_ptr &controller); /** * returns the global keybind context. From 0e70ea89356dc7284b018dd6731a574b7311eb5c Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 13 Dec 2023 02:47:53 +0100 Subject: [PATCH 086/771] pathing: Remove obsolete method. --- libopenage/pathfinding/a_star.cpp | 15 --------------- libopenage/pathfinding/a_star.h | 7 ------- 2 files changed, 22 deletions(-) diff --git a/libopenage/pathfinding/a_star.cpp b/libopenage/pathfinding/a_star.cpp index b53aed88f4..f80879eb91 100644 --- a/libopenage/pathfinding/a_star.cpp +++ b/libopenage/pathfinding/a_star.cpp @@ -37,21 +37,6 @@ Path to_point(coord::phys3 start, return a_star(start, valid_end, heuristic, passable); } - -// Path to_object(openage::TerrainObject *to_move, -// openage::TerrainObject *end, -// coord::phys_t rad) { -// coord::phys3 start = to_move->pos.draw; -// auto valid_end = [&](const coord::phys3 &pos) -> bool { -// return end->from_edge(pos) < rad; -// }; -// auto heuristic = [&](const coord::phys3 &pos) -> cost_t { -// return (end->from_edge(pos) - to_move->min_axis() / 2L).to_float(); -// }; -// return a_star(start, valid_end, heuristic, to_move->passable); -// } - - Path find_nearest(coord::phys3 start, std::function valid_end, std::function passable) { diff --git a/libopenage/pathfinding/a_star.h b/libopenage/pathfinding/a_star.h index a7cc5aa75e..23d3e76747 100644 --- a/libopenage/pathfinding/a_star.h +++ b/libopenage/pathfinding/a_star.h @@ -18,13 +18,6 @@ Path to_point(coord::phys3 start, coord::phys3 end, std::function passable); -/** - * path between 2 objects, with how close to come to end point - */ -// Path to_object(TerrainObject *to_move, -// TerrainObject *end, -// coord::phys_t rad); - /** * path to nearest object with lambda */ From 21cffdb07a9c538da2758352e4ba4b88e6706836 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 13 Dec 2023 02:54:05 +0100 Subject: [PATCH 087/771] gui: Remove obsolete method. --- .../gui/integration/private/gui_texture.cpp | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/libopenage/renderer/gui/integration/private/gui_texture.cpp b/libopenage/renderer/gui/integration/private/gui_texture.cpp index d1c568eae6..daa6f4020a 100644 --- a/libopenage/renderer/gui/integration/private/gui_texture.cpp +++ b/libopenage/renderer/gui/integration/private/gui_texture.cpp @@ -39,33 +39,6 @@ bool GuiTexture::isAtlasTexture() const { return openage::renderer::gui::isAtlasTexture(this->texture_handle); } -namespace { -// GLuint create_compatible_texture(GLuint texture_id, GLsizei w, GLsizei h) { -// glBindTexture(GL_TEXTURE_2D, texture_id); - -// GLint min_filter; -// GLint mag_filter; -// GLint iformat; - -// glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, &min_filter); -// glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, &mag_filter); -// glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &iformat); - -// GLuint new_texture_id; -// glGenTextures(1, &new_texture_id); -// glBindTexture(GL_TEXTURE_2D, new_texture_id); - -// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter); -// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter); - -// glTexImage2D(GL_TEXTURE_2D, 0, iformat, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); - -// glBindTexture(GL_TEXTURE_2D, 0); - -// return new_texture_id; -// } -} // namespace - QSGTexture *GuiTexture::removedFromAtlas(QRhiResourceUpdateBatch * /* resourceUpdates */ /* = nullptr */) const { if (this->isAtlasTexture()) { return this->standalone.get(); From 11075b0ad88771bf2f1a5a3a7fe91dee99825a57 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 13 Dec 2023 02:55:53 +0100 Subject: [PATCH 088/771] renderer: Remove specific command hint. --- libopenage/renderer/opengl/error.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libopenage/renderer/opengl/error.cpp b/libopenage/renderer/opengl/error.cpp index 790847fccc..c47bf5a85d 100644 --- a/libopenage/renderer/opengl/error.cpp +++ b/libopenage/renderer/opengl/error.cpp @@ -61,10 +61,11 @@ void gl_check_error() { // unknown error state errormsg = "unknown error"; } - throw Error(MSG(err) << "OpenGL error state after running draw method: " << glerrorstate << "\n" - "\t" + throw Error(MSG(err) << "OpenGL error state after running draw method: " + << glerrorstate << "\n" + "\t" << errormsg << "\n" - << "Run the game with --gl-debug to get more information: './run game --gl-debug'."); + << "Run the engine with --gl-debug to get more information."); } } From ca35303965dc6d77a3fcfa3eff0e17c3e8e96aaf Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 13 Dec 2023 03:07:46 +0100 Subject: [PATCH 089/771] renderer: Remove reinterpret cast from memcpy operations. --- libopenage/renderer/demo/demo_5.cpp | 2 +- libopenage/renderer/resources/mesh_data.cpp | 2 +- libopenage/renderer/stages/terrain/terrain_chunk.cpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libopenage/renderer/demo/demo_5.cpp b/libopenage/renderer/demo/demo_5.cpp index b8adc896e3..afea99a30b 100644 --- a/libopenage/renderer/demo/demo_5.cpp +++ b/libopenage/renderer/demo/demo_5.cpp @@ -78,7 +78,7 @@ void renderer_demo_5(const util::Path &path) { auto const vert_data_size = verts.size() * sizeof(float); std::vector vert_data(vert_data_size); - std::memcpy(vert_data.data(), reinterpret_cast(verts.data()), vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); resources::MeshData meshdata{std::move(vert_data), info}; diff --git a/libopenage/renderer/resources/mesh_data.cpp b/libopenage/renderer/resources/mesh_data.cpp index 74fb72b629..98c5d7dbdb 100644 --- a/libopenage/renderer/resources/mesh_data.cpp +++ b/libopenage/renderer/resources/mesh_data.cpp @@ -123,7 +123,7 @@ MeshData create_float_mesh(const std::array &src) { auto const data_size = size * sizeof(float); std::vector verts(data_size); - std::memcpy(verts.data(), reinterpret_cast(src.data()), data_size); + std::memcpy(verts.data(), src.data(), data_size); VertexInputInfo info{ {vertex_input_t::V2F32, vertex_input_t::V2F32}, diff --git a/libopenage/renderer/stages/terrain/terrain_chunk.cpp b/libopenage/renderer/stages/terrain/terrain_chunk.cpp index 8ca1b8970a..1dfa77b5c0 100644 --- a/libopenage/renderer/stages/terrain/terrain_chunk.cpp +++ b/libopenage/renderer/stages/terrain/terrain_chunk.cpp @@ -99,11 +99,11 @@ std::shared_ptr TerrainChunk::create_mesh() { auto const vert_data_size = dst_verts.size() * sizeof(float); std::vector vert_data(vert_data_size); - std::memcpy(vert_data.data(), reinterpret_cast(dst_verts.data()), vert_data_size); + std::memcpy(vert_data.data(), dst_verts.data(), vert_data_size); auto const idx_data_size = idxs.size() * sizeof(uint16_t); std::vector idx_data(idx_data_size); - std::memcpy(idx_data.data(), reinterpret_cast(idxs.data()), idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); resources::MeshData meshdata{std::move(vert_data), std::move(idx_data), info}; From 5ae997bd33c2d54e1a51b01e949f5a148ad4e120 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 13 Dec 2023 03:17:25 +0100 Subject: [PATCH 090/771] util: Remove obsolete classes. --- libopenage/util/CMakeLists.txt | 1 - libopenage/util/profiler.cpp | 202 --------------------------------- libopenage/util/profiler.h | 125 -------------------- 3 files changed, 328 deletions(-) delete mode 100644 libopenage/util/profiler.cpp delete mode 100644 libopenage/util/profiler.h diff --git a/libopenage/util/CMakeLists.txt b/libopenage/util/CMakeLists.txt index 1a2bee51e7..d278f4b427 100644 --- a/libopenage/util/CMakeLists.txt +++ b/libopenage/util/CMakeLists.txt @@ -21,7 +21,6 @@ add_sources(libopenage misc_test.cpp os.cpp path.cpp - profiler.cpp quaternion.cpp quaternion_test.cpp repr.cpp diff --git a/libopenage/util/profiler.cpp b/libopenage/util/profiler.cpp deleted file mode 100644 index 95ebd85af7..0000000000 --- a/libopenage/util/profiler.cpp +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#include "profiler.h" -#include "../renderer/color.h" -#include "misc.h" - -#include -#include -#include -#include - -namespace openage::util { - -// Profiler::Profiler() : -// {} - - -// Profiler::~Profiler() { -// this->unregister_all(); -// } - -// void Profiler::register_component(const std::string &com, color component_color) { -// if (this->registered(com)) { -// return; -// } - -// component_time_data cdt; -// cdt.display_name = com; -// cdt.drawing_color = component_color; - -// for (auto &val : cdt.history) { -// val = 0; -// } - -// this->components[com] = cdt; -// } - -// void Profiler::unregister_component(const std::string &com) { -// if (not this->registered(com)) { -// return; -// } - -// this->components.erase(com); -// } - -// void Profiler::unregister_all() { -// std::vector registered_components = this->registered_components(); - -// for (auto com : registered_components) { -// this->unregister_component(com); -// } -// } - -// std::vector Profiler::registered_components() { -// std::vector registered_components; -// for (auto &pair : this->components) { -// registered_components.push_back(pair.first); -// } - -// return registered_components; -// } - -// void Profiler::start_measure(const std::string &com, color component_color) { -// if (not this->engine_in_debug_mode()) { -// return; -// } - -// if (not this->registered(com)) { -// this->register_component(com, component_color); -// } - -// this->components[com].start = std::chrono::high_resolution_clock::now(); -// } - -// void Profiler::end_measure(const std::string &com) { -// if (not this->engine_in_debug_mode()) { -// return; -// } - -// if (this->registered(com)) { -// std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); -// this->components[com].duration = end - this->components[com].start; -// } -// } - -// void Profiler::draw_component_performance(const std::string &com) { -// color rgb = this->components[com].drawing_color; -// glColor4f(rgb.r, rgb.g, rgb.b, 1.0); - -// glLineWidth(1.0); -// glBegin(GL_LINE_STRIP); -// float x_offset = 0.0; -// float offset_factor = static_cast(PROFILER_CANVAS_WIDTH) / static_cast(MAX_DURATION_HISTORY); -// float percentage_factor = static_cast(PROFILER_CANVAS_HEIGHT) / 100.0; - -// for (auto i = this->insert_pos; mod(i, MAX_DURATION_HISTORY) != mod(this->insert_pos - 1, MAX_DURATION_HISTORY); ++i) { -// i = mod(i, MAX_DURATION_HISTORY); - -// auto percentage = this->components[com].history.at(i); -// glVertex3f(PROFILER_CANVAS_POSITION_X + x_offset, PROFILER_CANVAS_POSITION_Y + percentage * percentage_factor, 0.0); -// x_offset += offset_factor; -// } -// glEnd(); - -// // reset color -// glColor4f(1.0, 1.0, 1.0, 1.0); -// } - -// void Profiler::show(bool debug_mode) { -// if (debug_mode) { -// this->show(); -// } -// } - -// // void Profiler::show() { -// // this->draw_canvas(); -// // this->draw_legend(); - -// // for (auto com : this->components) { -// // this->draw_component_performance(com.first); -// // } -// // } - -// bool Profiler::registered(const std::string &com) const { -// return this->components.find(com) != this->components.end(); -// } - -// unsigned Profiler::size() const { -// return this->components.size(); -// } - -// void Profiler::start_frame_measure() { -// if (this->engine_in_debug_mode()) { -// this->frame_start = std::chrono::high_resolution_clock::now(); -// } -// } - -// void Profiler::end_frame_measure() { -// if (not this->engine_in_debug_mode()) { -// return; -// } - -// auto frame_end = std::chrono::high_resolution_clock::now(); -// this->frame_duration = frame_end - this->frame_start; - -// for (auto com : this->registered_components()) { -// double percentage = this->duration_to_percentage(this->components[com].duration); -// this->append_to_history(com, percentage); -// } - -// this->insert_pos++; -// } - -// // void Profiler::draw_canvas() { -// // glColor4f(0.2, 0.2, 0.2, PROFILER_CANVAS_ALPHA); -// // glRecti(PROFILER_CANVAS_POSITION_X, -// // PROFILER_CANVAS_POSITION_Y, -// // PROFILER_CANVAS_POSITION_X + PROFILER_CANVAS_WIDTH, -// // PROFILER_CANVAS_POSITION_Y + PROFILER_CANVAS_HEIGHT); -// // } - -// // void Profiler::draw_legend() { -// // int offset = 0; -// // for (auto com : this->components) { -// // glColor4f(com.second.drawing_color.r, com.second.drawing_color.g, com.second.drawing_color.b, 1.0); -// // int box_x = PROFILER_CANVAS_POSITION_X + 2; -// // int box_y = PROFILER_CANVAS_POSITION_Y - PROFILER_COM_BOX_HEIGHT - 2 - offset; -// // glRecti(box_x, box_y, box_x + PROFILER_COM_BOX_WIDTH, box_y + PROFILER_COM_BOX_HEIGHT); - -// // glColor4f(0.2, 0.2, 0.2, 1); -// // coord::viewport position = coord::viewport{box_x + PROFILER_COM_BOX_WIDTH + 2, box_y + 2}; -// // // this->display->render_text(position, 12, renderer::Colors::WHITE, "%s", com.second.display_name.c_str()); - -// // offset += PROFILER_COM_BOX_HEIGHT + 2; -// // } -// // } - -// double Profiler::duration_to_percentage(std::chrono::high_resolution_clock::duration duration) { -// double dur = std::chrono::duration_cast(duration).count(); -// double ref = std::chrono::duration_cast(this->frame_duration).count(); -// double percentage = dur / ref * 100; -// return percentage; -// } - -// void Profiler::append_to_history(const std::string &com, double percentage) { -// if (this->insert_pos == MAX_DURATION_HISTORY) { -// this->insert_pos = 0; -// } -// this->components[com].history[this->insert_pos] = percentage; -// } - -// bool Profiler::engine_in_debug_mode() { -// // if (this->display->drawing_debug_overlay.value) { -// // return true; -// // } -// // else { -// // return false; -// // } -// return true; -// } - -} // namespace openage::util diff --git a/libopenage/util/profiler.h b/libopenage/util/profiler.h deleted file mode 100644 index 1b3641d576..0000000000 --- a/libopenage/util/profiler.h +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. - -#pragma once - -#include -#include -#include -#include -#include - -constexpr int MAX_DURATION_HISTORY = 100; -constexpr int PROFILER_CANVAS_WIDTH = 250; -constexpr int PROFILER_CANVAS_HEIGHT = 120; -constexpr int PROFILER_CANVAS_POSITION_X = 0; -constexpr int PROFILER_CANVAS_POSITION_Y = 300; -constexpr float PROFILER_CANVAS_ALPHA = 0.6f; -constexpr int PROFILER_COM_BOX_WIDTH = 30; -constexpr int PROFILER_COM_BOX_HEIGHT = 15; - -namespace openage { - -namespace util { - -struct color { - float r, g, b; -}; - -struct component_time_data { - std::string display_name; - color drawing_color; - std::chrono::high_resolution_clock::time_point start; - std::chrono::high_resolution_clock::duration duration; - std::array history; -}; - -// class Profiler { -// public: -// Profiler(); -// ~Profiler(); - -// /** -// * registers a component -// * @param com the identifier to distinguish the components -// * @param component_color color of the plotted line -// */ -// void register_component(const std::string &com, color component_color); - -// /** -// * unregisters an individual component -// * @param com component name which should be unregistered -// */ -// void unregister_component(const std::string &com); - -// /** -// * unregisters all remaining components -// */ -// void unregister_all(); - -// /** -// *returns a vector of registered component names -// */ -// std::vector registered_components(); - -// /* -// * starts a measurement for the component com. If com is not yet -// * registered, its getting registered and the profiler uses the color -// * information given by component_color. The default value is white. -// */ -// void start_measure(const std::string &com, color component_color = {1.0, 1.0, 1.0}); - -// /* -// * stops the measurement for the component com. If com is not yet -// * registered it does nothing. -// */ -// void end_measure(const std::string &com); - -// /* -// * draws the profiler gui if debug_mode is set -// */ -// void show(bool debug_mode); - -// /* -// * draws the profiler gui -// */ -// void show(); - -// /* -// * true if the component com is already registered, otherwise false -// */ -// bool registered(const std::string &com) const; - -// /* -// * returns the number of registered components -// */ -// unsigned size() const; - -// /** -// * sets the start point for the actual frame which is used as a reference -// * value for the registered components -// */ -// void start_frame_measure(); - -// /** -// * sets the end point for the reference time used to compute the portions -// * of the components. Each recorded measurement for the registered components -// * get appended to their history complete the measurement. -// */ -// void end_frame_measure(); - -// private: -// // void draw_canvas(); -// // void draw_legend(); -// void draw_component_performance(const std::string &com); -// double duration_to_percentage(std::chrono::high_resolution_clock::duration duration); -// void append_to_history(const std::string &com, double percentage); -// bool engine_in_debug_mode(); - -// std::chrono::high_resolution_clock::time_point frame_start; -// std::chrono::high_resolution_clock::duration frame_duration; -// std::unordered_map components; -// int insert_pos = 0; -// }; - -} // namespace util -} // namespace openage From d07c4606b72d038562af78220b8cb0734f516bd0 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 13 Dec 2023 03:18:19 +0100 Subject: [PATCH 091/771] renderer: Comment out currently unused param. --- libopenage/renderer/stages/terrain/terrain_chunk.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/renderer/stages/terrain/terrain_chunk.cpp b/libopenage/renderer/stages/terrain/terrain_chunk.cpp index 1dfa77b5c0..dcea61585e 100644 --- a/libopenage/renderer/stages/terrain/terrain_chunk.cpp +++ b/libopenage/renderer/stages/terrain/terrain_chunk.cpp @@ -21,7 +21,7 @@ void TerrainChunk::set_render_entity(const std::shared_ptr this->render_entity = entity; } -void TerrainChunk::fetch_updates(const time::time_t &time) { +void TerrainChunk::fetch_updates(const time::time_t & /* time */) { // TODO: Don't create model if render entity is not set if (not this->render_entity) { return; From 10bc91da5a562d8b06063b332da60edb31bfda39 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 15 Dec 2023 15:55:26 +0100 Subject: [PATCH 092/771] fix comment indent with tabs. --- libopenage/gamestate/terrain.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libopenage/gamestate/terrain.h b/libopenage/gamestate/terrain.h index d91a432a02..c84b926d9a 100644 --- a/libopenage/gamestate/terrain.h +++ b/libopenage/gamestate/terrain.h @@ -43,11 +43,11 @@ class Terrain { /** * Attach a renderer which enables graphical display. - * - * TODO: We currently have to do attach this here too in addition to the terrain - * factory because the renderer gets attached AFTER the terrain is - * already created. In the future, the game should wait for the renderer - * before creating the terrain. + * + * TODO: We currently have to do attach this here too in addition to the terrain + * factory because the renderer gets attached AFTER the terrain is + * already created. In the future, the game should wait for the renderer + * before creating the terrain. * * @param render_factory Factory for creating connector objects for gamestate->renderer * communication. From ece991d72bb96dbcc5445d1b6cec406e91dbefbf Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 17 Dec 2023 16:55:31 +0100 Subject: [PATCH 093/771] ci: Overwrite exsting packages on macOS. --- .github/workflows/macosx-ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/macosx-ci.yml b/.github/workflows/macosx-ci.yml index e38c9e3e6e..eba3c5fd98 100644 --- a/.github/workflows/macosx-ci.yml +++ b/.github/workflows/macosx-ci.yml @@ -43,7 +43,7 @@ jobs: - name: Install clang / LLVM 15.0.0 run: | set -x - brew install wget + brew install --force wget mkdir -p /tmp/clang cd /tmp/clang wget https://github.com/llvm/llvm-project/releases/download/llvmorg-15.0.0/clang+llvm-15.0.0-x86_64-apple-darwin.tar.xz -O clang-15.0.0.tar.xz @@ -53,15 +53,15 @@ jobs: mv clang+llvm-15.0.0-x86_64-apple-darwin clang-15.0.0 ~/clang-15.0.0/bin/clang++ --version - name: Brew install DeJaVu fonts - run: brew tap homebrew/cask-fonts && brew install font-dejavu + run: brew tap homebrew/cask-fonts && brew install --force font-dejavu - name: Remove python's 2to3 link so that 'brew link' does not fail run: rm /usr/local/bin/2to3* && rm /usr/local/bin/idle3* - name: Install environment helpers with homebrew - run: brew install ccache + run: brew install --force ccache - name: Install dependencies with homebrew - run: brew install libepoxy freetype fontconfig harfbuzz opus opusfile qt6 libogg libpng toml11 eigen + run: brew install --force libepoxy freetype fontconfig harfbuzz opus opusfile qt6 libogg libpng toml11 eigen - name: Install nyan dependencies with homebrew - run: brew install flex make + run: brew install --force flex make - name: Install python3 packages # cython, numpy and pygments are in homebrew, # but "cython is keg-only, which means it was not symlinked into /usr/local" From 5e656c4b6eb3c36b3f9d1554155e74bd0bdef2c7 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 15 Dec 2023 19:54:51 +0100 Subject: [PATCH 094/771] doc: Changelog for release 0.5.3. --- doc/changelogs/engine/v0.5.3.md | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 doc/changelogs/engine/v0.5.3.md diff --git a/doc/changelogs/engine/v0.5.3.md b/doc/changelogs/engine/v0.5.3.md new file mode 100644 index 0000000000..23b72afe58 --- /dev/null +++ b/doc/changelogs/engine/v0.5.3.md @@ -0,0 +1,50 @@ +# [0.5.3] - 2023-12-15 +All notable changes for version [0.5.3] are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [0.4.0]. + +## Added + +- Temporary file/directory support for Python files +- More debug info in converter +- More fixed-point math functions +- `setuptools` is now conditional dependency for Python >= 3.12 && Cython < 3.1 + +## Changed + +- Make `main` the default entrypoint command for openage binary + +## Removed + +- Legacy subsystem code + - Asset management (yes, there were 3 deprecated asset managers) + - `openage::AssetManager` + - `openage::LegacyAssetManager` + - `openage::presenter::AssetManager` + - Deprecated Coordinate types (`libopenage/coord`) + - CoordManager + - Deprecated transformations between types + - Gamedata dummy classes (`libopenage/gamedata`) + - Old gamestate + - Game logic (`libopenage/gamestate/old`) + - Unit handling (`libopenage/unit`) + - Old input system (`libopenage/input/legacy`) + - Old GUI (`libopenage/gui`) + - Old renderer + - Logic (`libopenage/presenter/legacy`) + - Data classes (texure, etc.) + - Old Terrain (`libopenage/terrain`) + +## Fixed + +- Version tag format without `--long` crashes on tagged commits +- Dangling reference in modpack info file loading +- No graphics visible on Wayland +- Wrong anchor positions when sprite is mirrored +- Several typos in documentation + + +## Full commit log + +https://github.com/SFTtech/openage/compare/v0.5.2...v0.5.3 From 298e09d12b500458e779debd4eff3df13d21eeae Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 15 Dec 2023 19:57:22 +0100 Subject: [PATCH 095/771] Bump version to 0.5.3 --- openage_version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openage_version b/openage_version index cb0c939a93..be14282b7f 100644 --- a/openage_version +++ b/openage_version @@ -1 +1 @@ -0.5.2 +0.5.3 From 3c24463de190e8a7296b45c17150f36071400537 Mon Sep 17 00:00:00 2001 From: Mr-Bajs <93934125+Mr-Bajs@users.noreply.github.com> Date: Mon, 18 Dec 2023 14:31:21 +0000 Subject: [PATCH 096/771] Update README.md (#1612) Fixed a typo. flatpack => Flatpak. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db9b9a4a82..ccd2b66c0c 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ We strongly recommend to build the program from source to get the latest, greate * For **Linux** check at [repology](https://repology.org/project/openage/versions) if your distribution has any packages available. Otherwise you need to build from source. - We don't release `*.deb`, `*.rpm`, flatpack, snap or AppImage packages yet. + We don't release `*.deb`, `*.rpm`, Flatpak, snap or AppImage packages yet. * For **Windows** check our [release page](https://github.com/SFTtech/openage/releases) for the latest installer. Otherwise, you need to build from source. From edf26d060cefecdd48189b4a8426f03783172533 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Dec 2023 00:51:11 +0100 Subject: [PATCH 097/771] doc: Mention added/removed dependencies in changelog. --- doc/changelogs/engine/v0.5.3.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/changelogs/engine/v0.5.3.md b/doc/changelogs/engine/v0.5.3.md index 23b72afe58..a76735b68e 100644 --- a/doc/changelogs/engine/v0.5.3.md +++ b/doc/changelogs/engine/v0.5.3.md @@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Temporary file/directory support for Python files - More debug info in converter - More fixed-point math functions -- `setuptools` is now conditional dependency for Python >= 3.12 && Cython < 3.1 +- Dependencies + - Qt Multimedia + - `setuptools` is now conditional dependency for Python >= 3.12 && Cython < 3.1 ## Changed @@ -35,6 +37,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Logic (`libopenage/presenter/legacy`) - Data classes (texure, etc.) - Old Terrain (`libopenage/terrain`) +- Dependencies + - SDL2 + - SDL2 Image ## Fixed From 78051b7f894fdf7f7c6d44c05ac7239fe5a896cb Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 2 Dec 2023 21:48:46 +0100 Subject: [PATCH 098/771] doc: nyan data API v0.4.0 UML. --- doc/nyan/aoe2_nyan_tree.svg | 6619 +++++++++++++++++------------------ doc/nyan/aoe2_nyan_tree.uxf | 6324 ++++++++++++++++----------------- 2 files changed, 6480 insertions(+), 6463 deletions(-) diff --git a/doc/nyan/aoe2_nyan_tree.svg b/doc/nyan/aoe2_nyan_tree.svg index 8b45542911..42a2802c22 100644 --- a/doc/nyan/aoe2_nyan_tree.svg +++ b/doc/nyan/aoe2_nyan_tree.svg @@ -1,7 +1,7 @@ -Constructablestarting_progress : intconstruction_progress : set(Progress)TransformCarryRestockHarvestConstructAll ingame objectsare game entitiesProjectileBarracksSwordsmanRelicTreeFalseTruePatchPropertyPrioritypriority : intChainedBatchOrderedBatchUnorderedBatchChancechance : floatPrioritypriority : intBatchPropertyEffectBatcheffects : set(DiscreteEffect)properties : dict(BatchProperty, BatchProperty) = {}OwnsGameEntitygame_entity : GameEntityStateChangeActivestate_change : StateChangerResetResetProgressPropertyResistancePropertyAreaEffectrange : floatdropoff : DropoffTypeDiplomaticstances : set(DiplomaticStance)Costcost : CostEffectPropertyflacflacresistanceeffectMultipliermultiplier : floatModifierPropertyLockPoolslots : intLocklock_pools : set(LockPool)Locklock_pool : LockPoolAbilityPropertyMULTIXORXORNOTSUBSETMAXsize : intLogicGateinputs : set(LogicElement)BuySellExchangeModefee_multiplier : floatExchangeResourcesresource_a : Resourceresource_b : Resourceexchange_rate : ExchangeRateexchange_modes : set(ExchangeMode)ExchangeRatebase_price : floatprice_adjust : optional(dict(ExchangeMode, PriceMode)) = Noneprice_pool : optional(PricePool) = NoneAnyTransformPoolInternalDropSiteupdate_time : floatResourceContainerresource : Resourcemax_amount : intcarry_progress : set(Progress)ResourceStoragecontainers : set(ResourceContainer)MiscVariantElevationDifferenceHighmin_elevation_difference : optional(float) = NoneElevationDifferenceHighmin_elevation_difference : optional(float) = NoneAttributeAboveValueattribute : Attributethreshold : floatAttributeBelowPercentageattribute : Attributethreshold : floatAnimationOverlayoverlays : set(Animation)AnyAnyAnyTechTypeReplacegame_entities : set(GameEntity)Guardrange : floatPalettepalette : fileAttributeAbovePercentageattribute : Attributethreshold : floatNyanPatchStackedstack_limit : intSelectionBoxMatchToSpriteRectanglewidth : floatheight : floatMeanDistributionTypeDetectCloak (SWGB)range : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)NormalGatestances : set (DiplomaticStance)PassableModeallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Hitboxradius_x : floatradius_y : floatradius_z : floatProjectileHitTerrainProjectilePassThroughpass_through_range : intAttributeBelowValueattribute : Attributethreshold : floatTimertime : floatResourceSpotsDepletedonly_enabled : boolSelfAnyLiteralScopestances : set(DiplomaticStance)SUBSETMINsize : intORANDLogicElementonly_once : boolResearchablesexclude : set(ResearchableTech)Creatablesexclude : set(CreatableGameEntity)ProductionModeProductionQueuesize : intproduction_modes : set(ProductionMode)OwnStoragecontainer : EntityContainerNoStackLinearshift_x : intshift_y : intscale_factor : floatHyperbolicshift_x : intshift_y : intscale_factor : floatCalculationTypeStackedstack_limit : intcalculation_type : CalculationTypedistribution_type : DistributionTypeTerrainTypeMostHerdingLongestTimeInRangeClosestHerdingHerdableModeShadowTimeRelativeProgressIncreaseTimeRelativeProgressDecreaseTimeRelativeProgressIncreaseTimeRelativeProgressDecreaseAttributeChangeAdaptiveArrearAdvancePaymentModeResearchAttributeCostattributes : set(Attribute)researchables : set(ResearchableTech)CreationAttributeCostattributes : set(Attribute)creatables : set(CreatableGameEntity)AttributeCostamount : set(AttributeAmount)ResourceCostamount : set(ResourceAmount)Costpayment_mode : PaymentModeUnconditionalUnconditionalTimeRelativeProgressChangeTimeRelativeAttributeChangePricePoolDynamicchange_value : floatmin_price : floatmax_price : floatFixedPriceModeDepositResourcesOnProgressprogress_type : ProgressTyperesources : set(Resource)affected_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Attributename : TranslatedStringabbreviation : TranslatedStringOverlayTerrainterrain_overlay : TerrainTerrainRequirementallowed_types : set(TerrainType)blacklisted_terrains : set(Terrain)Terrainterrain : TerrainStateChangestate_change : StateChangerAoE1TradeRouteexchange_resources : set(Resource)trade_amount : intProgressTypeTimeRelativeProgressChangetype : ProgressTypeFlatAttributeIncreaseFlatAttributeDecreaseTimeRelativeAttributeChangetype : AttributeChangeTypeTimeRelativeProgressChangetype : ProgressTypetotal_change_time : floatTimeRelativeAttributeIncreaseTimeRelativeAttributeDecreaseTimeRelativeAttributeChangetype : AttributeChangeTypetotal_change_time : floatignore_protection : set(ProtectingAttribute)ProgressStatusprogress_type : ProgressTypeprogress : floatRefundOnConditioncondition : set(LogicElement)refund_amount : set(ResourceAmount)AnimationOverrideoverrides : set(AnimationOverride)ExecutionSoundsounds : set(Sound)InverseLinearAdjacentTilesVariantnorth : optional(GameEntity)north_east : optional(GameEntity)east : optional(GameEntity)south_east : optional(GameEntity)south : optional(GameEntity)south_west : optional(GameEntity)west : optional(GameEntity)north_west : optional(GameEntity)Placetile_snap_distance : floatclearance_size_x : floatclearance_size_y : floatallow_rotation : boolmax_elevation_difference : intEjectPlacementModeSendToContainerTypeLureTypeDiplomaticLineOfSightdiplomatic_stance : DiplomaticStanceTerrainterrain : TerrainTerrainterrain : TerrainNormalInContainerDiscreteEffectcontainers : set(EntityContainer)ability : ApplyDiscreteEffectInContainerContinuousEffectcontainers : set(EntityContainer)ability : ApplyContinuousEffectStateChangerenable_abilities : set(Ability)disable_abilities : set(Ability)enable_modifiers : set(Modifier)disable_modifiers : set(Modifier)transform_pool : optional(TransformPool) = Nonepriority : intopenage nyan data API v0.3.0openage nyan data API v0.4.0GameEntityScopeaffected_types : set(GameEntityType)blacklisted_entities : set(GameEntity)StandardRegenerateResourceSpotrate : ResourceRateresource_spot : ResourceSpotAoE2ProjectileAmountprovider_abilities : set(ApplyDiscreteEffect)receiver_abilities : set(ApplyDiscreteEffect)change_types : set(AttributeChangeType)Orange elements:Effects/Resistances thatcan be applied on othergame entitiesEffects/Resistances that can be applied on other game entitiesRevealline_of_sight : floataffected_types : set(GameEntityType)blacklisted_entities : set(GameEntity)ResearchTimeresearchables : set(ResearchableTech)StorageElementCapacitystorage_element : StorageElementDefinitionEntityContainerCapacitycontainer : EntityContainerHerdrange : floatstrength : intallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)InstantTechResearchtech : Techcondition : set(LogicElement)ResearchResourceCostresources : set(Resource)researchables : set(ResearchableTech)CreationResourceCostresources : set(Resource)creatables : set(CreatableGameEntity)CreationTimecreatables : set(CreatableGameEntity)ReloadTimeGatheringEfficiencyresource_spot : ResourceSpotAbsoluteProjectileAmountamount : floatStrayMoveModeAttackMoveModifierScopeExpectedPositionCurrentPositionTargetModeSendToContainertype : SendToContainerTypesearch_range : floatignore_containers : set(EntityContainer)SendToContainertype : SendToContainerTypestorages : set(EntityContainer)Scopedstances : set(DiplomaticStance)scope : ModifierScopeGameEntityFormationformation : Formationsubformation : SubformationSubformationordering_priority : intFormationsubformations : set(Subformation)FlatAttributeIncreaseFlatAttributeDecreaseFlatAttributeIncreaseFlatAttributeDecreaseFlatAttributeIncreaseFlatAttributeDecreaseFlatAttributeIncreaseFlatAttributeDecreaseAoE2TradeRouteTradeRoutetrade_resource : Resourcestart_trade_post : GameEntityend_trade_post : GameEntityTechtypes : set(TechType)name : TranslatedStringdescription : TranslatedMarkupFilelong_description : TranslatedMarkupFileupdates : orderedset(Patch)FallbackLinearNoDropoffDropoffTypeDiplomaticstances : set(DiplomaticStance)AnimationOverrideability : AnimatedAbilityanimations : set(Animation)priority : intCostcost : CostEntityContainerallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)storage_element_defs : set(StorageElementDefinition)slots : intcarry_progress : set(Progress)Visibilityvisible_in_fog : boolTurnturn_speed : floatLanguageietf_string : textLanguageSoundPairlanguage : Languagesound : SoundLanguageMarkupPairlanguage : Languagemarkup_file : fileLanguageTextPairlanguage : Languagestring : textLuretype : LureTypeLuretype : LureTypedestination : set(GameEntity)min_distance_to_destination : floatElevationDifferenceLowmin_elevation_difference : optional(float) = NoneSelfDiplomaticstances : set(DiplomaticStance)DiplomaticStanceExitContainerallowed_containers : set(EntityContainer)EnterContainerallowed_containers : set(EntityContainer)allowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)RangedDiscreteEffectmin_range : intmax_range : intHuntConvertTypeConvertSelfDestructRangedContinuousEffectmin_range : intmax_range : intMakeHarvestableresource_spot : ResourceSpotresist_condition : set(LogicElement)MakeHarvestableresource_spot : ResourceSpotGatherauto_resume : boolresume_search_range : floattargets : set(ResourceSpot)gather_rate : ResourceRatecontainer : ResourceContainerRepairMonkHealApplyContinuousEffecteffects : set(ContinuousEffect)application_delay : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)ApplyDiscreteEffectbatches : set(EffectBatch)reload_time : floatapplication_delay : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)FlatAttributeChangetype : AttributeChangeTypeblock_rate : AttributeRateContinuousResistanceAttributeRatetype : Attributerate : floatResourceRatetype : Resourcerate : floatDiscreteResistanceDiscreteEffectFlatAttributeChangetype : AttributeChangeTypemin_change_rate : optional(AttributeRate) = Nonemax_change_rate : optional(AttributeRate) = Nonechange_rate : AttributeRateignore_protection : set(ProtectingAttribute)ContinuousEffectAoE2Convertguaranteed_resist_rounds : intprotected_rounds : intprotection_round_recharge_time : floatAoE2Convertskip_guaranteed_rounds : intskip_protected_rounds : intConverttype : ConvertTypechance_resist : floatConverttype : ConvertTypemin_chance_success : optional(float) = Nonemax_chance_success : optional(float) = Nonechance_success : floatcost_fail : optional(Cost) = NoneAttributeChangeTypeFlatAttributeChangetype : AttributeChangeTypeblock_value : AttributeAmountFlatAttributeChangetype : AttributeChangeTypemin_change_value : optional(AttributeAmount) = Nonemax_change_value : optional(AttributeAmount) = Nonechange_value : AttributeAmountignore_protection : set(ProtectingAttribute)Effectors' sideResistors' sideEffectproperties : dict(EffectProperty, EffectProperty) = {}Resistanceproperties : dict(ResistanceProperty, ResistanceProperty) = {}HealthGameEntityTypeAttributeAmounttype : Attributeamount : intAccuracyaccuracy : floataccuracy_dispersion : floatdispersion_dropoff : DropOffTypetarget_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Cloak (SWGB)interrupted_by : set(Ability)interrupt_cooldown : floatLineOfSightrange : floatFaithShield (SWGB)ProtectingAttributeprotects : AttributeAttributeSettingattribute : Attributemin_value : intmax_value : intstarting_value : intTerrainOverlayterrain_overlay : TerrainAnimatedoverrides : set(AnimationOverride)Terrainsprite : fileCreatableGameEntitygame_entity : GameEntityvariants : set(Variant)cost : Costcreation_time : floatcreation_sounds : set(Sound)condition : set(LogicElement)placement_modes : set(PlacementMode)UseContingentamount : set(ResourceAmount)ProvideContingentamount : set(ResourceAmount)ResourceContingentmin_amount : intmax_amount : intLiteralscope : LiteralScopeProjectilearc : intaccuracy : set(Accuracy)target_mode : TargetModeignored_types : set(GameEntityType)unignore_entities : set(GameEntity)AttributeChangeTrackerattribute : Attributechange_progress : set(Progress)Hitboxhitbox : HitboxNamedname : TranslatedStringdescription : TranslatedMarkupFilelong_description : TranslatedMarkupFileDropResourcescontainers : set(ResourceContainer)search_range : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Herdableadjacent_discover_range : floatmode : HerdableModeStopPassablehitbox : Hitboxmode : PassableModeFoundationfoundation_terrain : TerrainPerspectiveVariantangle : intRestockauto_restock : booltarget : ResourceSpotrestock_time : floatmanual_cost : Costauto_cost : Costamount : intPassiveStandGroundDefensiveAggressiveTradePosttrade_routes : set(TradeRoute)Followrange : floatPatrolGameEntityStancesearch_range : floatability_preference : orderedset(Ability)type_preference : orderedset(GameEntityType)GameEntityStancestances: set(GameEntityStance)SendBackToTaskallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)TransferStoragestorage_element : GameEntitysource_container : EntityContainertarget_container : EntityContainerGameEntityProgressgame_entity : GameEntitystatus : ProgressStatusTechResearchedtech : TechVariantchanges : orderedset(Patch)priority : intActiveTransformTotarget_state : StateChangertransform_time : floattransform_progress : set(Progress)Selectableselection_box : SelectionBoxRallyPointWhite elements:AoE2 specific objectsYellow elements:Modifiers (handled byengine implementation)Modifiers (handled by engine implementation)Green elements:Abilities (handled by engineimplementation)Abilities (handled by engine implementation)Pink elements:Basic nyan API objectsElevationDifferenceLowmin_elevation_difference : optional(float) = NoneFlyoverrelative_angle : floatflyover_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Animationsprite : fileVillager Gather abilities canoverride the graphics ofIdle,Move,Die and Despawnvia CarryProgress objects withAnimationOverridesAbilities andStorageElementscan use theseOverride typesto change anyother ability'sanimation.If min_projectiles is greater than thenumber of Projectiles in projectiles,the last projectile in the orderedsetshould be usedIf min_projectiles is greater thanthe number of Projectiles inprojectiles, the last projectilein the orderedset should be usedIn AoE2 there is onlyone HarvestProgressState in the interval[0,100], but AoM hadmore.one HarvestProgress Statein the interval [0,100],but AoM had more.Stores what happens aftera percentage ofconstruction, damage,transformation, etc. isreachedProgressproperties : dict(ProgressProperty, ProgressProperty) = {}type : ProgressTypeleft_boundary : floatright_boundary : floatciv_setup patches the uniquegame_setup patches the uniquefeatures into the objects(graphics, techs, boni, abilities,etc.)RemoveStoragecontainer : EntityContainerstorage_elements : set(GameEntity)CollectStoragecontainer : EntityContainerstorage_elements : set(GameEntity)StorageElementDefinitionstorage_element : GameEntityelements_per_slot : intconflicts : set(StorageElementDefinition)state_change : optional(StateChanger) = NoneStoragecontainer : EntityContainerempty_condition : set(LogicElement)RandomVariantchance_share : floatResearchresearchables : set(ResearchableTech)Tradetrade_routes : set(TradeRoute)container : ResourceContainerCreatecreatables : set(CreatableGameEntity)RelicBonusGatheringRateresource_spot : ResourceSpotMoveSpeedAttributeSettingsValueattribute : AttributeFeitoriaBonusFoodAmountWoodAmountStoneAmountGoldAmountResourceAmounttype : Resourceamount : intContinuousResourcerates : set(ResourceRate)Modifier should only be used incases where Patches don'tModifier should only be usedin cases where Patches don'twork. For example, if thebonus is a percentage value orcontinuously stacks (likebonus is a percentage valueor continuously stacks (likeresources from the Feitoria).Modifier objects can still bepatched.IdlePlayerSetupname : TranslatedStringdescription : TranslatedMarkupFilelong_description : TranslatedMarkupFileleader_names : set(TranslatedString)modifiers : set(Modifier)starting_resources : set(ResourceAmount)game_setup : orderedset(Patch)Despawnactivation_condition : set(LogicElement)despawn_condition : set(LogicElement)despawn_time : floatstate_change : optional(StateChanger) = NoneTauntactivation_message : textdisplay_message : TranslatedStringsound : SoundLiveattributes : set(AttributeSetting)Harvestableresources : ResourceSpotharvest_progress : set(HarvestProgress)restock_progress : set(RestockProgress)harvest_progress : set(Progress)restock_progress : set(Progress)gatherer_limit : intharvestable_by_default : boolPassiveTransformTocondition : set(LogicElement)transform_time : floattarget_state : StateChangertransform_progress : set(Progress)Cheatactivation_message : textchanges : orderedset(Patch)Flyheight : floatResistanceresistances : set(Resistance)ShootProjectileprojectiles : orderedset(GameEntity)min_projectiles : intmax_projectiles : intmin_range : intmax_range : intreload_time : floatspawn_delay : floatprojectile_delay : floatrequire_turning : boolmanual_aiming_allowed : boolspawning_area_offset_x : floatspawning_area_offset_y : floatspawning_area_offset_z : floatspawning_area_width : floatspawning_area_height : floatspawning_area_randomness : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)RegenerateAttributerate : AttributeRateFormationformations : set(GameEntityFormation)Movespeed : floatmodes : set(MoveMode)CommandSoundsounds : set(Sound)Animatedanimations : set(Animation)Abilityproperties : dict(AbilityProperty, AbilityProperty) = {}Modpriority : intpatches : orderedset(Patch)Patchproperties : dict(PatchProperty, PatchProperty) = {}patch : NyanPatchModifierproperties : dict(ModifierProperty, ModifierProperty) = {}Soundplay_delay : floatsounds : orderedset(file)TerrainAmbientobject : GameEntitymax_density : intTerrainname : TranslatedStringtypes : set(TerrainType)terrain_graphic : Terrainsound : Soundambience : set(TerrainAmbient)ResearchableTechtech : Techcost : Costresearch_time : floatresearch_sounds : set(Sound)condition : set(LogicElement)TranslatedSoundtranslations : set(LanguageSoundPair)TranslatedMarkupFiletranslations : set(LanguageMarkupPair)TranslatedObjectTranslatedStringtranslations : set(LanguageTextPair)Resourcename : TranslatedStringmax_storage : intResourceSpotresource : Resourcemax_amount : intstarting_amount : intdecay_rate : floatDropSiteaccepts_from : set(ResourceContainer)GameEntitytypes : set(GameEntityType)abilities : set(Ability)modifiers : set(Modifier)variants : set(Variant)EntityObject - + // Uncomment the following line to change the fontsize and font: // fontsize=10 // fontfamily=SansSerif //possible: SansSerif,Serif,Monospaced @@ -24,27 +24,27 @@ // This text will be stored with each diagram; use it for notes. - 7 + 10 UMLClass - 1666 - 2520 - 70 - 42 + 1190 + 3600 + 100 + 60 -*Entity* +*Object* bg=red UMLClass - 1120 - 2506 - 210 - 84 + 410 + 3580 + 300 + 120 *GameEntity* @@ -59,10 +59,10 @@ variants : set(Variant) UMLClass - 4284 - 2191 - 203 - 56 + 4930 + 3130 + 290 + 80 *DropSite* bg=green @@ -74,10 +74,10 @@ accepts_from : set(ResourceContainer) Relation - 1323 - 2534 - 357 - 21 + 700 + 3620 + 510 + 30 lt=<<- 490.0;10.0;10.0;10.0 @@ -85,10 +85,10 @@ accepts_from : set(ResourceContainer) UMLClass - 1428 - 2184 - 210 - 84 + 850 + 3120 + 300 + 120 *ResourceSpot* @@ -103,10 +103,10 @@ decay_rate : float UMLClass - 1470 - 2310 - 126 - 56 + 910 + 3300 + 180 + 80 *Resource* bg=pink @@ -119,10 +119,10 @@ max_storage : int UMLClass - 833 - 1869 - 189 - 56 + 0 + 2670 + 270 + 80 *TranslatedString* bg=pink @@ -134,10 +134,10 @@ translations : set(LanguageTextPair) UMLClass - 1092 - 1981 - 105 - 42 + 370 + 2830 + 150 + 60 *TranslatedObject* @@ -147,10 +147,10 @@ bg=pink UMLClass - 1036 - 1869 - 203 - 56 + 290 + 2670 + 290 + 80 *TranslatedMarkupFile* bg=pink @@ -162,10 +162,10 @@ translations : set(LanguageMarkupPair) UMLClass - 1253 - 1869 - 196 - 56 + 600 + 2670 + 280 + 80 *TranslatedSound* bg=pink @@ -177,10 +177,10 @@ translations : set(LanguageSoundPair) Relation - 1134 - 1918 - 21 - 77 + 430 + 2740 + 30 + 110 lt=<<- 10.0;90.0;10.0;10.0 @@ -188,10 +188,10 @@ translations : set(LanguageSoundPair) Relation - 917 - 1918 - 238 - 42 + 120 + 2740 + 340 + 60 lt=- 320.0;40.0;10.0;40.0;10.0;10.0 @@ -199,10 +199,10 @@ translations : set(LanguageSoundPair) Relation - 1134 - 1918 - 231 - 42 + 430 + 2740 + 330 + 60 lt=- 10.0;40.0;310.0;40.0;310.0;10.0 @@ -210,10 +210,10 @@ translations : set(LanguageSoundPair) UMLClass - 3115 - 1694 - 224 - 91 + 3260 + 2420 + 320 + 130 *ResearchableTech* bg=pink @@ -229,10 +229,10 @@ condition : set(LogicElement) UMLClass - 1764 - 2611 - 224 - 91 + 1330 + 3730 + 320 + 130 *Terrain* bg=pink @@ -248,10 +248,10 @@ ambience : set(TerrainAmbient) UMLClass - 1890 - 2730 - 133 - 56 + 1510 + 3900 + 190 + 80 *TerrainAmbient* bg=pink @@ -264,10 +264,10 @@ max_density : int UMLClass - 2107 - 2275 - 133 - 56 + 1820 + 3250 + 190 + 80 *Sound* bg=pink @@ -280,10 +280,10 @@ sounds : orderedset(file) UMLClass - 1953 - 875 - 252 - 56 + 1600 + 1250 + 360 + 80 *Modifier* bg=pink @@ -295,10 +295,10 @@ properties : dict(ModifierProperty, ModifierProperty) = {} UMLClass - 1764 - 2471 - 238 - 56 + 1330 + 3530 + 340 + 80 *Patch* bg=pink @@ -311,10 +311,10 @@ patch : NyanPatch UMLClass - 2107 - 2457 - 147 - 56 + 1820 + 3510 + 210 + 80 *Mod* bg=pink @@ -327,10 +327,10 @@ patches : orderedset(Patch) Relation - 1729 - 2534 - 1785 - 21 + 1280 + 3620 + 2550 + 30 lt=<<- 10.0;10.0;2530.0;10.0 @@ -338,10 +338,10 @@ patches : orderedset(Patch) UMLClass - 3500 - 2520 - 238 - 63 + 3810 + 3600 + 340 + 90 *Ability* bg=pink @@ -353,10 +353,10 @@ properties : dict(AbilityProperty, AbilityProperty) = {} UMLClass - 3367 - 2807 - 126 - 56 + 3620 + 4010 + 180 + 80 *Animated* bg=pink @@ -368,10 +368,10 @@ animations : set(Animation) UMLClass - 3367 - 2744 - 126 - 56 + 3620 + 3920 + 180 + 80 *CommandSound* bg=pink @@ -384,10 +384,10 @@ sounds : set(Sound) UMLClass - 3850 - 2688 - 126 - 56 + 4310 + 3840 + 180 + 80 *Move* bg=green @@ -400,10 +400,10 @@ modes : set(MoveMode) UMLClass - 3682 - 2996 - 203 - 56 + 4070 + 4280 + 290 + 80 *Formation* bg=green @@ -415,10 +415,10 @@ formations : set(GameEntityFormation) UMLClass - 4235 - 3024 - 126 - 56 + 4860 + 4320 + 180 + 80 *RegenerateAttribute* bg=green @@ -430,10 +430,10 @@ rate : AttributeRate UMLClass - 5005 - 1365 - 224 - 238 + 5960 + 1950 + 320 + 340 *ShootProjectile* bg=green @@ -467,10 +467,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 4214 - 3164 - 147 - 56 + 4830 + 4520 + 210 + 80 *Resistance* bg=green @@ -482,10 +482,10 @@ resistances : set(Resistance) UMLClass - 3696 - 2688 - 112 - 56 + 4090 + 3840 + 160 + 80 *Fly* bg=green @@ -497,10 +497,10 @@ height : float Relation - 1190 - 1995 - 525 - 539 + 510 + 2850 + 750 + 770 lt=<<- 730.0;750.0;730.0;10.0;10.0;10.0 @@ -508,10 +508,10 @@ height : float UMLClass - 2310 - 2142 - 133 - 56 + 2110 + 3060 + 190 + 80 *Cheat* bg=pink @@ -524,10 +524,10 @@ changes : orderedset(Patch) Relation - 1589 - 2338 - 126 - 21 + 1080 + 3340 + 180 + 30 lt=- 160.0;10.0;10.0;10.0 @@ -535,10 +535,10 @@ changes : orderedset(Patch) Relation - 1526 - 2261 - 21 - 63 + 990 + 3230 + 30 + 90 lt=<. 10.0;70.0;10.0;10.0 @@ -546,10 +546,10 @@ changes : orderedset(Patch) Relation - 1631 - 2205 - 84 - 21 + 1140 + 3150 + 120 + 30 lt=- 10.0;10.0;100.0;10.0 @@ -557,10 +557,10 @@ changes : orderedset(Patch) Relation - 1911 - 2695 - 21 - 49 + 1540 + 3850 + 30 + 70 lt=<. 10.0;50.0;10.0;10.0 @@ -568,10 +568,10 @@ changes : orderedset(Patch) Relation - 2261 - 2226 - 63 - 21 + 2040 + 3180 + 90 + 30 lt=- 10.0;10.0;70.0;10.0 @@ -579,10 +579,10 @@ changes : orderedset(Patch) Relation - 1862 - 2534 - 21 - 91 + 1470 + 3620 + 30 + 130 lt=- 10.0;10.0;10.0;110.0 @@ -590,10 +590,10 @@ changes : orderedset(Patch) UMLClass - 5425 - 2513 - 203 - 77 + 6560 + 3590 + 290 + 110 *PassiveTransformTo* bg=green @@ -608,18 +608,18 @@ transform_progress : set(Progress) UMLClass - 4032 - 2023 - 210 - 98 + 4570 + 2890 + 300 + 140 *Harvestable* bg=green -- resources : ResourceSpot -harvest_progress : set(HarvestProgress) -restock_progress : set(RestockProgress) +harvest_progress : set(Progress) +restock_progress : set(Progress) gatherer_limit : int harvestable_by_default : bool @@ -627,10 +627,10 @@ harvestable_by_default : bool UMLClass - 4298 - 3241 - 168 - 56 + 4950 + 4630 + 240 + 80 *Live* bg=green @@ -642,10 +642,10 @@ attributes : set(AttributeSetting) UMLClass - 2310 - 2205 - 175 - 70 + 2110 + 3150 + 250 + 100 *Taunt* bg=pink @@ -659,10 +659,10 @@ sound : Sound Relation - 2170 - 2506 - 21 - 49 + 1910 + 3580 + 30 + 70 lt=- 10.0;10.0;10.0;50.0 @@ -670,10 +670,10 @@ sound : Sound Relation - 2072 - 924 - 21 - 1631 + 1770 + 1320 + 30 + 2330 lt=- 10.0;10.0;10.0;2310.0 @@ -681,10 +681,10 @@ sound : Sound Relation - 1862 - 2520 - 21 - 35 + 1470 + 3600 + 30 + 50 lt=- 10.0;10.0;10.0;30.0 @@ -692,10 +692,10 @@ sound : Sound Relation - 2261 - 2163 - 63 - 21 + 2040 + 3090 + 90 + 30 lt=- 70.0;10.0;10.0;10.0 @@ -703,10 +703,10 @@ sound : Sound UMLClass - 5327 - 2618 - 217 - 77 + 6420 + 3740 + 310 + 110 *Despawn* bg=green @@ -721,10 +721,10 @@ state_change : optional(StateChanger) = None UMLClass - 1274 - 3066 - 217 - 112 + 630 + 4380 + 310 + 160 *PlayerSetup* bg=pink @@ -742,10 +742,10 @@ game_setup : orderedset(Patch) Relation - 1372 - 2534 - 21 - 546 + 770 + 3620 + 30 + 780 lt=- 10.0;760.0;10.0;10.0 @@ -753,10 +753,10 @@ game_setup : orderedset(Patch) Relation - 2037 - 2534 - 21 - 294 + 1720 + 3620 + 30 + 420 lt=- 10.0;10.0;10.0;400.0 @@ -764,10 +764,10 @@ game_setup : orderedset(Patch) UMLClass - 5327 - 2569 - 84 - 42 + 6420 + 3670 + 120 + 60 *Idle* @@ -777,22 +777,29 @@ bg=green UMLNote - 1918 - 945 - 147 - 112 - - Modifier should only be used in cases where Patches don't work. For example, if the bonus is a percentage value or continuously stacks (like resources from the Feitoria). Modifier objects can still be patched. + 1550 + 1350 + 210 + 160 + + Modifier should only be used +in cases where Patches don't +work. For example, if the +bonus is a percentage value +or continuously stacks (like +resources from the Feitoria). +Modifier objects can still be +patched. bg=blue UMLClass - 1995 - 770 - 154 - 56 + 1660 + 1100 + 220 + 80 *ContinuousResource* bg=yellow @@ -804,10 +811,10 @@ rates : set(ResourceRate) UMLClass - 1470 - 2401 - 126 - 56 + 910 + 3430 + 180 + 80 *ResourceAmount* bg=pink @@ -820,10 +827,10 @@ amount : int UMLClass - 1204 - 2387 - 84 - 42 + 530 + 3410 + 120 + 60 *GoldAmount* @@ -832,10 +839,10 @@ amount : int UMLClass - 1204 - 2436 - 84 - 42 + 530 + 3480 + 120 + 60 *StoneAmount* @@ -844,10 +851,10 @@ amount : int UMLClass - 1204 - 2338 - 84 - 42 + 530 + 3340 + 120 + 60 *WoodAmount* @@ -856,10 +863,10 @@ amount : int UMLClass - 1204 - 2289 - 84 - 42 + 530 + 3270 + 120 + 60 *FoodAmount* @@ -868,10 +875,10 @@ amount : int Relation - 1589 - 2422 - 126 - 21 + 1080 + 3460 + 180 + 30 lt=- 160.0;10.0;10.0;10.0 @@ -879,10 +886,10 @@ amount : int Relation - 1526 - 2359 - 21 - 56 + 990 + 3370 + 30 + 80 lt=<. 10.0;10.0;10.0;60.0 @@ -890,10 +897,10 @@ amount : int Relation - 1295 - 2422 - 189 - 21 + 660 + 3460 + 270 + 30 lt=<<- 250.0;10.0;10.0;10.0 @@ -901,10 +908,10 @@ amount : int Relation - 1281 - 2303 - 35 - 70 + 640 + 3290 + 50 + 100 lt=- 10.0;10.0;30.0;10.0;30.0;80.0 @@ -912,10 +919,10 @@ amount : int Relation - 1281 - 2352 - 35 - 70 + 640 + 3360 + 50 + 100 lt=- 10.0;10.0;30.0;10.0;30.0;80.0 @@ -923,10 +930,10 @@ amount : int Relation - 1281 - 2415 - 35 - 56 + 640 + 3450 + 50 + 80 lt=- 10.0;60.0;30.0;60.0;30.0;10.0 @@ -934,10 +941,10 @@ amount : int Relation - 1281 - 2401 - 35 - 35 + 640 + 3430 + 50 + 50 lt=- 10.0;10.0;30.0;10.0;30.0;30.0 @@ -945,10 +952,10 @@ amount : int Relation - 2072 - 819 - 21 - 70 + 1770 + 1170 + 30 + 100 lt=<<- 10.0;80.0;10.0;10.0 @@ -956,10 +963,10 @@ amount : int UMLClass - 1974 - 686 - 84 - 42 + 1630 + 980 + 120 + 60 *FeitoriaBonus* @@ -968,10 +975,10 @@ amount : int Relation - 2009 - 721 - 84 - 63 + 1680 + 1030 + 120 + 90 lt=<<- 100.0;70.0;100.0;40.0;10.0;40.0;10.0;10.0 @@ -979,10 +986,10 @@ amount : int UMLClass - 1512 - 707 - 133 - 56 + 970 + 1010 + 190 + 80 *AttributeSettingsValue* bg=yellow @@ -994,10 +1001,10 @@ attribute : Attribute UMLClass - 1547 - 658 - 98 - 42 + 1020 + 940 + 140 + 60 *MoveSpeed* @@ -1007,10 +1014,10 @@ bg=yellow UMLClass - 1491 - 483 - 154 - 56 + 940 + 690 + 220 + 80 *GatheringRate* bg=yellow @@ -1022,10 +1029,10 @@ resource_spot : ResourceSpot Relation - 1673 - 336 - 420 - 532 + 1200 + 480 + 600 + 760 lt=- 580.0;740.0;10.0;740.0;10.0;10.0 @@ -1033,10 +1040,10 @@ resource_spot : ResourceSpot Relation - 1638 - 672 - 56 - 21 + 1150 + 960 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -1044,10 +1051,10 @@ resource_spot : ResourceSpot Relation - 1638 - 504 - 56 - 21 + 1150 + 720 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -1055,10 +1062,10 @@ resource_spot : ResourceSpot UMLClass - 2100 - 686 - 84 - 42 + 1810 + 980 + 120 + 60 *RelicBonus* @@ -1067,10 +1074,10 @@ resource_spot : ResourceSpot Relation - 2072 - 721 - 84 - 42 + 1770 + 1030 + 120 + 60 lt=- 10.0;40.0;100.0;40.0;100.0;10.0 @@ -1078,10 +1085,10 @@ resource_spot : ResourceSpot UMLClass - 3360 - 1806 - 196 - 56 + 3610 + 2580 + 280 + 80 *Create* bg=green @@ -1093,10 +1100,10 @@ creatables : set(CreatableGameEntity) UMLClass - 3892 - 1869 - 161 - 56 + 4370 + 2670 + 230 + 80 *Trade* bg=green @@ -1110,10 +1117,10 @@ container : ResourceContainer UMLClass - 3360 - 1694 - 196 - 56 + 3610 + 2420 + 280 + 80 *Research* bg=green @@ -1125,10 +1132,10 @@ researchables : set(ResearchableTech) UMLClass - 1092 - 2639 - 105 - 56 + 370 + 3770 + 150 + 80 *RandomVariant* @@ -1140,10 +1147,10 @@ chance_share : float Relation - 1358 - 2660 - 35 - 21 + 750 + 3800 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -1151,10 +1158,10 @@ chance_share : float UMLClass - 4067 - 1148 - 182 - 63 + 4620 + 1640 + 260 + 90 *Storage* bg=green @@ -1168,10 +1175,10 @@ empty_condition : set(LogicElement) UMLClass - 4319 - 1036 - 217 - 84 + 4980 + 1480 + 310 + 120 *StorageElementDefinition* bg=pink @@ -1186,21 +1193,21 @@ state_change : optional(StateChanger) = None Relation - 4151 - 1113 - 21 - 49 + 4740 + 1600 + 30 + 60 lt=<. - 10.0;10.0;10.0;50.0 + 10.0;10.0;10.0;40.0 UMLClass - 3927 - 1379 - 210 - 63 + 4420 + 1970 + 300 + 90 *CollectStorage* bg=green @@ -1213,10 +1220,10 @@ storage_elements : set(GameEntity) UMLClass - 3927 - 1309 - 210 - 63 + 4420 + 1870 + 300 + 90 *RemoveStorage* bg=green @@ -1229,22 +1236,25 @@ storage_elements : set(GameEntity) UMLNote - 1337 - 3185 - 154 - 56 + 720 + 4550 + 220 + 80 - civ_setup patches the unique features into the objects (graphics, techs, boni, abilities, etc.) + game_setup patches the unique +features into the objects +(graphics, techs, boni, abilities, +etc.) bg=blue UMLClass - 2254 - 3346 - 259 - 77 + 2030 + 4780 + 370 + 110 *Progress* bg=pink @@ -1260,10 +1270,10 @@ right_boundary : float Relation - 2331 - 2534 - 21 - 826 + 2140 + 3620 + 30 + 1180 lt=- 10.0;10.0;10.0;1160.0 @@ -1271,71 +1281,90 @@ right_boundary : float UMLNote - 2345 - 3276 - 133 - 63 + 2160 + 4680 + 190 + 90 - Stores what happens after a percentage of construction, damage, transformation, etc. is reached + Stores what happens after +a percentage of +construction, damage, +transformation, etc. is +reached bg=blue UMLNote - 2639 - 2954 - 119 - 63 + 2580 + 4220 + 200 + 90 - In AoE2 there is only one HarvestProgress State in the interval [0,100], but AoM had more. + In AoE2 there is only +one HarvestProgress State +in the interval [0,100], +but AoM had more. bg=blue UMLNote - 5236 - 1435 - 168 - 63 + 6290 + 2050 + 240 + 90 - If min_projectiles is greater than the number of Projectiles in projectiles, the last projectile in the orderedset should be used + If min_projectiles is greater than +the number of Projectiles in +projectiles, the last projectile +in the orderedset should be used bg=blue UMLNote - 2072 - 2632 - 91 - 84 - - Abilities and StorageElements can use these Override types -to change any other ability's animation. + 1770 + 3760 + 130 + 120 + + Abilities and +StorageElements +can use these +Override types +to change any +other ability's +animation. bg=blue UMLNote - 4284 - 2422 - 147 - 70 + 4930 + 3460 + 210 + 100 - Villager Gather abilities can override the graphics of Idle,Move,Die and Despawn via CarryProgress objects with AnimationOverrides + Villager Gather abilities can +override the graphics of +Idle,Move,Die and Despawn +via CarryProgress objects with +AnimationOverrides bg=blue UMLClass - 2310 - 2331 - 133 - 56 + 2110 + 3330 + 190 + 80 *Animation* bg=pink @@ -1347,10 +1376,10 @@ sprite : file Relation - 2233 - 2296 - 49 - 21 + 2000 + 3280 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -1358,10 +1387,10 @@ sprite : file UMLClass - 1141 - 168 - 175 - 63 + 440 + 240 + 250 + 90 *Flyover* bg=yellow @@ -1375,10 +1404,10 @@ blacklisted_entities : set(GameEntity) Relation - 1330 - 224 - 259 - 21 + 710 + 320 + 370 + 30 lt=- 350.0;10.0;10.0;10.0 @@ -1386,10 +1415,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 1092 - 238 - 224 - 49 + 370 + 340 + 320 + 70 *ElevationDifferenceLow* bg=yellow @@ -1401,10 +1430,10 @@ min_elevation_difference : optional(float) = None Relation - 1309 - 189 - 42 - 21 + 680 + 270 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -1412,10 +1441,10 @@ min_elevation_difference : optional(float) = None Relation - 1309 - 259 - 42 - 21 + 680 + 370 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -1424,9 +1453,9 @@ min_elevation_difference : optional(float) = None UMLNote 0 - 889 - 133 - 49 + 110 + 190 + 70 Pink elements: @@ -1439,9 +1468,9 @@ bg=pink UMLNote 0 - 945 - 133 - 49 + 190 + 190 + 70 Green elements: @@ -1453,9 +1482,9 @@ bg=green UMLNote 0 - 1001 - 133 - 49 + 270 + 190 + 70 Yellow elements: @@ -1467,9 +1496,9 @@ bg=yellow UMLNote 0 - 1127 - 133 - 49 + 450 + 190 + 70 White elements: @@ -1479,10 +1508,10 @@ AoE2 specific objects UMLClass - 3472 - 1911 - 84 - 42 + 3770 + 2730 + 120 + 60 *RallyPoint* @@ -1492,10 +1521,10 @@ bg=green UMLClass - 5124 - 2702 - 161 - 63 + 6130 + 3860 + 230 + 90 *Selectable* bg=green @@ -1507,10 +1536,10 @@ selection_box : SelectionBox UMLClass - 3472 - 3143 - 231 - 70 + 3770 + 4490 + 330 + 100 *ActiveTransformTo* bg=green @@ -1524,10 +1553,10 @@ transform_progress : set(Progress) UMLClass - 1218 - 2639 - 147 - 56 + 550 + 3770 + 210 + 80 *Variant* bg=pink @@ -1540,10 +1569,10 @@ priority : int Relation - 1190 - 2660 - 42 - 21 + 510 + 3800 + 60 + 30 lt=<<- 40.0;10.0;10.0;10.0 @@ -1551,10 +1580,10 @@ priority : int UMLClass - 1932 - 2982 - 147 - 56 + 1570 + 4260 + 210 + 80 *TechResearched* bg=pink @@ -1566,10 +1595,10 @@ tech : Tech UMLClass - 1918 - 3045 - 161 - 63 + 1550 + 4350 + 230 + 90 *GameEntityProgress* bg=pink @@ -1582,10 +1611,10 @@ status : ProgressStatus Relation - 2086 - 2954 - 21 - 630 + 1790 + 4220 + 30 + 900 lt=<<- 10.0;10.0;10.0;880.0 @@ -1593,10 +1622,10 @@ status : ProgressStatus UMLClass - 3955 - 1232 - 182 - 70 + 4460 + 1760 + 260 + 100 *TransferStorage* bg=green @@ -1610,10 +1639,10 @@ target_container : EntityContainer Relation - 2037 - 2576 - 49 - 21 + 1720 + 3680 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -1621,10 +1650,10 @@ target_container : EntityContainer UMLClass - 4179 - 1379 - 224 - 63 + 4780 + 1970 + 320 + 90 *SendBackToTask* bg=green @@ -1637,10 +1666,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 3682 - 3059 - 175 - 56 + 4070 + 4370 + 250 + 80 *GameEntityStance* bg=green @@ -1652,10 +1681,10 @@ stances: set(GameEntityStance) UMLClass - 3913 - 3066 - 238 - 63 + 4400 + 4380 + 340 + 90 *GameEntityStance* bg=pink @@ -1669,10 +1698,10 @@ type_preference : orderedset(GameEntityType) Relation - 3850 - 3080 - 77 - 21 + 4310 + 4400 + 110 + 30 lt=<. 90.0;10.0;10.0;10.0 @@ -1680,10 +1709,10 @@ type_preference : orderedset(GameEntityType) UMLClass - 4053 - 2688 - 77 - 42 + 4600 + 3840 + 110 + 60 *Patrol* @@ -1693,10 +1722,10 @@ bg=pink UMLClass - 4053 - 2737 - 112 - 56 + 4600 + 3910 + 160 + 80 *Follow* bg=pink @@ -1708,10 +1737,10 @@ range : float Relation - 4018 - 2660 - 21 - 287 + 4550 + 3800 + 30 + 410 lt=<<- 10.0;10.0;10.0;390.0 @@ -1719,10 +1748,10 @@ range : float Relation - 4018 - 2758 - 49 - 21 + 4550 + 3940 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -1730,10 +1759,10 @@ range : float UMLClass - 3892 - 1806 - 203 - 56 + 4370 + 2580 + 290 + 80 *TradePost* bg=green @@ -1745,10 +1774,10 @@ trade_routes : set(TradeRoute) UMLClass - 3885 - 3143 - 70 - 42 + 4360 + 4490 + 100 + 60 *Aggressive* @@ -1758,10 +1787,10 @@ bg=pink UMLClass - 3885 - 3192 - 70 - 42 + 4360 + 4560 + 100 + 60 *Defensive* @@ -1771,10 +1800,10 @@ bg=pink UMLClass - 3871 - 3241 - 84 - 42 + 4340 + 4630 + 120 + 60 *StandGround* @@ -1784,10 +1813,10 @@ bg=pink UMLClass - 3885 - 3290 - 70 - 42 + 4360 + 4700 + 100 + 60 *Passive* @@ -1797,10 +1826,10 @@ bg=pink Relation - 3948 - 3206 - 42 - 21 + 4450 + 4580 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -1808,10 +1837,10 @@ bg=pink Relation - 3969 - 3122 - 21 - 203 + 4480 + 4460 + 30 + 290 lt=<<- 10.0;10.0;10.0;270.0 @@ -1819,10 +1848,10 @@ bg=pink Relation - 3948 - 3255 - 42 - 21 + 4450 + 4650 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -1830,10 +1859,10 @@ bg=pink Relation - 3948 - 3304 - 42 - 21 + 4450 + 4720 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -1841,10 +1870,10 @@ bg=pink UMLClass - 4053 - 2128 - 189 - 98 + 4600 + 3040 + 270 + 140 *Restock* bg=green @@ -1861,10 +1890,10 @@ amount : int UMLClass - 1092 - 2702 - 119 - 56 + 370 + 3860 + 170 + 80 *PerspectiveVariant* @@ -1876,10 +1905,10 @@ angle : int Relation - 1204 - 2688 - 98 - 56 + 530 + 3840 + 140 + 80 lt=<<- 120.0;10.0;120.0;60.0;10.0;60.0 @@ -1887,10 +1916,10 @@ angle : int UMLClass - 5040 - 3332 - 154 - 56 + 6010 + 4760 + 220 + 80 *Foundation* bg=green @@ -1902,10 +1931,10 @@ foundation_terrain : Terrain UMLClass - 4403 - 3017 - 154 - 63 + 5100 + 4310 + 220 + 90 *Passable* bg=green @@ -1918,10 +1947,10 @@ mode : PassableMode UMLClass - 5327 - 2471 - 84 - 42 + 6420 + 3530 + 120 + 60 // Returns unit to Idle state @@ -1932,10 +1961,10 @@ bg=green UMLClass - 5327 - 2751 - 224 - 70 + 6420 + 3930 + 320 + 100 *Herdable* bg=green @@ -1948,10 +1977,10 @@ mode : HerdableMode UMLClass - 4284 - 2254 - 182 - 77 + 4930 + 3220 + 260 + 110 *DropResources* bg=green @@ -1967,10 +1996,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 4158 - 2947 - 203 - 70 + 4750 + 4210 + 290 + 100 *Named* bg=green @@ -1984,10 +2013,10 @@ long_description : TranslatedMarkupFile UMLClass - 4403 - 3087 - 98 - 56 + 5100 + 4410 + 140 + 80 *Hitbox* bg=green @@ -1999,10 +2028,10 @@ hitbox : Hitbox UMLClass - 4823 - 3332 - 175 - 56 + 5700 + 4760 + 250 + 80 *AttributeChangeTracker* bg=green @@ -2015,10 +2044,10 @@ change_progress : set(Progress) UMLClass - 5005 - 1246 - 224 - 105 + 5960 + 1780 + 320 + 150 *Projectile* bg=green @@ -2037,10 +2066,10 @@ unignore_entities : set(GameEntity) UMLClass - 1960 - 2905 - 147 - 56 + 1610 + 4150 + 210 + 80 *Literal* bg=pink @@ -2052,10 +2081,10 @@ scope : LiteralScope UMLClass - 1323 - 2310 - 126 - 56 + 700 + 3300 + 180 + 80 *ResourceContingent* bg=pink @@ -2068,10 +2097,10 @@ max_amount : int Relation - 1442 - 2331 - 42 - 21 + 870 + 3330 + 60 + 30 lt=<<- 40.0;10.0;10.0;10.0 @@ -2079,10 +2108,10 @@ max_amount : int UMLClass - 4284 - 2065 - 175 - 56 + 4930 + 2950 + 250 + 80 *ProvideContingent* bg=green @@ -2094,10 +2123,10 @@ amount : set(ResourceAmount) UMLClass - 4284 - 2128 - 175 - 56 + 4930 + 3040 + 250 + 80 *UseContingent* bg=green @@ -2109,10 +2138,10 @@ amount : set(ResourceAmount) UMLClass - 3115 - 1792 - 224 - 112 + 3260 + 2560 + 320 + 160 *CreatableGameEntity* bg=pink @@ -2131,10 +2160,10 @@ placement_modes : set(PlacementMode) Relation - 3332 - 1827 - 42 - 21 + 3570 + 2610 + 60 + 30 lt=<. 10.0;10.0;40.0;10.0 @@ -2142,10 +2171,10 @@ placement_modes : set(PlacementMode) UMLClass - 2310 - 2457 - 133 - 56 + 2110 + 3510 + 190 + 80 *Terrain* bg=pink @@ -2157,10 +2186,10 @@ sprite : file Relation - 2261 - 2163 - 21 - 392 + 2040 + 3090 + 30 + 560 lt=- 10.0;10.0;10.0;540.0 @@ -2168,10 +2197,10 @@ sprite : file Relation - 2261 - 2352 - 63 - 21 + 2040 + 3360 + 90 + 30 lt=- 70.0;10.0;10.0;10.0 @@ -2179,10 +2208,10 @@ sprite : file UMLClass - 2555 - 3262 - 161 - 56 + 2460 + 4660 + 230 + 80 *Animated* bg=pink @@ -2195,10 +2224,10 @@ overrides : set(AnimationOverride) UMLClass - 2555 - 3388 - 133 - 56 + 2460 + 4840 + 190 + 80 *TerrainOverlay* bg=pink @@ -2212,10 +2241,10 @@ terrain_overlay : Terrain Relation - 2527 - 3283 - 42 - 21 + 2420 + 4690 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -2223,10 +2252,10 @@ terrain_overlay : Terrain Relation - 2527 - 3409 - 42 - 21 + 2420 + 4870 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -2234,10 +2263,10 @@ terrain_overlay : Terrain UMLClass - 4305 - 3339 - 154 - 77 + 4960 + 4770 + 220 + 110 *AttributeSetting* bg=pink @@ -2252,10 +2281,10 @@ starting_value : int UMLClass - 4305 - 3535 - 154 - 56 + 4960 + 5050 + 220 + 80 *ProtectingAttribute* bg=pink @@ -2267,10 +2296,10 @@ protects : Attribute Relation - 4375 - 3290 - 21 - 63 + 5060 + 4700 + 30 + 90 lt=<. 10.0;70.0;10.0;10.0 @@ -2278,10 +2307,10 @@ protects : Attribute Relation - 4375 - 3507 - 21 - 42 + 5060 + 5010 + 30 + 60 lt=<<- 10.0;10.0;10.0;40.0 @@ -2289,10 +2318,10 @@ protects : Attribute UMLClass - 4333 - 3619 - 105 - 42 + 5000 + 5170 + 150 + 60 *Shield (SWGB)* @@ -2301,10 +2330,10 @@ protects : Attribute Relation - 4375 - 3584 - 21 - 49 + 5060 + 5120 + 30 + 70 lt=<<- 10.0;10.0;10.0;50.0 @@ -2312,10 +2341,10 @@ protects : Attribute UMLClass - 4501 - 3458 - 70 - 42 + 5240 + 4940 + 100 + 60 *Faith* @@ -2324,10 +2353,10 @@ protects : Attribute Relation - 4459 - 3465 - 56 - 21 + 5180 + 4950 + 80 + 30 lt=<<- 10.0;10.0;60.0;10.0 @@ -2335,10 +2364,10 @@ protects : Attribute UMLClass - 4235 - 3087 - 126 - 56 + 4860 + 4410 + 180 + 80 *LineOfSight* bg=green @@ -2350,10 +2379,10 @@ range : float UMLClass - 4403 - 2947 - 147 - 56 + 5100 + 4210 + 210 + 80 *Cloak (SWGB)* bg=green @@ -2366,10 +2395,10 @@ interrupt_cooldown : float UMLClass - 5264 - 1218 - 189 - 91 + 6330 + 1740 + 270 + 130 *Accuracy* bg=pink @@ -2386,10 +2415,10 @@ blacklisted_entities : set(GameEntity) Relation - 5222 - 1246 - 56 - 21 + 6270 + 1780 + 80 + 30 lt=<. 60.0;10.0;10.0;10.0 @@ -2397,10 +2426,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 4137 - 3423 - 112 - 56 + 4720 + 4890 + 160 + 80 *AttributeAmount* bg=pink @@ -2413,10 +2442,10 @@ amount : int Relation - 4242 - 3458 - 70 - 21 + 4870 + 4940 + 100 + 30 lt=<. 80.0;10.0;10.0;10.0 @@ -2424,10 +2453,10 @@ amount : int UMLClass - 1358 - 2485 - 98 - 42 + 750 + 3550 + 140 + 60 *GameEntityType* @@ -2437,10 +2466,10 @@ bg=pink Relation - 1323 - 2506 - 49 - 21 + 700 + 3580 + 70 + 30 lt=<. 50.0;10.0;10.0;10.0 @@ -2448,10 +2477,10 @@ bg=pink UMLClass - 4501 - 3507 - 70 - 42 + 5240 + 5010 + 100 + 60 *Health* @@ -2460,10 +2489,10 @@ bg=pink Relation - 4480 - 3465 - 35 - 77 + 5210 + 4950 + 50 + 110 lt=- 10.0;10.0;10.0;90.0;30.0;90.0 @@ -2471,10 +2500,10 @@ bg=pink Relation - 1400 - 2520 - 21 - 35 + 810 + 3600 + 30 + 50 lt=- 10.0;30.0;10.0;10.0 @@ -2482,10 +2511,10 @@ bg=pink Relation - 1505 - 2534 - 21 - 1491 + 960 + 3620 + 30 + 2130 lt=- 10.0;2110.0;10.0;10.0 @@ -2493,10 +2522,10 @@ bg=pink Relation - 1505 - 4004 - 294 - 42 + 960 + 5720 + 420 + 60 lt=- 10.0;10.0;400.0;10.0;400.0;40.0 @@ -2504,10 +2533,10 @@ bg=pink UMLClass - 1659 - 4032 - 273 - 56 + 1180 + 5760 + 390 + 80 *Resistance* bg=pink @@ -2519,10 +2548,10 @@ properties : dict(ResistanceProperty, ResistanceProperty) = {} UMLClass - 1113 - 4032 - 231 - 56 + 400 + 5760 + 330 + 80 *Effect* bg=pink @@ -2534,10 +2563,10 @@ properties : dict(EffectProperty, EffectProperty) = {} Relation - 1225 - 4004 - 301 - 42 + 560 + 5720 + 430 + 60 lt=- 410.0;10.0;10.0;10.0;10.0;40.0 @@ -2545,10 +2574,10 @@ properties : dict(EffectProperty, EffectProperty) = {} UMLNote - 1680 - 3976 - 98 - 28 + 1210 + 5680 + 140 + 40 Resistors' side bg=blue @@ -2557,10 +2586,10 @@ bg=blue UMLNote - 1232 - 3976 - 98 - 28 + 570 + 5680 + 140 + 40 Effectors' side bg=blue @@ -2569,10 +2598,10 @@ bg=blue UMLClass - 1302 - 4193 - 245 - 91 + 670 + 5990 + 350 + 130 *FlatAttributeChange* bg=pink @@ -2588,10 +2617,10 @@ ignore_protection : set(ProtectingAttribute) UMLClass - 1869 - 4193 - 175 - 63 + 1480 + 5990 + 250 + 90 *FlatAttributeChange* bg=pink @@ -2604,10 +2633,10 @@ block_value : AttributeAmount UMLClass - 1561 - 3339 - 133 - 42 + 1040 + 4770 + 190 + 60 *AttributeChangeType* @@ -2617,10 +2646,10 @@ bg=pink UMLClass - 1302 - 4473 - 210 - 91 + 670 + 6390 + 300 + 130 *Convert* bg=orange @@ -2636,10 +2665,10 @@ cost_fail : optional(Cost) = None UMLClass - 1869 - 4473 - 203 - 91 + 1480 + 6390 + 290 + 130 *Convert* bg=orange @@ -2652,10 +2681,10 @@ chance_resist : float UMLClass - 1351 - 4571 - 168 - 63 + 740 + 6530 + 240 + 90 *AoE2Convert* bg=orange @@ -2668,10 +2697,10 @@ skip_protected_rounds : int UMLClass - 1904 - 4571 - 203 - 70 + 1530 + 6530 + 290 + 100 *AoE2Convert* bg=orange @@ -2690,10 +2719,10 @@ protection_round_recharge_time : float UMLClass - 1057 - 4123 - 126 - 42 + 320 + 5890 + 180 + 60 *ContinuousEffect* @@ -2704,10 +2733,10 @@ bg=orange UMLClass - 952 - 4193 - 231 - 91 + 170 + 5990 + 330 + 130 *FlatAttributeChange* bg=pink @@ -2723,10 +2752,10 @@ ignore_protection : set(ProtectingAttribute) UMLClass - 1302 - 4123 - 119 - 42 + 670 + 5890 + 170 + 60 *DiscreteEffect* @@ -2736,10 +2765,10 @@ bg=orange UMLClass - 1869 - 4123 - 126 - 42 + 1480 + 5890 + 180 + 60 *DiscreteResistance* @@ -2749,10 +2778,10 @@ bg=orange UMLClass - 1470 - 2464 - 126 - 56 + 910 + 3520 + 180 + 80 *ResourceRate* bg=pink @@ -2765,10 +2794,10 @@ rate : float Relation - 1589 - 2485 - 126 - 21 + 1080 + 3550 + 180 + 30 lt=- 160.0;10.0;10.0;10.0 @@ -2776,10 +2805,10 @@ rate : float UMLClass - 4137 - 3486 - 112 - 56 + 4720 + 4980 + 160 + 80 *AttributeRate* bg=pink @@ -2792,10 +2821,10 @@ rate : float Relation - 1225 - 4081 - 21 - 42 + 560 + 5830 + 30 + 60 lt=<<- 10.0;10.0;10.0;40.0 @@ -2803,10 +2832,10 @@ rate : float Relation - 1113 - 4102 - 133 - 35 + 400 + 5860 + 190 + 50 lt=- 170.0;10.0;10.0;10.0;10.0;30.0 @@ -2814,10 +2843,10 @@ rate : float Relation - 1225 - 4102 - 147 - 35 + 560 + 5860 + 210 + 50 lt=- 10.0;10.0;190.0;10.0;190.0;30.0 @@ -2825,10 +2854,10 @@ rate : float Relation - 1652 - 4102 - 140 - 35 + 1170 + 5860 + 200 + 50 lt=- 180.0;10.0;10.0;10.0;10.0;30.0 @@ -2836,10 +2865,10 @@ rate : float Relation - 1778 - 4081 - 21 - 42 + 1350 + 5830 + 30 + 60 lt=<<- 10.0;10.0;10.0;40.0 @@ -2847,10 +2876,10 @@ rate : float Relation - 1771 - 4102 - 168 - 35 + 1340 + 5860 + 240 + 50 lt=- 10.0;10.0;220.0;10.0;220.0;30.0 @@ -2858,10 +2887,10 @@ rate : float UMLClass - 1596 - 4123 - 126 - 42 + 1090 + 5890 + 180 + 60 *ContinuousResistance* @@ -2871,10 +2900,10 @@ bg=orange Relation - 1505 - 3353 - 70 - 21 + 960 + 4790 + 100 + 30 lt=- 10.0;10.0;80.0;10.0 @@ -2882,10 +2911,10 @@ bg=orange UMLClass - 1575 - 4193 - 147 - 63 + 1060 + 5990 + 210 + 90 *FlatAttributeChange* bg=pink @@ -2898,10 +2927,10 @@ block_rate : AttributeRate Relation - 1274 - 4137 - 42 - 98 + 630 + 5910 + 60 + 140 lt=<<- 40.0;10.0;10.0;10.0;10.0;120.0;40.0;120.0 @@ -2909,10 +2938,10 @@ block_rate : AttributeRate Relation - 1176 - 4137 - 42 - 98 + 490 + 5910 + 60 + 140 lt=<<- 10.0;10.0;40.0;10.0;40.0;120.0;10.0;120.0 @@ -2920,10 +2949,10 @@ block_rate : AttributeRate Relation - 1274 - 4424 - 42 - 91 + 630 + 6320 + 60 + 130 lt=- 10.0;10.0;10.0;110.0;40.0;110.0 @@ -2931,10 +2960,10 @@ block_rate : AttributeRate Relation - 1330 - 4557 - 35 - 56 + 710 + 6510 + 50 + 80 lt=<<- 10.0;10.0;10.0;60.0;30.0;60.0 @@ -2942,10 +2971,10 @@ block_rate : AttributeRate Relation - 1883 - 4557 - 35 - 56 + 1500 + 6510 + 50 + 80 lt=<<- 10.0;10.0;10.0;60.0;30.0;60.0 @@ -2953,10 +2982,10 @@ block_rate : AttributeRate Relation - 1841 - 4214 - 42 - 231 + 1440 + 6020 + 60 + 330 lt=- 10.0;10.0;10.0;310.0;40.0;310.0 @@ -2964,10 +2993,10 @@ block_rate : AttributeRate Relation - 1841 - 4137 - 42 - 98 + 1440 + 5910 + 60 + 140 lt=<<- 40.0;10.0;10.0;10.0;10.0;120.0;40.0;120.0 @@ -2975,10 +3004,10 @@ block_rate : AttributeRate Relation - 1715 - 4137 - 42 - 98 + 1260 + 5910 + 60 + 140 lt=<<- 10.0;10.0;40.0;10.0;40.0;120.0;10.0;120.0 @@ -2986,10 +3015,10 @@ block_rate : AttributeRate UMLClass - 5187 - 1918 - 224 - 98 + 6220 + 2740 + 320 + 140 *ApplyDiscreteEffect* bg=green @@ -3009,10 +3038,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 5187 - 1792 - 224 - 84 + 6220 + 2560 + 320 + 120 *ApplyContinuousEffect* bg=green @@ -3029,10 +3058,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 5453 - 1722 - 98 - 42 + 6600 + 2460 + 140 + 60 *MonkHeal* @@ -3041,10 +3070,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 5453 - 1813 - 77 - 42 + 6600 + 2590 + 110 + 60 *Repair* @@ -3053,10 +3082,10 @@ blacklisted_entities : set(GameEntity) Relation - 1274 - 4214 - 42 - 231 + 630 + 6020 + 60 + 330 lt=- 10.0;10.0;10.0;310.0;40.0;310.0 @@ -3064,10 +3093,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 4053 - 2233 - 189 - 91 + 4600 + 3190 + 270 + 130 *Gather* bg=green @@ -3083,10 +3112,10 @@ container : ResourceContainer UMLClass - 1302 - 4403 - 182 - 56 + 670 + 6290 + 260 + 80 *MakeHarvestable* bg=orange @@ -3098,10 +3127,10 @@ resource_spot : ResourceSpot UMLClass - 1869 - 4403 - 238 - 56 + 1480 + 6290 + 340 + 80 *MakeHarvestable* bg=orange @@ -3114,10 +3143,10 @@ resist_condition : set(LogicElement) Relation - 5341 - 1736 - 126 - 21 + 6440 + 2480 + 180 + 30 lt=<<- 10.0;10.0;160.0;10.0 @@ -3125,10 +3154,10 @@ resist_condition : set(LogicElement) UMLClass - 5187 - 1715 - 161 - 56 + 6220 + 2450 + 230 + 80 *RangedContinuousEffect* bg=green @@ -3141,10 +3170,10 @@ max_range : int Relation - 5404 - 1827 - 63 - 21 + 6530 + 2610 + 90 + 30 lt=<<- 10.0;10.0;70.0;10.0 @@ -3152,10 +3181,10 @@ max_range : int UMLClass - 5453 - 1925 - 98 - 42 + 6600 + 2750 + 140 + 60 *SelfDestruct* @@ -3165,10 +3194,10 @@ max_range : int Relation - 5404 - 1939 - 63 - 21 + 6530 + 2770 + 90 + 30 lt=<<- 10.0;10.0;70.0;10.0 @@ -3176,10 +3205,10 @@ max_range : int UMLClass - 5215 - 2128 - 91 - 42 + 6260 + 3040 + 130 + 60 *Convert* @@ -3188,10 +3217,10 @@ max_range : int Relation - 5250 - 2086 - 21 - 56 + 6310 + 2980 + 30 + 80 lt=<<- 10.0;10.0;10.0;60.0 @@ -3199,10 +3228,10 @@ max_range : int UMLClass - 1561 - 3388 - 84 - 42 + 1040 + 4840 + 120 + 60 *ConvertType* @@ -3212,10 +3241,10 @@ bg=pink Relation - 1505 - 3402 - 70 - 21 + 960 + 4860 + 100 + 30 lt=- 10.0;10.0;80.0;10.0 @@ -3223,10 +3252,10 @@ bg=pink Relation - 1841 - 4424 - 42 - 98 + 1440 + 6320 + 60 + 140 lt=- 10.0;10.0;10.0;120.0;40.0;120.0 @@ -3234,10 +3263,10 @@ bg=pink UMLClass - 5453 - 1974 - 84 - 42 + 6600 + 2820 + 120 + 60 *Hunt* @@ -3246,10 +3275,10 @@ bg=pink UMLClass - 5187 - 2037 - 140 - 56 + 6220 + 2910 + 200 + 80 *RangedDiscreteEffect* bg=green @@ -3262,10 +3291,10 @@ max_range : int Relation - 5250 - 2009 - 21 - 42 + 6310 + 2870 + 30 + 60 lt=<<- 10.0;10.0;10.0;40.0 @@ -3273,10 +3302,10 @@ max_range : int UMLClass - 4179 - 1232 - 224 - 70 + 4780 + 1760 + 320 + 100 *EnterContainer* bg=green @@ -3290,10 +3319,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 4179 - 1309 - 182 - 63 + 4780 + 1870 + 260 + 90 *ExitContainer* bg=green @@ -3305,10 +3334,10 @@ allowed_containers : set(EntityContainer) UMLClass - 2310 - 2282 - 112 - 42 + 2110 + 3260 + 160 + 60 *DiplomaticStance* @@ -3318,10 +3347,10 @@ bg=pink Relation - 2261 - 2296 - 63 - 21 + 2040 + 3280 + 90 + 30 lt=- 70.0;10.0;10.0;10.0 @@ -3329,10 +3358,10 @@ bg=pink UMLClass - 3367 - 2870 - 154 - 56 + 3620 + 4100 + 220 + 80 *Diplomatic* bg=pink @@ -3344,10 +3373,10 @@ stances : set(DiplomaticStance) Relation - 1568 - 21 - 126 - 336 + 1050 + 30 + 180 + 480 lt=- 160.0;460.0;10.0;460.0;10.0;10.0 @@ -3355,10 +3384,10 @@ stances : set(DiplomaticStance) UMLClass - 2569 - 2324 - 70 - 42 + 2480 + 3320 + 100 + 60 *Self* @@ -3368,10 +3397,10 @@ bg=pink Relation - 2415 - 2296 - 203 - 21 + 2260 + 3280 + 290 + 30 lt=<<- 10.0;10.0;270.0;10.0 @@ -3379,10 +3408,10 @@ bg=pink Relation - 1673 - 140 - 224 - 217 + 1200 + 200 + 320 + 310 lt=- 300.0;10.0;170.0;10.0;170.0;290.0;10.0;290.0 @@ -3390,10 +3419,10 @@ bg=pink UMLClass - 1904 - 126 - 224 - 56 + 1530 + 180 + 320 + 80 *ElevationDifferenceLow* bg=yellow @@ -3405,10 +3434,10 @@ min_elevation_difference : optional(float) = None UMLClass - 994 - 4403 - 189 - 70 + 230 + 6290 + 270 + 100 *Lure* bg=orange @@ -3424,10 +3453,10 @@ min_distance_to_destination : float Relation - 1176 - 4214 - 42 - 224 + 490 + 6020 + 60 + 320 lt=- 40.0;10.0;40.0;300.0;10.0;300.0 @@ -3435,10 +3464,10 @@ min_distance_to_destination : float UMLClass - 1547 - 4403 - 175 - 70 + 1020 + 6290 + 250 + 100 *Lure* bg=orange @@ -3451,10 +3480,10 @@ type : LureType Relation - 1715 - 4214 - 42 - 224 + 1260 + 6020 + 60 + 320 lt=- 40.0;10.0;40.0;300.0;10.0;300.0 @@ -3462,10 +3491,10 @@ type : LureType UMLClass - 868 - 1778 - 112 - 56 + 50 + 2540 + 160 + 80 *LanguageTextPair* bg=pink @@ -3478,10 +3507,10 @@ string : text UMLClass - 1071 - 1778 - 140 - 56 + 340 + 2540 + 200 + 80 *LanguageMarkupPair* bg=pink @@ -3494,10 +3523,10 @@ markup_file : file UMLClass - 1295 - 1778 - 126 - 56 + 660 + 2540 + 180 + 80 *LanguageSoundPair* bg=pink @@ -3510,10 +3539,10 @@ sound : Sound Relation - 917 - 1827 - 21 - 56 + 120 + 2610 + 30 + 80 lt=<. 10.0;60.0;10.0;10.0 @@ -3521,10 +3550,10 @@ sound : Sound Relation - 1134 - 1827 - 21 - 56 + 430 + 2610 + 30 + 80 lt=<. 10.0;60.0;10.0;10.0 @@ -3532,10 +3561,10 @@ sound : Sound Relation - 1344 - 1827 - 21 - 56 + 730 + 2610 + 30 + 80 lt=<. 10.0;60.0;10.0;10.0 @@ -3543,10 +3572,10 @@ sound : Sound UMLClass - 1232 - 2030 - 119 - 56 + 570 + 2900 + 170 + 80 *Language* bg=pink @@ -3558,10 +3587,10 @@ ietf_string : text Relation - 1281 - 1995 - 21 - 49 + 640 + 2850 + 30 + 70 lt=- 10.0;50.0;10.0;10.0 @@ -3569,10 +3598,10 @@ ietf_string : text UMLClass - 3696 - 2751 - 112 - 56 + 4090 + 3930 + 160 + 80 *Turn* bg=green @@ -3584,10 +3613,10 @@ turn_speed : float UMLClass - 4403 - 3164 - 112 - 56 + 5100 + 4520 + 160 + 80 *Visibility* bg=green @@ -3599,10 +3628,10 @@ visible_in_fog : bool UMLClass - 4032 - 1036 - 245 - 84 + 4570 + 1480 + 350 + 130 *EntityContainer* bg=pink @@ -3618,10 +3647,10 @@ carry_progress : set(Progress) Relation - 4270 - 1071 - 63 - 21 + 4910 + 1530 + 90 + 30 lt=<. 70.0;10.0;10.0;10.0 @@ -3629,10 +3658,10 @@ carry_progress : set(Progress) UMLClass - 1694 - 3696 - 154 - 56 + 1230 + 5280 + 220 + 80 *Cost* bg=pink @@ -3644,10 +3673,10 @@ cost : Cost Relation - 1659 - 3780 - 49 - 21 + 1180 + 5400 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -3655,10 +3684,10 @@ cost : Cost UMLClass - 2072 - 2555 - 168 - 70 + 1770 + 3650 + 240 + 100 *AnimationOverride* bg=pink @@ -3672,10 +3701,10 @@ priority : int UMLClass - 1904 - 2380 - 154 - 56 + 1530 + 3400 + 220 + 80 *Diplomatic* bg=pink @@ -3687,10 +3716,10 @@ stances : set(DiplomaticStance) UMLClass - 1351 - 3290 - 112 - 42 + 740 + 4700 + 160 + 60 *DropoffType* @@ -3700,10 +3729,10 @@ bg=pink Relation - 1302 - 3304 - 63 - 21 + 670 + 4720 + 90 + 30 lt=<<- 70.0;10.0;10.0;10.0 @@ -3711,10 +3740,10 @@ bg=pink Relation - 1302 - 3304 - 42 - 70 + 670 + 4720 + 60 + 100 lt=- 40.0;10.0;40.0;80.0;10.0;80.0 @@ -3722,10 +3751,10 @@ bg=pink Relation - 1302 - 3255 - 42 - 70 + 670 + 4650 + 60 + 100 lt=- 40.0;80.0;40.0;10.0;10.0;10.0 @@ -3733,10 +3762,10 @@ bg=pink UMLClass - 1197 - 3241 - 112 - 42 + 520 + 4630 + 160 + 60 *NoDropoff* @@ -3746,10 +3775,10 @@ bg=pink UMLClass - 1197 - 3290 - 112 - 42 + 520 + 4700 + 160 + 60 *Linear* @@ -3759,10 +3788,10 @@ bg=pink UMLClass - 1729 - 3339 - 84 - 42 + 1280 + 4770 + 120 + 60 // This type is _only_ evaluated if all FlatAttributeChange Effects with other types are outside of the // range defined in the FlatAttributeChange Effect with type Fallback. @@ -3776,10 +3805,10 @@ bg=pink Relation - 1687 - 3353 - 56 - 21 + 1220 + 4790 + 80 + 30 lt=<<- 10.0;10.0;60.0;10.0 @@ -3787,10 +3816,10 @@ bg=pink Relation - 3332 - 1708 - 42 - 21 + 3570 + 2440 + 60 + 30 lt=<. 10.0;10.0;40.0;10.0 @@ -3798,10 +3827,10 @@ bg=pink UMLClass - 1603 - 2786 - 189 - 84 + 1100 + 3980 + 270 + 120 *Tech* bg=pink @@ -3817,10 +3846,10 @@ updates : orderedset(Patch) Relation - 1694 - 2555 - 21 - 245 + 1230 + 3650 + 30 + 350 lt=<<- 10.0;10.0;10.0;330.0 @@ -3828,10 +3857,10 @@ updates : orderedset(Patch) UMLClass - 4144 - 1813 - 168 - 70 + 4730 + 2590 + 240 + 100 // Determines traded resource and resource amount *TradeRoute* @@ -3846,10 +3875,10 @@ end_trade_post : GameEntity Relation - 4046 - 1876 - 147 - 42 + 4590 + 2680 + 210 + 60 lt=<. 190.0;10.0;190.0;40.0;10.0;40.0 @@ -3857,10 +3886,10 @@ end_trade_post : GameEntity UMLClass - 4361 - 1813 - 119 - 42 + 5040 + 2590 + 170 + 60 *AoE2TradeRoute* @@ -3871,10 +3900,10 @@ bg=pink UMLClass - 1337 - 4298 - 154 - 42 + 720 + 6140 + 220 + 60 *FlatAttributeDecrease* @@ -3884,10 +3913,10 @@ bg=orange UMLClass - 1337 - 4347 - 154 - 42 + 720 + 6210 + 220 + 60 *FlatAttributeIncrease* @@ -3897,10 +3926,10 @@ bg=orange Relation - 1316 - 4277 - 35 - 56 + 690 + 6110 + 50 + 80 lt=->> 30.0;60.0;10.0;60.0;10.0;10.0 @@ -3908,10 +3937,10 @@ bg=orange Relation - 1316 - 4312 - 35 - 70 + 690 + 6160 + 50 + 100 lt=- 30.0;80.0;10.0;80.0;10.0;10.0 @@ -3919,10 +3948,10 @@ bg=orange UMLClass - 1911 - 4298 - 154 - 42 + 1540 + 6140 + 220 + 60 *FlatAttributeDecrease* @@ -3932,10 +3961,10 @@ bg=orange UMLClass - 1911 - 4347 - 154 - 42 + 1540 + 6210 + 220 + 60 *FlatAttributeIncrease* @@ -3945,10 +3974,10 @@ bg=orange Relation - 1883 - 4249 - 21 - 133 + 1500 + 6070 + 30 + 190 lt=<<- 10.0;10.0;10.0;170.0 @@ -3956,10 +3985,10 @@ bg=orange Relation - 1883 - 4312 - 42 - 21 + 1500 + 6160 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -3967,10 +3996,10 @@ bg=orange UMLClass - 994 - 4298 - 154 - 42 + 230 + 6140 + 220 + 60 *FlatAttributeDecrease* @@ -3980,10 +4009,10 @@ bg=orange UMLClass - 994 - 4347 - 154 - 42 + 230 + 6210 + 220 + 60 *FlatAttributeIncrease* @@ -3993,10 +4022,10 @@ bg=orange Relation - 1141 - 4312 - 35 - 70 + 440 + 6160 + 50 + 100 lt=- 10.0;80.0;30.0;80.0;30.0;10.0 @@ -4004,10 +4033,10 @@ bg=orange Relation - 1141 - 4277 - 35 - 56 + 440 + 6110 + 50 + 80 lt=->> 10.0;60.0;30.0;60.0;30.0;10.0 @@ -4015,10 +4044,10 @@ bg=orange UMLClass - 1547 - 4298 - 154 - 42 + 1020 + 6140 + 220 + 60 *FlatAttributeDecrease* @@ -4028,10 +4057,10 @@ bg=orange UMLClass - 1547 - 4347 - 154 - 42 + 1020 + 6210 + 220 + 60 *FlatAttributeIncrease* @@ -4041,10 +4070,10 @@ bg=orange Relation - 1694 - 4312 - 35 - 70 + 1230 + 6160 + 50 + 100 lt=- 10.0;80.0;30.0;80.0;30.0;10.0 @@ -4052,10 +4081,10 @@ bg=orange Relation - 1694 - 4249 - 35 - 84 + 1230 + 6070 + 50 + 120 lt=->> 10.0;100.0;30.0;100.0;30.0;10.0 @@ -4063,10 +4092,10 @@ bg=orange UMLClass - 1876 - 2212 - 175 - 56 + 1490 + 3160 + 250 + 80 *Formation* bg=pink @@ -4078,10 +4107,10 @@ subformations : set(Subformation) Relation - 2044 - 2233 - 49 - 21 + 1730 + 3190 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -4089,10 +4118,10 @@ subformations : set(Subformation) UMLClass - 1911 - 2289 - 126 - 56 + 1540 + 3270 + 180 + 80 *Subformation* bg=pink @@ -4104,10 +4133,10 @@ ordering_priority : int UMLClass - 3913 - 2996 - 154 - 56 + 4400 + 4280 + 220 + 80 *GameEntityFormation* bg=pink @@ -4120,10 +4149,10 @@ subformation : Subformation Relation - 3878 - 3017 - 49 - 21 + 4350 + 4310 + 70 + 30 lt=<. 50.0;10.0;10.0;10.0 @@ -4131,10 +4160,10 @@ subformation : Subformation Relation - 2030 - 2310 - 63 - 21 + 1710 + 3300 + 90 + 30 lt=- 10.0;10.0;70.0;10.0 @@ -4142,10 +4171,10 @@ subformation : Subformation Relation - 1967 - 2261 - 21 - 42 + 1620 + 3230 + 30 + 60 lt=<. 10.0;40.0;10.0;10.0 @@ -4153,10 +4182,10 @@ subformation : Subformation UMLClass - 1813 - 1162 - 147 - 63 + 1400 + 1660 + 210 + 90 *Scoped* bg=pink @@ -4173,10 +4202,10 @@ scope : ModifierScope Relation - 5264 - 1764 - 21 - 42 + 6330 + 2520 + 30 + 60 lt=<<- 10.0;40.0;10.0;10.0 @@ -4184,10 +4213,10 @@ scope : ModifierScope UMLClass - 1302 - 4648 - 175 - 70 + 670 + 6640 + 250 + 100 *SendToContainer* bg=orange @@ -4200,10 +4229,10 @@ storages : set(EntityContainer) Relation - 1274 - 4494 - 42 - 196 + 630 + 6420 + 60 + 280 lt=- 10.0;10.0;10.0;260.0;40.0;260.0 @@ -4211,10 +4240,10 @@ storages : set(EntityContainer) UMLClass - 1869 - 4648 - 182 - 70 + 1480 + 6640 + 260 + 100 *SendToContainer* bg=orange @@ -4228,10 +4257,10 @@ ignore_containers : set(EntityContainer) Relation - 1841 - 4501 - 42 - 189 + 1440 + 6430 + 60 + 270 lt=- 10.0;10.0;10.0;250.0;40.0;250.0 @@ -4239,10 +4268,10 @@ ignore_containers : set(EntityContainer) Relation - 3549 - 1925 - 42 - 609 + 3880 + 2750 + 60 + 870 lt=<<- 40.0;850.0;40.0;10.0;10.0;10.0 @@ -4250,10 +4279,10 @@ ignore_containers : set(EntityContainer) Relation - 3549 - 1827 - 42 - 119 + 3880 + 2610 + 60 + 170 lt=- 40.0;150.0;40.0;10.0;10.0;10.0 @@ -4261,10 +4290,10 @@ ignore_containers : set(EntityContainer) Relation - 3549 - 1715 - 42 - 133 + 3880 + 2450 + 60 + 190 lt=- 40.0;170.0;40.0;10.0;10.0;10.0 @@ -4272,10 +4301,10 @@ ignore_containers : set(EntityContainer) Relation - 3731 - 2534 - 1708 - 21 + 4140 + 3620 + 2440 + 30 lt=<<- 10.0;10.0;2420.0;10.0 @@ -4283,10 +4312,10 @@ ignore_containers : set(EntityContainer) Relation - 3332 - 2590 - 21 - 385 + 3570 + 3700 + 30 + 550 lt=<<- 10.0;10.0;10.0;530.0 @@ -4294,10 +4323,10 @@ ignore_containers : set(EntityContainer) Relation - 3332 - 2765 - 49 - 21 + 3570 + 3950 + 70 + 30 lt=- 50.0;10.0;10.0;10.0 @@ -4305,10 +4334,10 @@ ignore_containers : set(EntityContainer) Relation - 3332 - 2828 - 49 - 21 + 3570 + 4040 + 70 + 30 lt=- 50.0;10.0;10.0;10.0 @@ -4316,10 +4345,10 @@ ignore_containers : set(EntityContainer) Relation - 3570 - 2576 - 21 - 581 + 3910 + 3680 + 30 + 830 lt=<<- 10.0;10.0;10.0;810.0 @@ -4327,10 +4356,10 @@ ignore_containers : set(EntityContainer) Relation - 3647 - 3017 - 49 - 84 + 4020 + 4310 + 70 + 120 lt=- 10.0;10.0;10.0;100.0;50.0;100.0 @@ -4338,10 +4367,10 @@ ignore_containers : set(EntityContainer) Relation - 3570 - 3017 - 126 - 21 + 3910 + 4310 + 180 + 30 lt=- 10.0;10.0;160.0;10.0 @@ -4349,10 +4378,10 @@ ignore_containers : set(EntityContainer) Relation - 3801 - 2709 - 42 - 84 + 4240 + 3870 + 60 + 120 lt=- 10.0;100.0;40.0;100.0;40.0;10.0 @@ -4360,10 +4389,10 @@ ignore_containers : set(EntityContainer) Relation - 3801 - 2534 - 42 - 196 + 4240 + 3620 + 60 + 280 lt=- 10.0;260.0;40.0;260.0;40.0;10.0 @@ -4371,10 +4400,10 @@ ignore_containers : set(EntityContainer) Relation - 3822 - 2709 - 42 - 21 + 4270 + 3870 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -4382,10 +4411,10 @@ ignore_containers : set(EntityContainer) Relation - 4088 - 1827 - 70 - 21 + 4650 + 2610 + 100 + 30 lt=<. 80.0;10.0;10.0;10.0 @@ -4393,10 +4422,10 @@ ignore_containers : set(EntityContainer) Relation - 4305 - 1827 - 70 - 21 + 4960 + 2610 + 100 + 30 lt=<<- 10.0;10.0;80.0;10.0 @@ -4404,10 +4433,10 @@ ignore_containers : set(EntityContainer) Relation - 4333 - 1827 - 21 - 84 + 5000 + 2610 + 30 + 120 lt=- 10.0;10.0;10.0;100.0 @@ -4415,10 +4444,10 @@ ignore_containers : set(EntityContainer) Relation - 3570 - 1750 - 336 - 21 + 3910 + 2500 + 480 + 30 lt=- 460.0;10.0;10.0;10.0 @@ -4426,10 +4455,10 @@ ignore_containers : set(EntityContainer) Relation - 3864 - 1750 - 42 - 98 + 4330 + 2500 + 60 + 140 lt=- 40.0;120.0;10.0;120.0;10.0;10.0 @@ -4437,10 +4466,10 @@ ignore_containers : set(EntityContainer) Relation - 3864 - 1827 - 42 - 84 + 4330 + 2610 + 60 + 120 lt=- 40.0;100.0;10.0;100.0;10.0;10.0 @@ -4448,10 +4477,10 @@ ignore_containers : set(EntityContainer) Relation - 4242 - 3493 - 70 - 21 + 4870 + 4990 + 100 + 30 lt=<. 80.0;10.0;10.0;10.0 @@ -4459,10 +4488,10 @@ ignore_containers : set(EntityContainer) Relation - 4375 - 2534 - 21 - 721 + 5060 + 3620 + 30 + 1030 lt=- 10.0;10.0;10.0;1010.0 @@ -4470,10 +4499,10 @@ ignore_containers : set(EntityContainer) Relation - 4354 - 2968 - 42 - 21 + 5030 + 4240 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4481,10 +4510,10 @@ ignore_containers : set(EntityContainer) Relation - 4354 - 3045 - 42 - 21 + 5030 + 4350 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4492,10 +4521,10 @@ ignore_containers : set(EntityContainer) Relation - 4375 - 3108 - 42 - 21 + 5060 + 4440 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4503,10 +4532,10 @@ ignore_containers : set(EntityContainer) Relation - 4354 - 3185 - 42 - 21 + 5030 + 4550 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4514,10 +4543,10 @@ ignore_containers : set(EntityContainer) Relation - 4375 - 3185 - 42 - 21 + 5060 + 4550 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4525,10 +4554,10 @@ ignore_containers : set(EntityContainer) Relation - 4354 - 3108 - 42 - 21 + 5030 + 4440 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -4536,10 +4565,10 @@ ignore_containers : set(EntityContainer) Relation - 4375 - 2968 - 42 - 21 + 5060 + 4240 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4547,10 +4576,10 @@ ignore_containers : set(EntityContainer) Relation - 5012 - 2534 - 21 - 840 + 5970 + 3620 + 30 + 1200 lt=- 10.0;10.0;10.0;1180.0 @@ -4558,10 +4587,10 @@ ignore_containers : set(EntityContainer) Relation - 4991 - 3353 - 42 - 21 + 5940 + 4790 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4569,10 +4598,10 @@ ignore_containers : set(EntityContainer) Relation - 5012 - 3353 - 42 - 21 + 5970 + 4790 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4580,10 +4609,10 @@ ignore_containers : set(EntityContainer) Relation - 4375 - 3038 - 42 - 21 + 5060 + 4340 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4591,10 +4620,10 @@ ignore_containers : set(EntityContainer) Relation - 5299 - 2485 - 42 - 70 + 6380 + 3550 + 60 + 100 lt=- 40.0;10.0;10.0;10.0;10.0;80.0 @@ -4602,10 +4631,10 @@ ignore_containers : set(EntityContainer) Relation - 5299 - 2534 - 21 - 259 + 6380 + 3620 + 30 + 370 lt=- 10.0;350.0;10.0;10.0 @@ -4613,10 +4642,10 @@ ignore_containers : set(EntityContainer) Relation - 5299 - 2772 - 42 - 21 + 6380 + 3960 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4624,10 +4653,10 @@ ignore_containers : set(EntityContainer) Relation - 5159 - 1869 - 147 - 686 + 6180 + 2670 + 210 + 980 lt=- 190.0;10.0;190.0;40.0;10.0;40.0;10.0;960.0 @@ -4635,10 +4664,10 @@ ignore_containers : set(EntityContainer) Relation - 5404 - 1988 - 63 - 21 + 6530 + 2840 + 90 + 30 lt=<<- 10.0;10.0;70.0;10.0 @@ -4646,10 +4675,10 @@ ignore_containers : set(EntityContainer) Relation - 5285 - 1890 - 21 - 42 + 6360 + 2700 + 30 + 60 lt=- 10.0;10.0;10.0;40.0 @@ -4657,10 +4686,10 @@ ignore_containers : set(EntityContainer) Relation - 4256 - 2065 - 21 - 490 + 4890 + 2950 + 30 + 700 lt=- 10.0;680.0;10.0;10.0 @@ -4668,10 +4697,10 @@ ignore_containers : set(EntityContainer) Relation - 4256 - 2086 - 42 - 21 + 4890 + 2980 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -4679,10 +4708,10 @@ ignore_containers : set(EntityContainer) Relation - 4235 - 2065 - 42 - 21 + 4860 + 2950 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -4690,10 +4719,10 @@ ignore_containers : set(EntityContainer) Relation - 4235 - 2149 - 42 - 21 + 4860 + 3070 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -4701,10 +4730,10 @@ ignore_containers : set(EntityContainer) Relation - 4256 - 2149 - 42 - 21 + 4890 + 3070 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -4712,10 +4741,10 @@ ignore_containers : set(EntityContainer) Relation - 4256 - 2212 - 42 - 21 + 4890 + 3160 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -4723,10 +4752,10 @@ ignore_containers : set(EntityContainer) Relation - 4256 - 2275 - 42 - 21 + 4890 + 3250 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -4734,10 +4763,10 @@ ignore_containers : set(EntityContainer) Relation - 4235 - 2254 - 42 - 21 + 4860 + 3220 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -4745,10 +4774,10 @@ ignore_containers : set(EntityContainer) Relation - 4151 - 1204 - 700 - 1351 + 4740 + 1720 + 1000 + 1930 lt=- 980.0;1910.0;980.0;360.0;10.0;360.0;10.0;10.0 @@ -4756,10 +4785,10 @@ ignore_containers : set(EntityContainer) Relation - 4830 - 1449 - 189 - 21 + 5710 + 2070 + 270 + 30 lt=- 10.0;10.0;250.0;10.0 @@ -4767,10 +4796,10 @@ ignore_containers : set(EntityContainer) Relation - 4977 - 1309 - 42 - 161 + 5920 + 1870 + 60 + 230 lt=- 40.0;10.0;10.0;10.0;10.0;210.0 @@ -4778,10 +4807,10 @@ ignore_containers : set(EntityContainer) Relation - 4151 - 1253 - 42 - 21 + 4740 + 1790 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4789,10 +4818,10 @@ ignore_containers : set(EntityContainer) Relation - 4151 - 1330 - 42 - 21 + 4740 + 1900 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4800,10 +4829,10 @@ ignore_containers : set(EntityContainer) Relation - 4151 - 1393 - 42 - 21 + 4740 + 1990 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4811,10 +4840,10 @@ ignore_containers : set(EntityContainer) Relation - 4130 - 1393 - 42 - 21 + 4710 + 1990 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4822,10 +4851,10 @@ ignore_containers : set(EntityContainer) Relation - 4130 - 1330 - 42 - 21 + 4710 + 1900 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4833,10 +4862,10 @@ ignore_containers : set(EntityContainer) Relation - 4130 - 1253 - 42 - 21 + 4710 + 1790 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -4844,10 +4873,10 @@ ignore_containers : set(EntityContainer) UMLClass - 5264 - 1323 - 98 - 42 + 6330 + 1890 + 140 + 60 *TargetMode* @@ -4857,10 +4886,10 @@ bg=pink Relation - 5222 - 1337 - 56 - 21 + 6270 + 1910 + 80 + 30 lt=<. 60.0;10.0;10.0;10.0 @@ -4868,10 +4897,10 @@ bg=pink UMLClass - 5411 - 1323 - 112 - 42 + 6540 + 1890 + 160 + 60 *CurrentPosition* @@ -4881,10 +4910,10 @@ bg=pink Relation - 5355 - 1337 - 70 - 21 + 6460 + 1910 + 100 + 30 lt=<<- 10.0;10.0;80.0;10.0 @@ -4892,10 +4921,10 @@ bg=pink UMLClass - 5411 - 1372 - 112 - 42 + 6540 + 1960 + 160 + 60 *ExpectedPosition* @@ -4905,10 +4934,10 @@ bg=pink Relation - 5383 - 1337 - 42 - 70 + 6500 + 1910 + 60 + 100 lt=- 10.0;10.0;10.0;80.0;40.0;80.0 @@ -4916,10 +4945,10 @@ bg=pink UMLClass - 1673 - 1169 - 98 - 42 + 1200 + 1670 + 140 + 60 *ModifierScope* @@ -4929,10 +4958,10 @@ bg=pink Relation - 1764 - 1183 - 63 - 21 + 1330 + 1690 + 90 + 30 lt=<. 10.0;10.0;70.0;10.0 @@ -4940,10 +4969,10 @@ bg=pink UMLClass - 4053 - 2863 - 77 - 42 + 4600 + 4090 + 110 + 60 *AttackMove* @@ -4953,10 +4982,10 @@ bg=pink Relation - 4018 - 2877 - 49 - 21 + 4550 + 4110 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -4964,10 +4993,10 @@ bg=pink UMLClass - 3990 - 2625 - 84 - 42 + 4510 + 3750 + 120 + 60 *MoveMode* @@ -4977,10 +5006,10 @@ bg=pink Relation - 3906 - 2639 - 98 - 63 + 4390 + 3770 + 140 + 90 lt=<. 120.0;10.0;10.0;10.0;10.0;70.0 @@ -4988,10 +5017,10 @@ bg=pink Relation - 1876 - 203 - 42 - 21 + 1490 + 290 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -4999,10 +5028,10 @@ bg=pink UMLClass - 1904 - 189 - 91 - 42 + 1530 + 270 + 130 + 60 *Stray* @@ -5012,10 +5041,10 @@ bg=yellow UMLClass - 2506 - 539 - 210 - 56 + 2390 + 770 + 300 + 80 *AbsoluteProjectileAmount* bg=yellow @@ -5027,10 +5056,10 @@ amount : float Relation - 2072 - 308 - 413 - 560 + 1770 + 440 + 590 + 800 lt=- 10.0;780.0;570.0;780.0;570.0;10.0 @@ -5038,10 +5067,10 @@ amount : float UMLClass - 1477 - 546 - 168 - 56 + 920 + 780 + 240 + 80 *GatheringEfficiency* bg=yellow @@ -5053,10 +5082,10 @@ resource_spot : ResourceSpot Relation - 1638 - 567 - 56 - 21 + 1150 + 810 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -5064,10 +5093,10 @@ resource_spot : ResourceSpot UMLClass - 1547 - 609 - 98 - 42 + 1020 + 870 + 140 + 60 *ReloadTime* @@ -5077,10 +5106,10 @@ bg=yellow Relation - 1638 - 623 - 56 - 21 + 1150 + 890 + 80 + 30 lt=- 10.0;10.0;60.0;10.0 @@ -5088,10 +5117,10 @@ bg=yellow UMLClass - 1715 - 707 - 196 - 56 + 1260 + 1010 + 280 + 80 *CreationTime* bg=yellow @@ -5103,10 +5132,10 @@ creatables : set(CreatableGameEntity) Relation - 1673 - 728 - 56 - 21 + 1200 + 1040 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -5114,10 +5143,10 @@ creatables : set(CreatableGameEntity) UMLClass - 1715 - 644 - 210 - 56 + 1260 + 920 + 300 + 80 *CreationResourceCost* bg=yellow @@ -5131,10 +5160,10 @@ creatables : set(CreatableGameEntity) Relation - 1673 - 672 - 56 - 21 + 1200 + 960 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -5142,10 +5171,10 @@ creatables : set(CreatableGameEntity) UMLClass - 1715 - 455 - 203 - 56 + 1260 + 650 + 290 + 80 *ResearchResourceCost* bg=yellow @@ -5159,10 +5188,10 @@ researchables : set(ResearchableTech) Relation - 1673 - 476 - 56 - 21 + 1200 + 680 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -5170,10 +5199,10 @@ researchables : set(ResearchableTech) Relation - 2464 - 560 - 56 - 21 + 2330 + 800 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -5181,10 +5210,10 @@ researchables : set(ResearchableTech) UMLClass - 2506 - 476 - 154 - 56 + 2390 + 680 + 220 + 80 // Immediately unlocks a Tech as soon as the requirements are fulfilled *InstantTechResearch* @@ -5198,10 +5227,10 @@ condition : set(LogicElement) Relation - 2464 - 497 - 56 - 21 + 2330 + 710 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -5209,10 +5238,10 @@ condition : set(LogicElement) UMLClass - 4284 - 2338 - 175 - 77 + 4930 + 3340 + 250 + 110 *Herd* bg=green @@ -5227,10 +5256,10 @@ blacklisted_entities : set(GameEntity) Relation - 4256 - 2359 - 42 - 21 + 4890 + 3370 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -5238,10 +5267,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 1449 - 357 - 196 - 56 + 880 + 510 + 280 + 80 *EntityContainerCapacity* bg=yellow @@ -5254,10 +5283,10 @@ container : EntityContainer UMLClass - 1442 - 420 - 203 - 56 + 870 + 600 + 290 + 80 *StorageElementCapacity* bg=yellow @@ -5270,10 +5299,10 @@ storage_element : StorageElementDefinition Relation - 1638 - 378 - 56 - 21 + 1150 + 540 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -5281,10 +5310,10 @@ storage_element : StorageElementDefinition Relation - 1638 - 441 - 56 - 21 + 1150 + 630 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -5292,10 +5321,10 @@ storage_element : StorageElementDefinition UMLClass - 1715 - 518 - 203 - 56 + 1260 + 740 + 290 + 80 *ResearchTime* bg=yellow @@ -5308,10 +5337,10 @@ researchables : set(ResearchableTech) Relation - 1673 - 539 - 56 - 21 + 1200 + 770 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -5319,10 +5348,10 @@ researchables : set(ResearchableTech) UMLClass - 2506 - 350 - 196 - 70 + 2390 + 500 + 280 + 100 // Reveal area around listed units *Reveal* @@ -5337,10 +5366,10 @@ blacklisted_entities : set(GameEntity) Relation - 2464 - 371 - 56 - 21 + 2330 + 530 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -5349,9 +5378,9 @@ blacklisted_entities : set(GameEntity) UMLNote 0 - 1057 - 133 - 63 + 350 + 190 + 90 Orange elements: @@ -5362,10 +5391,10 @@ bg=orange UMLClass - 2506 - 602 - 224 - 70 + 2390 + 860 + 320 + 100 // The change values and fire rate of the provider and receiver are compared (divided) // with the result being added to the projectile amount of the receiver @@ -5384,10 +5413,10 @@ change_types : set(AttributeChangeType) UMLClass - 4081 - 2394 - 161 - 56 + 4640 + 3420 + 230 + 80 *RegenerateResourceSpot* bg=green @@ -5400,10 +5429,10 @@ resource_spot : ResourceSpot Relation - 4235 - 2415 - 42 - 21 + 4860 + 3450 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -5411,10 +5440,10 @@ resource_spot : ResourceSpot UMLClass - 1638 - 1232 - 84 - 42 + 1150 + 1760 + 120 + 60 // Only affect yourself (default for modifiers in GameEntity) @@ -5425,10 +5454,10 @@ bg=pink Relation - 1715 - 1204 - 49 - 70 + 1260 + 1720 + 70 + 100 lt=<<- 50.0;10.0;50.0;80.0;10.0;80.0 @@ -5436,10 +5465,10 @@ bg=pink UMLClass - 1547 - 1281 - 175 - 56 + 1020 + 1830 + 250 + 80 // Affect all game entities in the list *GameEntityScope* @@ -5453,10 +5482,10 @@ blacklisted_entities : set(GameEntity) Relation - 1715 - 1253 - 49 - 63 + 1260 + 1790 + 70 + 90 lt=- 10.0;70.0;50.0;70.0;50.0;10.0 @@ -5465,20 +5494,20 @@ blacklisted_entities : set(GameEntity) Text 0 - 819 - 161 - 21 + 10 + 230 + 30 - openage nyan data API v0.3.0 + openage nyan data API v0.4.0 UMLClass - 1827 - 2107 - 224 - 98 + 1420 + 3010 + 320 + 140 *StateChanger* bg=pink @@ -5496,10 +5525,10 @@ priority : int Relation - 2044 - 2142 - 49 - 21 + 1730 + 3060 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -5507,10 +5536,10 @@ priority : int UMLClass - 2219 - 350 - 217 - 63 + 1980 + 500 + 310 + 90 // Apply effects when in a container *InContainerContinuousEffect* @@ -5524,10 +5553,10 @@ ability : ApplyContinuousEffect Relation - 2429 - 371 - 56 - 21 + 2280 + 530 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -5535,10 +5564,10 @@ ability : ApplyContinuousEffect UMLClass - 2219 - 427 - 217 - 63 + 1980 + 610 + 310 + 90 // Apply effects when in a container *InContainerDiscreteEffect* @@ -5552,10 +5581,10 @@ ability : ApplyDiscreteEffect Relation - 2429 - 448 - 56 - 21 + 2280 + 640 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -5563,10 +5592,10 @@ ability : ApplyDiscreteEffect UMLClass - 4053 - 2912 - 77 - 42 + 4600 + 4160 + 110 + 60 *Normal* @@ -5576,10 +5605,10 @@ bg=pink Relation - 4018 - 2926 - 49 - 21 + 4550 + 4180 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -5587,10 +5616,10 @@ bg=pink UMLClass - 1197 - 105 - 119 - 56 + 520 + 150 + 170 + 80 *Terrain* bg=yellow @@ -5602,10 +5631,10 @@ terrain : Terrain Relation - 1309 - 126 - 42 - 21 + 680 + 180 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -5613,10 +5642,10 @@ terrain : Terrain UMLClass - 1904 + 1530 0 - 119 - 56 + 170 + 80 *Terrain* bg=yellow @@ -5628,10 +5657,10 @@ terrain : Terrain Relation - 1876 - 21 - 21 - 252 + 1490 + 30 + 30 + 360 lt=- 10.0;340.0;10.0;10.0 @@ -5639,10 +5668,10 @@ terrain : Terrain UMLClass - 2506 - 287 - 196 - 56 + 2390 + 410 + 280 + 80 // Reveal area around listed units *DiplomaticLineOfSight* @@ -5655,10 +5684,10 @@ diplomatic_stance : DiplomaticStance Relation - 2464 - 308 - 56 - 21 + 2330 + 440 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -5666,10 +5695,10 @@ diplomatic_stance : DiplomaticStance UMLClass - 1561 - 3437 - 84 - 42 + 1040 + 4910 + 120 + 60 *LureType* @@ -5679,10 +5708,10 @@ bg=pink Relation - 1505 - 3451 - 70 - 21 + 960 + 4930 + 100 + 30 lt=- 10.0;10.0;80.0;10.0 @@ -5690,10 +5719,10 @@ bg=pink UMLClass - 1561 - 3486 - 140 - 42 + 1040 + 4980 + 200 + 60 *SendToContainerType* @@ -5703,10 +5732,10 @@ bg=pink Relation - 1505 - 3500 - 70 - 21 + 960 + 5000 + 100 + 30 lt=- 10.0;10.0;80.0;10.0 @@ -5714,10 +5743,10 @@ bg=pink UMLClass - 3248 - 1932 - 98 - 42 + 3450 + 2760 + 140 + 60 *PlacementMode* @@ -5727,10 +5756,10 @@ bg=pink Relation - 3276 - 1897 - 21 - 49 + 3490 + 2710 + 30 + 70 lt=<. 10.0;50.0;10.0;10.0 @@ -5738,10 +5767,10 @@ bg=pink UMLClass - 3332 - 2149 - 77 - 42 + 3570 + 3070 + 110 + 60 *Eject* @@ -5751,10 +5780,10 @@ bg=pink UMLClass - 3332 - 1988 - 147 - 91 + 3570 + 2840 + 210 + 130 *Place* bg=pink @@ -5770,10 +5799,10 @@ max_elevation_difference : int Relation - 3297 - 1967 - 21 - 273 + 3520 + 2810 + 30 + 390 lt=<<- 10.0;10.0;10.0;370.0 @@ -5781,10 +5810,10 @@ max_elevation_difference : int Relation - 3297 - 2009 - 49 - 21 + 3520 + 2870 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -5792,10 +5821,10 @@ max_elevation_difference : int UMLClass - 1092 - 2765 - 182 - 126 + 370 + 3950 + 260 + 180 *AdjacentTilesVariant* @@ -5814,10 +5843,10 @@ north_west : optional(GameEntity) Relation - 1267 - 2723 - 35 - 84 + 620 + 3890 + 50 + 120 lt=- 30.0;10.0;30.0;100.0;10.0;100.0 @@ -5825,10 +5854,10 @@ north_west : optional(GameEntity) UMLClass - 1197 - 3339 - 112 - 42 + 520 + 4770 + 160 + 60 *InverseLinear* @@ -5838,10 +5867,10 @@ bg=pink UMLClass - 3367 - 2681 - 126 - 56 + 3620 + 3830 + 180 + 80 *ExecutionSound* bg=pink @@ -5854,10 +5883,10 @@ sounds : set(Sound) Relation - 3332 - 2702 - 49 - 21 + 3570 + 3860 + 70 + 30 lt=- 50.0;10.0;10.0;10.0 @@ -5865,10 +5894,10 @@ sounds : set(Sound) Relation - 3332 - 2639 - 49 - 21 + 3570 + 3770 + 70 + 30 lt=- 50.0;10.0;10.0;10.0 @@ -5876,10 +5905,10 @@ sounds : set(Sound) UMLClass - 3367 - 2618 - 161 - 56 + 3620 + 3740 + 230 + 80 *AnimationOverride* bg=pink @@ -5891,10 +5920,10 @@ overrides : set(AnimationOverride) UMLClass - 2219 - 287 - 217 - 56 + 1980 + 410 + 310 + 80 *RefundOnCondition* bg=yellow @@ -5907,10 +5936,10 @@ refund_amount : set(ResourceAmount) Relation - 2429 - 308 - 56 - 21 + 2280 + 440 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -5918,10 +5947,10 @@ refund_amount : set(ResourceAmount) UMLClass - 1736 - 3087 - 161 - 56 + 1290 + 4410 + 230 + 80 *ProgressStatus* bg=pink @@ -5934,10 +5963,10 @@ progress : float Relation - 1890 - 3094 - 42 - 21 + 1510 + 4420 + 60 + 30 lt=<. 10.0;10.0;40.0;10.0 @@ -5945,10 +5974,10 @@ progress : float UMLClass - 952 - 4487 - 231 - 77 + 170 + 6410 + 330 + 110 *TimeRelativeAttributeChange* bg=pink @@ -5962,10 +5991,10 @@ ignore_protection : set(ProtectingAttribute) Relation - 1176 - 4417 - 42 - 119 + 490 + 6310 + 60 + 170 lt=- 40.0;10.0;40.0;150.0;10.0;150.0 @@ -5973,10 +6002,10 @@ ignore_protection : set(ProtectingAttribute) UMLClass - 973 - 4578 - 182 - 42 + 200 + 6540 + 260 + 60 *TimeRelativeAttributeDecrease* @@ -5986,10 +6015,10 @@ bg=orange UMLClass - 973 - 4627 - 182 - 42 + 200 + 6610 + 260 + 60 *TimeRelativeAttributeIncrease* @@ -5999,10 +6028,10 @@ bg=orange Relation - 1148 - 4592 - 35 - 70 + 450 + 6560 + 50 + 100 lt=- 10.0;80.0;30.0;80.0;30.0;10.0 @@ -6010,10 +6039,10 @@ bg=orange Relation - 1148 - 4557 - 35 - 56 + 450 + 6510 + 50 + 80 lt=->> 10.0;60.0;30.0;60.0;30.0;10.0 @@ -6021,10 +6050,10 @@ bg=orange UMLClass - 1008 - 4683 - 175 - 63 + 250 + 6690 + 250 + 90 *TimeRelativeProgressChange* bg=pink @@ -6037,10 +6066,10 @@ total_change_time : float Relation - 1176 - 4515 - 42 - 210 + 490 + 6450 + 60 + 300 lt=- 40.0;10.0;40.0;280.0;10.0;280.0 @@ -6048,10 +6077,10 @@ total_change_time : float UMLClass - 1547 - 4487 - 175 - 63 + 1020 + 6410 + 250 + 90 *TimeRelativeAttributeChange* bg=pink @@ -6063,10 +6092,10 @@ type : AttributeChangeType Relation - 1715 - 4417 - 42 - 112 + 1260 + 6310 + 60 + 160 lt=- 40.0;10.0;40.0;140.0;10.0;140.0 @@ -6074,10 +6103,10 @@ type : AttributeChangeType UMLClass - 1547 - 4578 - 154 - 42 + 1020 + 6540 + 220 + 60 *FlatAttributeDecrease* @@ -6087,10 +6116,10 @@ bg=orange UMLClass - 1547 - 4627 - 154 - 42 + 1020 + 6610 + 220 + 60 *FlatAttributeIncrease* @@ -6100,10 +6129,10 @@ bg=orange Relation - 1694 - 4592 - 35 - 70 + 1230 + 6560 + 50 + 100 lt=- 10.0;80.0;30.0;80.0;30.0;10.0 @@ -6111,10 +6140,10 @@ bg=orange Relation - 1694 - 4543 - 35 - 70 + 1230 + 6490 + 50 + 100 lt=->> 10.0;80.0;30.0;80.0;30.0;10.0 @@ -6122,10 +6151,10 @@ bg=orange UMLClass - 1547 - 4683 - 175 - 63 + 1020 + 6690 + 250 + 90 *TimeRelativeProgressChange* bg=pink @@ -6138,10 +6167,10 @@ type : ProgressType Relation - 1715 - 4508 - 42 - 217 + 1260 + 6440 + 60 + 310 lt=- 40.0;10.0;40.0;290.0;10.0;290.0 @@ -6149,10 +6178,10 @@ type : ProgressType UMLClass - 2387 - 2926 - 84 - 42 + 2220 + 4180 + 120 + 60 *ProgressType* @@ -6162,10 +6191,10 @@ bg=pink Relation - 2331 - 2940 - 70 - 21 + 2140 + 4200 + 100 + 30 lt=- 10.0;10.0;80.0;10.0 @@ -6173,10 +6202,10 @@ bg=pink UMLClass - 4361 - 1869 - 189 - 56 + 5040 + 2670 + 270 + 80 *AoE1TradeRoute* bg=pink @@ -6189,10 +6218,10 @@ trade_amount : int UMLClass - 2555 - 3451 - 147 - 56 + 2460 + 4930 + 210 + 80 *StateChange* bg=pink @@ -6204,10 +6233,10 @@ state_change : StateChanger Relation - 2527 - 3472 - 42 - 21 + 2420 + 4960 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -6215,10 +6244,10 @@ state_change : StateChanger UMLClass - 2555 - 3325 - 105 - 56 + 2460 + 4750 + 150 + 80 *Terrain* bg=pink @@ -6232,10 +6261,10 @@ terrain : Terrain Relation - 2527 - 3346 - 42 - 21 + 2420 + 4780 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -6243,10 +6272,10 @@ terrain : Terrain Relation - 5012 - 3290 - 42 - 21 + 5970 + 4700 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -6254,10 +6283,10 @@ terrain : Terrain UMLClass - 5040 - 3269 - 182 - 56 + 6010 + 4670 + 260 + 80 *TerrainRequirement* bg=green @@ -6270,10 +6299,10 @@ blacklisted_terrains : set(Terrain) UMLClass - 5040 - 3199 - 175 - 63 + 6010 + 4570 + 250 + 90 *OverlayTerrain* bg=green @@ -6285,10 +6314,10 @@ terrain_overlay : Terrain UMLClass - 4298 - 3451 - 168 - 63 + 4950 + 4930 + 240 + 90 *Attribute* bg=pink @@ -6302,10 +6331,10 @@ abbreviation : TranslatedString Relation - 4375 - 3409 - 21 - 56 + 5060 + 4870 + 30 + 80 lt=<. 10.0;60.0;10.0;10.0 @@ -6313,10 +6342,10 @@ abbreviation : TranslatedString UMLClass - 2219 - 504 - 217 - 84 + 1980 + 720 + 310 + 120 *DepositResourcesOnProgress* bg=yellow @@ -6331,10 +6360,10 @@ blacklisted_entities : set(GameEntity) Relation - 2429 - 525 - 56 - 21 + 2280 + 750 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -6342,10 +6371,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 3920 - 1491 - 98 - 42 + 4410 + 2130 + 140 + 60 *PriceMode* @@ -6355,10 +6384,10 @@ bg=pink Relation - 3962 - 1680 - 21 - 56 + 4470 + 2400 + 30 + 80 lt=<. 10.0;10.0;10.0;60.0 @@ -6366,10 +6395,10 @@ bg=pink Relation - 4011 - 1505 - 119 - 21 + 4540 + 2150 + 170 + 30 lt=<<- 10.0;10.0;150.0;10.0 @@ -6377,10 +6406,10 @@ bg=pink UMLClass - 4116 - 1491 - 77 - 42 + 4690 + 2130 + 110 + 60 *Fixed* @@ -6390,10 +6419,10 @@ bg=pink Relation - 3297 - 2163 - 49 - 21 + 3520 + 3090 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -6401,10 +6430,10 @@ bg=pink Relation - 4333 - 1890 - 42 - 21 + 5000 + 2700 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -6412,10 +6441,10 @@ bg=pink Relation - 4088 - 1505 - 21 - 70 + 4650 + 2150 + 30 + 100 lt=- 10.0;10.0;10.0;80.0 @@ -6423,10 +6452,10 @@ bg=pink Relation - 4088 - 1554 - 42 - 21 + 4650 + 2220 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -6434,10 +6463,10 @@ bg=pink UMLClass - 4116 - 1540 - 105 - 70 + 4690 + 2200 + 150 + 100 *Dynamic* bg=pink @@ -6451,10 +6480,10 @@ max_price : float UMLClass - 4172 - 1631 - 84 - 42 + 4770 + 2330 + 120 + 60 *PricePool* @@ -6464,10 +6493,10 @@ bg=pink Relation - 4123 - 1645 - 63 - 21 + 4700 + 2350 + 90 + 30 lt=<. 70.0;10.0;10.0;10.0 @@ -6475,10 +6504,10 @@ bg=pink UMLClass - 1358 - 56 - 175 - 42 + 750 + 80 + 250 + 60 *TimeRelativeAttributeChange* @@ -6488,10 +6517,10 @@ bg=yellow Relation - 1526 - 70 - 63 - 21 + 990 + 100 + 90 + 30 lt=- 70.0;10.0;10.0;10.0 @@ -6499,10 +6528,10 @@ bg=yellow UMLClass - 1372 - 7 - 161 - 42 + 770 + 10 + 230 + 60 *TimeRelativeProgressChange* @@ -6512,10 +6541,10 @@ bg=yellow Relation - 1526 - 21 - 63 - 21 + 990 + 30 + 90 + 30 lt=- 70.0;10.0;10.0;10.0 @@ -6523,10 +6552,10 @@ bg=yellow UMLClass - 1211 - 350 - 105 - 42 + 540 + 500 + 150 + 60 *Unconditional* @@ -6536,10 +6565,10 @@ bg=yellow Relation - 1330 - 126 - 21 - 259 + 710 + 180 + 30 + 370 lt=- 10.0;10.0;10.0;350.0 @@ -6547,10 +6576,10 @@ bg=yellow UMLClass - 1904 - 238 - 119 - 42 + 1530 + 340 + 170 + 60 *Unconditional* @@ -6560,10 +6589,10 @@ bg=yellow Relation - 1876 - 252 - 42 - 21 + 1490 + 360 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -6571,10 +6600,10 @@ bg=yellow UMLClass - 1540 - 1918 - 168 - 56 + 1010 + 2740 + 240 + 80 *Cost* bg=pink @@ -6586,10 +6615,10 @@ payment_mode : PaymentMode UMLClass - 1750 - 1883 - 161 - 56 + 1310 + 2690 + 230 + 80 *ResourceCost* bg=pink @@ -6601,10 +6630,10 @@ amount : set(ResourceAmount) UMLClass - 1750 - 1953 - 161 - 56 + 1310 + 2790 + 230 + 80 *AttributeCost* bg=pink @@ -6616,10 +6645,10 @@ amount : set(AttributeAmount) Relation - 1701 - 1939 - 49 - 21 + 1240 + 2770 + 70 + 30 lt=<<- 10.0;10.0;50.0;10.0 @@ -6627,10 +6656,10 @@ amount : set(AttributeAmount) Relation - 1729 - 1904 - 35 - 49 + 1280 + 2720 + 50 + 70 lt=- 10.0;50.0;10.0;10.0;30.0;10.0 @@ -6638,10 +6667,10 @@ amount : set(AttributeAmount) Relation - 1729 - 1932 - 35 - 63 + 1280 + 2760 + 50 + 90 lt=- 10.0;10.0;10.0;70.0;30.0;70.0 @@ -6649,10 +6678,10 @@ amount : set(AttributeAmount) UMLClass - 1715 - 581 - 210 - 56 + 1260 + 830 + 300 + 80 *CreationAttributeCost* bg=yellow @@ -6666,10 +6695,10 @@ creatables : set(CreatableGameEntity) Relation - 1673 - 602 - 56 - 21 + 1200 + 860 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -6677,10 +6706,10 @@ creatables : set(CreatableGameEntity) Relation - 1638 - 728 - 56 - 21 + 1150 + 1040 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -6688,10 +6717,10 @@ creatables : set(CreatableGameEntity) UMLClass - 1715 - 392 - 203 - 56 + 1260 + 560 + 290 + 80 *ResearchAttributeCost* bg=yellow @@ -6705,10 +6734,10 @@ researchables : set(ResearchableTech) Relation - 1673 - 413 - 56 - 21 + 1200 + 590 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -6716,10 +6745,10 @@ researchables : set(ResearchableTech) Relation - 1617 - 1967 - 21 - 49 + 1120 + 2810 + 30 + 70 lt=- 10.0;50.0;10.0;10.0 @@ -6727,10 +6756,10 @@ researchables : set(ResearchableTech) UMLClass - 1575 - 1848 - 98 - 42 + 1060 + 2640 + 140 + 60 *PaymentMode* @@ -6740,10 +6769,10 @@ bg=pink Relation - 1617 - 1883 - 21 - 49 + 1120 + 2690 + 30 + 70 lt=<. 10.0;50.0;10.0;10.0 @@ -6751,10 +6780,10 @@ bg=pink UMLClass - 1533 - 1694 - 77 - 42 + 1000 + 2420 + 110 + 60 *Advance* @@ -6764,10 +6793,10 @@ bg=pink UMLClass - 1533 - 1792 - 77 - 42 + 1000 + 2560 + 110 + 60 *Arrear* @@ -6777,10 +6806,10 @@ bg=pink UMLClass - 1533 - 1743 - 77 - 42 + 1000 + 2490 + 110 + 60 *Adaptive* @@ -6790,10 +6819,10 @@ bg=pink Relation - 1631 - 1659 - 21 - 203 + 1140 + 2370 + 30 + 290 lt=<<- 10.0;270.0;10.0;10.0 @@ -6801,10 +6830,10 @@ bg=pink Relation - 1603 - 1806 - 49 - 21 + 1100 + 2580 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -6812,10 +6841,10 @@ bg=pink Relation - 1603 - 1757 - 49 - 21 + 1100 + 2510 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -6823,10 +6852,10 @@ bg=pink Relation - 1603 - 1708 - 49 - 21 + 1100 + 2440 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -6834,10 +6863,10 @@ bg=pink UMLClass - 2548 - 2807 - 112 - 42 + 2450 + 4010 + 160 + 60 *AttributeChange* @@ -6847,10 +6876,10 @@ bg=pink Relation - 2464 - 2940 - 77 - 21 + 2330 + 4200 + 110 + 30 lt=<<- 10.0;10.0;90.0;10.0 @@ -6858,10 +6887,10 @@ bg=pink UMLClass - 973 - 4760 - 182 - 42 + 200 + 6800 + 260 + 60 *TimeRelativeProgressDecrease* @@ -6871,10 +6900,10 @@ bg=orange UMLClass - 973 - 4809 - 182 - 42 + 200 + 6870 + 260 + 60 *TimeRelativeProgressIncrease* @@ -6884,10 +6913,10 @@ bg=orange Relation - 1148 - 4774 - 35 - 70 + 450 + 6820 + 50 + 100 lt=- 10.0;80.0;30.0;80.0;30.0;10.0 @@ -6895,10 +6924,10 @@ bg=orange Relation - 1148 - 4739 - 35 - 56 + 450 + 6770 + 50 + 80 lt=->> 10.0;60.0;30.0;60.0;30.0;10.0 @@ -6906,10 +6935,10 @@ bg=orange UMLClass - 1519 - 4760 - 182 - 42 + 980 + 6800 + 260 + 60 *TimeRelativeProgressDecrease* @@ -6919,10 +6948,10 @@ bg=orange UMLClass - 1519 - 4809 - 182 - 42 + 980 + 6870 + 260 + 60 *TimeRelativeProgressIncrease* @@ -6932,10 +6961,10 @@ bg=orange Relation - 1694 - 4774 - 35 - 70 + 1230 + 6820 + 50 + 100 lt=- 10.0;80.0;30.0;80.0;30.0;10.0 @@ -6943,10 +6972,10 @@ bg=orange Relation - 1694 - 4739 - 35 - 56 + 1230 + 6770 + 50 + 80 lt=->> 10.0;60.0;30.0;60.0;30.0;10.0 @@ -6954,10 +6983,10 @@ bg=orange UMLClass - 1533 - 1645 - 77 - 42 + 1000 + 2350 + 110 + 60 *Shadow* @@ -6967,10 +6996,10 @@ bg=pink Relation - 1603 - 1659 - 49 - 21 + 1100 + 2370 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -6978,10 +7007,10 @@ bg=pink UMLClass - 5390 - 2842 - 112 - 42 + 6510 + 4060 + 160 + 60 *HerdableMode* @@ -6991,10 +7020,10 @@ bg=pink Relation - 5439 - 2814 - 21 - 42 + 6580 + 4020 + 30 + 60 lt=<. 10.0;40.0;10.0;10.0 @@ -7002,10 +7031,10 @@ bg=pink UMLClass - 5453 - 2905 - 112 - 42 + 6600 + 4150 + 160 + 60 *ClosestHerding* @@ -7015,10 +7044,10 @@ bg=pink Relation - 5418 - 2877 - 21 - 161 + 6550 + 4110 + 30 + 230 lt=<<- 10.0;10.0;10.0;210.0 @@ -7026,10 +7055,10 @@ bg=pink Relation - 5418 - 2919 - 49 - 21 + 6550 + 4170 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -7037,10 +7066,10 @@ bg=pink UMLClass - 5453 - 2954 - 140 - 42 + 6600 + 4220 + 200 + 60 *LongestTimeInRange* @@ -7050,10 +7079,10 @@ bg=pink UMLClass - 5453 - 3003 - 112 - 42 + 6600 + 4290 + 160 + 60 *MostHerding* @@ -7063,10 +7092,10 @@ bg=pink Relation - 5418 - 2968 - 49 - 21 + 6550 + 4240 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -7074,10 +7103,10 @@ bg=pink Relation - 5418 - 3017 - 49 - 21 + 6550 + 4310 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -7085,10 +7114,10 @@ bg=pink UMLClass - 1778 - 2730 - 84 - 42 + 1350 + 3900 + 120 + 60 *TerrainType* @@ -7098,10 +7127,10 @@ bg=pink Relation - 1813 - 2695 - 21 - 49 + 1400 + 3850 + 30 + 70 lt=<. 10.0;50.0;10.0;10.0 @@ -7109,10 +7138,10 @@ bg=pink UMLClass - 1694 - 3759 - 182 - 63 + 1230 + 5370 + 260 + 90 *Stacked* bg=pink @@ -7127,10 +7156,10 @@ distribution_type : DistributionType Relation - 1659 - 3717 - 49 - 21 + 1180 + 5310 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -7138,10 +7167,10 @@ distribution_type : DistributionType UMLClass - 1967 - 3773 - 112 - 42 + 1620 + 5390 + 160 + 60 *CalculationType* @@ -7155,10 +7184,10 @@ bg=pink Relation - 1869 - 3787 - 112 - 21 + 1480 + 5410 + 160 + 30 lt=<. 140.0;10.0;10.0;10.0 @@ -7166,10 +7195,10 @@ bg=pink UMLClass - 2023 - 3955 - 112 - 70 + 1700 + 5650 + 160 + 100 *Hyperbolic* bg=pink @@ -7186,10 +7215,10 @@ scale_factor : float Relation - 1995 - 3808 - 21 - 182 + 1660 + 5440 + 30 + 260 lt=<<- 10.0;10.0;10.0;240.0 @@ -7197,10 +7226,10 @@ scale_factor : float Relation - 1883 - 4361 - 42 - 21 + 1500 + 6230 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -7208,10 +7237,10 @@ scale_factor : float Relation - 1995 - 3969 - 42 - 21 + 1660 + 5670 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -7219,10 +7248,10 @@ scale_factor : float UMLClass - 2023 - 3878 - 112 - 70 + 1700 + 5540 + 160 + 100 *Linear* bg=pink @@ -7239,10 +7268,10 @@ scale_factor : float Relation - 1995 - 3892 - 42 - 21 + 1660 + 5560 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -7250,10 +7279,10 @@ scale_factor : float UMLClass - 2023 - 3829 - 84 - 42 + 1700 + 5470 + 120 + 60 *NoStack* @@ -7266,10 +7295,10 @@ bg=pink Relation - 1995 - 3843 - 42 - 21 + 1660 + 5490 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -7277,10 +7306,10 @@ bg=pink UMLClass - 3332 - 2198 - 133 - 56 + 3570 + 3140 + 190 + 80 *OwnStorage* bg=pink @@ -7292,10 +7321,10 @@ container : EntityContainer Relation - 3297 - 2219 - 49 - 21 + 3520 + 3170 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -7303,10 +7332,10 @@ container : EntityContainer UMLClass - 3339 - 1631 - 217 - 56 + 3580 + 2330 + 310 + 80 *ProductionQueue* bg=green @@ -7319,10 +7348,10 @@ production_modes : set(ProductionMode) Relation - 3549 - 1652 - 42 - 84 + 3880 + 2360 + 60 + 120 lt=- 40.0;100.0;40.0;10.0;10.0;10.0 @@ -7330,10 +7359,10 @@ production_modes : set(ProductionMode) UMLClass - 3402 - 1561 - 112 - 42 + 3670 + 2230 + 160 + 60 *ProductionMode* @@ -7343,10 +7372,10 @@ bg=pink Relation - 3451 - 1596 - 21 - 49 + 3740 + 2280 + 30 + 70 lt=<. 10.0;10.0;10.0;50.0 @@ -7354,10 +7383,10 @@ bg=pink UMLClass - 3591 - 1617 - 189 - 56 + 3940 + 2310 + 270 + 80 *Creatables* bg=pink @@ -7369,10 +7398,10 @@ exclude : set(CreatableGameEntity) Relation - 3507 - 1575 - 98 - 21 + 3820 + 2250 + 140 + 30 lt=<<- 10.0;10.0;120.0;10.0 @@ -7380,10 +7409,10 @@ exclude : set(CreatableGameEntity) UMLClass - 3591 - 1554 - 168 - 56 + 3940 + 2220 + 240 + 80 *Researchables* bg=pink @@ -7395,10 +7424,10 @@ exclude : set(ResearchableTech) Relation - 3563 - 1575 - 42 - 84 + 3900 + 2250 + 60 + 120 lt=- 10.0;10.0;10.0;100.0;40.0;100.0 @@ -7406,10 +7435,10 @@ exclude : set(ResearchableTech) UMLClass - 1981 - 2814 - 140 - 56 + 1640 + 4020 + 200 + 80 *LogicElement* bg=pink @@ -7421,10 +7450,10 @@ only_once : bool UMLClass - 2226 - 2884 - 70 - 42 + 1990 + 4120 + 100 + 60 *AND* @@ -7434,10 +7463,10 @@ bg=pink Relation - 2205 - 2863 - 21 - 224 + 1960 + 4090 + 30 + 320 lt=<<- 10.0;10.0;10.0;300.0 @@ -7445,10 +7474,10 @@ bg=pink UMLClass - 2226 - 2933 - 70 - 42 + 1990 + 4190 + 100 + 60 *OR* @@ -7458,10 +7487,10 @@ bg=pink UMLClass - 2226 - 2982 - 98 - 56 + 1990 + 4260 + 140 + 80 *SUBSETMIN* bg=pink @@ -7473,10 +7502,10 @@ size : int Relation - 2191 - 2947 - 35 - 21 + 1940 + 4210 + 50 + 30 lt=- 30.0;10.0;10.0;10.0 @@ -7484,10 +7513,10 @@ size : int Relation - 2205 - 2947 - 35 - 21 + 1960 + 4210 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -7495,10 +7524,10 @@ size : int Relation - 2205 - 2898 - 35 - 21 + 1960 + 4140 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -7506,10 +7535,10 @@ size : int Relation - 5278 - 2723 - 42 - 21 + 6350 + 3890 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -7517,10 +7546,10 @@ size : int Relation - 5299 - 2653 - 42 - 21 + 6380 + 3790 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -7528,10 +7557,10 @@ size : int Relation - 5299 - 2583 - 42 - 21 + 6380 + 3690 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -7539,10 +7568,10 @@ size : int UMLClass - 1771 - 2905 - 147 - 56 + 1340 + 4150 + 210 + 80 *LiteralScope* bg=pink @@ -7554,10 +7583,10 @@ stances : set(DiplomaticStance) Relation - 1911 - 2926 - 63 - 21 + 1540 + 4180 + 90 + 30 lt=<. 10.0;10.0;70.0;10.0 @@ -7565,10 +7594,10 @@ stances : set(DiplomaticStance) UMLClass - 1750 - 2975 - 70 - 42 + 1310 + 4250 + 100 + 60 *Any* @@ -7579,10 +7608,10 @@ bg=pink Relation - 1827 - 2954 - 21 - 105 + 1420 + 4220 + 30 + 150 lt=<<- 10.0;10.0;10.0;130.0 @@ -7590,10 +7619,10 @@ bg=pink UMLClass - 1750 - 3024 - 70 - 42 + 1310 + 4320 + 100 + 60 *Self* @@ -7603,10 +7632,10 @@ bg=pink Relation - 1813 - 3038 - 35 - 21 + 1400 + 4340 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -7614,10 +7643,10 @@ bg=pink Relation - 1813 - 2989 - 35 - 21 + 1400 + 4270 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -7625,10 +7654,10 @@ bg=pink Relation - 2072 - 3003 - 35 - 21 + 1770 + 4290 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -7636,10 +7665,10 @@ bg=pink Relation - 2072 - 3066 - 35 - 21 + 1770 + 4380 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -7647,10 +7676,10 @@ bg=pink UMLClass - 1925 - 3115 - 154 - 56 + 1560 + 4450 + 220 + 80 *ResourceSpotsDepleted* bg=pink @@ -7662,10 +7691,10 @@ only_enabled : bool UMLClass - 1925 - 3178 - 154 - 56 + 1560 + 4540 + 220 + 80 *Timer* bg=pink @@ -7677,10 +7706,10 @@ time : float Relation - 2072 - 3136 - 35 - 21 + 1770 + 4480 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -7688,10 +7717,10 @@ time : float Relation - 2072 - 3199 - 35 - 21 + 1770 + 4570 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -7699,10 +7728,10 @@ time : float UMLClass - 1925 - 3241 - 154 - 56 + 1560 + 4630 + 220 + 80 *AttributeBelowValue* bg=pink @@ -7715,10 +7744,10 @@ threshold : float UMLClass - 1925 - 3430 - 154 - 56 + 1560 + 4900 + 220 + 80 *ProjectilePassThrough* bg=pink @@ -7730,10 +7759,10 @@ pass_through_range : int UMLClass - 1946 - 3493 - 133 - 42 + 1590 + 4990 + 190 + 60 *ProjectileHitTerrain* @@ -7743,10 +7772,10 @@ bg=pink Relation - 2072 - 3262 - 35 - 21 + 1770 + 4660 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -7754,10 +7783,10 @@ bg=pink Relation - 2072 - 3451 - 35 - 21 + 1770 + 4930 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -7765,10 +7794,10 @@ bg=pink Relation - 2072 - 3507 - 35 - 21 + 1770 + 5010 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -7776,10 +7805,10 @@ bg=pink UMLClass - 4529 - 3087 - 98 - 70 + 5280 + 4410 + 140 + 100 *Hitbox* bg=pink @@ -7793,10 +7822,10 @@ radius_z : float Relation - 4494 - 3108 - 49 - 21 + 5230 + 4440 + 70 + 30 lt=<. 50.0;10.0;10.0;10.0 @@ -7804,10 +7833,10 @@ radius_z : float UMLClass - 4599 - 3017 - 224 - 63 + 5380 + 4310 + 320 + 90 *PassableMode* bg=pink @@ -7820,10 +7849,10 @@ blacklisted_entities : set(GameEntity) Relation - 4550 - 3038 - 63 - 21 + 5310 + 4340 + 90 + 30 lt=<. 70.0;10.0;10.0;10.0 @@ -7831,10 +7860,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 4753 - 3143 - 154 - 56 + 5600 + 4490 + 220 + 80 *Gate* bg=pink @@ -7846,10 +7875,10 @@ stances : set (DiplomaticStance) UMLClass - 4753 - 3094 - 77 - 42 + 5600 + 4420 + 110 + 60 *Normal* @@ -7859,10 +7888,10 @@ bg=pink Relation - 4725 - 3073 - 21 - 105 + 5560 + 4390 + 30 + 150 lt=<<- 10.0;10.0;10.0;130.0 @@ -7870,10 +7899,10 @@ bg=pink Relation - 4725 - 3108 - 42 - 21 + 5560 + 4440 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -7881,10 +7910,10 @@ bg=pink Relation - 4725 - 3157 - 42 - 21 + 5560 + 4510 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -7892,10 +7921,10 @@ bg=pink UMLClass - 4403 - 2870 - 189 - 70 + 5100 + 4100 + 270 + 100 *DetectCloak (SWGB)* bg=green @@ -7909,10 +7938,10 @@ blacklisted_entities : set(GameEntity) Relation - 4375 - 2891 - 42 - 21 + 5060 + 4130 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -7920,10 +7949,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 1827 - 3843 - 112 - 42 + 1420 + 5490 + 160 + 60 *DistributionType* @@ -7939,10 +7968,10 @@ bg=pink Relation - 1785 - 3815 - 56 - 63 + 1360 + 5450 + 80 + 90 lt=<. 60.0;70.0;10.0;70.0;10.0;10.0 @@ -7950,10 +7979,10 @@ bg=pink UMLClass - 1890 - 3899 - 70 - 42 + 1510 + 5570 + 100 + 60 *Mean* @@ -7963,10 +7992,10 @@ bg=pink Relation - 1862 - 3878 - 21 - 56 + 1470 + 5540 + 30 + 80 lt=<<- 10.0;10.0;10.0;60.0 @@ -7974,10 +8003,10 @@ bg=pink Relation - 1862 - 3913 - 42 - 21 + 1470 + 5590 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -7985,10 +8014,10 @@ bg=pink UMLClass - 5208 - 2856 - 98 - 63 + 6250 + 4080 + 140 + 90 *Rectangle* bg=pink @@ -8001,10 +8030,10 @@ height : float UMLClass - 5082 - 2856 - 98 - 42 + 6070 + 4080 + 140 + 60 *MatchToSprite* @@ -8014,10 +8043,10 @@ bg=pink UMLClass - 5145 - 2786 - 98 - 42 + 6160 + 3980 + 140 + 60 *SelectionBox* @@ -8027,10 +8056,10 @@ bg=pink Relation - 5187 - 2758 - 21 - 42 + 6220 + 3940 + 30 + 60 lt=<. 10.0;40.0;10.0;10.0 @@ -8038,10 +8067,10 @@ bg=pink Relation - 5215 - 2821 - 21 - 49 + 6260 + 4030 + 30 + 70 lt=<<- 10.0;10.0;10.0;50.0 @@ -8049,10 +8078,10 @@ bg=pink Relation - 5159 - 2821 - 21 - 49 + 6180 + 4030 + 30 + 70 lt=<<- 10.0;10.0;10.0;50.0 @@ -8060,10 +8089,10 @@ bg=pink UMLClass - 1841 - 1232 - 119 - 56 + 1440 + 1760 + 170 + 80 *Stacked* bg=pink @@ -8075,10 +8104,10 @@ stack_limit : int Relation - 1988 - 1134 - 21 - 203 + 1650 + 1620 + 30 + 290 lt=<<- 10.0;10.0;10.0;270.0 @@ -8086,10 +8115,10 @@ stack_limit : int Relation - 1694 - 2744 - 98 - 21 + 1230 + 3920 + 140 + 30 lt=- 10.0;10.0;120.0;10.0 @@ -8097,10 +8126,10 @@ stack_limit : int Relation - 5012 - 3220 - 42 - 21 + 5970 + 4600 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -8108,10 +8137,10 @@ stack_limit : int UMLClass - 1736 - 2324 - 84 - 42 + 1290 + 3320 + 120 + 60 *NyanPatch* @@ -8121,10 +8150,10 @@ bg=pink Relation - 1694 - 2338 - 56 - 21 + 1230 + 3340 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -8132,10 +8161,10 @@ bg=pink UMLClass - 2107 - 3241 - 154 - 56 + 1820 + 4630 + 220 + 80 *AttributeAbovePercentage* bg=pink @@ -8148,10 +8177,10 @@ threshold : float Relation - 2072 - 3325 - 35 - 21 + 1770 + 4750 + 50 + 30 lt=- 30.0;10.0;10.0;10.0 @@ -8159,10 +8188,10 @@ threshold : float Relation - 2261 - 2478 - 63 - 21 + 2040 + 3540 + 90 + 30 lt=- 70.0;10.0;10.0;10.0 @@ -8170,10 +8199,10 @@ threshold : float UMLClass - 2310 - 2394 - 133 - 56 + 2110 + 3420 + 190 + 80 *Palette* bg=pink @@ -8185,10 +8214,10 @@ palette : file Relation - 2261 - 2415 - 63 - 21 + 2040 + 3450 + 90 + 30 lt=- 70.0;10.0;10.0;10.0 @@ -8196,10 +8225,10 @@ palette : file Relation - 4018 - 2702 - 49 - 21 + 4550 + 3860 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -8207,10 +8236,10 @@ palette : file UMLClass - 4053 - 2800 - 112 - 56 + 4600 + 4000 + 160 + 80 *Guard* bg=pink @@ -8222,10 +8251,10 @@ range : float Relation - 4018 - 2821 - 49 - 21 + 4550 + 4030 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -8233,10 +8262,10 @@ range : float UMLClass - 3332 - 2086 - 147 - 56 + 3570 + 2980 + 210 + 80 *Replace* bg=pink @@ -8248,10 +8277,10 @@ game_entities : set(GameEntity) Relation - 3297 - 2107 - 49 - 21 + 3520 + 3010 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -8259,10 +8288,10 @@ game_entities : set(GameEntity) UMLClass - 1582 - 2709 - 84 - 42 + 1070 + 3870 + 120 + 60 *TechType* @@ -8272,10 +8301,10 @@ bg=pink Relation - 1659 - 2723 - 56 - 21 + 1180 + 3890 + 80 + 30 lt=- 10.0;10.0;60.0;10.0 @@ -8283,10 +8312,10 @@ bg=pink Relation - 1617 - 2744 - 21 - 56 + 1120 + 3920 + 30 + 80 lt=<. 10.0;10.0;10.0;60.0 @@ -8294,10 +8323,10 @@ bg=pink UMLClass - 1596 - 2646 - 70 - 42 + 1090 + 3780 + 100 + 60 *Any* @@ -8307,10 +8336,10 @@ bg=pink Relation - 1659 - 2660 - 56 - 21 + 1180 + 3800 + 80 + 30 lt=- 10.0;10.0;60.0;10.0 @@ -8318,10 +8347,10 @@ bg=pink Relation - 1624 - 2681 - 21 - 42 + 1130 + 3830 + 30 + 60 lt=<<- 10.0;40.0;10.0;10.0 @@ -8329,10 +8358,10 @@ bg=pink UMLClass - 1813 - 2793 - 70 - 42 + 1400 + 3990 + 100 + 60 *Any* @@ -8342,10 +8371,10 @@ bg=pink Relation - 1834 - 2765 - 21 - 42 + 1430 + 3950 + 30 + 60 lt=<<- 10.0;10.0;10.0;40.0 @@ -8353,10 +8382,10 @@ bg=pink UMLClass - 1337 - 2436 - 70 - 42 + 720 + 3480 + 100 + 60 *Any* @@ -8366,10 +8395,10 @@ bg=pink Relation - 1400 - 2450 - 49 - 49 + 810 + 3500 + 70 + 70 lt=<<- 50.0;50.0;50.0;10.0;10.0;10.0 @@ -8377,10 +8406,10 @@ bg=pink UMLClass - 2555 - 3199 - 161 - 56 + 2460 + 4570 + 230 + 80 *AnimationOverlay* bg=pink @@ -8393,10 +8422,10 @@ overlays : set(Animation) Relation - 2527 - 3220 - 42 - 21 + 2420 + 4600 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -8404,10 +8433,10 @@ overlays : set(Animation) UMLClass - 2107 - 3178 - 154 - 56 + 1820 + 4540 + 220 + 80 *AttributeBelowPercentage* bg=pink @@ -8420,10 +8449,10 @@ threshold : float UMLClass - 1925 - 3304 - 154 - 56 + 1560 + 4720 + 220 + 80 *AttributeAboveValue* bg=pink @@ -8436,10 +8465,10 @@ threshold : float Relation - 2086 - 3199 - 35 - 21 + 1790 + 4570 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -8447,10 +8476,10 @@ threshold : float Relation - 2086 - 3262 - 35 - 21 + 1790 + 4660 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -8458,10 +8487,10 @@ threshold : float UMLClass - 1092 - 294 - 224 - 49 + 370 + 420 + 320 + 70 *ElevationDifferenceHigh* bg=yellow @@ -8473,10 +8502,10 @@ min_elevation_difference : optional(float) = None Relation - 1309 - 315 - 42 - 21 + 680 + 450 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -8484,10 +8513,10 @@ min_elevation_difference : optional(float) = None Relation - 1309 - 364 - 42 - 21 + 680 + 520 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -8495,10 +8524,10 @@ min_elevation_difference : optional(float) = None UMLClass - 1904 - 63 - 224 - 56 + 1530 + 90 + 320 + 80 *ElevationDifferenceHigh* bg=yellow @@ -8510,10 +8539,10 @@ min_elevation_difference : optional(float) = None Relation - 1876 - 84 - 42 - 21 + 1490 + 120 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -8521,10 +8550,10 @@ min_elevation_difference : optional(float) = None UMLClass - 1092 - 2898 - 91 - 42 + 370 + 4140 + 130 + 60 *MiscVariant* @@ -8534,10 +8563,10 @@ bg=pink Relation - 1176 - 2779 - 126 - 154 + 490 + 3970 + 180 + 220 lt=- 160.0;10.0;160.0;200.0;10.0;200.0 @@ -8545,10 +8574,10 @@ bg=pink UMLClass - 4074 - 2331 - 168 - 56 + 4630 + 3330 + 240 + 80 *ResourceStorage* bg=green @@ -8560,10 +8589,10 @@ containers : set(ResourceContainer) Relation - 4235 - 2352 - 42 - 21 + 4860 + 3360 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -8571,10 +8600,10 @@ containers : set(ResourceContainer) UMLClass - 3885 - 2331 - 168 - 63 + 4360 + 3330 + 240 + 100 *ResourceContainer* bg=pink @@ -8588,10 +8617,10 @@ carry_progress : set(Progress) Relation - 4046 - 2352 - 42 - 21 + 4590 + 3360 + 60 + 30 lt=<. 10.0;10.0;40.0;10.0 @@ -8599,10 +8628,10 @@ carry_progress : set(Progress) UMLClass - 3906 - 2415 - 126 - 56 + 4390 + 3460 + 180 + 80 *InternalDropSite* bg=pink @@ -8614,10 +8643,10 @@ update_time : float Relation - 3962 - 2387 - 21 - 42 + 4470 + 3420 + 30 + 60 lt=<<- 10.0;10.0;10.0;40.0 @@ -8625,10 +8654,10 @@ update_time : float UMLClass - 1715 - 2107 - 98 - 42 + 1260 + 3010 + 140 + 60 *TransformPool* @@ -8638,10 +8667,10 @@ bg=pink Relation - 1806 - 2121 - 35 - 21 + 1390 + 3030 + 50 + 30 lt=<. 10.0;10.0;30.0;10.0 @@ -8649,10 +8678,10 @@ bg=pink Relation - 3948 - 3157 - 42 - 21 + 4450 + 4510 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 @@ -8660,10 +8689,10 @@ bg=pink UMLClass - 2485 - 2324 - 70 - 42 + 2360 + 3320 + 100 + 60 *Any* @@ -8673,10 +8702,10 @@ bg=pink Relation - 2513 - 2296 - 21 - 42 + 2400 + 3280 + 30 + 60 lt=- 10.0;40.0;10.0;10.0 @@ -8684,10 +8713,10 @@ bg=pink Relation - 2597 - 2296 - 21 - 42 + 2520 + 3280 + 30 + 60 lt=- 10.0;40.0;10.0;10.0 @@ -8695,10 +8724,10 @@ bg=pink UMLClass - 3843 - 1617 - 287 - 70 + 4300 + 2310 + 410 + 100 *ExchangeRate* bg=pink @@ -8712,10 +8741,10 @@ price_pool : optional(PricePool) = None UMLClass - 3892 - 1722 - 182 - 77 + 4370 + 2460 + 260 + 110 *ExchangeResources* bg=green @@ -8730,10 +8759,10 @@ exchange_modes : set(ExchangeMode) Relation - 3962 - 1526 - 21 - 105 + 4470 + 2180 + 30 + 150 lt=<. 10.0;10.0;10.0;130.0 @@ -8741,10 +8770,10 @@ exchange_modes : set(ExchangeMode) UMLClass - 4102 - 1729 - 112 - 56 + 4670 + 2470 + 160 + 80 *ExchangeMode* bg=pink @@ -8756,10 +8785,10 @@ fee_multiplier : float Relation - 4067 - 1750 - 49 - 21 + 4620 + 2500 + 70 + 30 lt=<. 50.0;10.0;10.0;10.0 @@ -8767,10 +8796,10 @@ fee_multiplier : float UMLClass - 4270 - 1708 - 70 - 42 + 4910 + 2440 + 100 + 60 *Sell* @@ -8780,10 +8809,10 @@ bg=pink UMLClass - 4270 - 1757 - 70 - 42 + 4910 + 2510 + 100 + 60 *Buy* @@ -8793,10 +8822,10 @@ bg=pink Relation - 4207 - 1736 - 77 - 21 + 4820 + 2480 + 110 + 30 lt=<<- 10.0;10.0;90.0;10.0 @@ -8804,10 +8833,10 @@ bg=pink Relation - 4207 - 1757 - 77 - 21 + 4820 + 2510 + 110 + 30 lt=<<- 10.0;10.0;90.0;10.0 @@ -8815,10 +8844,10 @@ bg=pink Relation - 2030 - 2863 - 21 - 56 + 1710 + 4090 + 30 + 80 lt=<<- 10.0;10.0;10.0;60.0 @@ -8826,10 +8855,10 @@ bg=pink Relation - 2114 - 2835 - 49 - 21 + 1830 + 4050 + 70 + 30 lt=<<- 10.0;10.0;50.0;10.0 @@ -8837,10 +8866,10 @@ bg=pink UMLClass - 2149 - 2814 - 133 - 56 + 1880 + 4020 + 190 + 80 *LogicGate* bg=pink @@ -8852,10 +8881,10 @@ inputs : set(LogicElement) UMLClass - 2226 - 3045 - 98 - 56 + 1990 + 4350 + 140 + 80 *SUBSETMAX* bg=pink @@ -8867,10 +8896,10 @@ size : int Relation - 2205 - 3066 - 35 - 21 + 1960 + 4380 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -8878,10 +8907,10 @@ size : int UMLClass - 2128 - 2884 - 70 - 42 + 1850 + 4120 + 100 + 60 *NOT* @@ -8891,10 +8920,10 @@ bg=pink Relation - 2191 - 2898 - 35 - 21 + 1940 + 4140 + 50 + 30 lt=- 30.0;10.0;10.0;10.0 @@ -8902,10 +8931,10 @@ bg=pink UMLClass - 2128 - 2933 - 70 - 42 + 1850 + 4190 + 100 + 60 *XOR* @@ -8915,10 +8944,10 @@ bg=pink Relation - 2205 - 3003 - 35 - 21 + 1960 + 4290 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -8926,10 +8955,10 @@ bg=pink UMLClass - 2128 - 2982 - 70 - 42 + 1850 + 4260 + 100 + 60 *MULTIXOR* @@ -8939,10 +8968,10 @@ bg=pink Relation - 2191 - 2996 - 35 - 21 + 1940 + 4280 + 50 + 30 lt=- 30.0;10.0;10.0;10.0 @@ -8950,10 +8979,10 @@ bg=pink UMLClass - 3290 - 2555 - 105 - 42 + 3510 + 3650 + 150 + 60 *AbilityProperty* @@ -8963,10 +8992,10 @@ bg=pink Relation - 3332 - 2534 - 21 - 35 + 3570 + 3620 + 30 + 50 lt=- 10.0;10.0;10.0;30.0 @@ -8974,10 +9003,10 @@ bg=pink Relation - 3332 - 2891 - 49 - 21 + 3570 + 4130 + 70 + 30 lt=- 50.0;10.0;10.0;10.0 @@ -8985,10 +9014,10 @@ bg=pink UMLClass - 3367 - 2933 - 154 - 56 + 3620 + 4190 + 220 + 80 *Lock* bg=pink @@ -9000,10 +9029,10 @@ lock_pool : LockPool Relation - 3332 - 2954 - 49 - 21 + 3570 + 4220 + 70 + 30 lt=- 50.0;10.0;10.0;10.0 @@ -9011,10 +9040,10 @@ lock_pool : LockPool UMLClass - 4403 - 2807 - 133 - 56 + 5100 + 4010 + 190 + 80 *Lock* bg=green @@ -9026,10 +9055,10 @@ lock_pools : set(LockPool) Relation - 4375 - 2828 - 42 - 21 + 5060 + 4040 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -9037,10 +9066,10 @@ lock_pools : set(LockPool) UMLClass - 4564 - 2807 - 105 - 56 + 5330 + 4010 + 150 + 80 *LockPool* bg=pink @@ -9052,10 +9081,10 @@ slots : int Relation - 4529 - 2828 - 49 - 21 + 5280 + 4040 + 70 + 30 lt=<. 50.0;10.0;10.0;10.0 @@ -9063,10 +9092,10 @@ slots : int UMLClass - 1918 - 1099 - 112 - 42 + 1550 + 1570 + 160 + 60 *ModifierProperty* @@ -9076,10 +9105,10 @@ bg=pink Relation - 2023 - 1113 - 70 - 21 + 1700 + 1590 + 100 + 30 lt=- 80.0;10.0;10.0;10.0 @@ -9087,10 +9116,10 @@ bg=pink Relation - 1953 - 1183 - 56 - 21 + 1600 + 1690 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -9098,10 +9127,10 @@ bg=pink Relation - 1953 - 1253 - 56 - 21 + 1600 + 1790 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -9109,10 +9138,10 @@ bg=pink UMLClass - 1841 - 1295 - 119 - 56 + 1440 + 1850 + 170 + 80 *Multiplier* bg=pink @@ -9124,10 +9153,10 @@ multiplier : float Relation - 1953 - 1316 - 56 - 21 + 1600 + 1880 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -9135,10 +9164,10 @@ multiplier : float Relation - 2464 - 623 - 56 - 21 + 2330 + 890 + 80 + 30 lt=- 60.0;10.0;10.0;10.0 @@ -9146,10 +9175,10 @@ multiplier : float UMLNote - 1589 - 315 - 42 - 21 + 1080 + 450 + 60 + 30 effect bg=blue @@ -9158,10 +9187,10 @@ bg=blue UMLNote - 1722 - 315 - 63 - 21 + 1270 + 450 + 90 + 30 resistance bg=blue @@ -9170,10 +9199,10 @@ bg=blue UMLNote - 1344 - 203 - 42 - 21 + 730 + 290 + 60 + 30 flac bg=blue @@ -9182,10 +9211,10 @@ bg=blue UMLNote - 1806 - 119 - 42 - 21 + 1390 + 170 + 60 + 30 flac bg=blue @@ -9194,10 +9223,10 @@ bg=blue UMLClass - 1316 - 3843 - 98 - 42 + 690 + 5490 + 140 + 60 *EffectProperty* @@ -9207,10 +9236,10 @@ bg=pink Relation - 1407 - 3857 - 119 - 21 + 820 + 5510 + 170 + 30 lt=- 10.0;10.0;150.0;10.0 @@ -9218,10 +9247,10 @@ bg=pink Relation - 1358 - 3591 - 21 - 266 + 750 + 5130 + 30 + 380 lt=<<- 10.0;360.0;10.0;10.0 @@ -9229,10 +9258,10 @@ bg=pink UMLClass - 1197 - 3759 - 140 - 56 + 520 + 5370 + 200 + 80 *Cost* bg=pink @@ -9244,10 +9273,10 @@ cost : Cost UMLClass - 1169 - 3696 - 168 - 56 + 480 + 5280 + 240 + 80 *Diplomatic* bg=pink @@ -9259,10 +9288,10 @@ stances : set(DiplomaticStance) Relation - 1456 - 3304 - 70 - 21 + 890 + 4720 + 100 + 30 lt=- 10.0;10.0;80.0;10.0 @@ -9270,10 +9299,10 @@ stances : set(DiplomaticStance) UMLClass - 1218 - 3633 - 119 - 56 + 550 + 5190 + 170 + 80 *AreaEffect* bg=pink @@ -9286,10 +9315,10 @@ dropoff : DropoffType Relation - 1330 - 3654 - 49 - 21 + 710 + 5220 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -9297,10 +9326,10 @@ dropoff : DropoffType Relation - 1330 - 3717 - 49 - 21 + 710 + 5310 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -9308,10 +9337,10 @@ dropoff : DropoffType Relation - 1330 - 3780 - 49 - 21 + 710 + 5400 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -9319,10 +9348,10 @@ dropoff : DropoffType UMLClass - 1617 - 3843 - 105 - 42 + 1120 + 5490 + 150 + 60 *ResistanceProperty* @@ -9332,10 +9361,10 @@ bg=pink Relation - 1505 - 3857 - 126 - 21 + 960 + 5510 + 180 + 30 lt=- 10.0;10.0;160.0;10.0 @@ -9343,10 +9372,10 @@ bg=pink Relation - 1659 - 3717 - 21 - 140 + 1180 + 5310 + 30 + 200 lt=<<- 10.0;180.0;10.0;10.0 @@ -9354,10 +9383,10 @@ bg=pink Relation - 2527 - 3171 - 21 - 322 + 2420 + 4530 + 30 + 460 lt=<<- 10.0;10.0;10.0;440.0 @@ -9365,10 +9394,10 @@ bg=pink UMLClass - 2457 - 3136 - 112 - 42 + 2320 + 4480 + 160 + 60 *ProgressProperty* @@ -9379,10 +9408,10 @@ bg=pink Relation - 2331 - 3150 - 140 - 21 + 2140 + 4500 + 200 + 30 lt=- 10.0;10.0;180.0;10.0 @@ -9390,10 +9419,10 @@ bg=pink Relation - 2205 - 2618 - 21 - 56 + 1960 + 3740 + 30 + 80 lt=<<- 10.0;10.0;10.0;60.0 @@ -9401,10 +9430,10 @@ bg=pink UMLClass - 2177 - 2660 - 70 - 42 + 1920 + 3800 + 100 + 60 *Reset* @@ -9414,10 +9443,10 @@ bg=pink UMLClass - 1729 - 2156 - 70 - 42 + 1280 + 3080 + 100 + 60 *Reset* @@ -9427,10 +9456,10 @@ bg=pink Relation - 1792 - 2170 - 49 - 21 + 1370 + 3100 + 70 + 30 lt=<<- 50.0;10.0;10.0;10.0 @@ -9438,10 +9467,10 @@ bg=pink UMLClass - 1946 - 3542 - 133 - 56 + 1590 + 5060 + 190 + 80 *StateChangeActive* bg=pink @@ -9453,10 +9482,10 @@ state_change : StateChanger Relation - 2072 - 3563 - 35 - 21 + 1770 + 5090 + 50 + 30 lt=- 10.0;10.0;30.0;10.0 @@ -9464,10 +9493,10 @@ state_change : StateChanger UMLClass - 1925 - 3367 - 154 - 56 + 1560 + 4810 + 220 + 80 *OwnsGameEntity* bg=pink @@ -9479,10 +9508,10 @@ game_entity : GameEntity Relation - 2072 - 3388 - 35 - 21 + 1770 + 4840 + 50 + 30 lt=- 30.0;10.0;10.0;10.0 @@ -9490,10 +9519,10 @@ game_entity : GameEntity UMLClass - 5397 - 2037 - 231 - 56 + 6520 + 2910 + 330 + 80 *EffectBatch* bg=pink @@ -9507,10 +9536,10 @@ properties : dict(BatchProperty, BatchProperty) = {} UMLClass - 5670 - 2044 - 105 - 42 + 6910 + 2920 + 150 + 60 *BatchProperty* @@ -9521,10 +9550,10 @@ bg=pink UMLClass - 5719 - 2107 - 119 - 56 + 6980 + 3010 + 170 + 80 *Priority* bg=pink @@ -9537,10 +9566,10 @@ priority : int Relation - 5691 - 2079 - 21 - 133 + 6940 + 2970 + 30 + 190 lt=<<- 10.0;10.0;10.0;170.0 @@ -9548,10 +9577,10 @@ priority : int Relation - 5691 - 2128 - 42 - 21 + 6940 + 3040 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -9559,10 +9588,10 @@ priority : int UMLClass - 5719 - 2170 - 119 - 56 + 6980 + 3100 + 170 + 80 *Chance* bg=pink @@ -9575,10 +9604,10 @@ chance : float Relation - 5691 - 2191 - 42 - 21 + 6940 + 3130 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -9586,10 +9615,10 @@ chance : float Relation - 5621 - 2058 - 63 - 21 + 6840 + 2940 + 90 + 30 lt=<. 70.0;10.0;10.0;10.0 @@ -9597,10 +9626,10 @@ chance : float Relation - 5355 - 2009 - 56 - 70 + 6460 + 2870 + 80 + 100 lt=<. 60.0;80.0;10.0;80.0;10.0;10.0 @@ -9608,10 +9637,10 @@ chance : float UMLClass - 5467 - 2107 - 105 - 42 + 6620 + 3010 + 150 + 60 *UnorderedBatch* @@ -9622,10 +9651,10 @@ bg=pink Relation - 5439 - 2086 - 21 - 154 + 6580 + 2980 + 30 + 220 lt=<<- 10.0;10.0;10.0;200.0 @@ -9633,10 +9662,10 @@ bg=pink Relation - 5439 - 2121 - 42 - 21 + 6580 + 3030 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -9644,10 +9673,10 @@ bg=pink UMLClass - 5467 - 2156 - 105 - 42 + 6620 + 3080 + 150 + 60 *OrderedBatch* @@ -9659,10 +9688,10 @@ bg=pink Relation - 5439 - 2170 - 42 - 21 + 6580 + 3100 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -9670,10 +9699,10 @@ bg=pink UMLClass - 5467 - 2205 - 105 - 42 + 6620 + 3150 + 150 + 60 *ChainedBatch* @@ -9684,10 +9713,10 @@ bg=pink Relation - 5439 - 2219 - 42 - 21 + 6580 + 3170 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -9695,10 +9724,10 @@ bg=pink UMLClass - 1197 - 3570 - 140 - 56 + 520 + 5100 + 200 + 80 *Priority* bg=pink @@ -9710,10 +9739,10 @@ priority : int Relation - 1330 - 3591 - 49 - 21 + 710 + 5130 + 70 + 30 lt=- 10.0;10.0;50.0;10.0 @@ -9721,10 +9750,10 @@ priority : int UMLClass - 1799 - 2387 - 84 - 42 + 1380 + 3410 + 120 + 60 *PatchProperty* @@ -9734,10 +9763,10 @@ bg=pink Relation - 1876 - 2401 - 42 - 21 + 1490 + 3430 + 60 + 30 lt=<<- 10.0;10.0;40.0;10.0 @@ -9745,10 +9774,10 @@ bg=pink Relation - 1778 - 2359 - 21 - 126 + 1350 + 3370 + 30 + 180 lt=<. 10.0;10.0;10.0;160.0 @@ -9756,10 +9785,10 @@ bg=pink Relation - 1834 - 2422 - 21 - 63 + 1430 + 3460 + 30 + 90 lt=<. 10.0;10.0;10.0;70.0 @@ -9767,10 +9796,10 @@ bg=pink Relation - 1960 - 2828 - 35 - 21 + 1610 + 4040 + 50 + 30 lt=<<- 30.0;10.0;10.0;10.0 @@ -9778,10 +9807,10 @@ bg=pink UMLClass - 1897 - 2849 - 70 - 42 + 1520 + 4070 + 100 + 60 *True* @@ -9791,10 +9820,10 @@ bg=pink UMLClass - 1897 - 2800 - 70 - 42 + 1520 + 4000 + 100 + 60 *False* @@ -9804,10 +9833,10 @@ bg=pink Relation - 1960 - 2849 - 35 - 21 + 1610 + 4070 + 50 + 30 lt=<<- 30.0;10.0;10.0;10.0 @@ -9815,10 +9844,10 @@ bg=pink Relation - 1876 - 21 - 42 - 21 + 1490 + 30 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -9826,10 +9855,10 @@ bg=pink Relation - 1876 - 147 - 42 - 21 + 1490 + 210 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -9837,10 +9866,10 @@ bg=pink UMLClass - 945 - 2541 - 84 - 42 + 160 + 3630 + 120 + 60 *Tree* @@ -9849,10 +9878,10 @@ bg=pink UMLClass - 945 - 2590 - 84 - 42 + 160 + 3700 + 120 + 60 *Relic* @@ -9861,10 +9890,10 @@ bg=pink Relation - 1043 - 2534 - 91 - 21 + 300 + 3620 + 130 + 30 lt=<<- 110.0;10.0;10.0;10.0 @@ -9872,10 +9901,10 @@ bg=pink Relation - 1043 - 2457 - 21 - 217 + 300 + 3510 + 30 + 310 lt=- 10.0;10.0;10.0;290.0 @@ -9883,10 +9912,10 @@ bg=pink Relation - 1022 - 2604 - 42 - 21 + 270 + 3720 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -9894,10 +9923,10 @@ bg=pink UMLClass - 945 - 2492 - 84 - 42 + 160 + 3560 + 120 + 60 *Swordsman* @@ -9906,10 +9935,10 @@ bg=pink UMLClass - 945 - 2443 - 84 - 42 + 160 + 3490 + 120 + 60 *Barracks* @@ -9918,10 +9947,10 @@ bg=pink Relation - 1022 - 2555 - 42 - 21 + 270 + 3650 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -9929,10 +9958,10 @@ bg=pink Relation - 1022 - 2506 - 42 - 21 + 270 + 3580 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -9940,10 +9969,10 @@ bg=pink Relation - 1022 - 2457 - 42 - 21 + 270 + 3510 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -9951,10 +9980,10 @@ bg=pink UMLClass - 945 - 2639 - 84 - 42 + 160 + 3770 + 120 + 60 *Projectile* @@ -9963,10 +9992,10 @@ bg=pink Relation - 1022 - 2653 - 42 - 21 + 270 + 3790 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -9974,22 +10003,23 @@ bg=pink UMLNote - 840 - 2443 - 98 - 49 + 10 + 3490 + 140 + 70 - All ingame objects are game entities + All ingame objects +are game entities bg=blue UMLClass - 2548 - 2905 - 84 - 42 + 2450 + 4150 + 120 + 60 *Construct* @@ -9999,10 +10029,10 @@ bg=pink UMLClass - 2548 - 2954 - 84 - 42 + 2450 + 4220 + 120 + 60 *Harvest* @@ -10012,10 +10042,10 @@ bg=pink UMLClass - 2548 - 3003 - 84 - 42 + 2450 + 4290 + 120 + 60 *Restock* @@ -10025,10 +10055,10 @@ bg=pink UMLClass - 2548 - 2856 - 84 - 42 + 2450 + 4080 + 120 + 60 *Carry* @@ -10038,10 +10068,10 @@ bg=pink UMLClass - 2548 - 3052 - 84 - 42 + 2450 + 4360 + 120 + 60 *Transform* @@ -10051,10 +10081,10 @@ bg=pink Relation - 2520 - 2821 - 21 - 266 + 2410 + 4030 + 30 + 380 lt=- 10.0;10.0;10.0;360.0 @@ -10062,10 +10092,10 @@ bg=pink Relation - 2520 - 2821 - 42 - 21 + 2410 + 4030 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -10073,10 +10103,10 @@ bg=pink Relation - 2520 - 2870 - 42 - 21 + 2410 + 4100 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -10084,10 +10114,10 @@ bg=pink Relation - 2520 - 2919 - 42 - 21 + 2410 + 4170 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -10095,10 +10125,10 @@ bg=pink Relation - 2520 - 2968 - 42 - 21 + 2410 + 4240 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -10106,10 +10136,10 @@ bg=pink Relation - 2520 - 3017 - 42 - 21 + 2410 + 4310 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -10117,10 +10147,10 @@ bg=pink Relation - 2520 - 3066 - 42 - 21 + 2410 + 4380 + 60 + 30 lt=- 10.0;10.0;40.0;10.0 @@ -10128,10 +10158,10 @@ bg=pink UMLClass - 4823 - 3269 - 175 - 56 + 5700 + 4670 + 250 + 80 *Constructable* bg=green @@ -10144,10 +10174,10 @@ construction_progress : set(Progress) Relation - 4991 - 3290 - 42 - 21 + 5940 + 4700 + 60 + 30 lt=- 40.0;10.0;10.0;10.0 From e67471a78734f3573455a2ffe535b9058e74d90d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 22:26:27 +0100 Subject: [PATCH 099/771] doc: nyan data API v0.4.0 changelog. --- doc/changelogs/nyan_api/v0.4.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changelogs/nyan_api/v0.4.0.md b/doc/changelogs/nyan_api/v0.4.0.md index 5926977d97..c1ad5188e9 100644 --- a/doc/changelogs/nyan_api/v0.4.0.md +++ b/doc/changelogs/nyan_api/v0.4.0.md @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Rename `Entity` object to `Object` ## Added -### Utility module +### Ability module - Add `container : ResourceContainer` member to `Trade` ### Utility module From 408fc171552bc96a30549d05fceeb9d692fd9d1d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 22:29:37 +0100 Subject: [PATCH 100/771] doc: nyan data API v0.4.1 UML. --- doc/nyan/aoe2_nyan_tree.svg | 5553 ++++++++++++++++++----------------- doc/nyan/aoe2_nyan_tree.uxf | 473 ++- 2 files changed, 3366 insertions(+), 2660 deletions(-) diff --git a/doc/nyan/aoe2_nyan_tree.svg b/doc/nyan/aoe2_nyan_tree.svg index 42a2802c22..8a50988dbe 100644 --- a/doc/nyan/aoe2_nyan_tree.svg +++ b/doc/nyan/aoe2_nyan_tree.svg @@ -9,816 +9,996 @@ >NextCommandMoveNextCommandIdleCommandInQueueConditionnext : NodeCommandInQueueWaitAbilityWaittime : floatEventUnit behaviour graphActivitygraph : ActivityAbilitynext : Nodeability : abstract(Ability)XOREventGatenext : dict(Event, Node)XORGatenext : orderedset(Condition)default : NodeEndActivitystart : StartStartnext : NodeNodeConstructablestarting_progress : intconstruction_progress : set(Progress)TransformCarryRestockHarvestConstructAll ingame objectsare game entitiesProjectileBarracksSwordsmanRelicTreeFalseTruePatchPropertyPrioritypriority : intChainedBatchOrderedBatchUnorderedBatchChancechance : floatPrioritypriority : intBatchPropertyEffectBatcheffects : set(DiscreteEffect)properties : dict(BatchProperty, BatchProperty) = {}game_entity : GameEntityStateChangeActivestate_change : StateChangerResetResetProgressPropertyResistancePropertyAreaEffectrange : floatdropoff : DropoffTypeDiplomaticstances : set(DiplomaticStance)Costcost : CostEffectPropertyflacflacresistanceeffectMultipliermultiplier : floatModifierPropertyLockPoolslots : intLocklock_pools : set(LockPool)lock_pool : LockPoolAbilityPropertyMULTIXORXORNOTSUBSETMAXsize : intLogicGateinputs : set(LogicElement)BuySellExchangeModefee_multiplier : floatExchangeResourcesresource_a : Resourceresource_b : Resourceexchange_rate : ExchangeRateexchange_modes : set(ExchangeMode)ExchangeRatebase_price : floatprice_adjust : optional(dict(ExchangeMode, PriceMode)) = Noneprice_pool : optional(PricePool) = NoneAnyTransformPoolInternalDropSiteupdate_time : floatResourceContainerresource : Resourcemax_amount : intcarry_progress : set(Progress)ResourceStoragecontainers : set(ResourceContainer)MiscVariantElevationDifferenceHighmin_elevation_difference : optional(float) = NoneElevationDifferenceHighmin_elevation_difference : optional(float) = Nonethreshold : floatAnimationOverlayoverlays : set(Animation)AnyAnyAnyTechTypeReplacegame_entities : set(GameEntity)Guardrange : floatPalettepalette : filethreshold : floatNyanPatchStackedstack_limit : intSelectionBoxMatchToSpriteRectanglewidth : floatheight : floatMeanDistributionTypeDetectCloak (SWGB)range : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Normalstances : set (DiplomaticStance)PassableModeallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Hitboxradius_x : floatradius_y : floatradius_z : floatProjectileHitTerrainonly_enabled : boolSelfAnyLiteralScopestances : set(DiplomaticStance)SUBSETMINsize : intORANDLogicElementonly_once : boolResearchablesexclude : set(ResearchableTech)Creatablesexclude : set(CreatableGameEntity)ProductionModeProductionQueuesize : intproduction_modes : set(ProductionMode)OwnStoragecontainer : EntityContainerNoStackLinearshift_x : intshift_y : intscale_factor : floatHyperbolicshift_x : intshift_y : intscale_factor : floatCalculationTypeStackedstack_limit : intcalculation_type : CalculationTypedistribution_type : DistributionTypeTerrainTypeMostHerdingLongestTimeInRangeClosestHerdingHerdableModeShadowTimeRelativeProgressIncreaseTimeRelativeProgressDecreaseTimeRelativeProgressIncreaseTimeRelativeProgressDecreaseAttributeChangeAdaptiveArrearAdvancePaymentModeResearchAttributeCostattributes : set(Attribute)researchables : set(ResearchableTech)CreationAttributeCostattributes : set(Attribute)creatables : set(CreatableGameEntity)AttributeCostamount : set(AttributeAmount)ResourceCostamount : set(ResourceAmount)Costpayment_mode : PaymentModeUnconditionalUnconditionalTimeRelativeProgressChangeTimeRelativeAttributeChangePricePoolDynamicchange_value : floatmin_price : floatmax_price : floatFixedPriceModeDepositResourcesOnProgressprogress_type : ProgressTyperesources : set(Resource)affected_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Attributename : TranslatedStringabbreviation : TranslatedStringOverlayTerrainterrain_overlay : TerrainTerrainRequirementallowed_types : set(TerrainType)blacklisted_terrains : set(Terrain)Terrainterrain : TerrainStateChangestate_change : StateChangerAoE1TradeRouteexchange_resources : set(Resource)trade_amount : intProgressTypeTimeRelativeProgressChangetype : ProgressTypeFlatAttributeIncreaseFlatAttributeDecreaseTimeRelativeAttributeChangetype : AttributeChangeTypeTimeRelativeProgressChangetype : ProgressTypetotal_change_time : floatTimeRelativeAttributeIncreaseTimeRelativeAttributeDecreaseTimeRelativeAttributeChangetype : AttributeChangeTypetotal_change_time : floatignore_protection : set(ProtectingAttribute)ProgressStatusprogress_type : ProgressTypeprogress : floatRefundOnConditioncondition : set(LogicElement)refund_amount : set(ResourceAmount)AnimationOverrideoverrides : set(AnimationOverride)ExecutionSoundsounds : set(Sound)InverseLinearAdjacentTilesVariantnorth : optional(GameEntity)north_east : optional(GameEntity)east : optional(GameEntity)south_east : optional(GameEntity)south : optional(GameEntity)south_west : optional(GameEntity)west : optional(GameEntity)north_west : optional(GameEntity)Placetile_snap_distance : floatclearance_size_x : floatclearance_size_y : floatallow_rotation : boolmax_elevation_difference : intEjectPlacementModeSendToContainerTypeLureTypeDiplomaticLineOfSightdiplomatic_stance : DiplomaticStanceTerrainterrain : TerrainTerrainterrain : TerrainNormalInContainerDiscreteEffectcontainers : set(EntityContainer)ability : ApplyDiscreteEffectInContainerContinuousEffectcontainers : set(EntityContainer)ability : ApplyContinuousEffectStateChangerenable_abilities : set(Ability)disable_abilities : set(Ability)enable_modifiers : set(Modifier)disable_modifiers : set(Modifier)transform_pool : optional(TransformPool) = Nonepriority : intopenage nyan data API v0.4.0openage nyan data API v0.4.1GameEntityScopeaffected_types : set(GameEntityType)blacklisted_entities : set(GameEntity)StandardRegenerateResourceSpotrate : ResourceRateresource_spot : ResourceSpotAoE2ProjectileAmountprovider_abilities : set(ApplyDiscreteEffect)receiver_abilities : set(ApplyDiscreteEffect)change_types : set(AttributeChangeType)Orange elements:Effects/Resistances that can be applied on other game entitiesEffects/Resistances that canbe applied on other gameentitiesRevealline_of_sight : floataffected_types : set(GameEntityType)blacklisted_entities : set(GameEntity)ResearchTimeresearchables : set(ResearchableTech)StorageElementCapacitystorage_element : StorageElementDefinitionEntityContainerCapacitycontainer : EntityContainerHerdrange : floatstrength : intallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)condition : set(LogicElement)ResearchResourceCostresources : set(Resource)researchables : set(ResearchableTech)CreationResourceCostresources : set(Resource)creatables : set(CreatableGameEntity)CreationTimecreatables : set(CreatableGameEntity)ReloadTimeGatheringEfficiencyresource_spot : ResourceSpotAbsoluteProjectileAmountamount : floatStrayMoveModeAttackMoveModifierScopeExpectedPositionCurrentPositionTargetModeSendToContainertype : SendToContainerTypesearch_range : floatignore_containers : set(EntityContainer)SendToContainertype : SendToContainerTypestorages : set(EntityContainer)Scopedstances : set(DiplomaticStance)scope : ModifierScopesubformation : SubformationSubformationordering_priority : intFormationsubformations : set(Subformation)FlatAttributeIncreaseFlatAttributeDecreaseFlatAttributeIncreaseFlatAttributeDecreaseFlatAttributeIncreaseFlatAttributeDecreaseFlatAttributeIncreaseFlatAttributeDecreaseAoE2TradeRouteTradeRoutetrade_resource : Resourcestart_trade_post : GameEntityend_trade_post : GameEntityTechtypes : set(TechType)name : TranslatedStringdescription : TranslatedMarkupFilelong_description : TranslatedMarkupFileupdates : orderedset(Patch)FallbackLinearNoDropoffDropoffTypestances : set(DiplomaticStance)AnimationOverrideability : AnimatedAbilityanimations : set(Animation)priority : intcost : CostEntityContainerallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)storage_element_defs : set(StorageElementDefinition)slots : intcarry_progress : set(Progress)Visibilityvisible_in_fog : boolTurnturn_speed : floatLanguageietf_string : textLanguageSoundPairlanguage : Languagesound : SoundLanguageMarkupPairlanguage : Languagemarkup_file : fileLanguageTextPairlanguage : Languagestring : textLuretype : LureTypeLuretype : LureTypedestination : set(GameEntity)min_distance_to_destination : floatElevationDifferenceLowmin_elevation_difference : optional(float) = NoneSelfstances : set(DiplomaticStance)DiplomaticStanceExitContainerallowed_containers : set(EntityContainer)EnterContainerallowed_containers : set(EntityContainer)allowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)RangedDiscreteEffectmin_range : intmax_range : intHuntConvertTypeConvertSelfDestructRangedContinuousEffectmin_range : intmax_range : intMakeHarvestableresource_spot : ResourceSpotresist_condition : set(LogicElement)MakeHarvestableresource_spot : ResourceSpotGatherauto_resume : boolresume_search_range : floattargets : set(ResourceSpot)gather_rate : ResourceRatecontainer : ResourceContainerRepairMonkHealApplyContinuousEffecteffects : set(ContinuousEffect)application_delay : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)ApplyDiscreteEffectbatches : set(EffectBatch)reload_time : floatapplication_delay : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)FlatAttributeChangetype : AttributeChangeTypeblock_rate : AttributeRateContinuousResistanceAttributeRatetype : Attributerate : floatResourceRatetype : Resourcerate : floatDiscreteResistanceDiscreteEffectFlatAttributeChangetype : AttributeChangeTypemin_change_rate : optional(AttributeRate) = Nonemax_change_rate : optional(AttributeRate) = Nonechange_rate : AttributeRateignore_protection : set(ProtectingAttribute)ContinuousEffectAoE2Convertguaranteed_resist_rounds : intprotected_rounds : intprotection_round_recharge_time : floatAoE2Convertskip_guaranteed_rounds : intskip_protected_rounds : intConverttype : ConvertTypechance_resist : floatConverttype : ConvertTypemin_chance_success : optional(float) = Nonemax_chance_success : optional(float) = Nonechance_success : floatcost_fail : optional(Cost) = NoneAttributeChangeTypeFlatAttributeChangetype : AttributeChangeTypeblock_value : AttributeAmountFlatAttributeChangetype : AttributeChangeTypemin_change_value : optional(AttributeAmount) = Nonemax_change_value : optional(AttributeAmount) = Nonechange_value : AttributeAmountignore_protection : set(ProtectingAttribute)Effectors' sideResistors' sideEffectproperties : dict(EffectProperty, EffectProperty) = {}Resistanceproperties : dict(ResistanceProperty, ResistanceProperty) = {}HealthGameEntityTypeAttributeAmounttype : Attributeamount : intAccuracyaccuracy : floataccuracy_dispersion : floatdispersion_dropoff : DropOffTypetarget_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Cloak (SWGB)interrupted_by : set(Ability)interrupt_cooldown : floatLineOfSightrange : floatFaithShield (SWGB)protects : AttributeAttributeSettingattribute : Attributemin_value : intmax_value : intstarting_value : intTerrainOverlayterrain_overlay : TerrainAnimatedoverrides : set(AnimationOverride)Terrainsprite : fileCreatableGameEntitygame_entity : GameEntityvariants : set(Variant)cost : Costcreation_time : floatcreation_sounds : set(Sound)condition : set(LogicElement)placement_modes : set(PlacementMode)UseContingentamount : set(ResourceAmount)ProvideContingentamount : set(ResourceAmount)ResourceContingentmin_amount : intmax_amount : intLiteralscope : LiteralScopeProjectilearc : intaccuracy : set(Accuracy)target_mode : TargetModeignored_types : set(GameEntityType)unignore_entities : set(GameEntity)AttributeChangeTrackerattribute : Attributechange_progress : set(Progress)Hitboxhitbox : HitboxNamedname : TranslatedStringdescription : TranslatedMarkupFilelong_description : TranslatedMarkupFileDropResourcescontainers : set(ResourceContainer)search_range : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Herdableadjacent_discover_range : floatmode : HerdableModeStopPassablehitbox : Hitboxmode : PassableModefoundation_terrain : TerrainPerspectiveVariantangle : intRestockauto_restock : booltarget : ResourceSpotrestock_time : floatmanual_cost : Costauto_cost : Costamount : intPassiveStandGroundDefensiveAggressiveTradePosttrade_routes : set(TradeRoute)Followrange : floatPatrolGameEntityStancesearch_range : floatability_preference : orderedset(Ability)type_preference : orderedset(GameEntityType)GameEntityStancestances: set(GameEntityStance)SendBackToTaskallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)TransferStoragestorage_element : GameEntitysource_container : EntityContainertarget_container : EntityContainerGameEntityProgressgame_entity : GameEntitystatus : ProgressStatusTechResearchedtech : TechVariantchanges : orderedset(Patch)priority : intActiveTransformTotarget_state : StateChangertransform_time : floattransform_progress : set(Progress)Selectableselection_box : SelectionBoxRallyPointWhite elements:AoE2 specific objectsYellow elements:Modifiers (handled by engine implementation)Modifiers (handled by engineimplementation)Green elements:Abilities (handled by engine implementation)Abilities (handled by engineimplementation)Pink elements:Basic nyan API objectsElevationDifferenceLowmin_elevation_difference : optional(float) = NoneFlyoverrelative_angle : floatflyover_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Animationsprite : fileVillager Gather abilities canoverride the graphics ofIdle,Move,Die and Despawnvia CarryProgress objects withAnimationOverridesAbilities andStorageElementscan use theseOverride typesto change anyother ability'sanimation.If min_projectiles is greater thanthe number of Projectiles inprojectiles, the last projectilein the orderedset should be usedIn AoE2 there is onlyone HarvestProgress Statein the interval [0,100],but AoM had more.Stores what happens aftera percentage ofconstruction, damage,transformation, etc. isreachedProgressproperties : dict(ProgressProperty, ProgressProperty) = {}type : ProgressTypeleft_boundary : floatright_boundary : floatetc.)RemoveStoragecontainer : EntityContainerstorage_elements : set(GameEntity)CollectStoragecontainer : EntityContainerstorage_elements : set(GameEntity)StorageElementDefinitionstorage_element : GameEntityelements_per_slot : intconflicts : set(StorageElementDefinition)state_change : optional(StateChanger) = NoneStoragecontainer : EntityContainerempty_condition : set(LogicElement)RandomVariantchance_share : floatResearchresearchables : set(ResearchableTech)Tradetrade_routes : set(TradeRoute)container : ResourceContainerCreatecreatables : set(CreatableGameEntity)RelicBonusresource_spot : ResourceSpotMoveSpeedAttributeSettingsValueattribute : AttributeFeitoriaBonusFoodAmountWoodAmountStoneAmountGoldAmountResourceAmounttype : Resourceamount : intrates : set(ResourceRate)Modifier should only be usedin cases where Patches don'twork. For example, if thebonus is a percentage valueor continuously stacks (likeresources from the Feitoria).Modifier objects can still bepatched.IdlePlayerSetupname : TranslatedStringdescription : TranslatedMarkupFilelong_description : TranslatedMarkupFileleader_names : set(TranslatedString)modifiers : set(Modifier)starting_resources : set(ResourceAmount)game_setup : orderedset(Patch)Despawnactivation_condition : set(LogicElement)despawn_condition : set(LogicElement)despawn_time : floatstate_change : optional(StateChanger) = NoneTauntactivation_message : textdisplay_message : TranslatedStringsound : SoundLiveattributes : set(AttributeSetting)Harvestableresources : ResourceSpotharvest_progress : set(Progress)restock_progress : set(Progress)gatherer_limit : intharvestable_by_default : boolPassiveTransformTocondition : set(LogicElement)transform_time : floattarget_state : StateChangertransform_progress : set(Progress)Cheatactivation_message : textchanges : orderedset(Patch)Flyheight : floatResistanceresistances : set(Resistance)ShootProjectileprojectiles : orderedset(GameEntity)min_projectiles : intmax_projectiles : intmin_range : intmax_range : intreload_time : floatspawn_delay : floatprojectile_delay : floatrequire_turning : boolmanual_aiming_allowed : boolspawning_area_offset_x : floatspawning_area_offset_y : floatspawning_area_offset_z : floatspawning_area_width : floatspawning_area_height : floatspawning_area_randomness : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)RegenerateAttributerate : AttributeRateFormationformations : set(GameEntityFormation)Movespeed : floatmodes : set(MoveMode)CommandSoundsounds : set(Sound)Animatedanimations : set(Animation)Abilityproperties : dict(AbilityProperty, AbilityProperty) = {}Modpriority : intpatches : orderedset(Patch)Patchproperties : dict(PatchProperty, PatchProperty) = {}patch : NyanPatchModifierproperties : dict(ModifierProperty, ModifierProperty) = {}Soundplay_delay : floatsounds : orderedset(file)TerrainAmbientobject : GameEntitymax_density : intTerrainname : TranslatedStringtypes : set(TerrainType)terrain_graphic : Terrainsound : Soundambience : set(TerrainAmbient)ResearchableTechtech : Techcost : Costresearch_time : floatresearch_sounds : set(Sound)condition : set(LogicElement)TranslatedSoundtranslations : set(LanguageSoundPair)TranslatedMarkupFiletranslations : set(LanguageMarkupPair)TranslatedObjectTranslatedStringtranslations : set(LanguageTextPair)Resourcename : TranslatedStringmax_storage : intResourceSpotresource : Resourcemax_amount : intstarting_amount : intdecay_rate : floatDropSiteaccepts_from : set(ResourceContainer)GameEntitytypes : set(GameEntityType)abilities : set(Ability)modifiers : set(Modifier)variants : set(Variant)Object UMLNote 0 - 110 + 100 190 70 @@ -1468,13 +1468,14 @@ bg=pink UMLNote 0 - 190 + 180 190 70 Green elements: -Abilities (handled by engine implementation) +Abilities (handled by engine +implementation) bg=green @@ -1482,13 +1483,14 @@ bg=green UMLNote 0 - 270 + 260 190 70 Yellow elements: -Modifiers (handled by engine implementation) +Modifiers (handled by engine +implementation) bg=yellow @@ -1496,7 +1498,7 @@ bg=yellow UMLNote 0 - 450 + 440 190 70 @@ -5378,13 +5380,15 @@ blacklisted_entities : set(GameEntity) UMLNote 0 - 350 + 340 190 90 Orange elements: -Effects/Resistances that can be applied on other game entities +Effects/Resistances that can +be applied on other game +entities bg=orange @@ -5494,11 +5498,11 @@ blacklisted_entities : set(GameEntity) Text 0 - 10 + 0 230 30 - openage nyan data API v0.4.0 + openage nyan data API v0.4.1 @@ -6775,7 +6779,7 @@ bg=pink 70 lt=<. - 10.0;50.0;10.0;10.0 + 10.0;10.0;10.0;50.0 UMLClass @@ -10182,4 +10186,451 @@ construction_progress : set(Progress) lt=- 40.0;10.0;10.0;10.0 + + UMLClass + + 2200 + 2230 + 110 + 60 + + +*Node* +bg=pink + + + + Relation + + 2250 + 2280 + 30 + 280 + + lt=<<- + 10.0;10.0;10.0;260.0 + + + UMLClass + + 2070 + 2320 + 160 + 80 + + *Start* +bg=pink + +-- +next : Node + + + + Relation + + 2220 + 2350 + 60 + 30 + + lt=- + 10.0;10.0;40.0;10.0 + + + UMLClass + + 1850 + 2320 + 150 + 80 + + *Activity* +bg=pink + +-- +start : Start + + + + Relation + + 1990 + 2350 + 100 + 30 + + lt=<. + 80.0;10.0;10.0;10.0 + + + UMLClass + + 2120 + 2420 + 110 + 60 + + +*End* +bg=pink + + + + Relation + + 2220 + 2440 + 60 + 30 + + lt=- + 10.0;10.0;40.0;10.0 + + + UMLClass + + 2290 + 2320 + 220 + 80 + + *XORGate* +bg=pink + +-- +next : orderedset(Condition) +default : Node + + + + UMLClass + + 2290 + 2410 + 220 + 80 + + *XOREventGate* +bg=pink + +-- +next : dict(Event, Node) + + + + UMLClass + + 2060 + 2500 + 170 + 90 + + *Ability* +bg=pink + +-- +next : Node +ability : abstract(Ability) + + + + Relation + + 2250 + 2350 + 60 + 30 + + lt=- + 40.0;10.0;10.0;10.0 + + + Relation + + 2250 + 2440 + 60 + 30 + + lt=- + 10.0;10.0;40.0;10.0 + + + Relation + + 2220 + 2530 + 60 + 30 + + lt=- + 40.0;10.0;10.0;10.0 + + + UMLClass + + 3710 + 3360 + 180 + 90 + + *Activity* +bg=green + +-- +graph : Activity + + + + Relation + + 3880 + 3390 + 60 + 30 + + lt=- + 10.0;10.0;40.0;10.0 + + + UMLNote + + 1790 + 2270 + 160 + 30 + + Unit behaviour graph +bg=blue + + + + UMLClass + + 2560 + 2420 + 110 + 60 + + +*Event* +bg=pink + + + + Relation + + 2500 + 2440 + 80 + 30 + + lt=<. + 60.0;10.0;10.0;10.0 + + + Relation + + 2580 + 2470 + 30 + 240 + + lt=<<- + 10.0;10.0;10.0;220.0 + + + UMLClass + + 2610 + 2500 + 160 + 80 + + *Wait* +bg=pink + +-- +time : float + + + + UMLClass + + 2610 + 2590 + 120 + 60 + + +*WaitAbility* +bg=pink + + + + Relation + + 2580 + 2530 + 50 + 30 + + lt=- + 10.0;10.0;30.0;10.0 + + + Relation + + 2580 + 2610 + 50 + 30 + + lt=- + 10.0;10.0;30.0;10.0 + + + UMLClass + + 2610 + 2660 + 160 + 60 + + +*CommandInQueue* +bg=pink + + + + Relation + + 2580 + 2680 + 50 + 30 + + lt=- + 10.0;10.0;30.0;10.0 + + + UMLClass + + 2520 + 2070 + 140 + 80 + + *Condition* +bg=pink + +-- +next : Node + + + + Relation + + 2450 + 2100 + 90 + 240 + + lt=<. + 70.0;10.0;10.0;10.0;10.0;220.0 + + + UMLClass + + 2610 + 2170 + 160 + 60 + + +*CommandInQueue* +bg=pink + + + + Relation + + 2580 + 2140 + 30 + 220 + + lt=<<- + 10.0;10.0;10.0;200.0 + + + Relation + + 2580 + 2190 + 50 + 30 + + lt=- + 10.0;10.0;30.0;10.0 + + + UMLClass + + 2610 + 2240 + 160 + 60 + + +*NextCommandIdle* +bg=pink + + + + Relation + + 2580 + 2260 + 50 + 30 + + lt=- + 10.0;10.0;30.0;10.0 + + + Relation + + 1770 + 2350 + 100 + 30 + + lt=- + 10.0;10.0;80.0;10.0 + + + UMLClass + + 2610 + 2310 + 160 + 60 + + +*NextCommandMove* +bg=pink + + + + Relation + + 2580 + 2330 + 50 + 30 + + lt=- + 10.0;10.0;30.0;10.0 + From ebf840c1ed079e26d58d3083082f388e304fe19f Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 22:29:25 +0100 Subject: [PATCH 101/771] doc: nyan data API v0.4.1 reference. --- doc/nyan/api_reference/reference_ability.md | 14 +- doc/nyan/api_reference/reference_util.md | 170 ++++++++++++++++++++ 2 files changed, 183 insertions(+), 1 deletion(-) diff --git a/doc/nyan/api_reference/reference_ability.md b/doc/nyan/api_reference/reference_ability.md index 6349ddc704..098492089e 100644 --- a/doc/nyan/api_reference/reference_ability.md +++ b/doc/nyan/api_reference/reference_ability.md @@ -130,7 +130,7 @@ the ability with this property cannot become active. ## ability.type.ActiveTransformTo ```python -TransformTo(Ability): +ActiveTransformTo(Ability): target_state : StateChanger transform_time : float transform_progress : set(Progress) @@ -147,6 +147,18 @@ The time for the transformation to complete. **transform_progress** A set of `Progress` objects that can activate state changes and animation overrides while the transformation progresses. The objects in the set must have progress type `Restock`. +## ability.type.Activity + +```python +Activity(Ability): + graph: Activity +``` + +Defines the behaviour of a game entity. The behaviour is modelled as a directed node graph. Nodes in the graph correspond to actions that execute for the game entity or conditional queries and event triggers that indicate which path to take next. By traversing the node graph along its paths, the game entities actions are determined. See the [activity control flow](/doc/code/game_simulation/activity.md) documentation for more information. + +**graph** +Node graph that defines the behaviour of the game entity. + ## ability.type.ApplyContinuousEffect ```python diff --git a/doc/nyan/api_reference/reference_util.md b/doc/nyan/api_reference/reference_util.md index 1dd19de1a8..66fced3ff6 100644 --- a/doc/nyan/api_reference/reference_util.md +++ b/doc/nyan/api_reference/reference_util.md @@ -30,6 +30,176 @@ Game entities types for which the accuracy value can be used. **blacklisted_entities** Blacklists game entities that have one of the types listed in `target_types`, but should not be covered by this `Accuracy` object. +## util.activity.Activity + +```python +Activity(Object): + start : Start +``` + +Stores a node graph for the behaviour of a game entity. Activities are assigned to game entities with the `Activity` ability. + +**start** +Starting node of the activity. + +## util.activity.condition.Condition + +```python +Condition(Object): + node : Node +``` + +Generalization object for conditions that can be used in `XORGate` nodes. + +**node** +Node that is visited when the condition is true. + +## util.activity.condition.type.CommandInQueue + +```python +CommandInQueue(Condition): + pass +``` + +Is true when the command queue is not empty when the node is visited. + +## util.activity.condition.type.NextCommandIdle + +```python +NextCommandIdle(Condition): + pass +``` + +Is true when the next command in the queue is of type `Idle`. + +## util.activity.condition.type.NextCommandMove + +```python +NextCommandMove(Condition): + pass +``` + +Is true when the next command in the queue is of type `Move`. + +## util.activity.event.Event + +```python +Event(Object): + pass +``` + +Generalization object for events that can be used in `XOREventGate` nodes. + +## util.activity.event.type.CommandInQueue + +```python +CommandInQueue(Event): + pass +``` + +Fires after a new command has been added to the game entity's command queue. + +## util.activity.event.type.Wait + +```python +Wait(Event): + time : float +``` + +Fires after a certain amount of time has passed. + +**time** +Time in seconds to wait. + +If the value is zero or negative, the event fires immediately. + +## util.activity.event.type.WaitAbility + +```python +WaitAbility(Event): + pass +``` + +Fires at the exact time when a previously visited ability node has finished executing. + +In other words, the event fires when the ability is done with the associated task. For example, in case of the `Move` ability, the event fires when the game entity has reached its destination. + +## util.activity.node.Node + +```python +Node(Object): + pass +``` + +Generalization object for nodes in an activity graph. + +## util.activity.node.type.Ability + +```python +Ability(Node): + next : Node + ability : abstract(Ability) +``` + +Executes an ability of the game entity when the node is visited. + +**next** +Next node in the activity graph. + +**ability** +Ability that is executed. + +This can reference a specific ability of the game entity or an abstract API object from the `engine.ability.type` namespace. If a specific ability is referenced, the ability must be assigned to the game entity and must not be disabled. Otherwise, the ability is not executed. If an API object is referenced, the first active ability with the same type as the API object is executed. + +## util.activity.node.type.End + +```python +End(Node): + pass +``` + +End of an activity. Does nothing. + +## util.activity.node.type.Start + +```python +Start(Node): + next : Node +``` + +Start of an activity. Does nothing but pointing to the next node. + +**next** +Next node in the activity graph. + +## util.activity.node.type.XOREventGate + +```python +XOREventGate(Node): + next : dict(Event, Node) +``` + +Gateway that branches the activity graph when a certain event occurs. Events are registered immediately when the node is visited and cancelled when the node is left. + +**next** +Mapping of events to the next node in the activity graph. The first event that occurs is used to determine the next node. + +## util.activity.node.type.XORGate + +```python +XORGate(Node): + next : orderedset(Condition) + default : Node +``` + +Gateway that branches the activity graph depending on the result of conditional queries. Queries are executed immediately when the node is visited. + +**next** +Mapping of conditional queries to the next node in the activity graph. The first query that evaluates to true is used to determine the next node. If no query evaluates to true, the `default` node is used. + +**default** +Default node that is used if no query evaluates to true. + ## util.animation_override.AnimationOverride ```python From ca6f46df3597a68d25947de2e9f57da58b01c753 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 00:35:31 +0100 Subject: [PATCH 102/771] doc: nyan data API v0.4.1 changelog. --- doc/changelogs/nyan_api/v0.4.1.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 doc/changelogs/nyan_api/v0.4.1.md diff --git a/doc/changelogs/nyan_api/v0.4.1.md b/doc/changelogs/nyan_api/v0.4.1.md new file mode 100644 index 0000000000..829c09d410 --- /dev/null +++ b/doc/changelogs/nyan_api/v0.4.1.md @@ -0,0 +1,31 @@ +# [0.4.1] - 2023-12-02 +All notable changes for version [v0.4.1] are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Added +### Ability module +- Add `Activity(Ability)` object; defines the behaviour of a game entity + +### Utility module +- Add `Activity(Entity)` object; stores behaviour node graph of a game entity +- Add `Node(Entity)` object; node in behaviour node graph +- Add `Ability(Node)` object +- Add `End(Node)` object +- Add `Start(Node)` object +- Add `XORGate(Node)` object +- Add `XOREventGate(Node)` object +- Add `Condition(Object)` object +- Add `CommandInQueue(Condition)` object +- Add `NextCommandIdle(Condition)` object +- Add `NextCommandMove(Condition)` object +- Add `Event(Entity)` object; event for behaviour node graph +- Add `Wait(Event)` object +- Add `WaitAbility(Event)` object +- Add `CommandInQueue(Event)` object + + +## Reference visualization + +TBD From 74fdc931b189b7292402e00c4fa7f2bd34a6841a Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Dec 2023 17:25:19 +0100 Subject: [PATCH 103/771] doc: nyan data API v0.4.0 reference visualization. --- doc/changelogs/nyan_api/v0.4.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changelogs/nyan_api/v0.4.0.md b/doc/changelogs/nyan_api/v0.4.0.md index c1ad5188e9..4cf30034b1 100644 --- a/doc/changelogs/nyan_api/v0.4.0.md +++ b/doc/changelogs/nyan_api/v0.4.0.md @@ -56,4 +56,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Reference visualization -TBD +* [Gamedata](https://github.com/SFTtech/openage/blob/78051b7f894fdf7f7c6d44c05ac7239fe5a896cb/doc/nyan/aoe2_nyan_tree.svg) From 7cd5ccfe9fab1c3bf06a20d7b3a414e12645ffd3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Dec 2023 17:25:27 +0100 Subject: [PATCH 104/771] doc: nyan data API v0.4.1 reference visualization. --- doc/changelogs/nyan_api/v0.4.1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changelogs/nyan_api/v0.4.1.md b/doc/changelogs/nyan_api/v0.4.1.md index 829c09d410..d34ef0a04e 100644 --- a/doc/changelogs/nyan_api/v0.4.1.md +++ b/doc/changelogs/nyan_api/v0.4.1.md @@ -28,4 +28,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Reference visualization -TBD +* [Gamedata](https://github.com/SFTtech/openage/blob/408fc171552bc96a30549d05fceeb9d692fd9d1d/doc/nyan/aoe2_nyan_tree.svg) From 78bebc5b5c964c1e122d2a82657b9cec7c0e99b9 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 4 Dec 2023 23:29:44 +0100 Subject: [PATCH 105/771] convert: Load nyan data API v0.4.1. --- .../convert/service/read/nyan_api_loader.py | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/openage/convert/service/read/nyan_api_loader.py b/openage/convert/service/read/nyan_api_loader.py index 067c4d1065..24bd7c10ef 100644 --- a/openage/convert/service/read/nyan_api_loader.py +++ b/openage/convert/service/read/nyan_api_loader.py @@ -111,6 +111,13 @@ def _create_objects(api_objects: dict[str, NyanObject]) -> None: nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) + # engine.ability.type.Activity + parents = [api_objects["engine.ability.Ability"]] + nyan_object = NyanObject("Activity", parents) + fqon = "engine.ability.type.Activity" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + # engine.ability.type.ApplyContinuousEffect parents = [api_objects["engine.ability.Ability"]] nyan_object = NyanObject("ApplyContinuousEffect", parents) @@ -518,6 +525,111 @@ def _create_objects(api_objects: dict[str, NyanObject]) -> None: nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) + # engine.util.activity.Activity + parents = [api_objects["engine.root.Object"]] + nyan_object = NyanObject("Activity", parents) + fqon = "engine.util.activity.Activity" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.condition.Condition + parents = [api_objects["engine.root.Object"]] + nyan_object = NyanObject("Condition", parents) + fqon = "engine.util.activity.condition.Condition" + 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) + fqon = "engine.util.activity.condition.type.CommandInQueue" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.condition.type.NextCommandIdle + parents = [api_objects["engine.util.activity.condition.Condition"]] + nyan_object = NyanObject("NextCommandIdle", parents) + fqon = "engine.util.activity.condition.type.NextCommandIdle" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.condition.type.NextCommandMove + parents = [api_objects["engine.util.activity.condition.Condition"]] + nyan_object = NyanObject("NextCommandMove", parents) + fqon = "engine.util.activity.condition.type.NextCommandMove" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.event.Event + parents = [api_objects["engine.root.Object"]] + nyan_object = NyanObject("Event", parents) + fqon = "engine.util.activity.event.Event" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.event.type.CommandInQueue + parents = [api_objects["engine.util.activity.event.Event"]] + nyan_object = NyanObject("CommandInQueue", parents) + fqon = "engine.util.activity.event.type.CommandInQueue" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.event.type.Wait + parents = [api_objects["engine.util.activity.event.Event"]] + nyan_object = NyanObject("Wait", parents) + fqon = "engine.util.activity.event.type.Wait" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.event.type.WaitAbility + parents = [api_objects["engine.util.activity.event.Event"]] + nyan_object = NyanObject("WaitAbility", parents) + fqon = "engine.util.activity.event.type.WaitAbility" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.node.Node + parents = [api_objects["engine.root.Object"]] + nyan_object = NyanObject("Node", parents) + fqon = "engine.util.activity.node.Node" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.node.type.Ability + parents = [api_objects["engine.util.activity.node.Node"]] + nyan_object = NyanObject("Ability", parents) + fqon = "engine.util.activity.node.type.Ability" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.node.type.End + parents = [api_objects["engine.util.activity.node.Node"]] + nyan_object = NyanObject("End", parents) + fqon = "engine.util.activity.node.type.End" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.node.type.Start + parents = [api_objects["engine.util.activity.node.Node"]] + nyan_object = NyanObject("Start", parents) + fqon = "engine.util.activity.node.type.Start" + 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) + fqon = "engine.util.activity.node.type.XOREventGate" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.node.type.XORGate + parents = [api_objects["engine.util.activity.node.Node"]] + nyan_object = NyanObject("XORGate", parents) + fqon = "engine.util.activity.node.type.XORGate" + 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) @@ -2489,6 +2601,13 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: member = NyanMember("transform_progress", member_type, None, None, 0) api_object.add_member(member) + # engine.ability.type.Activity + api_object = api_objects["engine.ability.type.Activity"] + + member_type = NyanMemberType(api_objects["engine.util.activity.Activity"]) + member = NyanMember("graph", member_type, None, None, 0) + api_object.add_member(member) + # engine.ability.type.ApplyContinuousEffect api_object = api_objects["engine.ability.type.ApplyContinuousEffect"] @@ -3154,6 +3273,64 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: member = NyanMember("blacklisted_entities", member_type, None, None, 0) api_object.add_member(member) + # engine.util.activity.Activity + api_object = api_objects["engine.util.activity.Activity"] + + member_type = NyanMemberType(api_objects["engine.util.activity.node.type.Start"]) + member = NyanMember("start", member_type, None, None, 0) + api_object.add_member(member) + + # engine.util.activity.condition.Condition + api_object = api_objects["engine.util.activity.condition.Condition"] + + member_type = NyanMemberType(api_objects["engine.util.activity.node.Node"]) + member = NyanMember("next", 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"] + + member = NyanMember("time", N_FLOAT, None, None, 0) + api_object.add_member(member) + + # engine.util.activity.node.type.Ability + api_object = api_objects["engine.util.activity.node.type.Ability"] + + 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.ability.Ability"]) + member_type = NyanMemberType(MemberType.ABSTRACT, (subtype,)) + member = NyanMember("ability", member_type, None, None, 0) + api_object.add_member(member) + + # engine.util.activity.node.type.Start + api_object = api_objects["engine.util.activity.node.type.Start"] + + member_type = NyanMemberType(api_objects["engine.util.activity.node.Node"]) + member = NyanMember("next", 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"] + + key_type = NyanMemberType(api_objects["engine.util.activity.event.Event"]) + 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.activity.node.type.XORGate + api_object = api_objects["engine.util.activity.node.type.XORGate"] + + elem_type = NyanMemberType(api_objects["engine.util.activity.condition.Condition"]) + member_type = NyanMemberType(MemberType.ORDEREDSET, (elem_type,)) + member = NyanMember("next", 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.animation_override.AnimationOverride api_object = api_objects["engine.util.animation_override.AnimationOverride"] From b7e693aefef95c052a3ccb1571ac56f374863494 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 7 Dec 2023 23:02:44 +0100 Subject: [PATCH 106/771] convert: Basic activity for units. --- .../conversion/aoc/pregen_processor.py | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) diff --git a/openage/convert/processor/conversion/aoc/pregen_processor.py b/openage/convert/processor/conversion/aoc/pregen_processor.py index fe6151ea20..ba41e11d86 100644 --- a/openage/convert/processor/conversion/aoc/pregen_processor.py +++ b/openage/convert/processor/conversion/aoc/pregen_processor.py @@ -35,6 +35,7 @@ def generate(cls, full_data_set: GenieObjectContainer) -> None: # Stores pregenerated raw API objects as a container pregen_converter_group = ConverterObjectGroup("pregen") + cls.generate_activities(full_data_set, pregen_converter_group) cls.generate_attributes(full_data_set, pregen_converter_group) cls.generate_diplomatic_stances(full_data_set, pregen_converter_group) cls.generate_team_property(full_data_set, pregen_converter_group) @@ -62,6 +63,297 @@ def generate(cls, full_data_set: GenieObjectContainer) -> None: raise RuntimeError(f"{repr(pregen_object)}: Pregenerated object is not ready " "for export. Member or object not initialized.") + @staticmethod + def generate_activities( + full_data_set: GenieObjectContainer, + pregen_converter_group: ConverterObjectGroup + ) -> None: + """ + Generate the activities for game entity behaviour. + + :param full_data_set: GenieObjectContainer instance that + contains all relevant data for the conversion + process. + :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer + :param pregen_converter_group: GenieObjectGroup instance that stores + pregenerated API objects for referencing with + ForwardRef + :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup + """ + pregen_nyan_objects = full_data_set.pregen_nyan_objects + api_objects = full_data_set.nyan_api_objects + + activity_parent = "engine.util.activity.Activity" + activity_location = "data/util/activity/" + + # Node types + start_parent = "engine.util.activity.node.type.Start" + end_parent = "engine.util.activity.node.type.End" + ability_parent = "engine.util.activity.node.type.Ability" + xor_parent = "engine.util.activity.node.type.XORGate" + xor_event_parent = "engine.util.activity.node.type.XOREventGate" + + # 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" + + # ======================================================================= + # Default (Start -> Ability(Idle) -> End) + # ======================================================================= + default_ref_in_modpack = "util.activity.types.Default" + default_raw_api_object = RawAPIObject(default_ref_in_modpack, + "Default", api_objects, + activity_location) + default_raw_api_object.set_filename("types") + default_raw_api_object.add_raw_parent(activity_parent) + + start_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Default.Start") + default_raw_api_object.add_raw_member("start", start_forward_ref, + activity_parent) + + pregen_converter_group.add_raw_api_object(default_raw_api_object) + pregen_nyan_objects.update({default_ref_in_modpack: default_raw_api_object}) + + unit_forward_ref = ForwardRef(pregen_converter_group, default_ref_in_modpack) + + # Start + start_ref_in_modpack = "util.activity.types.Default.Start" + start_raw_api_object = RawAPIObject(start_ref_in_modpack, + "Start", api_objects) + start_raw_api_object.set_location(unit_forward_ref) + start_raw_api_object.add_raw_parent(start_parent) + + idle_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Default.Idle") + start_raw_api_object.add_raw_member("next", idle_forward_ref, + start_parent) + + pregen_converter_group.add_raw_api_object(start_raw_api_object) + pregen_nyan_objects.update({start_ref_in_modpack: start_raw_api_object}) + + # Idle + idle_ref_in_modpack = "util.activity.types.Default.Idle" + idle_raw_api_object = RawAPIObject(idle_ref_in_modpack, + "Idle", api_objects) + idle_raw_api_object.set_location(unit_forward_ref) + idle_raw_api_object.add_raw_parent(ability_parent) + + end_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Default.End") + idle_raw_api_object.add_raw_member("next", end_forward_ref, + ability_parent) + idle_raw_api_object.add_raw_member("ability", + api_objects["engine.ability.type.Idle"], + ability_parent) + + pregen_converter_group.add_raw_api_object(idle_raw_api_object) + pregen_nyan_objects.update({idle_ref_in_modpack: idle_raw_api_object}) + + # End + end_ref_in_modpack = "util.activity.types.Default.End" + end_raw_api_object = RawAPIObject(end_ref_in_modpack, + "End", api_objects) + end_raw_api_object.set_location(unit_forward_ref) + end_raw_api_object.add_raw_parent(end_parent) + + pregen_converter_group.add_raw_api_object(end_raw_api_object) + pregen_nyan_objects.update({end_ref_in_modpack: end_raw_api_object}) + + # ======================================================================= + # Units + # ======================================================================= + unit_ref_in_modpack = "util.activity.types.Unit" + unit_raw_api_object = RawAPIObject(unit_ref_in_modpack, + "Unit", api_objects, + activity_location) + unit_raw_api_object.set_filename("types") + unit_raw_api_object.add_raw_parent(activity_parent) + + start_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.Start") + unit_raw_api_object.add_raw_member("start", start_forward_ref, + activity_parent) + + pregen_converter_group.add_raw_api_object(unit_raw_api_object) + pregen_nyan_objects.update({unit_ref_in_modpack: unit_raw_api_object}) + + unit_forward_ref = ForwardRef(pregen_converter_group, unit_ref_in_modpack) + + # Start + start_ref_in_modpack = "util.activity.types.Unit.Start" + start_raw_api_object = RawAPIObject(start_ref_in_modpack, + "Start", api_objects) + start_raw_api_object.set_location(unit_forward_ref) + start_raw_api_object.add_raw_parent(start_parent) + + idle_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.Idle") + start_raw_api_object.add_raw_member("next", idle_forward_ref, + start_parent) + + pregen_converter_group.add_raw_api_object(start_raw_api_object) + pregen_nyan_objects.update({start_ref_in_modpack: start_raw_api_object}) + + # Idle + idle_ref_in_modpack = "util.activity.types.Unit.Idle" + idle_raw_api_object = RawAPIObject(idle_ref_in_modpack, + "Idle", api_objects) + idle_raw_api_object.set_location(unit_forward_ref) + idle_raw_api_object.add_raw_parent(ability_parent) + + queue_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.CheckQueue") + idle_raw_api_object.add_raw_member("next", queue_forward_ref, + ability_parent) + idle_raw_api_object.add_raw_member("ability", + api_objects["engine.ability.type.Idle"], + ability_parent) + + pregen_converter_group.add_raw_api_object(idle_raw_api_object) + pregen_nyan_objects.update({idle_ref_in_modpack: idle_raw_api_object}) + + # Check if command is in queue + queue_ref_in_modpack = "util.activity.types.Unit.CheckQueue" + queue_raw_api_object = RawAPIObject(queue_ref_in_modpack, + "CheckQueue", api_objects) + queue_raw_api_object.set_location(unit_forward_ref) + queue_raw_api_object.add_raw_parent(xor_parent) + + condition_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.CommandInQueue") + queue_raw_api_object.add_raw_member("next", + [condition_forward_ref], + xor_parent) + command_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.WaitForCommand") + queue_raw_api_object.add_raw_member("default", + command_forward_ref, + xor_parent) + + pregen_converter_group.add_raw_api_object(queue_raw_api_object) + pregen_nyan_objects.update({queue_ref_in_modpack: queue_raw_api_object}) + + # condition for command in queue + condition_ref_in_modpack = "util.activity.types.Unit.CommandInQueue" + 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) + + branch_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.BranchCommand") + condition_raw_api_object.add_raw_member("next", + branch_forward_ref, + condition_parent) + + pregen_converter_group.add_raw_api_object(condition_raw_api_object) + pregen_nyan_objects.update({condition_ref_in_modpack: condition_raw_api_object}) + + # Wait for Command + command_ref_in_modpack = "util.activity.types.Unit.WaitForCommand" + command_raw_api_object = RawAPIObject(command_ref_in_modpack, + "WaitForCommand", api_objects) + command_raw_api_object.set_location(unit_forward_ref) + command_raw_api_object.add_raw_parent(xor_event_parent) + + event_api_object = api_objects["engine.util.activity.event.type.CommandInQueue"] + branch_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.BranchCommand") + command_raw_api_object.add_raw_member("next", + {event_api_object: branch_forward_ref}, + xor_event_parent) + + pregen_converter_group.add_raw_api_object(command_raw_api_object) + pregen_nyan_objects.update({command_ref_in_modpack: command_raw_api_object}) + + # Branch on command type + branch_ref_in_modpack = "util.activity.types.Unit.BranchCommand" + 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) + + 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) + 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) + + 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_raw_api_object = RawAPIObject(condition_ref_in_modpack, + "NextCommandMove", api_objects) + condition_raw_api_object.set_location(branch_forward_ref) + condition_raw_api_object.add_raw_parent(condition_next_move_parent) + + move_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.Move") + condition_raw_api_object.add_raw_member("next", + move_forward_ref, + condition_parent) + + pregen_converter_group.add_raw_api_object(condition_raw_api_object) + pregen_nyan_objects.update({condition_ref_in_modpack: condition_raw_api_object}) + + # Move + move_ref_in_modpack = "util.activity.types.Unit.Move" + move_raw_api_object = RawAPIObject(move_ref_in_modpack, + "Move", api_objects) + move_raw_api_object.set_location(unit_forward_ref) + move_raw_api_object.add_raw_parent(ability_parent) + + wait_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.Wait") + move_raw_api_object.add_raw_member("next", wait_forward_ref, + ability_parent) + move_raw_api_object.add_raw_member("ability", + api_objects["engine.ability.type.Move"], + ability_parent) + + 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_raw_api_object = RawAPIObject(wait_ref_in_modpack, + "Wait", 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"] + wait_raw_api_object.add_raw_member("next", + { + wait_finish: queue_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) + + pregen_converter_group.add_raw_api_object(wait_raw_api_object) + pregen_nyan_objects.update({wait_ref_in_modpack: wait_raw_api_object}) + + # End + end_ref_in_modpack = "util.activity.types.Unit.End" + end_raw_api_object = RawAPIObject(end_ref_in_modpack, + "End", api_objects) + end_raw_api_object.set_location(unit_forward_ref) + end_raw_api_object.add_raw_parent(end_parent) + + pregen_converter_group.add_raw_api_object(end_raw_api_object) + pregen_nyan_objects.update({end_ref_in_modpack: end_raw_api_object}) + @staticmethod def generate_attributes( full_data_set: GenieObjectContainer, From b62266d91aebc54fa4d4f65adc17e73aa512ac9b Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 10 Dec 2023 23:59:57 +0100 Subject: [PATCH 107/771] convert: Export new API version as modpack. --- openage/convert/service/init/api_export_required.py | 2 +- openage/convert/tool/api_export.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openage/convert/service/init/api_export_required.py b/openage/convert/service/init/api_export_required.py index 61b8f5ba4d..105527e176 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.4.0" +CURRENT_API_VERSION = "0.4.1" def api_export_required(asset_dir: UnionPath) -> bool: diff --git a/openage/convert/tool/api_export.py b/openage/convert/tool/api_export.py index e0d14e703e..0e35ec4602 100644 --- a/openage/convert/tool/api_export.py +++ b/openage/convert/tool/api_export.py @@ -76,7 +76,7 @@ def create_modpack() -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("engine", "0.4.0", repo="openage") + mod_def.set_info("engine", "0.4.1", "0.4.1", repo="openage") mod_def.add_include("**") From 33a53f3fcb87e19d4d511bbc7edda7b8b73be37d Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 11 Dec 2023 00:30:03 +0100 Subject: [PATCH 108/771] convert: Activity ability. --- .../conversion/aoc/ability_subprocessor.py | 41 ++++++++++++++++++- .../conversion/aoc/modpack_subprocessor.py | 2 +- .../conversion/aoc/nyan_subprocessor.py | 1 + .../aoc_demo/modpack_subprocessor.py | 2 +- .../conversion/de1/modpack_subprocessor.py | 2 +- .../conversion/de2/modpack_subprocessor.py | 2 +- .../conversion/de2/nyan_subprocessor.py | 1 + .../conversion/hd/modpack_subprocessor.py | 2 +- .../conversion/ror/modpack_subprocessor.py | 2 +- .../conversion/ror/nyan_subprocessor.py | 3 +- .../conversion/ror/pregen_subprocessor.py | 1 + .../conversion/swgbcc/modpack_subprocessor.py | 2 +- .../conversion/swgbcc/nyan_subprocessor.py | 1 + .../conversion/swgbcc/pregen_subprocessor.py | 1 + 14 files changed, 54 insertions(+), 9 deletions(-) diff --git a/openage/convert/processor/conversion/aoc/ability_subprocessor.py b/openage/convert/processor/conversion/aoc/ability_subprocessor.py index 8e53371b2f..ffa0a98e57 100644 --- a/openage/convert/processor/conversion/aoc/ability_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/ability_subprocessor.py @@ -302,6 +302,45 @@ 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, @@ -614,7 +653,7 @@ def apply_discrete_effect_ability( return ability_forward_ref @staticmethod - def attribute_change_tracker_ability(line) -> ForwardRef: + def attribute_change_tracker_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the AttributeChangeTracker ability to a line. diff --git a/openage/convert/processor/conversion/aoc/modpack_subprocessor.py b/openage/convert/processor/conversion/aoc/modpack_subprocessor.py index 8d54d68fed..29764c5823 100644 --- a/openage/convert/processor/conversion/aoc/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/modpack_subprocessor.py @@ -44,7 +44,7 @@ def _get_aoe2_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("aoe2_base", "0.5", versionstr="1.0c", repo="openage") + mod_def.set_info("aoe2_base", "0.5.1", versionstr="1.0c", repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/aoc/nyan_subprocessor.py b/openage/convert/processor/conversion/aoc/nyan_subprocessor.py index b519412ca3..d280b5d9c1 100644 --- a/openage/convert/processor/conversion/aoc/nyan_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/nyan_subprocessor.py @@ -219,6 +219,7 @@ def unit_line_to_game_entity(unit_line: GenieUnitLineGroup) -> None: # ======================================================================= abilities_set = [] + abilities_set.append(AoCAbilitySubprocessor.activity_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.death_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.delete_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.despawn_ability(unit_line)) diff --git a/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py b/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py index ffd171214e..8ae5c83705 100644 --- a/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py @@ -40,7 +40,7 @@ def _get_demo_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("trial_base", "0.5", versionstr="Trial", repo="openage") + mod_def.set_info("trial_base", "0.5.1", versionstr="Trial", repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/de1/modpack_subprocessor.py b/openage/convert/processor/conversion/de1/modpack_subprocessor.py index 9cc3f5a95d..1a431e81d2 100644 --- a/openage/convert/processor/conversion/de1/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/de1/modpack_subprocessor.py @@ -40,7 +40,7 @@ def _get_aoe1_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("de1_base", "0.5", versionstr="1.0a", repo="openage") + mod_def.set_info("de1_base", "0.5.1", versionstr="1.0a", repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/de2/modpack_subprocessor.py b/openage/convert/processor/conversion/de2/modpack_subprocessor.py index 22d9d220e2..810ff96752 100644 --- a/openage/convert/processor/conversion/de2/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/de2/modpack_subprocessor.py @@ -40,7 +40,7 @@ def _get_aoe2_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("de2_base", "0.5", versionstr="1.0c", repo="openage") + mod_def.set_info("de2_base", "0.5.1", versionstr="1.0c", repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/de2/nyan_subprocessor.py b/openage/convert/processor/conversion/de2/nyan_subprocessor.py index b8f40000ef..242f67e7cd 100644 --- a/openage/convert/processor/conversion/de2/nyan_subprocessor.py +++ b/openage/convert/processor/conversion/de2/nyan_subprocessor.py @@ -219,6 +219,7 @@ def unit_line_to_game_entity(unit_line: GenieUnitLineGroup) -> None: # ======================================================================= abilities_set = [] + abilities_set.append(AoCAbilitySubprocessor.activity_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.death_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.delete_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.despawn_ability(unit_line)) diff --git a/openage/convert/processor/conversion/hd/modpack_subprocessor.py b/openage/convert/processor/conversion/hd/modpack_subprocessor.py index 50e77ba711..8be52ac744 100644 --- a/openage/convert/processor/conversion/hd/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/hd/modpack_subprocessor.py @@ -40,7 +40,7 @@ def _get_aoe2_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("hd_base", "0.5", versionstr="5.8", repo="openage") + mod_def.set_info("hd_base", "0.5.1", versionstr="5.8", repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/ror/modpack_subprocessor.py b/openage/convert/processor/conversion/ror/modpack_subprocessor.py index dfe485b003..ff7746e861 100644 --- a/openage/convert/processor/conversion/ror/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/ror/modpack_subprocessor.py @@ -41,7 +41,7 @@ def _get_aoe1_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("aoe1_base", "0.5", versionstr="1.0a", repo="openage") + mod_def.set_info("aoe1_base", "0.5.1", versionstr="1.0a", repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/ror/nyan_subprocessor.py b/openage/convert/processor/conversion/ror/nyan_subprocessor.py index 75262402ae..a06a86e1dc 100644 --- a/openage/convert/processor/conversion/ror/nyan_subprocessor.py +++ b/openage/convert/processor/conversion/ror/nyan_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 the openage authors. See copying.md for legal info. +# Copyright 2020-2023 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-lines,too-many-locals,too-many-statements,too-many-branches # @@ -209,6 +209,7 @@ def unit_line_to_game_entity(unit_line): # ======================================================================= abilities_set = [] + abilities_set.append(AoCAbilitySubprocessor.activity_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.death_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.delete_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.despawn_ability(unit_line)) diff --git a/openage/convert/processor/conversion/ror/pregen_subprocessor.py b/openage/convert/processor/conversion/ror/pregen_subprocessor.py index 4bb4eb0ad1..271110bedc 100644 --- a/openage/convert/processor/conversion/ror/pregen_subprocessor.py +++ b/openage/convert/processor/conversion/ror/pregen_subprocessor.py @@ -32,6 +32,7 @@ def generate(cls, full_data_set: GenieObjectContainer) -> None: # Stores pregenerated raw API objects as a container pregen_converter_group = ConverterObjectGroup("pregen") + AoCPregenSubprocessor.generate_activities(full_data_set, pregen_converter_group) AoCPregenSubprocessor.generate_attributes(full_data_set, pregen_converter_group) AoCPregenSubprocessor.generate_diplomatic_stances(full_data_set, pregen_converter_group) AoCPregenSubprocessor.generate_entity_types(full_data_set, pregen_converter_group) diff --git a/openage/convert/processor/conversion/swgbcc/modpack_subprocessor.py b/openage/convert/processor/conversion/swgbcc/modpack_subprocessor.py index cef278f9eb..5a69a47516 100644 --- a/openage/convert/processor/conversion/swgbcc/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/swgbcc/modpack_subprocessor.py @@ -40,7 +40,7 @@ def _get_swgb_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("swgb_base", "0.5", versionstr="1.1-gog4", repo="openage") + mod_def.set_info("swgb_base", "0.5.1", versionstr="1.1-gog4", repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/swgbcc/nyan_subprocessor.py b/openage/convert/processor/conversion/swgbcc/nyan_subprocessor.py index b48b15d309..9fd2c52242 100644 --- a/openage/convert/processor/conversion/swgbcc/nyan_subprocessor.py +++ b/openage/convert/processor/conversion/swgbcc/nyan_subprocessor.py @@ -217,6 +217,7 @@ def unit_line_to_game_entity(unit_line: GenieUnitLineGroup) -> None: # ======================================================================= abilities_set = [] + abilities_set.append(AoCAbilitySubprocessor.activity_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.death_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.delete_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.despawn_ability(unit_line)) diff --git a/openage/convert/processor/conversion/swgbcc/pregen_subprocessor.py b/openage/convert/processor/conversion/swgbcc/pregen_subprocessor.py index be0d20e705..a9114648c7 100644 --- a/openage/convert/processor/conversion/swgbcc/pregen_subprocessor.py +++ b/openage/convert/processor/conversion/swgbcc/pregen_subprocessor.py @@ -37,6 +37,7 @@ def generate(cls, full_data_set: GenieObjectContainer) -> None: # Stores pregenerated raw API objects as a container pregen_converter_group = ConverterObjectGroup("pregen") + AoCPregenSubprocessor.generate_activities(full_data_set, pregen_converter_group) AoCPregenSubprocessor.generate_attributes(full_data_set, pregen_converter_group) AoCPregenSubprocessor.generate_diplomatic_stances(full_data_set, pregen_converter_group) AoCPregenSubprocessor.generate_team_property(full_data_set, pregen_converter_group) From 884a15f6c6537fa57f2cd9e9514e986afb0f2719 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 17 Dec 2023 09:49:15 +0100 Subject: [PATCH 109/771] gamestate: Docstrings for activity. --- libopenage/gamestate/activity/activity.h | 35 +++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/libopenage/gamestate/activity/activity.h b/libopenage/gamestate/activity/activity.h index f5be9d254a..5bc93b565c 100644 --- a/libopenage/gamestate/activity/activity.h +++ b/libopenage/gamestate/activity/activity.h @@ -18,19 +18,52 @@ using activity_label = std::string; */ class Activity { public: + /** + * Create a new activity. + * + * @param id Unique ID. + * @param label Human-readable label (optional). + * @param start Start node in the graph. + */ Activity(activity_id id, activity_label label = "", - const std::shared_ptr &start = {}); + const std::shared_ptr &start = nullptr); + /** + * Get the unique ID of this activity. + * + * @return Unique ID. + */ activity_id get_id() const; + /** + * Get the human-readable label of this activity. + * + * @return Human-readable label. + */ const activity_label get_label() const; + /** + * Get the start node of this activity. + * + * @return Start node. + */ const std::shared_ptr &get_start() const; private: + /** + * Unique ID. + */ const activity_id id; + + /** + * Human-readable label. + */ const activity_label label; + + /** + * Start node. + */ std::shared_ptr start; }; From 33ced296e10957384e1b720b011e38c74d0ee273 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 17 Dec 2023 09:49:38 +0100 Subject: [PATCH 110/771] gamestate: API interface for nyan activities. --- libopenage/gamestate/api/CMakeLists.txt | 1 + libopenage/gamestate/api/activity.cpp | 65 +++++++++++++++++++++ libopenage/gamestate/api/activity.h | 76 +++++++++++++++++++++++++ libopenage/gamestate/api/definitions.h | 28 ++++++++- 4 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 libopenage/gamestate/api/activity.cpp create mode 100644 libopenage/gamestate/api/activity.h diff --git a/libopenage/gamestate/api/CMakeLists.txt b/libopenage/gamestate/api/CMakeLists.txt index f079f11be3..f3ad8d7b40 100644 --- a/libopenage/gamestate/api/CMakeLists.txt +++ b/libopenage/gamestate/api/CMakeLists.txt @@ -1,5 +1,6 @@ add_sources(libopenage ability.cpp + activity.cpp animation.cpp definitions.cpp patch.cpp diff --git a/libopenage/gamestate/api/activity.cpp b/libopenage/gamestate/api/activity.cpp new file mode 100644 index 0000000000..19e67e65d6 --- /dev/null +++ b/libopenage/gamestate/api/activity.cpp @@ -0,0 +1,65 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "activity.h" + +#include "gamestate/api/definitions.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"; +} + +nyan::Object APIActivity::get_start(const nyan::Object &activity) { + auto obj_value = activity.get("Activity.start"); + + std::shared_ptr db_view = activity.get_view(); + return db_view->get_object(obj_value->get_name()); +} + + +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"; +} + +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); +} + +std::vector APIActivityNode::get_next(const nyan::Object &node) { + switch (APIActivityNode::get_type(node)) { + // 0 next nodes + case activity::node_t::END: { + return {}; + } + // 1 next node + case activity::node_t::TASK_SYSTEM: + case activity::node_t::START: { + auto next = node.get("Node.next"); + std::shared_ptr db_view = node.get_view(); + return {db_view->get_object(next->get_name())}; + } + // 1+ next nodes + case activity::node_t::XOR_GATE: + case activity::node_t::XOR_EVENT_GATE: { + auto next = node.get("Node.next"); + std::shared_ptr db_view = node.get_view(); + + std::vector next_nodes; + for (auto &next_node : next->get()) { + auto next_node_value = std::dynamic_pointer_cast(next_node.second.get_ptr()); + next_nodes.push_back(db_view->get_object(next_node_value->get_name())); + } + + return next_nodes; + } + default: + throw Error(MSG(err) << "Unknown activity node type."); + } +} + +} // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/activity.h b/libopenage/gamestate/api/activity.h new file mode 100644 index 0000000000..28e7c88dbe --- /dev/null +++ b/libopenage/gamestate/api/activity.h @@ -0,0 +1,76 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include + +#include "gamestate/activity/types.h" + + +namespace openage::gamestate { + +namespace api { + +/** + * Helper class for creating Activity objects from the nyan API. + */ +class APIActivity { +public: + /** + * Check if a nyan object is an Activity (type == \p engine.util.activity.Activity). + * + * @param obj nyan object. + * + * @return true if the object is an activity, else false. + */ + static bool is_activity(const nyan::Object &obj); + + /** + * Get the start node of an activity. + * + * @param activity nyan object. + * + * @return nyan object handle of the start node. + */ + static nyan::Object get_start(const nyan::Object &activity); +}; + + +class APIActivityNode { +public: + /** + * Check if a nyan object is a node (type == \p engine.util.activity.node.Node). + * + * @param obj nyan object. + * + * @return true if the object is a node, else false. + */ + static bool is_node(const nyan::Object &obj); + + /** + * Get the type of a node. + * + * @param node nyan object. + * + * @return Type of the node. + */ + static activity::node_t get_type(const nyan::Object &node); + + /** + * Get the next nodes of a node. + * + * The number of next nodes depends on the type of the node and can range + * from 0 (end nodes) to n (gateways). + * + * @param node nyan object. + * + * @return nyan object handles of the next nodes. + */ + static std::vector get_next(const nyan::Object &node); +}; + + +} // namespace api +} // namespace openage::gamestate diff --git a/libopenage/gamestate/api/definitions.h b/libopenage/gamestate/api/definitions.h index d06553c89e..0a2c452a0a 100644 --- a/libopenage/gamestate/api/definitions.h +++ b/libopenage/gamestate/api/definitions.h @@ -8,12 +8,15 @@ #include #include "datastructure/constexpr_map.h" +#include "gamestate/activity/types.h" #include "gamestate/api/types.h" namespace openage::gamestate::api { -/** Maps internal ability types to nyan API values **/ +/** + * Maps internal ability types to nyan API values. + **/ static const auto ABILITY_DEFS = datastructure::create_const_map( std::pair(ability_t::IDLE, nyan::ValueHolder(std::make_shared("engine.ability.type.Idle"))), @@ -24,8 +27,9 @@ static const auto ABILITY_DEFS = datastructure::create_const_map("engine.ability.type.Turn")))); - -/** Maps internal property types to nyan API values **/ +/** + * Maps internal property types to nyan API values. + **/ static const auto ABILITY_PROPERTY_DEFS = datastructure::create_const_map( std::pair(ability_property_t::ANIMATED, nyan::ValueHolder(std::make_shared("engine.ability.property.type.Animated"))), @@ -40,6 +44,24 @@ static const auto ABILITY_PROPERTY_DEFS = datastructure::create_const_map("engine.ability.property.type.Lock")))); +/** + * Maps API activity node types to engine node types. + */ +static const auto ACTIVITY_NODE_DEFS = 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.XORGate", + activity::node_t::XOR_GATE), + std::pair("engine.util.activity.node.type.XOREventGate", + activity::node_t::XOR_EVENT_GATE)); + +/** + * Maps internal patch property types to nyan API values. + **/ static const auto PATCH_PROPERTY_DEFS = datastructure::create_const_map( std::pair(patch_property_t::DIPLOMATIC, nyan::ValueHolder(std::make_shared("engine.patch.property.type.Diplomatic")))); From 943e67a6a2fd7e044d9ca075096d0e4b255ae743 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 17 Dec 2023 16:53:26 +0100 Subject: [PATCH 111/771] gamestate: Create activity from nyan data. --- libopenage/gamestate/activity/activity.cpp | 4 +- libopenage/gamestate/activity/activity.h | 6 +- libopenage/gamestate/entity_factory.cpp | 90 +++++++++++++++++++++- libopenage/gamestate/entity_factory.h | 5 ++ 4 files changed, 96 insertions(+), 9 deletions(-) diff --git a/libopenage/gamestate/activity/activity.cpp b/libopenage/gamestate/activity/activity.cpp index 7ee478eda8..9af770b8fd 100644 --- a/libopenage/gamestate/activity/activity.cpp +++ b/libopenage/gamestate/activity/activity.cpp @@ -6,8 +6,8 @@ namespace openage::gamestate::activity { Activity::Activity(activity_id id, - activity_label label, - const std::shared_ptr &start) : + const std::shared_ptr &start, + activity_label label) : id{id}, label{label}, start{start} { diff --git a/libopenage/gamestate/activity/activity.h b/libopenage/gamestate/activity/activity.h index 5bc93b565c..d0832068ad 100644 --- a/libopenage/gamestate/activity/activity.h +++ b/libopenage/gamestate/activity/activity.h @@ -22,12 +22,12 @@ class Activity { * Create a new activity. * * @param id Unique ID. - * @param label Human-readable label (optional). * @param start Start node in the graph. + * @param label Human-readable label (optional). */ Activity(activity_id id, - activity_label label = "", - const std::shared_ptr &start = nullptr); + const std::shared_ptr &start, + activity_label label = ""); /** * Get the unique ID of this activity. diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index f6c3d6f660..d8ff2361b8 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -19,6 +19,7 @@ #include "gamestate/activity/start_node.h" #include "gamestate/activity/task_system_node.h" #include "gamestate/activity/xor_node.h" +#include "gamestate/api/activity.h" #include "gamestate/component/api/idle.h" #include "gamestate/component/api/live.h" #include "gamestate/component/api/move.h" @@ -149,7 +150,7 @@ std::shared_ptr create_test_activity() { return 1; // idle->get_id(); }); - return std::make_shared(0, "test", start); + return std::make_shared(0, start, "test"); } EntityFactory::EntityFactory() : @@ -210,6 +211,7 @@ void EntityFactory::init_components(const std::shared_ptrget_object(nyan_entity); nyan::set_t abilities = nyan_obj.get_set("GameEntity.abilities"); + std::optional activity_ability; for (const auto &ability_val : abilities) { auto ability_fqon = std::dynamic_pointer_cast(ability_val.get_ptr())->get_name(); auto ability_obj = owner_db_view->get_object(ability_fqon); @@ -247,11 +249,91 @@ void EntityFactory::init_components(const std::shared_ptr(loop, create_test_activity()); + entity->add_component(activity); + } +} + +void EntityFactory::init_activity(const std::shared_ptr &loop, + const std::shared_ptr &owner_db_view, + const std::shared_ptr &entity, + const nyan::Object &ability) { + nyan::Object graph = ability.get_object("Activity.graph"); + auto start_obj = api::APIActivity::get_start(graph); + + size_t node_id = 0; + + std::deque nyan_nodes; + std::unordered_map> node_id_map{}; + std::unordered_map visited{}; + std::shared_ptr start_node; + + // First pass: create all nodes using breadth-first search + nyan_nodes.push_back(start_obj); + while (!nyan_nodes.empty()) { + auto node = nyan_nodes.front(); + nyan_nodes.pop_front(); + + if (visited.contains(node.get_name())) { + continue; + } + + // Create the node + switch (api::APIActivityNode::get_type(node)) { + case activity::node_t::END: + break; + case activity::node_t::START: + start_node = std::make_shared(node_id); + node_id_map[node_id] = start_node; + break; + case activity::node_t::TASK_SYSTEM: + node_id_map[node_id] = std::make_shared(node_id); + break; + case activity::node_t::XOR_GATE: + node_id_map[node_id] = std::make_shared(node_id); + break; + case activity::node_t::XOR_EVENT_GATE: + node_id_map[node_id] = std::make_shared(node_id); + break; + default: + throw Error{ERR << "Unknown activity node type"}; + } + + // Get the node's outputs + auto next_nodes = api::APIActivityNode::get_next(node); + nyan_nodes.insert(nyan_nodes.end(), next_nodes.begin(), next_nodes.end()); + + visited.insert({node.get_name(), node_id}); + node_id++; } - // must be initialized after all other components - auto activity = std::make_shared(loop, create_test_activity()); - entity->add_component(activity); + // Second pass: connect the nodes + for (const auto &node : visited) { + auto nyan_node = owner_db_view->get_object(node.first); + auto activity_node = node_id_map[node.second]; + + auto next_nodes = api::APIActivityNode::get_next(nyan_node); + for (const auto &next_node : next_nodes) { + auto next_node_id = visited[next_node.get_name()]; + auto next_engine_node = node_id_map[next_node_id]; + + activity_node->add_output(next_engine_node); + } + } + + auto activity = std::make_shared(0, start_node, graph.get_name()); + + auto component = std::make_shared(loop, activity); + entity->add_component(component); } entity_id_t EntityFactory::get_next_entity_id() { diff --git a/libopenage/gamestate/entity_factory.h b/libopenage/gamestate/entity_factory.h index 5a369d995e..728a91b820 100644 --- a/libopenage/gamestate/entity_factory.h +++ b/libopenage/gamestate/entity_factory.h @@ -92,6 +92,11 @@ class EntityFactory { const std::shared_ptr &entity, const nyan::fqon_t &nyan_entity); + void init_activity(const std::shared_ptr &loop, + const std::shared_ptr &owner_db_view, + const std::shared_ptr &entity, + const nyan::Object &ability); + /** * Get a unique ID for creating a game entity. * From eba3c96ac9a2fd5f7850dc9359e99b307305f56e Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 18 Dec 2023 01:23:35 +0100 Subject: [PATCH 112/771] gamestate: Rename activity node files. --- libopenage/gamestate/activity/CMakeLists.txt | 4 ++-- libopenage/gamestate/activity/tests.cpp | 4 ++-- .../activity/{event_node.cpp => xor_event_gate.cpp} | 10 +++++----- .../activity/{event_node.h => xor_event_gate.h} | 0 .../gamestate/activity/{xor_node.cpp => xor_gate.cpp} | 8 ++++---- .../gamestate/activity/{xor_node.h => xor_gate.h} | 0 libopenage/gamestate/entity_factory.cpp | 4 ++-- libopenage/gamestate/system/activity.cpp | 4 ++-- 8 files changed, 17 insertions(+), 17 deletions(-) rename libopenage/gamestate/activity/{event_node.cpp => xor_event_gate.cpp} (75%) rename libopenage/gamestate/activity/{event_node.h => xor_event_gate.h} (100%) rename libopenage/gamestate/activity/{xor_node.cpp => xor_gate.cpp} (73%) rename libopenage/gamestate/activity/{xor_node.h => xor_gate.h} (100%) diff --git a/libopenage/gamestate/activity/CMakeLists.txt b/libopenage/gamestate/activity/CMakeLists.txt index 27b43c1776..b005c32001 100644 --- a/libopenage/gamestate/activity/CMakeLists.txt +++ b/libopenage/gamestate/activity/CMakeLists.txt @@ -1,12 +1,12 @@ add_sources(libopenage activity.cpp end_node.cpp - event_node.cpp node.cpp start_node.cpp task_node.cpp task_system_node.cpp tests.cpp types.cpp - xor_node.cpp + xor_event_gate.cpp + xor_gate.cpp ) diff --git a/libopenage/gamestate/activity/tests.cpp b/libopenage/gamestate/activity/tests.cpp index 7588e5a9b0..cc0f64a691 100644 --- a/libopenage/gamestate/activity/tests.cpp +++ b/libopenage/gamestate/activity/tests.cpp @@ -15,12 +15,12 @@ #include "log/message.h" #include "gamestate/activity/end_node.h" -#include "gamestate/activity/event_node.h" #include "gamestate/activity/node.h" #include "gamestate/activity/start_node.h" #include "gamestate/activity/task_node.h" #include "gamestate/activity/types.h" -#include "gamestate/activity/xor_node.h" +#include "gamestate/activity/xor_event_gate.h" +#include "gamestate/activity/xor_gate.h" #include "time/time.h" diff --git a/libopenage/gamestate/activity/event_node.cpp b/libopenage/gamestate/activity/xor_event_gate.cpp similarity index 75% rename from libopenage/gamestate/activity/event_node.cpp rename to libopenage/gamestate/activity/xor_event_gate.cpp index 3d28e007a3..f3b4ddb0e9 100644 --- a/libopenage/gamestate/activity/event_node.cpp +++ b/libopenage/gamestate/activity/xor_event_gate.cpp @@ -1,6 +1,6 @@ // Copyright 2023-2023 the openage authors. See copying.md for legal info. -#include "event_node.h" +#include "xor_event_gate.h" #include @@ -8,10 +8,10 @@ namespace openage::gamestate::activity { XorEventGate::XorEventGate(node_id id, - node_label label, - const std::vector> &outputs, - event_primer_func_t primer_func, - event_next_func_t next_func) : + node_label label, + const std::vector> &outputs, + event_primer_func_t primer_func, + event_next_func_t next_func) : Node{id, label, outputs}, primer_func{primer_func}, next_func{next_func} { diff --git a/libopenage/gamestate/activity/event_node.h b/libopenage/gamestate/activity/xor_event_gate.h similarity index 100% rename from libopenage/gamestate/activity/event_node.h rename to libopenage/gamestate/activity/xor_event_gate.h diff --git a/libopenage/gamestate/activity/xor_node.cpp b/libopenage/gamestate/activity/xor_gate.cpp similarity index 73% rename from libopenage/gamestate/activity/xor_node.cpp rename to libopenage/gamestate/activity/xor_gate.cpp index 644362f877..771dbfbe29 100644 --- a/libopenage/gamestate/activity/xor_node.cpp +++ b/libopenage/gamestate/activity/xor_gate.cpp @@ -1,6 +1,6 @@ // Copyright 2023-2023 the openage authors. See copying.md for legal info. -#include "xor_node.h" +#include "xor_gate.h" #include @@ -8,9 +8,9 @@ namespace openage::gamestate::activity { XorGate::XorGate(node_id id, - node_label label, - const std::vector> &outputs, - condition_func_t condition_func) : + node_label label, + const std::vector> &outputs, + condition_func_t condition_func) : Node{id, label, outputs}, condition_func{condition_func} { } diff --git a/libopenage/gamestate/activity/xor_node.h b/libopenage/gamestate/activity/xor_gate.h similarity index 100% rename from libopenage/gamestate/activity/xor_node.h rename to libopenage/gamestate/activity/xor_gate.h diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index d8ff2361b8..95ac3c5397 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -15,10 +15,10 @@ #include "event/event_loop.h" #include "gamestate/activity/activity.h" #include "gamestate/activity/end_node.h" -#include "gamestate/activity/event_node.h" #include "gamestate/activity/start_node.h" #include "gamestate/activity/task_system_node.h" -#include "gamestate/activity/xor_node.h" +#include "gamestate/activity/xor_event_gate.h" +#include "gamestate/activity/xor_gate.h" #include "gamestate/api/activity.h" #include "gamestate/component/api/idle.h" #include "gamestate/component/api/live.h" diff --git a/libopenage/gamestate/system/activity.cpp b/libopenage/gamestate/system/activity.cpp index 240b5c7c50..fe37142bfe 100644 --- a/libopenage/gamestate/system/activity.cpp +++ b/libopenage/gamestate/system/activity.cpp @@ -9,13 +9,13 @@ #include "error/error.h" #include "log/message.h" -#include "gamestate/activity/event_node.h" #include "gamestate/activity/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_node.h" +#include "gamestate/activity/xor_event_gate.h" +#include "gamestate/activity/xor_gate.h" #include "gamestate/component/internal/activity.h" #include "gamestate/component/types.h" #include "gamestate/game_entity.h" From a9e7d4474b5d072d7b5f760d05caa18f26b7148d Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 18 Dec 2023 02:09:41 +0100 Subject: [PATCH 113/771] gamestate: Refactor XorGate to use one condition per node. --- libopenage/gamestate/activity/end_node.cpp | 4 +- libopenage/gamestate/activity/end_node.h | 4 +- libopenage/gamestate/activity/node.cpp | 10 +- libopenage/gamestate/activity/node.h | 20 ++-- libopenage/gamestate/activity/start_node.cpp | 6 +- libopenage/gamestate/activity/start_node.h | 6 +- libopenage/gamestate/activity/task_node.cpp | 6 +- libopenage/gamestate/activity/task_node.h | 6 +- .../gamestate/activity/task_system_node.cpp | 6 +- .../gamestate/activity/task_system_node.h | 6 +- .../gamestate/activity/xor_event_gate.cpp | 4 +- .../gamestate/activity/xor_event_gate.h | 18 ++-- libopenage/gamestate/activity/xor_gate.cpp | 60 +++++++++++- libopenage/gamestate/activity/xor_gate.h | 96 +++++++++++++++++-- libopenage/gamestate/api/definitions.h | 6 +- 15 files changed, 201 insertions(+), 57 deletions(-) diff --git a/libopenage/gamestate/activity/end_node.cpp b/libopenage/gamestate/activity/end_node.cpp index 01ecfebade..bb5de4898d 100644 --- a/libopenage/gamestate/activity/end_node.cpp +++ b/libopenage/gamestate/activity/end_node.cpp @@ -8,8 +8,8 @@ namespace openage::gamestate::activity { -EndNode::EndNode(node_id id, - node_label label) : +EndNode::EndNode(node_id_t id, + node_label_t label) : Node{id, label, {}} { } diff --git a/libopenage/gamestate/activity/end_node.h b/libopenage/gamestate/activity/end_node.h index c068f39475..ff8754cc18 100644 --- a/libopenage/gamestate/activity/end_node.h +++ b/libopenage/gamestate/activity/end_node.h @@ -26,8 +26,8 @@ class EndNode : public Node { * @param id Unique identifier for this node. * @param label Human-readable label (optional). */ - EndNode(node_id id, - node_label label = "End"); + EndNode(node_id_t id, + node_label_t label = "End"); virtual ~EndNode() = default; inline node_t get_type() const override { diff --git a/libopenage/gamestate/activity/node.cpp b/libopenage/gamestate/activity/node.cpp index 5c7f007d74..833b4031eb 100644 --- a/libopenage/gamestate/activity/node.cpp +++ b/libopenage/gamestate/activity/node.cpp @@ -10,8 +10,8 @@ namespace openage::gamestate::activity { -Node::Node(node_id id, - node_label label, +Node::Node(node_id_t id, + node_label_t label, const std::vector> &outputs) : outputs{}, id{id}, @@ -22,11 +22,11 @@ Node::Node(node_id id, } } -node_id Node::get_id() const { +node_id_t Node::get_id() const { return this->id; } -const node_label Node::get_label() const { +const node_label_t Node::get_label() const { return this->label; } @@ -42,7 +42,7 @@ std::string Node::str() const { return ret.str(); } -const std::shared_ptr &Node::next(node_id id) const { +const std::shared_ptr &Node::next(node_id_t id) const { if (not this->outputs.contains(id)) [[unlikely]] { throw Error{MSG(err) << "Node " << this->str() << " has no output with id " << id}; } diff --git a/libopenage/gamestate/activity/node.h b/libopenage/gamestate/activity/node.h index 9a8edfcf9f..303793fa99 100644 --- a/libopenage/gamestate/activity/node.h +++ b/libopenage/gamestate/activity/node.h @@ -13,8 +13,8 @@ namespace openage::gamestate::activity { -using node_id = size_t; -using node_label = std::string; +using node_id_t = size_t; +using node_label_t = std::string; /** * Node in the flow graph describing the activity. @@ -28,8 +28,8 @@ class Node { * @param label Human-readable label (optional). * @param outputs Output nodes. */ - Node(node_id id, - node_label label = "", + Node(node_id_t id, + node_label_t label = "", const std::vector> &outputs = {}); virtual ~Node() = default; @@ -45,14 +45,14 @@ class Node { * * @return The unique identifier. */ - node_id get_id() const; + node_id_t get_id() const; /** * Get the human-readable label for this node. * * @return Human-readable label. */ - const node_label get_label() const; + const node_label_t get_label() const; /** * Get a human-readable string representation of this node. @@ -67,7 +67,7 @@ class Node { * @param id Unique identifier of the output node. * @return Output node. */ - const std::shared_ptr &next(node_id id) const; + const std::shared_ptr &next(node_id_t id) const; /** * Add an output node. @@ -80,18 +80,18 @@ class Node { /** * Output nodes. */ - std::unordered_map> outputs; + std::unordered_map> outputs; private: /** * Unique identifier for this node. */ - const node_id id; + const node_id_t id; /** * Human-readable label. */ - const node_label label; + const node_label_t label; }; } // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/start_node.cpp b/libopenage/gamestate/activity/start_node.cpp index 8686fc2f4e..a6b400fb1e 100644 --- a/libopenage/gamestate/activity/start_node.cpp +++ b/libopenage/gamestate/activity/start_node.cpp @@ -8,8 +8,8 @@ namespace openage::gamestate::activity { -StartNode::StartNode(node_id id, - node_label label, +StartNode::StartNode(node_id_t id, + node_label_t label, const std::shared_ptr &output) : Node{id, label} { if (output) { @@ -22,7 +22,7 @@ void StartNode::add_output(const std::shared_ptr &output) { this->outputs.emplace(output->get_id(), output); } -node_id StartNode::get_next() const { +node_id_t StartNode::get_next() const { return (*this->outputs.begin()).first; } diff --git a/libopenage/gamestate/activity/start_node.h b/libopenage/gamestate/activity/start_node.h index bec65e6ea7..125827b8ac 100644 --- a/libopenage/gamestate/activity/start_node.h +++ b/libopenage/gamestate/activity/start_node.h @@ -25,8 +25,8 @@ class StartNode : public Node { * @param label Human-readable label (optional). * @param output Next node to visit (can be set later). */ - StartNode(node_id id, - node_label label = "Start", + StartNode(node_id_t id, + node_label_t label = "Start", const std::shared_ptr &output = nullptr); virtual ~StartNode() = default; @@ -49,7 +49,7 @@ class StartNode : public Node { * @param time Current time. * @return Next node to visit. */ - node_id get_next() const; + node_id_t get_next() const; }; } // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/task_node.cpp b/libopenage/gamestate/activity/task_node.cpp index 36b5de6663..59186eb451 100644 --- a/libopenage/gamestate/activity/task_node.cpp +++ b/libopenage/gamestate/activity/task_node.cpp @@ -8,8 +8,8 @@ namespace openage::gamestate::activity { -TaskCustom::TaskCustom(node_id id, - node_label label, +TaskCustom::TaskCustom(node_id_t id, + node_label_t label, const std::shared_ptr &output, task_func_t task_func) : Node{id, label}, @@ -32,7 +32,7 @@ task_func_t TaskCustom::get_task_func() const { return this->task_func; } -node_id TaskCustom::get_next() const { +node_id_t TaskCustom::get_next() const { return (*this->outputs.begin()).first; } diff --git a/libopenage/gamestate/activity/task_node.h b/libopenage/gamestate/activity/task_node.h index 0efd3f784b..b3c7cb822b 100644 --- a/libopenage/gamestate/activity/task_node.h +++ b/libopenage/gamestate/activity/task_node.h @@ -39,8 +39,8 @@ class TaskCustom : public Node { * @param task_func Action to perform when visiting this node (can be set later). * @param output Next node to visit (optional). */ - TaskCustom(node_id id, - node_label label = "TaskCustom", + TaskCustom(node_id_t id, + node_label_t label = "TaskCustom", const std::shared_ptr &output = nullptr, task_func_t task_func = no_task); virtual ~TaskCustom() = default; @@ -76,7 +76,7 @@ class TaskCustom : public Node { * @param time Current time. * @return Next node to visit. */ - node_id get_next() const; + node_id_t get_next() const; private: /** diff --git a/libopenage/gamestate/activity/task_system_node.cpp b/libopenage/gamestate/activity/task_system_node.cpp index 158bcc41b2..c73e00475e 100644 --- a/libopenage/gamestate/activity/task_system_node.cpp +++ b/libopenage/gamestate/activity/task_system_node.cpp @@ -8,8 +8,8 @@ namespace openage::gamestate::activity { -TaskSystemNode::TaskSystemNode(node_id id, - node_label label, +TaskSystemNode::TaskSystemNode(node_id_t id, + node_label_t label, const std::shared_ptr &output, system::system_id_t system_id) : Node{id, label}, @@ -32,7 +32,7 @@ system::system_id_t TaskSystemNode::get_system_id() const { return this->system_id; } -node_id TaskSystemNode::get_next() const { +node_id_t TaskSystemNode::get_next() const { return (*this->outputs.begin()).first; } diff --git a/libopenage/gamestate/activity/task_system_node.h b/libopenage/gamestate/activity/task_system_node.h index 8f9e6b1163..d594fcdae2 100644 --- a/libopenage/gamestate/activity/task_system_node.h +++ b/libopenage/gamestate/activity/task_system_node.h @@ -25,8 +25,8 @@ class TaskSystemNode : public Node { * @param output Next node to visit (optional). * @param system_id System to run when visiting this node (can be set later). */ - TaskSystemNode(node_id id, - node_label label = "TaskSystem", + TaskSystemNode(node_id_t id, + node_label_t label = "TaskSystem", const std::shared_ptr &output = nullptr, system::system_id_t system_id = system::system_id_t::NONE); virtual ~TaskSystemNode() = default; @@ -62,7 +62,7 @@ class TaskSystemNode : public Node { * @param time Current time. * @return Next node to visit. */ - node_id get_next() const; + node_id_t get_next() const; private: /** diff --git a/libopenage/gamestate/activity/xor_event_gate.cpp b/libopenage/gamestate/activity/xor_event_gate.cpp index f3b4ddb0e9..e76cb60558 100644 --- a/libopenage/gamestate/activity/xor_event_gate.cpp +++ b/libopenage/gamestate/activity/xor_event_gate.cpp @@ -7,8 +7,8 @@ namespace openage::gamestate::activity { -XorEventGate::XorEventGate(node_id id, - node_label label, +XorEventGate::XorEventGate(node_id_t id, + node_label_t label, const std::vector> &outputs, event_primer_func_t primer_func, event_next_func_t next_func) : diff --git a/libopenage/gamestate/activity/xor_event_gate.h b/libopenage/gamestate/activity/xor_event_gate.h index a753682b68..a27e79d891 100644 --- a/libopenage/gamestate/activity/xor_event_gate.h +++ b/libopenage/gamestate/activity/xor_event_gate.h @@ -54,12 +54,15 @@ using event_primer_func_t = std::function &, - const std::shared_ptr &, - const std::shared_ptr &)>; +using event_next_func_t = std::function &, + const std::shared_ptr &, + const std::shared_ptr &)>; +/** + * Default primer function that throws an error. + */ static const event_primer_func_t no_event = [](const time::time_t &, const std::shared_ptr &, const std::shared_ptr &, @@ -68,6 +71,9 @@ static const event_primer_func_t no_event = [](const time::time_t &, return event_store_t{}; }; +/** + * Default next function that throws an error. + */ static const event_next_func_t no_next = [](const time::time_t &, const std::shared_ptr &, const std::shared_ptr &, @@ -91,8 +97,8 @@ class XorEventGate : public Node { * @param primer_func Function to create and register the event. * @param next_func Function to decide which node to visit after the event is handled. */ - XorEventGate(node_id id, - node_label label = "Event", + XorEventGate(node_id_t id, + node_label_t label = "Event", const std::vector> &outputs = {}, event_primer_func_t primer_func = no_event, event_next_func_t next_func = no_next); diff --git a/libopenage/gamestate/activity/xor_gate.cpp b/libopenage/gamestate/activity/xor_gate.cpp index 771dbfbe29..73cf8a5270 100644 --- a/libopenage/gamestate/activity/xor_gate.cpp +++ b/libopenage/gamestate/activity/xor_gate.cpp @@ -7,18 +7,52 @@ namespace openage::gamestate::activity { -XorGate::XorGate(node_id id, - node_label label, +XorGate::XorGate(node_id_t id, + node_label_t label, const std::vector> &outputs, condition_func_t condition_func) : Node{id, label, outputs}, - condition_func{condition_func} { + condition_func{condition_func}, + conditions{}, + default_id{std::nullopt} { +} + +XorGate::XorGate(node_id_t id, + node_label_t label, + const std::vector> &outputs, + const std::vector &conditions, + const node_id_t default_id) : + Node{id, label, outputs}, + condition_func{no_condition}, + conditions{}, + default_id{std::nullopt} { + if (conditions.size() != outputs.size()) [[unlikely]] { + throw Error{MSG(err) << "XorGate " << this->str() << " has " << outputs.size() + << " outputs but " << conditions.size() << " conditions"}; + } + + for (size_t i = 0; i < conditions.size(); ++i) { + this->conditions.emplace(outputs[i]->get_id(), conditions[i]); + } + + this->set_default_id(default_id); } void XorGate::add_output(const std::shared_ptr &output) { this->outputs.emplace(output->get_id(), output); } +void XorGate::add_output(const std::shared_ptr &output, + const condition_t condition_func) { + this->outputs.emplace(output->get_id(), output); + this->conditions.emplace(output->get_id(), condition_func); + + // If this is the first output, set it as the default. + if (not this->default_id) [[unlikely]] { + this->default_id = output->get_id(); + } +} + void XorGate::set_condition_func(condition_func_t condition_func) { this->condition_func = condition_func; } @@ -27,4 +61,24 @@ condition_func_t XorGate::get_condition_func() const { return this->condition_func; } +const std::map &XorGate::get_conditions() const { + return this->conditions; +} + +node_id_t XorGate::get_default_id() const { + if (not this->default_id) [[unlikely]] { + throw Error{MSG(err) << "XorGate " << this->str() << " has no default output"}; + } + + return this->default_id.value(); +} + +void XorGate::set_default_id(node_id_t id) { + if (not this->outputs.contains(id)) [[unlikely]] { + throw Error{MSG(err) << "XorGate " << this->str() << " has no output with id " << id}; + } + + this->default_id = id; +} + } // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/xor_gate.h b/libopenage/gamestate/activity/xor_gate.h index 39efca6209..adc2b5bf05 100644 --- a/libopenage/gamestate/activity/xor_gate.h +++ b/libopenage/gamestate/activity/xor_gate.h @@ -3,7 +3,9 @@ #pragma once #include +#include #include +#include #include #include "error/error.h" @@ -19,17 +21,38 @@ class GameEntity; namespace activity { -using condition_func_t = std::function &)>; +/** + * 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 &)>; + +/** + * Function that determines which output node is chosen. + * + * @param time Current game time. + * @param entity Entity that is executing the activity. + * @return ID of the output node that is chosen. + */ +using condition_func_t = std::function &)>; +/** + * Default condition function that throws an error. + */ static const condition_func_t no_condition = [](const time::time_t &, - const std::shared_ptr &) -> node_id { + const std::shared_ptr &) -> node_id_t { throw Error{MSG(err) << "No condition function set."}; }; /** - * Chooses one of its output nodes based on a condition. + * Chooses one of its output nodes based on conditions. */ class XorGate : public Node { public: @@ -42,10 +65,26 @@ class XorGate : public Node { * @param condition_func Function that determines which output node is chosen (can be set later). * This must be a valid node ID of one of the output nodes. */ - XorGate(node_id id, - node_label label = "ExclusiveGateway", + XorGate(node_id_t id, + node_label_t label = "ExclusiveGateway", const std::vector> &outputs = {}, condition_func_t condition_func = no_condition); + + /** + * Creates a new condition node. + * + * @param id Unique identifier of the node. + * @param label Label of the node. + * @param outputs Output nodes. + * @param conditions Conditions for each output node. + * @param default_id Default output node ID. + */ + XorGate(node_id_t id, + node_label_t label, + const std::vector> &outputs, + const std::vector &conditions, + const node_id_t default_id); + virtual ~XorGate() = default; inline node_t get_type() const override { @@ -59,6 +98,16 @@ class XorGate : public Node { */ void add_output(const std::shared_ptr &output) override; + /** + * Add an output node. + * + * @param output Output node. + * @param condition_func 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); + /** * Set the function that determines which output node is chosen. * @@ -74,11 +123,46 @@ class XorGate : public Node { */ condition_func_t get_condition_func() const; + /** + * Get the output->condition mappings. + * + * @return Conditions for each output node. + */ + const std::map &get_conditions() const; + + /** + * Get the ID of the default output node. + * + * @return Default output node ID. + */ + node_id_t get_default_id() const; + + /** + * Set the ID of the default output node. + * + * The ID must be a valid node ID of one of the output nodes. + * + * @param id Default output node ID. + */ + void set_default_id(node_id_t id); + private: /** * Determines which output node is chosen. */ condition_func_t condition_func; + + /** + * Maps output node IDs to condition functions. + * + * Conditions are checked in order they appear in the map. + */ + std::map conditions; + + /** + * Default output node ID. Chosen if no condition is true. + */ + std::optional default_id; }; } // namespace activity diff --git a/libopenage/gamestate/api/definitions.h b/libopenage/gamestate/api/definitions.h index 0a2c452a0a..d97372962f 100644 --- a/libopenage/gamestate/api/definitions.h +++ b/libopenage/gamestate/api/definitions.h @@ -16,7 +16,7 @@ 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::IDLE, nyan::ValueHolder(std::make_shared("engine.ability.type.Idle"))), @@ -29,7 +29,7 @@ static const auto ABILITY_DEFS = datastructure::create_const_map( std::pair(ability_property_t::ANIMATED, nyan::ValueHolder(std::make_shared("engine.ability.property.type.Animated"))), @@ -61,7 +61,7 @@ static const auto ACTIVITY_NODE_DEFS = datastructure::create_const_map( std::pair(patch_property_t::DIPLOMATIC, nyan::ValueHolder(std::make_shared("engine.patch.property.type.Diplomatic")))); From c586d7f91a2215de49dc3c679a758b9c98986650 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 18 Dec 2023 02:26:39 +0100 Subject: [PATCH 114/771] gamestate: Adjust activity demo for new condition. --- libopenage/gamestate/activity/tests.cpp | 34 ++++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/libopenage/gamestate/activity/tests.cpp b/libopenage/gamestate/activity/tests.cpp index cc0f64a691..b43cf9ed9f 100644 --- a/libopenage/gamestate/activity/tests.cpp +++ b/libopenage/gamestate/activity/tests.cpp @@ -138,8 +138,14 @@ const std::shared_ptr activity_flow(const std::shared_ptr(current); - auto condition = node->get_condition_func(); - auto next_id = condition(0, nullptr); + auto next_id = node->get_default_id(); + for (auto &condition : node->get_conditions()) { + auto condition_func = condition.second; + if (condition_func(0, nullptr)) { + next_id = condition.first; + break; + } + } current = node->next(next_id); } break; case activity::node_t::XOR_EVENT_GATE: { @@ -203,23 +209,27 @@ void activity_demo() { }); // Conditional branch - size_t counter = 0; - xor_node->add_output(task1); - xor_node->add_output(event_node); - xor_node->set_condition_func([&](const time::time_t & /* time */, - const std::shared_ptr & /* entity */) { + static size_t counter = 0; + activity::condition_t branch_task1 = [&](const time::time_t & /* time */, + const std::shared_ptr & /* entity */) { 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() << ")"); counter++; - return task1->get_id(); + return true; } - + return false; + }; + xor_node->add_output(task1, branch_task1); + activity::condition_t branch_event = [&](const time::time_t & /* time */, + const std::shared_ptr & /* entity */) { + // No check needed here, the event node is always selected log::log(INFO << "Selecting path 2 (to event node " << event_node->get_id() << ")"); - - return event_node->get_id(); - }); + return true; + }; + xor_node->add_output(event_node, branch_event); + xor_node->set_default_id(event_node->get_id()); // event node event_node->add_output(task2); From c960ab86f75304cf988106b990b8ba5fb1153541 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 18 Dec 2023 02:46:54 +0100 Subject: [PATCH 115/771] gamestate: Remove old condition functionality. --- libopenage/gamestate/activity/end_node.cpp | 4 -- libopenage/gamestate/activity/end_node.h | 9 ---- libopenage/gamestate/activity/node.h | 7 --- libopenage/gamestate/activity/start_node.h | 2 +- libopenage/gamestate/activity/task_node.h | 2 +- .../gamestate/activity/task_system_node.h | 2 +- .../gamestate/activity/xor_event_gate.h | 2 +- libopenage/gamestate/activity/xor_gate.cpp | 20 +------ libopenage/gamestate/activity/xor_gate.h | 52 +------------------ libopenage/gamestate/entity_factory.cpp | 41 ++++++++------- libopenage/gamestate/system/activity.cpp | 10 +++- 11 files changed, 38 insertions(+), 113 deletions(-) diff --git a/libopenage/gamestate/activity/end_node.cpp b/libopenage/gamestate/activity/end_node.cpp index bb5de4898d..f933a08172 100644 --- a/libopenage/gamestate/activity/end_node.cpp +++ b/libopenage/gamestate/activity/end_node.cpp @@ -13,8 +13,4 @@ EndNode::EndNode(node_id_t id, Node{id, label, {}} { } -void EndNode::add_output(const std::shared_ptr & /* output */) { - throw Error{ERR << "End node cannot have outputs"}; -} - } // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/end_node.h b/libopenage/gamestate/activity/end_node.h index ff8754cc18..e20323d04a 100644 --- a/libopenage/gamestate/activity/end_node.h +++ b/libopenage/gamestate/activity/end_node.h @@ -33,15 +33,6 @@ class EndNode : public Node { inline node_t get_type() const override { return node_t::END; } - - /** - * Throws an error since end nodes are not supposed to have outputs - * - * @param output Output node. - * - * @throws openage::Error - */ - [[noreturn]] void add_output(const std::shared_ptr &output) override; }; } // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/node.h b/libopenage/gamestate/activity/node.h index 303793fa99..ed56277930 100644 --- a/libopenage/gamestate/activity/node.h +++ b/libopenage/gamestate/activity/node.h @@ -69,13 +69,6 @@ class Node { */ const std::shared_ptr &next(node_id_t id) const; - /** - * Add an output node. - * - * @param output Output node. - */ - virtual void add_output(const std::shared_ptr &output) = 0; - protected: /** * Output nodes. diff --git a/libopenage/gamestate/activity/start_node.h b/libopenage/gamestate/activity/start_node.h index 125827b8ac..6a9406b381 100644 --- a/libopenage/gamestate/activity/start_node.h +++ b/libopenage/gamestate/activity/start_node.h @@ -41,7 +41,7 @@ class StartNode : public Node { * * @param output Output node. */ - void add_output(const std::shared_ptr &output) override; + void add_output(const std::shared_ptr &output); /** * Get the next node to visit. diff --git a/libopenage/gamestate/activity/task_node.h b/libopenage/gamestate/activity/task_node.h index b3c7cb822b..a5eddf59c2 100644 --- a/libopenage/gamestate/activity/task_node.h +++ b/libopenage/gamestate/activity/task_node.h @@ -54,7 +54,7 @@ class TaskCustom : public Node { * * @param output Output node. */ - void add_output(const std::shared_ptr &output) override; + void add_output(const std::shared_ptr &output); /** * Set the task function. diff --git a/libopenage/gamestate/activity/task_system_node.h b/libopenage/gamestate/activity/task_system_node.h index d594fcdae2..74513cbd64 100644 --- a/libopenage/gamestate/activity/task_system_node.h +++ b/libopenage/gamestate/activity/task_system_node.h @@ -40,7 +40,7 @@ class TaskSystemNode : public Node { * * @param output Output node. */ - void add_output(const std::shared_ptr &output) override; + void add_output(const std::shared_ptr &output); /** * Set the system id. diff --git a/libopenage/gamestate/activity/xor_event_gate.h b/libopenage/gamestate/activity/xor_event_gate.h index a27e79d891..bce572376b 100644 --- a/libopenage/gamestate/activity/xor_event_gate.h +++ b/libopenage/gamestate/activity/xor_event_gate.h @@ -113,7 +113,7 @@ class XorEventGate : public Node { * * @param output Output node. */ - void add_output(const std::shared_ptr &output) override; + void add_output(const std::shared_ptr &output); /** * Set the function to create the event. diff --git a/libopenage/gamestate/activity/xor_gate.cpp b/libopenage/gamestate/activity/xor_gate.cpp index 73cf8a5270..6069968ba1 100644 --- a/libopenage/gamestate/activity/xor_gate.cpp +++ b/libopenage/gamestate/activity/xor_gate.cpp @@ -8,11 +8,8 @@ namespace openage::gamestate::activity { XorGate::XorGate(node_id_t id, - node_label_t label, - const std::vector> &outputs, - condition_func_t condition_func) : - Node{id, label, outputs}, - condition_func{condition_func}, + node_label_t label) : + Node{id, label, {}}, conditions{}, default_id{std::nullopt} { } @@ -23,7 +20,6 @@ XorGate::XorGate(node_id_t id, const std::vector &conditions, const node_id_t default_id) : Node{id, label, outputs}, - condition_func{no_condition}, conditions{}, default_id{std::nullopt} { if (conditions.size() != outputs.size()) [[unlikely]] { @@ -38,10 +34,6 @@ XorGate::XorGate(node_id_t id, this->set_default_id(default_id); } -void XorGate::add_output(const std::shared_ptr &output) { - this->outputs.emplace(output->get_id(), output); -} - void XorGate::add_output(const std::shared_ptr &output, const condition_t condition_func) { this->outputs.emplace(output->get_id(), output); @@ -53,14 +45,6 @@ void XorGate::add_output(const std::shared_ptr &output, } } -void XorGate::set_condition_func(condition_func_t condition_func) { - this->condition_func = condition_func; -} - -condition_func_t XorGate::get_condition_func() const { - return this->condition_func; -} - 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 adc2b5bf05..34870472cf 100644 --- a/libopenage/gamestate/activity/xor_gate.h +++ b/libopenage/gamestate/activity/xor_gate.h @@ -32,24 +32,6 @@ namespace activity { using condition_t = std::function &)>; -/** - * Function that determines which output node is chosen. - * - * @param time Current game time. - * @param entity Entity that is executing the activity. - * @return ID of the output node that is chosen. - */ -using condition_func_t = std::function &)>; - -/** - * Default condition function that throws an error. - */ -static const condition_func_t no_condition = [](const time::time_t &, - const std::shared_ptr &) -> node_id_t { - throw Error{MSG(err) << "No condition function set."}; -}; - /** * Chooses one of its output nodes based on conditions. @@ -61,14 +43,9 @@ class XorGate : public Node { * * @param id Unique identifier of the node. * @param label Label of the node (optional). - * @param outputs Output nodes (can be set later). - * @param condition_func Function that determines which output node is chosen (can be set later). - * This must be a valid node ID of one of the output nodes. */ XorGate(node_id_t id, - node_label_t label = "ExclusiveGateway", - const std::vector> &outputs = {}, - condition_func_t condition_func = no_condition); + node_label_t label = "ExclusiveGateway"); /** * Creates a new condition node. @@ -95,34 +72,12 @@ class XorGate : public Node { * Add an output node. * * @param output Output node. - */ - void add_output(const std::shared_ptr &output) override; - - /** - * Add an output node. - * - * @param output Output node. * @param condition_func 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); - /** - * Set the function that determines which output node is chosen. - * - * @param condition_func Function that determines which output node is chosen. - * This must be a valid node ID of one of the output nodes. - */ - void set_condition_func(condition_func_t condition_func); - - /** - * Get the function that determines which output node is chosen. - * - * @return Function that determines which output node is chosen. - */ - condition_func_t get_condition_func() const; - /** * Get the output->condition mappings. * @@ -147,11 +102,6 @@ class XorGate : public Node { void set_default_id(node_id_t id); private: - /** - * Determines which output node is chosen. - */ - condition_func_t condition_func; - /** * Maps output node IDs to condition functions. * diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index 95ac3c5397..4727e6cda5 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -79,16 +79,21 @@ std::shared_ptr create_test_activity() { idle->add_output(condition_moveable); idle->set_system_id(system::system_id_t::IDLE); - condition_moveable->add_output(wait_for_command); - condition_moveable->add_output(end); - condition_moveable->set_condition_func([&](const time::time_t & /* time */, - const std::shared_ptr &entity) { - if (entity->has_component(component::component_t::MOVE)) { - return 3; // wait_for_command->get_id(); - } - - return 7; // end->get_id(); - }); + // wait_for_command branch + activity::condition_t wait_branch = [&](const time::time_t & /* time */, + const std::shared_ptr &entity) { + return entity->has_component(component::component_t::MOVE); + }; + condition_moveable->add_output(wait_for_command, wait_branch); + + // end branch + activity::condition_t end_branch = [&](const time::time_t & /* time */, + const std::shared_ptr & /* entity */) { + // no checks, this is the default branch + return true; + }; + condition_moveable->add_output(end, end_branch); + condition_moveable->set_default_id(end->get_id()); wait_for_command->add_output(move); wait_for_command->set_primer_func([](const time::time_t & /* time */, @@ -254,13 +259,13 @@ void EntityFactory::init_components(const std::shared_ptr(loop, create_test_activity()); - entity->add_component(activity); - } + // if (activity_ability) { + // init_activity(loop, owner_db_view, entity, activity_ability.value()); + // } + // else { + auto activity = std::make_shared(loop, create_test_activity()); + entity->add_component(activity); + // } } void EntityFactory::init_activity(const std::shared_ptr &loop, @@ -326,7 +331,7 @@ void EntityFactory::init_activity(const std::shared_ptradd_output(next_engine_node); + // activity_node->add_output(next_engine_node); } } diff --git a/libopenage/gamestate/system/activity.cpp b/libopenage/gamestate/system/activity.cpp index fe37142bfe..ec363f56ce 100644 --- a/libopenage/gamestate/system/activity.cpp +++ b/libopenage/gamestate/system/activity.cpp @@ -82,8 +82,14 @@ void Activity::advance(const std::shared_ptr &entity, } break; case activity::node_t::XOR_GATE: { auto node = std::static_pointer_cast(current_node); - auto condition = node->get_condition_func(); - auto next_id = condition(start_time, entity); + auto next_id = node->get_default_id(); + for (auto &condition : node->get_conditions()) { + auto condition_func = condition.second; + if (condition_func(start_time, entity)) { + next_id = condition.first; + break; + } + } current_node = node->next(next_id); } break; case activity::node_t::XOR_EVENT_GATE: { From b381dc576b2d15c2625e488d92e1839d3256863a Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 19 Dec 2023 00:50:16 +0100 Subject: [PATCH 116/771] gamestate: Refactor XorEventGate to use one event primer per node. --- .../gamestate/activity/xor_event_gate.cpp | 36 ++++++++ .../gamestate/activity/xor_event_gate.h | 91 ++++++++++++++++--- 2 files changed, 114 insertions(+), 13 deletions(-) diff --git a/libopenage/gamestate/activity/xor_event_gate.cpp b/libopenage/gamestate/activity/xor_event_gate.cpp index e76cb60558..b0d498fc18 100644 --- a/libopenage/gamestate/activity/xor_event_gate.cpp +++ b/libopenage/gamestate/activity/xor_event_gate.cpp @@ -17,10 +17,42 @@ XorEventGate::XorEventGate(node_id_t id, next_func{next_func} { } +XorEventGate::XorEventGate(node_id_t id, + node_label_t label) : + Node{id, label}, + primer_func{no_event}, + next_func{no_next}, + primers{} { +} + +XorEventGate::XorEventGate(node_id_t id, + node_label_t label, + const std::vector> &outputs, + const std::map &primers) : + Node{id, label, outputs}, + primer_func{no_event}, + next_func{no_next}, + primers{} { + if (primers.size() != outputs.size()) { + throw Error{MSG(err) << "XorEventGate " << this->str() << " has " << outputs.size() + << " outputs but " << primers.size() << " primers"}; + } + + for (const auto &[id, primer] : primers) { + this->primers.emplace(id, primer); + } +} + void XorEventGate::add_output(const std::shared_ptr &output) { this->outputs.emplace(output->get_id(), output); } +void XorEventGate::add_output(const std::shared_ptr &output, + const event_primer_t &primer) { + this->outputs.emplace(output->get_id(), output); + this->primers.emplace(output->get_id(), primer); +} + void XorEventGate::set_primer_func(event_primer_func_t primer_func) { this->primer_func = primer_func; } @@ -37,4 +69,8 @@ event_next_func_t XorEventGate::get_next_func() const { return this->next_func; } +const std::map &XorEventGate::get_primers() const { + return this->primers; +} + } // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/xor_event_gate.h b/libopenage/gamestate/activity/xor_event_gate.h index bce572376b..67b15603fe 100644 --- a/libopenage/gamestate/activity/xor_event_gate.h +++ b/libopenage/gamestate/activity/xor_event_gate.h @@ -3,6 +3,7 @@ #pragma once #include +#include #include #include @@ -28,9 +29,27 @@ namespace activity { using event_store_t = std::vector>; -/* */ + +/** + * 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. + * + * @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 &)>; + /** - * Create and register an event on the event loop + * Create and register events on the event loop * * @param time Time at which the primer function is executed. * @param entity Game entity that the node is associated with. @@ -96,12 +115,35 @@ class XorEventGate : public Node { * @param outputs Output nodes (can be set later). * @param primer_func Function to create and register the event. * @param next_func Function to decide which node to visit after the event is handled. + */ + [[deprecated]] XorEventGate(node_id_t id, + node_label_t label = "Event", + const std::vector> &outputs = {}, + event_primer_func_t primer_func = no_event, + event_next_func_t next_func = no_next); + + /** + * Create a new exclusive event gateway. + * + * @param id Unique identifier for this node. + * @param label Human-readable label (optional). */ XorEventGate(node_id_t id, - node_label_t label = "Event", - const std::vector> &outputs = {}, - event_primer_func_t primer_func = no_event, - event_next_func_t next_func = no_next); + node_label_t label /* = "EventGateWay" */); + + /** + * Create a new exclusive event gateway. + * + * @param id Unique identifier for this node. + * @param label Human-readable label. + * @param outputs Output nodes. + * @param primers Event primers for each output node. + */ + XorEventGate(node_id_t id, + node_label_t label, + const std::vector> &outputs, + const std::map &primers); + virtual ~XorEventGate() = default; inline node_t get_type() const override { @@ -113,46 +155,69 @@ class XorEventGate : public Node { * * @param output Output node. */ - void add_output(const std::shared_ptr &output); + [[deprecated]] void add_output(const std::shared_ptr &output); + + /** + * Add an output node. + * + * @param output Output node. + * @param primer Creation function for the event associated with the output node. + */ + void add_output(const std::shared_ptr &output, + const event_primer_t &primer); /** * Set the function to create the event. * * @param primer_func Event creation function. */ - void set_primer_func(event_primer_func_t primer_func); + [[deprecated]] void set_primer_func(event_primer_func_t primer_func); /** * Set the function to decide which node to visit after the event is handled. * * @param next_func Next node function. */ - void set_next_func(event_next_func_t next_func); + [[deprecated]] void set_next_func(event_next_func_t next_func); /** * Get the function to create the event. * * @return Event creation function. */ - event_primer_func_t get_primer_func() const; + [[deprecated]] event_primer_func_t get_primer_func() const; /** * Get the function to decide which node to visit after the event is handled. * * @return Next node function. */ - event_next_func_t get_next_func() const; + [[deprecated]] event_next_func_t get_next_func() const; + + /** + * Get the output->event primer mappings. + * + * @return Event primer functions for each output node. + */ + const std::map &get_primers() const; private: /** * Creates the event when the node is visited. */ - event_primer_func_t primer_func; + [[deprecated]] event_primer_func_t primer_func; /** * Decide which node to visit after the event is handled. */ - event_next_func_t next_func; + [[deprecated]] event_next_func_t next_func; + + /** + * Maps output node IDs to event primer functions. + * + * Events are created and registered on the event loop when the node is visited. + */ + std::map primers; }; } // namespace activity From 7de149d2665bf8a570d567e4bfb80dd0f8c5dc13 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 19 Dec 2023 01:38:36 +0100 Subject: [PATCH 117/771] gamestate: Adjust activity demo for new event primers. --- libopenage/event/event.h | 6 ++- libopenage/gamestate/activity/tests.cpp | 69 +++++++++++++++---------- 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/libopenage/event/event.h b/libopenage/event/event.h index 72c943d807..34fcbc0cee 100644 --- a/libopenage/event/event.h +++ b/libopenage/event/event.h @@ -12,6 +12,8 @@ namespace openage::event { class EventEntity; +using event_hash_t = size_t; + /** * The actual one event that may be called - it is used to manage the event itself. * It does not need to be stored. @@ -36,7 +38,7 @@ class Event : public std::enable_shared_from_this { */ void reschedule(const time::time_t reference_time); - size_t hash() const { + event_hash_t hash() const { return this->myhash; } @@ -111,7 +113,7 @@ class Event : public std::enable_shared_from_this { time::time_t last_change_time = time::time_t::min_value(); /** Precalculated std::hash for the event */ - size_t myhash; + event_hash_t myhash; }; diff --git a/libopenage/gamestate/activity/tests.cpp b/libopenage/gamestate/activity/tests.cpp index b43cf9ed9f..f2191ebdb1 100644 --- a/libopenage/gamestate/activity/tests.cpp +++ b/libopenage/gamestate/activity/tests.cpp @@ -33,7 +33,8 @@ namespace openage::gamestate::tests { * @param current_node Node where the control flow starts from. * @return Node where the control flow should continue. */ -const std::shared_ptr activity_flow(const std::shared_ptr ¤t_node); +const std::shared_ptr activity_flow(const std::shared_ptr ¤t_node, + const std::optional ev_params = std::nullopt); /** @@ -57,11 +58,11 @@ class TestActivityManager : public event::EventEntity { return "TestActivityManager"; } - void run() { + void run(const std::optional ev_params = std::nullopt) { if (not current_node) { throw Error{ERR << "No current node given"}; } - this->current_node = activity_flow(this->current_node); + this->current_node = activity_flow(this->current_node, ev_params); } std::shared_ptr current_node; @@ -93,9 +94,9 @@ class TestActivityHandler : public event::OnceEventHandler { const std::shared_ptr &target, const std::shared_ptr & /* state */, const time::time_t & /* time */, - const param_map & /* params */) override { + const param_map ¶ms) override { auto mgr_target = std::dynamic_pointer_cast(target); - mgr_target->run(); + mgr_target->run(params); } time::time_t predict_invoke_time(const std::shared_ptr & /* target */, @@ -106,14 +107,27 @@ class TestActivityHandler : public event::OnceEventHandler { }; -const std::shared_ptr activity_flow(const std::shared_ptr ¤t_node) { +const std::shared_ptr activity_flow(const std::shared_ptr ¤t_node, + const std::optional ev_params) { + // events that are currently being listened for + // in the gamestate these are stored in the activity component + static std::vector> events; + auto current = current_node; if (current->get_type() == activity::node_t::XOR_EVENT_GATE) { - auto node = std::static_pointer_cast(current); - auto event_next = node->get_next_func(); - auto next_id = event_next(0, nullptr, nullptr, nullptr); - current = node->next(next_id); + log::log(INFO << "Continuing from event node"); + if (not ev_params.has_value()) { + throw Error{ERR << "XorEventGate: No event parameters given on continue"}; + } + + auto next_id = ev_params.value().get("next"); + current = current->next(next_id); + + // cancel all other events that the manager may have been waiting for + for (auto &event : events) { + event->cancel(0); + } } while (current->get_type() != activity::node_t::END) { @@ -150,10 +164,14 @@ const std::shared_ptr activity_flow(const std::shared_ptr(current); - auto event_primer = node->get_primer_func(); - event_primer(0, nullptr, nullptr, nullptr); + auto event_primers = node->get_primers(); + for (auto &primer : event_primers) { + auto ev = primer.second(0, nullptr, nullptr, nullptr); + events.push_back(ev); + } // wait for event + log::log(INFO << "Waiting for event"); return current; } break; default: @@ -163,7 +181,7 @@ const std::shared_ptr activity_flow(const std::shared_ptrstr()); } - log::log(INFO << "Reached end note: " << current->str()); + log::log(INFO << "Reached end node: " << current->str()); return current; } @@ -232,25 +250,20 @@ void activity_demo() { xor_node->set_default_id(event_node->get_id()); // event node - event_node->add_output(task2); - event_node->set_primer_func([&](const time::time_t & /* time */, - const std::shared_ptr & /* entity */, - const std::shared_ptr & /* loop */, - const std::shared_ptr & /* state */) { + activity::event_primer_t primer = [&](const time::time_t & /* time */, + const std::shared_ptr & /* entity */, + const std::shared_ptr & /* loop */, + const std::shared_ptr & /* state */) { log::log(INFO << "Setting up event"); + event::EventHandler::param_map::map_t params{{"next", task2->get_id()}}; auto ev = loop->create_event("test.activity", mgr, state, - 0); - return activity::event_store_t{ev}; - }); - event_node->set_next_func([&task2](const time::time_t & /* time */, - const std::shared_ptr & /* entity */, - const std::shared_ptr & /* loop */, - const std::shared_ptr & /* state */) { - log::log(INFO << "Selecting next node (task node " << task2->get_id() << ")"); - return task2->get_id(); - }); + 0, + params); + return ev; + }; + event_node->add_output(task2, primer); // task 2 task2->add_output(end); From 611821172a977c20817b0840e4b811d3623a258a Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 19 Dec 2023 02:31:20 +0100 Subject: [PATCH 118/771] gamestate: Use new event priming functionality. --- libopenage/gamestate/activity/tests.cpp | 11 ++- .../gamestate/activity/xor_event_gate.cpp | 34 ---------- .../gamestate/activity/xor_event_gate.h | 66 ++---------------- libopenage/gamestate/entity_factory.cpp | 68 +++++++------------ .../gamestate/event/process_command.cpp | 8 +-- libopenage/gamestate/event/wait.cpp | 8 +-- libopenage/gamestate/manager.cpp | 5 +- libopenage/gamestate/manager.h | 5 +- libopenage/gamestate/system/activity.cpp | 27 +++++--- libopenage/gamestate/system/activity.h | 17 +++-- 10 files changed, 79 insertions(+), 170 deletions(-) diff --git a/libopenage/gamestate/activity/tests.cpp b/libopenage/gamestate/activity/tests.cpp index f2191ebdb1..64997c9c74 100644 --- a/libopenage/gamestate/activity/tests.cpp +++ b/libopenage/gamestate/activity/tests.cpp @@ -166,7 +166,11 @@ const std::shared_ptr activity_flow(const std::shared_ptr(current); auto event_primers = node->get_primers(); for (auto &primer : event_primers) { - auto ev = primer.second(0, nullptr, nullptr, nullptr); + auto ev = primer.second(0, + nullptr, + nullptr, + nullptr, + primer.first); events.push_back(ev); } @@ -253,9 +257,10 @@ void activity_demo() { activity::event_primer_t primer = [&](const time::time_t & /* time */, const std::shared_ptr & /* entity */, const std::shared_ptr & /* loop */, - const std::shared_ptr & /* state */) { + const std::shared_ptr & /* state */, + size_t next_id) { log::log(INFO << "Setting up event"); - event::EventHandler::param_map::map_t params{{"next", task2->get_id()}}; + event::EventHandler::param_map::map_t params{{"next", next_id}}; auto ev = loop->create_event("test.activity", mgr, state, diff --git a/libopenage/gamestate/activity/xor_event_gate.cpp b/libopenage/gamestate/activity/xor_event_gate.cpp index b0d498fc18..f018cfd96a 100644 --- a/libopenage/gamestate/activity/xor_event_gate.cpp +++ b/libopenage/gamestate/activity/xor_event_gate.cpp @@ -7,21 +7,9 @@ namespace openage::gamestate::activity { -XorEventGate::XorEventGate(node_id_t id, - node_label_t label, - const std::vector> &outputs, - event_primer_func_t primer_func, - event_next_func_t next_func) : - Node{id, label, outputs}, - primer_func{primer_func}, - next_func{next_func} { -} - XorEventGate::XorEventGate(node_id_t id, node_label_t label) : Node{id, label}, - primer_func{no_event}, - next_func{no_next}, primers{} { } @@ -30,8 +18,6 @@ XorEventGate::XorEventGate(node_id_t id, const std::vector> &outputs, const std::map &primers) : Node{id, label, outputs}, - primer_func{no_event}, - next_func{no_next}, primers{} { if (primers.size() != outputs.size()) { throw Error{MSG(err) << "XorEventGate " << this->str() << " has " << outputs.size() @@ -43,32 +29,12 @@ XorEventGate::XorEventGate(node_id_t id, } } -void XorEventGate::add_output(const std::shared_ptr &output) { - this->outputs.emplace(output->get_id(), output); -} - void XorEventGate::add_output(const std::shared_ptr &output, const event_primer_t &primer) { this->outputs.emplace(output->get_id(), output); this->primers.emplace(output->get_id(), primer); } -void XorEventGate::set_primer_func(event_primer_func_t primer_func) { - this->primer_func = primer_func; -} - -void XorEventGate::set_next_func(event_next_func_t next_func) { - this->next_func = next_func; -} - -event_primer_func_t XorEventGate::get_primer_func() const { - return this->primer_func; -} - -event_next_func_t XorEventGate::get_next_func() const { - return this->next_func; -} - const std::map &XorEventGate::get_primers() const { return this->primers; } diff --git a/libopenage/gamestate/activity/xor_event_gate.h b/libopenage/gamestate/activity/xor_event_gate.h index 67b15603fe..ac9334fe92 100644 --- a/libopenage/gamestate/activity/xor_event_gate.h +++ b/libopenage/gamestate/activity/xor_event_gate.h @@ -40,13 +40,15 @@ using event_store_t = std::vector>; * @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 &)>; + const std::shared_ptr &, + size_t next_id)>; /** * Create and register events on the event loop @@ -112,24 +114,9 @@ class XorEventGate : public Node { * * @param id Unique identifier for this node. * @param label Human-readable label (optional). - * @param outputs Output nodes (can be set later). - * @param primer_func Function to create and register the event. - * @param next_func Function to decide which node to visit after the event is handled. - */ - [[deprecated]] XorEventGate(node_id_t id, - node_label_t label = "Event", - const std::vector> &outputs = {}, - event_primer_func_t primer_func = no_event, - event_next_func_t next_func = no_next); - - /** - * Create a new exclusive event gateway. - * - * @param id Unique identifier for this node. - * @param label Human-readable label (optional). */ XorEventGate(node_id_t id, - node_label_t label /* = "EventGateWay" */); + node_label_t label = "EventGateWay"); /** * Create a new exclusive event gateway. @@ -154,46 +141,11 @@ class XorEventGate : public Node { * Add an output node. * * @param output Output node. - */ - [[deprecated]] void add_output(const std::shared_ptr &output); - - /** - * Add an output node. - * - * @param output Output node. * @param primer Creation function for the event associated with the output node. */ void add_output(const std::shared_ptr &output, const event_primer_t &primer); - /** - * Set the function to create the event. - * - * @param primer_func Event creation function. - */ - [[deprecated]] void set_primer_func(event_primer_func_t primer_func); - - /** - * Set the function to decide which node to visit after the event is handled. - * - * @param next_func Next node function. - */ - [[deprecated]] void set_next_func(event_next_func_t next_func); - - /** - * Get the function to create the event. - * - * @return Event creation function. - */ - [[deprecated]] event_primer_func_t get_primer_func() const; - - /** - * Get the function to decide which node to visit after the event is handled. - * - * @return Next node function. - */ - [[deprecated]] event_next_func_t get_next_func() const; - /** * Get the output->event primer mappings. * @@ -202,16 +154,6 @@ class XorEventGate : public Node { const std::map &get_primers() const; private: - /** - * Creates the event when the node is visited. - */ - [[deprecated]] event_primer_func_t primer_func; - - /** - * Decide which node to visit after the event is handled. - */ - [[deprecated]] event_next_func_t next_func; - /** * Maps output node IDs to event primer functions. * diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index 4727e6cda5..60f0f3e4cc 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -95,65 +95,47 @@ std::shared_ptr create_test_activity() { condition_moveable->add_output(end, end_branch); condition_moveable->set_default_id(end->get_id()); - wait_for_command->add_output(move); - wait_for_command->set_primer_func([](const time::time_t & /* time */, - const std::shared_ptr &entity, - const std::shared_ptr &loop, - const std::shared_ptr &state) { + activity::event_primer_t process_primer = [](const time::time_t &, + const std::shared_ptr &entity, + const std::shared_ptr &loop, + const std::shared_ptr &state, + size_t next_id) { + event::EventHandler::param_map::map_t params{{"next", next_id}}; // move->get_id(); auto ev = loop->create_event("game.process_command", entity->get_manager(), state, // event is not executed until a command is available - std::numeric_limits::max()); + std::numeric_limits::max(), + params); auto entity_queue = std::dynamic_pointer_cast( entity->get_component(component::component_t::COMMANDQUEUE)); auto &queue = const_cast> &>(entity_queue->get_queue()); queue.add_dependent(ev); - return activity::event_store_t{ev}; - }); - wait_for_command->set_next_func([](const time::time_t &time, - const std::shared_ptr &entity, - const std::shared_ptr &, - const std::shared_ptr &) { - auto entity_queue = std::dynamic_pointer_cast( - entity->get_component(component::component_t::COMMANDQUEUE)); - auto &queue = entity_queue->get_queue(); - - if (queue.empty(time)) { - throw Error{ERR << "Command queue is empty"}; - } - auto &com = queue.front(time); - if (com->get_type() == component::command::command_t::MOVE) { - return 5; // move->get_id(); - } - - throw Error{ERR << "Unknown command type"}; - }); + return ev; + }; + wait_for_command->add_output(move, process_primer); move->add_output(wait_for_move); move->set_system_id(system::system_id_t::MOVE_COMMAND); - wait_for_move->add_output(idle); - wait_for_move->add_output(condition_command); - wait_for_move->add_output(end); - wait_for_move->set_primer_func([](const time::time_t &time, - const std::shared_ptr &entity, - const std::shared_ptr &loop, - const std::shared_ptr &state) { + activity::event_primer_t primer = [](const time::time_t &time, + const std::shared_ptr &entity, + const std::shared_ptr &loop, + const std::shared_ptr &state, + size_t next_id) { + event::EventHandler::param_map::map_t params{{"next", next_id}}; // idle->get_id(); auto ev = loop->create_event("game.wait", entity->get_manager(), state, - time); - - return activity::event_store_t{ev}; - }); - wait_for_move->set_next_func([&](const time::time_t &, - const std::shared_ptr &, - const std::shared_ptr &, - const std::shared_ptr &) { - return 1; // idle->get_id(); - }); + time, + params); + + return ev; + }; + wait_for_move->add_output(idle, primer); + // wait_for_move->add_output(condition_command, primer); + // wait_for_move->add_output(end, primer); return std::make_shared(0, start, "test"); } diff --git a/libopenage/gamestate/event/process_command.cpp b/libopenage/gamestate/event/process_command.cpp index 4783fe89c8..1f4045214d 100644 --- a/libopenage/gamestate/event/process_command.cpp +++ b/libopenage/gamestate/event/process_command.cpp @@ -19,14 +19,14 @@ void ProcessCommandHandler::invoke(openage::event::EventLoop & /* loop */, const std::shared_ptr &target, const std::shared_ptr & /* state */, const time::time_t &time, - const param_map & /* params */) { + const param_map ¶ms) { auto mgr = std::dynamic_pointer_cast(target); - mgr->run_activity_system(time); + mgr->run_activity_system(time, params); } time::time_t ProcessCommandHandler::predict_invoke_time(const std::shared_ptr & /* target */, - const std::shared_ptr & /* state */, - const time::time_t &at) { + const std::shared_ptr & /* state */, + const time::time_t &at) { return at; } diff --git a/libopenage/gamestate/event/wait.cpp b/libopenage/gamestate/event/wait.cpp index 2d067086fa..b1bcf256cf 100644 --- a/libopenage/gamestate/event/wait.cpp +++ b/libopenage/gamestate/event/wait.cpp @@ -19,14 +19,14 @@ void WaitHandler::invoke(openage::event::EventLoop & /* loop */, const std::shared_ptr &target, const std::shared_ptr & /* state */, const time::time_t &time, - const param_map & /* params */) { + const param_map ¶ms) { auto mgr = std::dynamic_pointer_cast(target); - mgr->run_activity_system(time); + mgr->run_activity_system(time, params); } time::time_t WaitHandler::predict_invoke_time(const std::shared_ptr & /* target */, - const std::shared_ptr & /* state */, - const time::time_t &at) { + const std::shared_ptr & /* state */, + const time::time_t &at) { return at; } diff --git a/libopenage/gamestate/manager.cpp b/libopenage/gamestate/manager.cpp index 9c8f6c1899..f00ccab22c 100644 --- a/libopenage/gamestate/manager.cpp +++ b/libopenage/gamestate/manager.cpp @@ -20,9 +20,10 @@ GameEntityManager::GameEntityManager(const std::shared_ptr &ev_params) { log::log(DBG << "Running activity system for entity " << this->game_entity->get_id()); - system::Activity::advance(this->game_entity, time, this->loop, this->state); + system::Activity::advance(time, this->game_entity, this->loop, this->state, ev_params); } size_t GameEntityManager::id() const { diff --git a/libopenage/gamestate/manager.h b/libopenage/gamestate/manager.h index 6131c94bd2..8d6976c806 100644 --- a/libopenage/gamestate/manager.h +++ b/libopenage/gamestate/manager.h @@ -4,9 +4,11 @@ #include #include +#include #include #include "event/evententity.h" +#include "event/eventhandler.h" #include "time/time.h" @@ -26,7 +28,8 @@ class GameEntityManager : public openage::event::EventEntity { const std::shared_ptr &game_entity); ~GameEntityManager() = default; - void run_activity_system(const time::time_t &time); + void run_activity_system(const time::time_t &time, + const std::optional &ev_params = std::nullopt); size_t id() const override; std::string idstr() const override; diff --git a/libopenage/gamestate/system/activity.cpp b/libopenage/gamestate/system/activity.cpp index ec363f56ce..f63488edd0 100644 --- a/libopenage/gamestate/system/activity.cpp +++ b/libopenage/gamestate/system/activity.cpp @@ -27,10 +27,11 @@ namespace openage::gamestate::system { -void Activity::advance(const std::shared_ptr &entity, - const time::time_t &start_time, +void Activity::advance(const time::time_t &start_time, + const std::shared_ptr &entity, const std::shared_ptr &loop, - const std::shared_ptr &state) { + const std::shared_ptr &state, + const std::optional &ev_params) { auto activity_component = std::dynamic_pointer_cast( entity->get_component(component::component_t::ACTIVITY)); auto current_node = activity_component->get_node(start_time); @@ -44,10 +45,12 @@ void Activity::advance(const std::shared_ptr &entity, if (current_node->get_type() == activity::node_t::XOR_EVENT_GATE) { // returning to a event gateway means that the event has been triggered // move to the next node here - auto node = std::static_pointer_cast(current_node); - auto event_next = node->get_next_func(); - auto next_id = event_next(start_time, entity, loop, state); - current_node = node->next(next_id); + if (not ev_params.has_value()) { + throw Error{ERR << "XorEventGate: No event parameters given on continue"}; + } + + auto next_id = ev_params.value().get("next"); + current_node = current_node->next(next_id); // cancel all other events that the manager may have been waiting for activity_component->cancel_events(start_time); @@ -94,9 +97,13 @@ void Activity::advance(const std::shared_ptr &entity, } break; case activity::node_t::XOR_EVENT_GATE: { auto node = std::static_pointer_cast(current_node); - auto event_primer = node->get_primer_func(); - auto evs = event_primer(start_time + event_wait_time, entity, loop, state); - for (auto &ev : evs) { + auto event_primers = node->get_primers(); + for (auto &primer : event_primers) { + auto ev = primer.second(start_time + event_wait_time, + entity, + loop, + state, + primer.first); activity_component->add_event(ev); } diff --git a/libopenage/gamestate/system/activity.h b/libopenage/gamestate/system/activity.h index d5283fceca..d6c10215fc 100644 --- a/libopenage/gamestate/system/activity.h +++ b/libopenage/gamestate/system/activity.h @@ -3,9 +3,11 @@ #pragma once #include +#include -#include "time/time.h" +#include "event/eventhandler.h" #include "gamestate/system/types.h" +#include "time/time.h" namespace openage { @@ -25,13 +27,14 @@ class Activity { /** * Advance in the activity flow graph of the game entity. * - * @param entity Game entity. * @param start_time Start time of change. + * @param entity Game entity. */ - static void advance(const std::shared_ptr &entity, - const time::time_t &start_time, + static void advance(const time::time_t &start_time, + const std::shared_ptr &entity, const std::shared_ptr &loop, - const std::shared_ptr &state); + const std::shared_ptr &state, + const std::optional &ev_params = std::nullopt); private: /** @@ -44,8 +47,8 @@ class Activity { * @return Runtime of the change in simulation time. */ static const time::time_t handle_subsystem(const std::shared_ptr &entity, - const time::time_t &start_time, - system_id_t system_id); + const time::time_t &start_time, + system_id_t system_id); }; } // namespace system From 26d04bb48e630b96a3c8c48c94f70005d65c8206 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 19 Dec 2023 22:59:40 +0100 Subject: [PATCH 119/771] gamestate: Remove unused functions. --- .../gamestate/activity/xor_event_gate.h | 55 ------------------- 1 file changed, 55 deletions(-) diff --git a/libopenage/gamestate/activity/xor_event_gate.h b/libopenage/gamestate/activity/xor_event_gate.h index ac9334fe92..6c4912d714 100644 --- a/libopenage/gamestate/activity/xor_event_gate.h +++ b/libopenage/gamestate/activity/xor_event_gate.h @@ -27,8 +27,6 @@ class GameState; namespace activity { -using event_store_t = std::vector>; - /** * Create and register an event on the event loop. @@ -50,59 +48,6 @@ using event_primer_t = std::function(cons const std::shared_ptr &, size_t next_id)>; -/** - * Create and register events on the event loop - * - * @param time Time at which the primer function is executed. - * @param entity Game entity that the node is associated with. - * @param loop Event loop that events are registered on. - * @param state Game state. - * - * @return List of events registered on the event loop. - */ -using event_primer_func_t = std::function &, - const std::shared_ptr &, - const std::shared_ptr &)>; - -/** - * Decide which node to visit after the event is handled. - * - * @param time Time at which the next function is executed. - * @param entity Game entity that the node is associated with. - * @param loop Event loop that events are registered on. - * @param state Game state. - * - * @return ID of the next node to visit. - */ -using event_next_func_t = std::function &, - const std::shared_ptr &, - const std::shared_ptr &)>; - - -/** - * Default primer function that throws an error. - */ -static const event_primer_func_t no_event = [](const time::time_t &, - const std::shared_ptr &, - const std::shared_ptr &, - const std::shared_ptr &) { - throw Error{ERR << "No event primer function registered."}; - return event_store_t{}; -}; - -/** - * Default next function that throws an error. - */ -static const event_next_func_t no_next = [](const time::time_t &, - const std::shared_ptr &, - const std::shared_ptr &, - const std::shared_ptr &) { - throw Error{ERR << "No event next function registered."}; - return 0; -}; - /** * Waits for an event to be executed before continuing the control flow. From 64dfb075d0d02e88833bcd6683fa9aa188ecd2c4 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 19 Dec 2023 23:42:44 +0100 Subject: [PATCH 120/771] gamestate: Add breakout event to default actvity graph. --- libopenage/gamestate/entity_factory.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index 60f0f3e4cc..bafa82b9f2 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -119,11 +119,11 @@ std::shared_ptr create_test_activity() { move->add_output(wait_for_move); move->set_system_id(system::system_id_t::MOVE_COMMAND); - activity::event_primer_t primer = [](const time::time_t &time, - const std::shared_ptr &entity, - const std::shared_ptr &loop, - const std::shared_ptr &state, - size_t next_id) { + activity::event_primer_t wait_primer = [](const time::time_t &time, + const std::shared_ptr &entity, + const std::shared_ptr &loop, + const std::shared_ptr &state, + size_t next_id) { event::EventHandler::param_map::map_t params{{"next", next_id}}; // idle->get_id(); auto ev = loop->create_event("game.wait", entity->get_manager(), @@ -133,9 +133,8 @@ std::shared_ptr create_test_activity() { return ev; }; - wait_for_move->add_output(idle, primer); - // wait_for_move->add_output(condition_command, primer); - // wait_for_move->add_output(end, primer); + wait_for_move->add_output(idle, wait_primer); + wait_for_move->add_output(move, process_primer); return std::make_shared(0, start, "test"); } From 7e9dbd100d6b4a631b8042bca47f0171c7b52142 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 20 Dec 2023 01:23:41 +0100 Subject: [PATCH 121/771] gamestate: Predefined activity event functions. --- libopenage/gamestate/entity_factory.cpp | 41 +++---------------- .../gamestate/event/process_command.cpp | 23 +++++++++++ libopenage/gamestate/event/process_command.h | 26 +++++++++++- libopenage/gamestate/event/wait.cpp | 20 +++++++++ libopenage/gamestate/event/wait.h | 27 +++++++++++- 5 files changed, 97 insertions(+), 40 deletions(-) diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index bafa82b9f2..e9b000d7ae 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -39,6 +39,8 @@ #include "time/time.h" #include "util/fixed_point.h" +#include "gamestate/event/process_command.h" +#include "gamestate/event/wait.h" namespace openage::gamestate { @@ -95,46 +97,13 @@ std::shared_ptr create_test_activity() { condition_moveable->add_output(end, end_branch); condition_moveable->set_default_id(end->get_id()); - activity::event_primer_t process_primer = [](const time::time_t &, - const std::shared_ptr &entity, - const std::shared_ptr &loop, - const std::shared_ptr &state, - size_t next_id) { - event::EventHandler::param_map::map_t params{{"next", next_id}}; // move->get_id(); - auto ev = loop->create_event("game.process_command", - entity->get_manager(), - state, - // event is not executed until a command is available - std::numeric_limits::max(), - params); - auto entity_queue = std::dynamic_pointer_cast( - entity->get_component(component::component_t::COMMANDQUEUE)); - auto &queue = const_cast> &>(entity_queue->get_queue()); - queue.add_dependent(ev); - - return ev; - }; - wait_for_command->add_output(move, process_primer); + wait_for_command->add_output(move, gamestate::event::primer_process_command); move->add_output(wait_for_move); move->set_system_id(system::system_id_t::MOVE_COMMAND); - activity::event_primer_t wait_primer = [](const time::time_t &time, - const std::shared_ptr &entity, - const std::shared_ptr &loop, - const std::shared_ptr &state, - size_t next_id) { - event::EventHandler::param_map::map_t params{{"next", next_id}}; // idle->get_id(); - auto ev = loop->create_event("game.wait", - entity->get_manager(), - state, - time, - params); - - return ev; - }; - wait_for_move->add_output(idle, wait_primer); - wait_for_move->add_output(move, process_primer); + wait_for_move->add_output(idle, gamestate::event::primer_wait); + wait_for_move->add_output(move, gamestate::event::primer_process_command); return std::make_shared(0, start, "test"); } diff --git a/libopenage/gamestate/event/process_command.cpp b/libopenage/gamestate/event/process_command.cpp index 1f4045214d..bf5497bc16 100644 --- a/libopenage/gamestate/event/process_command.cpp +++ b/libopenage/gamestate/event/process_command.cpp @@ -2,6 +2,10 @@ #include "process_command.h" +#include "event/event_loop.h" +#include "gamestate/component/internal/command_queue.h" +#include "gamestate/game_entity.h" +#include "gamestate/game_state.h" #include "gamestate/manager.h" @@ -30,5 +34,24 @@ time::time_t ProcessCommandHandler::predict_invoke_time(const std::shared_ptr primer_process_command(const time::time_t &, + const std::shared_ptr &entity, + const std::shared_ptr &loop, + const std::shared_ptr &state, + size_t next_id) { + openage::event::EventHandler::param_map::map_t params{{"next", next_id}}; // move->get_id(); + auto ev = loop->create_event("game.process_command", + entity->get_manager(), + state, + // event is not executed until a command is available + std::numeric_limits::max(), + params); + auto entity_queue = std::dynamic_pointer_cast( + entity->get_component(component::component_t::COMMANDQUEUE)); + auto &queue = const_cast> &>(entity_queue->get_queue()); + queue.add_dependent(ev); + + return ev; +}; } // namespace openage::gamestate::event diff --git a/libopenage/gamestate/event/process_command.h b/libopenage/gamestate/event/process_command.h index 8b940dd616..fb59558fcb 100644 --- a/libopenage/gamestate/event/process_command.h +++ b/libopenage/gamestate/event/process_command.h @@ -17,8 +17,12 @@ class EventEntity; class State; } // namespace event -namespace gamestate::event { +namespace gamestate { +class GameEntity; +class GameState; + +namespace event { /** * Process a command from a game entity command queue. */ @@ -42,5 +46,23 @@ class ProcessCommandHandler : public openage::event::OnceEventHandler { }; -} // namespace gamestate::event +/** + * Primer for process command events in the activity system. + * + * @param time Current simulation time. + * @param entity Game entity. + * @param loop Event loop that the event is registered on. + * @param state Game state. + * @param next_id ID of the next node in the activity graph. + * + * @return Scheduled event. + */ +std::shared_ptr primer_process_command(const time::time_t &, + const std::shared_ptr &entity, + const std::shared_ptr &loop, + const std::shared_ptr &state, + size_t next_id); + +} // namespace event +} // namespace gamestate } // namespace openage diff --git a/libopenage/gamestate/event/wait.cpp b/libopenage/gamestate/event/wait.cpp index b1bcf256cf..955cdd7b91 100644 --- a/libopenage/gamestate/event/wait.cpp +++ b/libopenage/gamestate/event/wait.cpp @@ -2,6 +2,9 @@ #include "wait.h" +#include "event/event_loop.h" +#include "gamestate/game_entity.h" +#include "gamestate/game_state.h" #include "gamestate/manager.h" @@ -30,4 +33,21 @@ time::time_t WaitHandler::predict_invoke_time(const std::shared_ptr primer_wait(const time::time_t &time, + const std::shared_ptr &entity, + const std::shared_ptr &loop, + const std::shared_ptr &state, + size_t next_id) { + openage::event::EventHandler::param_map::map_t params{{"next", next_id}}; + auto ev = loop->create_event("game.wait", + entity->get_manager(), + state, + time, + params); + + return ev; +}; + + } // namespace openage::gamestate::event diff --git a/libopenage/gamestate/event/wait.h b/libopenage/gamestate/event/wait.h index 2c0d9732f2..a4b4c20fcd 100644 --- a/libopenage/gamestate/event/wait.h +++ b/libopenage/gamestate/event/wait.h @@ -17,7 +17,11 @@ class EventEntity; class State; } // namespace event -namespace gamestate::event { +namespace gamestate { +class GameEntity; +class GameState; + +namespace event { /** * Waits until the event is handled and calls back the entity manager. @@ -40,5 +44,24 @@ class WaitHandler : public openage::event::OnceEventHandler { const std::shared_ptr &state, const time::time_t &at) override; }; -} // namespace gamestate::event + +/** + * Primer for wait events in the activity system. + * + * @param time Wait until this time. If the time is in the past, the event is executed immediately. + * @param entity Game entity. + * @param loop Event loop that the event is registered on. + * @param state Game state. + * @param next_id ID of the next node in the activity graph. + * + * @return Scheduled event. + */ +std::shared_ptr primer_wait(const time::time_t &time, + const std::shared_ptr &entity, + const std::shared_ptr &loop, + const std::shared_ptr &state, + size_t next_id); + +} // namespace event +} // namespace gamestate } // namespace openage From 3027ce9646b0e8f8bca58fe8df3b2e77d32711b3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 20 Dec 2023 02:33:36 +0100 Subject: [PATCH 122/771] gamestate: Get event primer from API definition. --- libopenage/gamestate/api/activity.cpp | 10 +++++++++ libopenage/gamestate/api/activity.h | 29 +++++++++++++++++++++++++- libopenage/gamestate/api/definitions.h | 12 +++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/libopenage/gamestate/api/activity.cpp b/libopenage/gamestate/api/activity.cpp index 19e67e65d6..34d7896e8b 100644 --- a/libopenage/gamestate/api/activity.cpp +++ b/libopenage/gamestate/api/activity.cpp @@ -62,4 +62,14 @@ std::vector APIActivityNode::get_next(const nyan::Object &node) { } } +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"; +} + +activity::event_primer_t APIActivityEvent::get_primer(const nyan::Object &event) { + nyan::fqon_t immediate_parent = event.get_parents()[0]; + return ACTIVITY_EVENT_PRIMERS.get(immediate_parent); +} + } // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/activity.h b/libopenage/gamestate/api/activity.h index 28e7c88dbe..cc365da449 100644 --- a/libopenage/gamestate/api/activity.h +++ b/libopenage/gamestate/api/activity.h @@ -7,6 +7,7 @@ #include #include "gamestate/activity/types.h" +#include "gamestate/activity/xor_event_gate.h" namespace openage::gamestate { @@ -37,7 +38,9 @@ class APIActivity { static nyan::Object get_start(const nyan::Object &activity); }; - +/** + * Helper class for creating Activity node objects from the nyan API. + */ class APIActivityNode { public: /** @@ -71,6 +74,30 @@ class APIActivityNode { static std::vector get_next(const nyan::Object &node); }; +/** + * Helper class for creating Activity event objects from the nyan API. + */ +class APIActivityEvent { +public: + /** + * Check if a nyan object is an event (type == \p engine.util.activity.event.Event). + * + * @param obj nyan object. + * + * @return true if the object is an event, else false. + */ + static bool is_event(const nyan::Object &obj); + + /** + * Get the primer function for an event type. + * + * @param event nyan object. + * + * @return Event primer function. + */ + static activity::event_primer_t get_primer(const nyan::Object &event); +}; + } // namespace api } // namespace openage::gamestate diff --git a/libopenage/gamestate/api/definitions.h b/libopenage/gamestate/api/definitions.h index d97372962f..f9caacdbc2 100644 --- a/libopenage/gamestate/api/definitions.h +++ b/libopenage/gamestate/api/definitions.h @@ -11,6 +11,10 @@ #include "gamestate/activity/types.h" #include "gamestate/api/types.h" +#include "gamestate/activity/xor_event_gate.h" +#include "gamestate/event/process_command.h" +#include "gamestate/event/wait.h" + namespace openage::gamestate::api { @@ -59,6 +63,14 @@ static const auto ACTIVITY_NODE_DEFS = datastructure::create_const_map( + std::pair("engine.util.activity.event.type.Command", + std::function(gamestate::event::primer_process_command)), + std::pair("engine.util.activity.event.type.Wait", + std::function(gamestate::event::primer_wait)), + std::pair("engine.util.activity.event.type.WaitAbility", + std::function(gamestate::event::primer_wait))); + /** * Maps internal patch property types to nyan API values. */ From ca4f15e4271864254e28e67144931295df867b44 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 20 Dec 2023 02:34:26 +0100 Subject: [PATCH 123/771] gamestate: Create connections between nodes in activity graph. --- libopenage/gamestate/entity_factory.cpp | 34 ++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index e9b000d7ae..14f833eef7 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -252,6 +252,7 @@ void EntityFactory::init_activity(const std::shared_ptr(node_id); + // TODO: Set system ID break; case activity::node_t::XOR_GATE: node_id_map[node_id] = std::make_shared(node_id); @@ -272,16 +273,41 @@ void EntityFactory::init_activity(const std::shared_ptrget_object(node.first); - auto activity_node = node_id_map[node.second]; + for (const auto ¤t_node : visited) { + auto nyan_node = owner_db_view->get_object(current_node.first); + auto activity_node = node_id_map[current_node.second]; auto next_nodes = api::APIActivityNode::get_next(nyan_node); for (const auto &next_node : next_nodes) { auto next_node_id = visited[next_node.get_name()]; auto next_engine_node = node_id_map[next_node_id]; - // activity_node->add_output(next_engine_node); + switch (activity_node->get_type()) { + case activity::node_t::END: + break; + case activity::node_t::START: { + auto start = std::static_pointer_cast(activity_node); + start->add_output(next_engine_node); + break; + } + case activity::node_t::TASK_SYSTEM: { + auto task_system = std::static_pointer_cast(activity_node); + task_system->add_output(next_engine_node); + break; + } + case activity::node_t::XOR_GATE: { + auto xor_gate = std::static_pointer_cast(activity_node); + // TODO: Add conditions + break; + } + case activity::node_t::XOR_EVENT_GATE: { + auto xor_event_gate = std::static_pointer_cast(activity_node); + xor_event_gate->add_output(next_engine_node, api::APIActivityEvent::get_primer(next_node)); + break; + } + default: + throw Error{ERR << "Unknown activity node type"}; + } } } From 24d83c02da142a7f09279e0b69d07c3b8fd15b86 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Dec 2023 03:53:40 +0100 Subject: [PATCH 124/771] gamestate: Set default node separately from other outputs. --- libopenage/gamestate/activity/tests.cpp | 4 +-- libopenage/gamestate/activity/xor_gate.cpp | 30 ++++++++-------------- libopenage/gamestate/activity/xor_gate.h | 22 ++++++++-------- libopenage/gamestate/entity_factory.cpp | 8 +----- libopenage/gamestate/system/activity.cpp | 2 +- 5 files changed, 25 insertions(+), 41 deletions(-) diff --git a/libopenage/gamestate/activity/tests.cpp b/libopenage/gamestate/activity/tests.cpp index 64997c9c74..1ddad0e7e8 100644 --- a/libopenage/gamestate/activity/tests.cpp +++ b/libopenage/gamestate/activity/tests.cpp @@ -152,7 +152,7 @@ const std::shared_ptr activity_flow(const std::shared_ptr(current); - auto next_id = node->get_default_id(); + auto next_id = node->get_default()->get_id(); for (auto &condition : node->get_conditions()) { auto condition_func = condition.second; if (condition_func(0, nullptr)) { @@ -251,7 +251,7 @@ void activity_demo() { return true; }; xor_node->add_output(event_node, branch_event); - xor_node->set_default_id(event_node->get_id()); + xor_node->set_default(event_node); // event node activity::event_primer_t primer = [&](const time::time_t & /* time */, diff --git a/libopenage/gamestate/activity/xor_gate.cpp b/libopenage/gamestate/activity/xor_gate.cpp index 6069968ba1..5d37908f8b 100644 --- a/libopenage/gamestate/activity/xor_gate.cpp +++ b/libopenage/gamestate/activity/xor_gate.cpp @@ -11,17 +11,17 @@ XorGate::XorGate(node_id_t id, node_label_t label) : Node{id, label, {}}, conditions{}, - default_id{std::nullopt} { + default_node{nullptr} { } XorGate::XorGate(node_id_t id, node_label_t label, const std::vector> &outputs, const std::vector &conditions, - const node_id_t default_id) : + const std::shared_ptr &default_node) : Node{id, label, outputs}, conditions{}, - default_id{std::nullopt} { + default_node{default_node} { if (conditions.size() != outputs.size()) [[unlikely]] { throw Error{MSG(err) << "XorGate " << this->str() << " has " << outputs.size() << " outputs but " << conditions.size() << " conditions"}; @@ -30,39 +30,29 @@ XorGate::XorGate(node_id_t id, for (size_t i = 0; i < conditions.size(); ++i) { this->conditions.emplace(outputs[i]->get_id(), conditions[i]); } - - this->set_default_id(default_id); } void XorGate::add_output(const std::shared_ptr &output, const condition_t condition_func) { this->outputs.emplace(output->get_id(), output); this->conditions.emplace(output->get_id(), condition_func); - - // If this is the first output, set it as the default. - if (not this->default_id) [[unlikely]] { - this->default_id = output->get_id(); - } } const std::map &XorGate::get_conditions() const { return this->conditions; } -node_id_t XorGate::get_default_id() const { - if (not this->default_id) [[unlikely]] { - throw Error{MSG(err) << "XorGate " << this->str() << " has no default output"}; - } - - return this->default_id.value(); +const std::shared_ptr &XorGate::get_default() const { + return this->default_node; } -void XorGate::set_default_id(node_id_t id) { - if (not this->outputs.contains(id)) [[unlikely]] { - throw Error{MSG(err) << "XorGate " << this->str() << " has no output with id " << id}; +void XorGate::set_default(const std::shared_ptr &node) { + if (this->default_node != nullptr) { + throw Error{MSG(err) << "XorGate " << this->str() << " already has a default node"}; } - this->default_id = id; + this->outputs.emplace(node->get_id(), node); + this->default_node = node; } } // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/xor_gate.h b/libopenage/gamestate/activity/xor_gate.h index 34870472cf..0e85a4bf21 100644 --- a/libopenage/gamestate/activity/xor_gate.h +++ b/libopenage/gamestate/activity/xor_gate.h @@ -54,13 +54,13 @@ class XorGate : public Node { * @param label Label of the node. * @param outputs Output nodes. * @param conditions Conditions for each output node. - * @param default_id Default output node ID. + * @param default_node Default output node. Chosen if no condition is true. */ XorGate(node_id_t id, node_label_t label, const std::vector> &outputs, const std::vector &conditions, - const node_id_t default_id); + const std::shared_ptr &default_node); virtual ~XorGate() = default; @@ -86,20 +86,20 @@ class XorGate : public Node { const std::map &get_conditions() const; /** - * Get the ID of the default output node. + * Get the default output node. * - * @return Default output node ID. + * @return Default output node. */ - node_id_t get_default_id() const; + const std::shared_ptr &get_default() const; /** - * Set the ID of the default output node. + * Set the the default output node. * - * The ID must be a valid node ID of one of the output nodes. + * This node is chosen if no condition is true. * - * @param id Default output node ID. + * @param node Default output node. */ - void set_default_id(node_id_t id); + void set_default(const std::shared_ptr &node); private: /** @@ -110,9 +110,9 @@ class XorGate : public Node { std::map conditions; /** - * Default output node ID. Chosen if no condition is true. + * Default output node. Chosen if no condition is true. */ - std::optional default_id; + std::shared_ptr default_node; }; } // namespace activity diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index 14f833eef7..09600c8b7a 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -89,13 +89,7 @@ std::shared_ptr create_test_activity() { condition_moveable->add_output(wait_for_command, wait_branch); // end branch - activity::condition_t end_branch = [&](const time::time_t & /* time */, - const std::shared_ptr & /* entity */) { - // no checks, this is the default branch - return true; - }; - condition_moveable->add_output(end, end_branch); - condition_moveable->set_default_id(end->get_id()); + condition_moveable->set_default(end); wait_for_command->add_output(move, gamestate::event::primer_process_command); diff --git a/libopenage/gamestate/system/activity.cpp b/libopenage/gamestate/system/activity.cpp index f63488edd0..49371fba30 100644 --- a/libopenage/gamestate/system/activity.cpp +++ b/libopenage/gamestate/system/activity.cpp @@ -85,7 +85,7 @@ void Activity::advance(const time::time_t &start_time, } break; case activity::node_t::XOR_GATE: { auto node = std::static_pointer_cast(current_node); - auto next_id = node->get_default_id(); + 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)) { From a07f7d2269360f93fd7b476b6971764bd32bde61 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Dec 2023 23:00:18 +0100 Subject: [PATCH 125/771] refactor: Move event primers into activity subfolders. --- libopenage/gamestate/activity/CMakeLists.txt | 2 + .../gamestate/activity/event/CMakeLists.txt | 4 ++ .../activity/event/command_in_queue.cpp | 35 +++++++++++++++ .../activity/event/command_in_queue.h | 42 ++++++++++++++++++ libopenage/gamestate/activity/event/wait.cpp | 29 ++++++++++++ libopenage/gamestate/activity/event/wait.h | 44 +++++++++++++++++++ libopenage/gamestate/api/definitions.h | 13 +++--- libopenage/gamestate/entity_factory.cpp | 11 +++-- .../gamestate/event/process_command.cpp | 24 ---------- libopenage/gamestate/event/process_command.h | 17 ------- libopenage/gamestate/event/wait.cpp | 19 -------- libopenage/gamestate/event/wait.h | 17 ------- 12 files changed, 167 insertions(+), 90 deletions(-) create mode 100644 libopenage/gamestate/activity/event/CMakeLists.txt create mode 100644 libopenage/gamestate/activity/event/command_in_queue.cpp create mode 100644 libopenage/gamestate/activity/event/command_in_queue.h create mode 100644 libopenage/gamestate/activity/event/wait.cpp create mode 100644 libopenage/gamestate/activity/event/wait.h diff --git a/libopenage/gamestate/activity/CMakeLists.txt b/libopenage/gamestate/activity/CMakeLists.txt index b005c32001..8d07a244a2 100644 --- a/libopenage/gamestate/activity/CMakeLists.txt +++ b/libopenage/gamestate/activity/CMakeLists.txt @@ -10,3 +10,5 @@ add_sources(libopenage xor_event_gate.cpp xor_gate.cpp ) + +add_subdirectory("event") diff --git a/libopenage/gamestate/activity/event/CMakeLists.txt b/libopenage/gamestate/activity/event/CMakeLists.txt new file mode 100644 index 0000000000..863fa6d28a --- /dev/null +++ b/libopenage/gamestate/activity/event/CMakeLists.txt @@ -0,0 +1,4 @@ +add_sources(libopenage + command_in_queue.cpp + wait.cpp +) diff --git a/libopenage/gamestate/activity/event/command_in_queue.cpp b/libopenage/gamestate/activity/event/command_in_queue.cpp new file mode 100644 index 0000000000..51a82750d0 --- /dev/null +++ b/libopenage/gamestate/activity/event/command_in_queue.cpp @@ -0,0 +1,35 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "command_in_queue.h" + +#include "event/event_loop.h" +#include "event/evententity.h" +#include "gamestate/component/internal/command_queue.h" +#include "gamestate/game_entity.h" +#include "gamestate/game_state.h" +#include "gamestate/manager.h" + + +namespace openage::gamestate::activity { + +std::shared_ptr primer_command_in_queue(const time::time_t &, + const std::shared_ptr &entity, + const std::shared_ptr &loop, + const std::shared_ptr &state, + size_t next_id) { + openage::event::EventHandler::param_map::map_t params{{"next", next_id}}; // move->get_id(); + auto ev = loop->create_event("game.process_command", + entity->get_manager(), + state, + // event is not executed until a command is available + std::numeric_limits::max(), + params); + auto entity_queue = std::dynamic_pointer_cast( + entity->get_component(component::component_t::COMMANDQUEUE)); + auto &queue = const_cast> &>(entity_queue->get_queue()); + queue.add_dependent(ev); + + return ev; +}; + +} // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/event/command_in_queue.h b/libopenage/gamestate/activity/event/command_in_queue.h new file mode 100644 index 0000000000..ebb71c166d --- /dev/null +++ b/libopenage/gamestate/activity/event/command_in_queue.h @@ -0,0 +1,42 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "time/time.h" + + +namespace openage { +namespace event { +class Event; +class EventLoop; +} // namespace event + +namespace gamestate { +class GameEntity; +class GameState; + +namespace activity { + +/** + * Primer for command in queue events in the activity system. + * + * @param time Current simulation time. + * @param entity Game entity. + * @param loop Event loop that the event is registered on. + * @param state Game state. + * @param next_id ID of the next node in the activity graph. + * + * @return Scheduled event. + */ +std::shared_ptr primer_command_in_queue(const time::time_t &, + const std::shared_ptr &entity, + const std::shared_ptr &loop, + const std::shared_ptr &state, + size_t next_id); + +} // namespace activity +} // namespace gamestate +} // namespace openage diff --git a/libopenage/gamestate/activity/event/wait.cpp b/libopenage/gamestate/activity/event/wait.cpp new file mode 100644 index 0000000000..6bfd03f632 --- /dev/null +++ b/libopenage/gamestate/activity/event/wait.cpp @@ -0,0 +1,29 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "wait.h" + +#include "event/event_loop.h" +#include "event/evententity.h" +#include "gamestate/game_entity.h" +#include "gamestate/game_state.h" +#include "gamestate/manager.h" + + +namespace openage::gamestate::activity { + +std::shared_ptr primer_wait(const time::time_t &time, + const std::shared_ptr &entity, + const std::shared_ptr &loop, + const std::shared_ptr &state, + size_t next_id) { + openage::event::EventHandler::param_map::map_t params{{"next", next_id}}; + auto ev = loop->create_event("game.wait", + entity->get_manager(), + state, + time, + params); + + return ev; +}; + +} // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/event/wait.h b/libopenage/gamestate/activity/event/wait.h new file mode 100644 index 0000000000..c33be2a5be --- /dev/null +++ b/libopenage/gamestate/activity/event/wait.h @@ -0,0 +1,44 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "time/time.h" + + +namespace openage { +namespace event { +class Event; +class EventLoop; +} // namespace event + +namespace gamestate { +class GameEntity; +class GameState; + +namespace activity { + + +/** + * Primer for wait events in the activity system. + * + * @param time Wait until this time. If the time is in the past, the event is executed immediately. + * @param entity Game entity. + * @param loop Event loop that the event is registered on. + * @param state Game state. + * @param next_id ID of the next node in the activity graph. + * + * @return Scheduled event. + */ +std::shared_ptr primer_wait(const time::time_t &time, + const std::shared_ptr &entity, + const std::shared_ptr &loop, + const std::shared_ptr &state, + size_t next_id); + + +} // namespace activity +} // namespace gamestate +} // namespace openage diff --git a/libopenage/gamestate/api/definitions.h b/libopenage/gamestate/api/definitions.h index f9caacdbc2..89f9ed17a7 100644 --- a/libopenage/gamestate/api/definitions.h +++ b/libopenage/gamestate/api/definitions.h @@ -8,12 +8,11 @@ #include #include "datastructure/constexpr_map.h" +#include "gamestate/activity/event/command_in_queue.h" +#include "gamestate/activity/event/wait.h" #include "gamestate/activity/types.h" -#include "gamestate/api/types.h" - #include "gamestate/activity/xor_event_gate.h" -#include "gamestate/event/process_command.h" -#include "gamestate/event/wait.h" +#include "gamestate/api/types.h" namespace openage::gamestate::api { @@ -65,11 +64,11 @@ static const auto ACTIVITY_NODE_DEFS = datastructure::create_const_map( std::pair("engine.util.activity.event.type.Command", - std::function(gamestate::event::primer_process_command)), + std::function(gamestate::activity::primer_command_in_queue)), std::pair("engine.util.activity.event.type.Wait", - std::function(gamestate::event::primer_wait)), + std::function(gamestate::activity::primer_wait)), std::pair("engine.util.activity.event.type.WaitAbility", - std::function(gamestate::event::primer_wait))); + std::function(gamestate::activity::primer_wait))); /** * Maps internal patch property types to nyan API values. diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index 09600c8b7a..f08bef7e6c 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -15,6 +15,8 @@ #include "event/event_loop.h" #include "gamestate/activity/activity.h" #include "gamestate/activity/end_node.h" +#include "gamestate/activity/event/command_in_queue.h" +#include "gamestate/activity/event/wait.h" #include "gamestate/activity/start_node.h" #include "gamestate/activity/task_system_node.h" #include "gamestate/activity/xor_event_gate.h" @@ -39,9 +41,6 @@ #include "time/time.h" #include "util/fixed_point.h" -#include "gamestate/event/process_command.h" -#include "gamestate/event/wait.h" - namespace openage::gamestate { /** @@ -91,13 +90,13 @@ std::shared_ptr create_test_activity() { // end branch condition_moveable->set_default(end); - wait_for_command->add_output(move, gamestate::event::primer_process_command); + wait_for_command->add_output(move, gamestate::activity::primer_command_in_queue); move->add_output(wait_for_move); move->set_system_id(system::system_id_t::MOVE_COMMAND); - wait_for_move->add_output(idle, gamestate::event::primer_wait); - wait_for_move->add_output(move, gamestate::event::primer_process_command); + wait_for_move->add_output(idle, gamestate::activity::primer_wait); + wait_for_move->add_output(move, gamestate::activity::primer_command_in_queue); return std::make_shared(0, start, "test"); } diff --git a/libopenage/gamestate/event/process_command.cpp b/libopenage/gamestate/event/process_command.cpp index bf5497bc16..d00762d8c9 100644 --- a/libopenage/gamestate/event/process_command.cpp +++ b/libopenage/gamestate/event/process_command.cpp @@ -2,10 +2,6 @@ #include "process_command.h" -#include "event/event_loop.h" -#include "gamestate/component/internal/command_queue.h" -#include "gamestate/game_entity.h" -#include "gamestate/game_state.h" #include "gamestate/manager.h" @@ -34,24 +30,4 @@ time::time_t ProcessCommandHandler::predict_invoke_time(const std::shared_ptr primer_process_command(const time::time_t &, - const std::shared_ptr &entity, - const std::shared_ptr &loop, - const std::shared_ptr &state, - size_t next_id) { - openage::event::EventHandler::param_map::map_t params{{"next", next_id}}; // move->get_id(); - auto ev = loop->create_event("game.process_command", - entity->get_manager(), - state, - // event is not executed until a command is available - std::numeric_limits::max(), - params); - auto entity_queue = std::dynamic_pointer_cast( - entity->get_component(component::component_t::COMMANDQUEUE)); - auto &queue = const_cast> &>(entity_queue->get_queue()); - queue.add_dependent(ev); - - return ev; -}; - } // namespace openage::gamestate::event diff --git a/libopenage/gamestate/event/process_command.h b/libopenage/gamestate/event/process_command.h index fb59558fcb..db4fb18157 100644 --- a/libopenage/gamestate/event/process_command.h +++ b/libopenage/gamestate/event/process_command.h @@ -46,23 +46,6 @@ class ProcessCommandHandler : public openage::event::OnceEventHandler { }; -/** - * Primer for process command events in the activity system. - * - * @param time Current simulation time. - * @param entity Game entity. - * @param loop Event loop that the event is registered on. - * @param state Game state. - * @param next_id ID of the next node in the activity graph. - * - * @return Scheduled event. - */ -std::shared_ptr primer_process_command(const time::time_t &, - const std::shared_ptr &entity, - const std::shared_ptr &loop, - const std::shared_ptr &state, - size_t next_id); - } // namespace event } // namespace gamestate } // namespace openage diff --git a/libopenage/gamestate/event/wait.cpp b/libopenage/gamestate/event/wait.cpp index 955cdd7b91..eb2e793334 100644 --- a/libopenage/gamestate/event/wait.cpp +++ b/libopenage/gamestate/event/wait.cpp @@ -2,9 +2,6 @@ #include "wait.h" -#include "event/event_loop.h" -#include "gamestate/game_entity.h" -#include "gamestate/game_state.h" #include "gamestate/manager.h" @@ -34,20 +31,4 @@ time::time_t WaitHandler::predict_invoke_time(const std::shared_ptr primer_wait(const time::time_t &time, - const std::shared_ptr &entity, - const std::shared_ptr &loop, - const std::shared_ptr &state, - size_t next_id) { - openage::event::EventHandler::param_map::map_t params{{"next", next_id}}; - auto ev = loop->create_event("game.wait", - entity->get_manager(), - state, - time, - params); - - return ev; -}; - - } // namespace openage::gamestate::event diff --git a/libopenage/gamestate/event/wait.h b/libopenage/gamestate/event/wait.h index a4b4c20fcd..1c74015984 100644 --- a/libopenage/gamestate/event/wait.h +++ b/libopenage/gamestate/event/wait.h @@ -45,23 +45,6 @@ class WaitHandler : public openage::event::OnceEventHandler { const time::time_t &at) override; }; -/** - * Primer for wait events in the activity system. - * - * @param time Wait until this time. If the time is in the past, the event is executed immediately. - * @param entity Game entity. - * @param loop Event loop that the event is registered on. - * @param state Game state. - * @param next_id ID of the next node in the activity graph. - * - * @return Scheduled event. - */ -std::shared_ptr primer_wait(const time::time_t &time, - const std::shared_ptr &entity, - const std::shared_ptr &loop, - const std::shared_ptr &state, - size_t next_id); - } // namespace event } // namespace gamestate } // namespace openage From 7c12205132727aec7fdec9451fd53111e1b1a320 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Dec 2023 23:36:16 +0100 Subject: [PATCH 126/771] gamestate: Add built-in condition functions to activity system. --- libopenage/gamestate/activity/CMakeLists.txt | 1 + .../activity/condition/CMakeLists.txt | 4 ++ .../activity/condition/command_in_queue.cpp | 19 +++++++++ .../activity/condition/command_in_queue.h | 28 +++++++++++++ .../activity/condition/next_command.cpp | 37 ++++++++++++++++++ .../activity/condition/next_command.h | 39 +++++++++++++++++++ libopenage/gamestate/api/definitions.h | 13 +++++++ 7 files changed, 141 insertions(+) create mode 100644 libopenage/gamestate/activity/condition/CMakeLists.txt create mode 100644 libopenage/gamestate/activity/condition/command_in_queue.cpp create mode 100644 libopenage/gamestate/activity/condition/command_in_queue.h create mode 100644 libopenage/gamestate/activity/condition/next_command.cpp create mode 100644 libopenage/gamestate/activity/condition/next_command.h diff --git a/libopenage/gamestate/activity/CMakeLists.txt b/libopenage/gamestate/activity/CMakeLists.txt index 8d07a244a2..78a78e7ab0 100644 --- a/libopenage/gamestate/activity/CMakeLists.txt +++ b/libopenage/gamestate/activity/CMakeLists.txt @@ -12,3 +12,4 @@ add_sources(libopenage ) add_subdirectory("event") +add_subdirectory("condition") diff --git a/libopenage/gamestate/activity/condition/CMakeLists.txt b/libopenage/gamestate/activity/condition/CMakeLists.txt new file mode 100644 index 0000000000..aabd159b0c --- /dev/null +++ b/libopenage/gamestate/activity/condition/CMakeLists.txt @@ -0,0 +1,4 @@ +add_sources(libopenage + command_in_queue.cpp + next_command.cpp +) diff --git a/libopenage/gamestate/activity/condition/command_in_queue.cpp b/libopenage/gamestate/activity/condition/command_in_queue.cpp new file mode 100644 index 0000000000..7e300701f1 --- /dev/null +++ b/libopenage/gamestate/activity/condition/command_in_queue.cpp @@ -0,0 +1,19 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "next_command.h" + +#include "gamestate/component/internal/command_queue.h" +#include "gamestate/game_entity.h" + + +namespace openage::gamestate::activity { + +bool command_in_queue(const time::time_t &time, + const std::shared_ptr &entity) { + auto command_queue = std::dynamic_pointer_cast( + entity->get_component(component::component_t::COMMANDQUEUE)); + + return not command_queue->get_queue().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 new file mode 100644 index 0000000000..67c7a794cc --- /dev/null +++ b/libopenage/gamestate/activity/condition/command_in_queue.h @@ -0,0 +1,28 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "time/time.h" + + +namespace openage::gamestate { +class GameEntity; + +namespace activity { + +/** + * Condition for command in queue check in the activity system. + * + * @param time Time when the condition is checked. + * @param entity Game entity. + * + * @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); + +} // namespace activity +} // namespace openage::gamestate diff --git a/libopenage/gamestate/activity/condition/next_command.cpp b/libopenage/gamestate/activity/condition/next_command.cpp new file mode 100644 index 0000000000..c0b619a783 --- /dev/null +++ b/libopenage/gamestate/activity/condition/next_command.cpp @@ -0,0 +1,37 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "next_command.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) { + auto command_queue = std::dynamic_pointer_cast( + entity->get_component(component::component_t::COMMANDQUEUE)); + + if (command_queue->get_queue().empty(time)) { + return false; + } + + auto command = command_queue->get_queue().front(time); + return command->get_type() == component::command::command_t::MOVE; +} + +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 command = command_queue->get_queue().front(time); + return command->get_type() == component::command::command_t::MOVE; +} + +} // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/condition/next_command.h b/libopenage/gamestate/activity/condition/next_command.h new file mode 100644 index 0000000000..046a18cec6 --- /dev/null +++ b/libopenage/gamestate/activity/condition/next_command.h @@ -0,0 +1,39 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "time/time.h" + + +namespace openage::gamestate { +class GameEntity; + +namespace activity { + +/** + * Condition for next command check in the activity system. + * + * @param time Time when the condition is checked. + * @param entity Game entity. + * + * @return true if the entity has 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. + * + * @param time Time when the condition is checked. + * @param entity Game entity. + * + * @return true if the entity has a move command next in the queue, false otherwise. + */ +bool next_command_move(const time::time_t &time, + const std::shared_ptr &entity); + +} // namespace activity +} // namespace openage::gamestate diff --git a/libopenage/gamestate/api/definitions.h b/libopenage/gamestate/api/definitions.h index 89f9ed17a7..cceaa277c3 100644 --- a/libopenage/gamestate/api/definitions.h +++ b/libopenage/gamestate/api/definitions.h @@ -8,10 +8,13 @@ #include #include "datastructure/constexpr_map.h" +#include "gamestate/activity/condition/command_in_queue.h" +#include "gamestate/activity/condition/next_command.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/api/types.h" @@ -62,6 +65,16 @@ static const auto ACTIVITY_NODE_DEFS = datastructure::create_const_map( + std::pair("engine.util.activity.condition.type.CommandInQueue", + std::function(gamestate::activity::command_in_queue)), + // TODO: API object assignment is inconsistent here + // Ideally all conditions should be an activity condition type + std::pair("engine.util.command.type.Idle", + std::function(gamestate::activity::next_command_idle)), + std::pair("engine.util.command.type.Move", + std::function(gamestate::activity::next_command_move))); + static const auto ACTIVITY_EVENT_PRIMERS = datastructure::create_const_map( std::pair("engine.util.activity.event.type.Command", std::function(gamestate::activity::primer_command_in_queue)), From 831a4f89a16d389e48f0f82f3ec24c60f983306e Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Dec 2023 23:59:34 +0100 Subject: [PATCH 127/771] gamestate: Use built-in condition for example activity. --- libopenage/gamestate/entity_factory.cpp | 43 +++++++++++++------------ 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index f08bef7e6c..9376f775a3 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -14,6 +14,7 @@ #include "curve/queue.h" #include "event/event_loop.h" #include "gamestate/activity/activity.h" +#include "gamestate/activity/condition/command_in_queue.h" #include "gamestate/activity/end_node.h" #include "gamestate/activity/event/command_in_queue.h" #include "gamestate/activity/event/wait.h" @@ -49,19 +50,9 @@ namespace openage::gamestate { * The activity is as follows: * |------------------------------------------------------| * | v - * Start -> Idle -> Condition -> Wait for command -> Move -> Wait for move -> End - * ^ | - * |------------------------------------------------------| - * - * TODO: Should be: - * |----------------------------------------------------------------------| - * | v - * Start -> Idle -> -> Condition -> Wait for command <-> Condition -> Move -> Wait or command -> End - * ^ |^ | - * |---------------------------------------------||---------------------| - * (new condition in the middle: check if there is a command, if not go back to wait for command) - * (new node: go back to node 1 if there is no command) - * (node 5: wait for a command OR for a the wait time) + * Start -> Idle -> Condition -> Condition -> Wait for command -> Move -> Wait for move -> End + * ^ | + * |----------------------------------------------------------------| * * TODO: Replace with config */ @@ -69,33 +60,45 @@ std::shared_ptr create_test_activity() { auto start = std::make_shared(0); auto idle = std::make_shared(1, "Idle"); auto condition_moveable = std::make_shared(2); - auto wait_for_command = std::make_shared(3); - auto condition_command = std::make_shared(4); + auto condition_command = std::make_shared(3); + auto wait_for_command = std::make_shared(4); auto move = std::make_shared(5, "Move"); auto wait_for_move = std::make_shared(6); auto end = std::make_shared(7); start->add_output(idle); + // idle after start idle->add_output(condition_moveable); idle->set_system_id(system::system_id_t::IDLE); - // wait_for_command branch - activity::condition_t wait_branch = [&](const time::time_t & /* time */, - const std::shared_ptr &entity) { + // branch 1: check if the entity is moveable + activity::condition_t command_branch = [&](const time::time_t & /* time */, + const std::shared_ptr &entity) { return entity->has_component(component::component_t::MOVE); }; - condition_moveable->add_output(wait_for_command, wait_branch); + condition_moveable->add_output(condition_command, command_branch); - // end 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); + + // default: if there is no command, wait for a command + condition_command->set_default(wait_for_command); + + // wait for a command event wait_for_command->add_output(move, gamestate::activity::primer_command_in_queue); + // move move->add_output(wait_for_move); move->set_system_id(system::system_id_t::MOVE_COMMAND); + // branch 1: wait for move event to finish wait_for_move->add_output(idle, gamestate::activity::primer_wait); + + // branch 2: wait for a new command event wait_for_move->add_output(move, gamestate::activity::primer_command_in_queue); return std::make_shared(0, start, "test"); From 97460f0a599b5ec122435c76747f15940964b265 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 00:04:58 +0100 Subject: [PATCH 128/771] gamestate: Fix command not returned by reference. --- libopenage/gamestate/component/internal/command_queue.cpp | 2 +- libopenage/gamestate/component/internal/command_queue.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/gamestate/component/internal/command_queue.cpp b/libopenage/gamestate/component/internal/command_queue.cpp index 6896f63cdf..b1e8ce1d47 100644 --- a/libopenage/gamestate/component/internal/command_queue.cpp +++ b/libopenage/gamestate/component/internal/command_queue.cpp @@ -26,7 +26,7 @@ const curve::Queue> &CommandQueue::get_queue() return this->command_queue; } -std::shared_ptr CommandQueue::pop_command(const time::time_t &time) { +const std::shared_ptr &CommandQueue::pop_command(const time::time_t &time) { return this->command_queue.pop_front(time); } diff --git a/libopenage/gamestate/component/internal/command_queue.h b/libopenage/gamestate/component/internal/command_queue.h index 6c16d5cbc7..4c63327a3b 100644 --- a/libopenage/gamestate/component/internal/command_queue.h +++ b/libopenage/gamestate/component/internal/command_queue.h @@ -53,7 +53,7 @@ class CommandQueue : public InternalComponent { * * @return Command in the front of the queue or nullptr if the queue is empty. */ - std::shared_ptr pop_command(const time::time_t &time); + const std::shared_ptr &pop_command(const time::time_t &time); private: /** From 8db98076121bc50e497379b5c92e944d6e441ed6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 01:39:04 +0100 Subject: [PATCH 129/771] gamestate: Detect condition from API objects. --- libopenage/gamestate/api/activity.cpp | 23 ++++++++++++++++++++++- libopenage/gamestate/api/activity.h | 25 +++++++++++++++++++++++++ libopenage/gamestate/api/definitions.h | 6 ++---- libopenage/gamestate/entity_factory.cpp | 2 +- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/libopenage/gamestate/api/activity.cpp b/libopenage/gamestate/api/activity.cpp index 34d7896e8b..b47f1046cd 100644 --- a/libopenage/gamestate/api/activity.cpp +++ b/libopenage/gamestate/api/activity.cpp @@ -44,7 +44,18 @@ std::vector APIActivityNode::get_next(const nyan::Object &node) { return {db_view->get_object(next->get_name())}; } // 1+ next nodes - case activity::node_t::XOR_GATE: + case activity::node_t::XOR_GATE: { + auto next = node.get("Node.next"); + std::shared_ptr db_view = node.get_view(); + + std::vector next_nodes; + for (auto &next_node : next->get()) { + auto next_node_value = std::dynamic_pointer_cast(next_node.get_ptr()); + next_nodes.push_back(db_view->get_object(next_node_value->get_name())); + } + + return next_nodes; + } case activity::node_t::XOR_EVENT_GATE: { auto next = node.get("Node.next"); std::shared_ptr db_view = node.get_view(); @@ -62,6 +73,16 @@ std::vector APIActivityNode::get_next(const nyan::Object &node) { } } +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"; +} + +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); +} + 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"; diff --git a/libopenage/gamestate/api/activity.h b/libopenage/gamestate/api/activity.h index cc365da449..e3eeeef661 100644 --- a/libopenage/gamestate/api/activity.h +++ b/libopenage/gamestate/api/activity.h @@ -8,6 +8,7 @@ #include "gamestate/activity/types.h" #include "gamestate/activity/xor_event_gate.h" +#include "gamestate/activity/xor_gate.h" namespace openage::gamestate { @@ -74,6 +75,30 @@ class APIActivityNode { static std::vector get_next(const nyan::Object &node); }; +/** + * Helper class for creating Activity condition objects from the nyan API. + */ +class APIActivityCondition { +public: + /** + * Check if a nyan object is a condition (type == \p engine.util.activity.condition.Condition). + * + * @param obj nyan object. + * + * @return true if the object is a condition, else false. + */ + static bool is_condition(const nyan::Object &obj); + + /** + * Get the condition function for a condition. + * + * @param condition nyan object. + * + * @return Condition function. + */ + static activity::condition_t get_condition(const nyan::Object &condition); +}; + /** * Helper class for creating Activity event objects from the nyan API. */ diff --git a/libopenage/gamestate/api/definitions.h b/libopenage/gamestate/api/definitions.h index cceaa277c3..56877de4e3 100644 --- a/libopenage/gamestate/api/definitions.h +++ b/libopenage/gamestate/api/definitions.h @@ -68,11 +68,9 @@ static const auto ACTIVITY_NODE_DEFS = datastructure::create_const_map( std::pair("engine.util.activity.condition.type.CommandInQueue", std::function(gamestate::activity::command_in_queue)), - // TODO: API object assignment is inconsistent here - // Ideally all conditions should be an activity condition type - std::pair("engine.util.command.type.Idle", + std::pair("engine.util.activity.condition.type.NextCommandIdle", std::function(gamestate::activity::next_command_idle)), - std::pair("engine.util.command.type.Move", + std::pair("engine.util.activity.condition.type.NextCommandMove", std::function(gamestate::activity::next_command_move))); static const auto ACTIVITY_EVENT_PRIMERS = datastructure::create_const_map( diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index 9376f775a3..97bc79a789 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -293,7 +293,7 @@ void EntityFactory::init_activity(const std::shared_ptr(activity_node); - // TODO: Add conditions + xor_gate->add_output(next_engine_node, api::APIActivityCondition::get_condition(nyan_node)); break; } case activity::node_t::XOR_EVENT_GATE: { From 31018e338d5ae61dc283f48dab01a5662085d9bb Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 01:57:20 +0100 Subject: [PATCH 130/771] gamestate: Detect system task from API objects. --- libopenage/gamestate/api/activity.cpp | 11 +++++++++++ libopenage/gamestate/api/activity.h | 10 ++++++++++ libopenage/gamestate/api/definitions.h | 15 +++++++++++++++ libopenage/gamestate/entity_factory.cpp | 12 +++++++----- 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/libopenage/gamestate/api/activity.cpp b/libopenage/gamestate/api/activity.cpp index b47f1046cd..82c5e6028a 100644 --- a/libopenage/gamestate/api/activity.cpp +++ b/libopenage/gamestate/api/activity.cpp @@ -73,6 +73,17 @@ std::vector APIActivityNode::get_next(const nyan::Object &node) { } } +system::system_id_t APIActivityNode::get_system_id(const nyan::Object &ability_node) { + auto ability = ability_node.get("Ability.ability"); + std::shared_ptr db_view = ability_node.get_view(); + + if (not ACTIVITY_TASK_SYSTEM_DEFS.contains(ability->get_name())) [[unlikely]] { + throw Error(MSG(err) << "Ability '" << ability->get_name() << "' has no associated system defined."); + } + + return ACTIVITY_TASK_SYSTEM_DEFS.get(ability->get_name()); +} + 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"; diff --git a/libopenage/gamestate/api/activity.h b/libopenage/gamestate/api/activity.h index e3eeeef661..f1736d8d1c 100644 --- a/libopenage/gamestate/api/activity.h +++ b/libopenage/gamestate/api/activity.h @@ -9,6 +9,7 @@ #include "gamestate/activity/types.h" #include "gamestate/activity/xor_event_gate.h" #include "gamestate/activity/xor_gate.h" +#include "gamestate/system/types.h" namespace openage::gamestate { @@ -73,6 +74,15 @@ class APIActivityNode { * @return nyan object handles of the next nodes. */ static std::vector get_next(const nyan::Object &node); + + /** + * Get the system id of an Ability node. + * + * @param node nyan object. + * + * @return System ID of the node. + */ + static system::system_id_t get_system_id(const nyan::Object &ability_node); }; /** diff --git a/libopenage/gamestate/api/definitions.h b/libopenage/gamestate/api/definitions.h index 56877de4e3..ee435bf048 100644 --- a/libopenage/gamestate/api/definitions.h +++ b/libopenage/gamestate/api/definitions.h @@ -16,6 +16,7 @@ #include "gamestate/activity/xor_event_gate.h" #include "gamestate/activity/xor_gate.h" #include "gamestate/api/types.h" +#include "gamestate/system/types.h" namespace openage::gamestate::api { @@ -65,6 +66,20 @@ static const auto ACTIVITY_NODE_DEFS = datastructure::create_const_map( + std::pair("engine.ability.type.Idle", + system::system_id_t::IDLE), + std::pair("engine.ability.type.Move", + system::system_id_t::MOVE_COMMAND)); + +/** + * Maps API activity condition types to engine condition types. + */ static const auto ACTIVITY_CONDITIONS = datastructure::create_const_map( std::pair("engine.util.activity.condition.type.CommandInQueue", std::function(gamestate::activity::command_in_queue)), diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index 97bc79a789..9abba34597 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -246,10 +246,12 @@ void EntityFactory::init_activity(const std::shared_ptr(node_id); node_id_map[node_id] = start_node; break; - case activity::node_t::TASK_SYSTEM: - node_id_map[node_id] = std::make_shared(node_id); - // TODO: Set system ID + case activity::node_t::TASK_SYSTEM: { + auto task_node = std::make_shared(node_id); + task_node->set_system_id(api::APIActivityNode::get_system_id(node)); + node_id_map[node_id] = task_node; break; + } case activity::node_t::XOR_GATE: node_id_map[node_id] = std::make_shared(node_id); break; @@ -257,7 +259,7 @@ void EntityFactory::init_activity(const std::shared_ptr(node_id); break; default: - throw Error{ERR << "Unknown activity node type"}; + throw Error{ERR << "Unknown activity node type of node: " << node.get_name()}; } // Get the node's outputs @@ -302,7 +304,7 @@ void EntityFactory::init_activity(const std::shared_ptr Date: Sun, 24 Dec 2023 02:09:20 +0100 Subject: [PATCH 131/771] gamestate: Fix reading from nyan activity condition API object. --- libopenage/gamestate/api/activity.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libopenage/gamestate/api/activity.cpp b/libopenage/gamestate/api/activity.cpp index 82c5e6028a..088c047103 100644 --- a/libopenage/gamestate/api/activity.cpp +++ b/libopenage/gamestate/api/activity.cpp @@ -45,12 +45,15 @@ std::vector APIActivityNode::get_next(const nyan::Object &node) { } // 1+ next nodes case activity::node_t::XOR_GATE: { - auto next = node.get("Node.next"); + auto conditions = node.get("Node.next"); std::shared_ptr db_view = node.get_view(); std::vector next_nodes; - for (auto &next_node : next->get()) { - auto next_node_value = std::dynamic_pointer_cast(next_node.get_ptr()); + for (auto &condition : conditions->get()) { + auto condition_fqon = std::dynamic_pointer_cast(condition.get_ptr()); + auto condition_obj = db_view->get_object(condition_fqon->get_name()); + + auto next_node_value = condition_obj.get("Condition.next"); next_nodes.push_back(db_view->get_object(next_node_value->get_name())); } From 537830aaf457ff66a524c7951af4933dd6fc90b6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 03:13:44 +0100 Subject: [PATCH 132/771] gamestate: Fix wrong member ID lookup. --- libopenage/gamestate/api/activity.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libopenage/gamestate/api/activity.cpp b/libopenage/gamestate/api/activity.cpp index 088c047103..f48db49867 100644 --- a/libopenage/gamestate/api/activity.cpp +++ b/libopenage/gamestate/api/activity.cpp @@ -37,15 +37,19 @@ std::vector APIActivityNode::get_next(const nyan::Object &node) { return {}; } // 1 next node - case activity::node_t::TASK_SYSTEM: + case activity::node_t::TASK_SYSTEM: { + auto next = node.get("Ability.next"); + std::shared_ptr db_view = node.get_view(); + return {db_view->get_object(next->get_name())}; + } case activity::node_t::START: { - auto next = node.get("Node.next"); + auto next = node.get("Start.next"); std::shared_ptr db_view = node.get_view(); return {db_view->get_object(next->get_name())}; } // 1+ next nodes case activity::node_t::XOR_GATE: { - auto conditions = node.get("Node.next"); + auto conditions = node.get("XORGate.next"); std::shared_ptr db_view = node.get_view(); std::vector next_nodes; @@ -60,7 +64,7 @@ std::vector APIActivityNode::get_next(const nyan::Object &node) { return next_nodes; } case activity::node_t::XOR_EVENT_GATE: { - auto next = node.get("Node.next"); + auto next = node.get("XOREventGate.next"); std::shared_ptr db_view = node.get_view(); std::vector next_nodes; From 30a6234abd58eaa0f5f54629aeceee1cfcabba63 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 03:39:58 +0100 Subject: [PATCH 133/771] gamestate: Fix lookup of activity conditions/events. --- libopenage/gamestate/api/activity.cpp | 10 ++- libopenage/gamestate/api/definitions.h | 2 +- libopenage/gamestate/entity_factory.cpp | 99 ++++++++++++++++--------- 3 files changed, 70 insertions(+), 41 deletions(-) diff --git a/libopenage/gamestate/api/activity.cpp b/libopenage/gamestate/api/activity.cpp index f48db49867..63dea05e6e 100644 --- a/libopenage/gamestate/api/activity.cpp +++ b/libopenage/gamestate/api/activity.cpp @@ -54,13 +54,16 @@ std::vector APIActivityNode::get_next(const nyan::Object &node) { std::vector next_nodes; for (auto &condition : conditions->get()) { - auto condition_fqon = std::dynamic_pointer_cast(condition.get_ptr()); - auto condition_obj = db_view->get_object(condition_fqon->get_name()); + auto condition_value = std::dynamic_pointer_cast(condition.get_ptr()); + auto condition_obj = db_view->get_object(condition_value->get_name()); auto next_node_value = condition_obj.get("Condition.next"); next_nodes.push_back(db_view->get_object(next_node_value->get_name())); } + auto default_next = node.get("XORGate.default"); + next_nodes.push_back(db_view->get_object(default_next->get_name())); + return next_nodes; } case activity::node_t::XOR_EVENT_GATE: { @@ -107,8 +110,7 @@ bool APIActivityEvent::is_event(const nyan::Object &obj) { } activity::event_primer_t APIActivityEvent::get_primer(const nyan::Object &event) { - nyan::fqon_t immediate_parent = event.get_parents()[0]; - return ACTIVITY_EVENT_PRIMERS.get(immediate_parent); + return ACTIVITY_EVENT_PRIMERS.get(event.get_name()); } } // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/definitions.h b/libopenage/gamestate/api/definitions.h index ee435bf048..f982571f94 100644 --- a/libopenage/gamestate/api/definitions.h +++ b/libopenage/gamestate/api/definitions.h @@ -89,7 +89,7 @@ static const auto ACTIVITY_CONDITIONS = datastructure::create_const_map( - std::pair("engine.util.activity.event.type.Command", + std::pair("engine.util.activity.event.type.CommandInQueue", std::function(gamestate::activity::primer_command_in_queue)), std::pair("engine.util.activity.event.type.Wait", std::function(gamestate::activity::primer_wait)), diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index 9abba34597..cc9009b7b9 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -205,13 +205,13 @@ void EntityFactory::init_components(const std::shared_ptr(loop, create_test_activity()); - entity->add_component(activity); - // } + if (activity_ability) { + init_activity(loop, owner_db_view, entity, activity_ability.value()); + } + else { + auto activity = std::make_shared(loop, create_test_activity()); + entity->add_component(activity); + } } void EntityFactory::init_activity(const std::shared_ptr &loop, @@ -275,37 +275,64 @@ void EntityFactory::init_activity(const std::shared_ptrget_object(current_node.first); auto activity_node = node_id_map[current_node.second]; - auto next_nodes = api::APIActivityNode::get_next(nyan_node); - for (const auto &next_node : next_nodes) { - auto next_node_id = visited[next_node.get_name()]; - auto next_engine_node = node_id_map[next_node_id]; - - switch (activity_node->get_type()) { - case activity::node_t::END: - break; - case activity::node_t::START: { - auto start = std::static_pointer_cast(activity_node); - start->add_output(next_engine_node); - break; - } - case activity::node_t::TASK_SYSTEM: { - auto task_system = std::static_pointer_cast(activity_node); - task_system->add_output(next_engine_node); - break; - } - case activity::node_t::XOR_GATE: { - auto xor_gate = std::static_pointer_cast(activity_node); - xor_gate->add_output(next_engine_node, api::APIActivityCondition::get_condition(nyan_node)); - break; - } - case activity::node_t::XOR_EVENT_GATE: { - auto xor_event_gate = std::static_pointer_cast(activity_node); - xor_event_gate->add_output(next_engine_node, api::APIActivityEvent::get_primer(next_node)); - break; + switch (activity_node->get_type()) { + case activity::node_t::END: + break; + case activity::node_t::START: { + auto start = std::static_pointer_cast(activity_node); + auto output_fqon = nyan_node.get("Start.next")->get_name(); + auto output_id = visited[output_fqon]; + auto output_node = node_id_map[output_id]; + start->add_output(output_node); + break; + } + case activity::node_t::TASK_SYSTEM: { + auto task_system = std::static_pointer_cast(activity_node); + auto output_fqon = nyan_node.get("Ability.next")->get_name(); + auto output_id = visited[output_fqon]; + auto output_node = node_id_map[output_id]; + task_system->add_output(output_node); + break; + } + case activity::node_t::XOR_GATE: { + auto xor_gate = std::static_pointer_cast(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 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)); } - default: - throw Error{ERR << "Unknown activity node type of node: " << current_node.first}; + + auto default_fqon = nyan_node.get("XORGate.default")->get_name(); + auto default_id = visited[default_fqon]; + auto default_node = node_id_map[default_id]; + xor_gate->set_default(default_node); + break; + } + case activity::node_t::XOR_EVENT_GATE: { + auto xor_event_gate = std::static_pointer_cast(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_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_obj = owner_db_view->get_object(next_node_value->get_name()); + + auto output_id = visited[next_node_obj.get_name()]; + auto output_node = node_id_map[output_id]; + + xor_event_gate->add_output(output_node, api::APIActivityEvent::get_primer(event_obj)); } + break; + } + default: + throw Error{ERR << "Unknown activity node type of node: " << current_node.first}; } } From bc1f82dacf9398301eb7ae8ec6f10e131510c9ef Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 21:55:41 +0100 Subject: [PATCH 134/771] curve: Fix queue behaviour when popping elements. --- libopenage/curve/queue.h | 85 +++++++++++++++---- libopenage/curve/tests/container.cpp | 40 ++++++++- .../component/internal/command_queue.cpp | 2 +- .../component/internal/command_queue.h | 2 +- 4 files changed, 108 insertions(+), 21 deletions(-) diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index 3ac8d20d9a..a6a0a142ef 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -53,7 +53,7 @@ class Queue : public event::EventEntity { EventEntity{loop}, _id{id}, _idstr{idstr}, - last_front{this->container.begin()} {} + last_pop{time::time_t::zero()} {} // prevent accidental copy of queue Queue(const Queue &) = delete; @@ -69,12 +69,13 @@ class Queue : public event::EventEntity { const T &front(const time::time_t &time) const; /** - * Get the first element in the queue at the given time. + * Get the first element in the queue at the given time and remove it from + * the queue. * * @param time The time to get the element at. * @param value Queue element. */ - const T &pop_front(const time::time_t &time); + const T pop_front(const time::time_t &time); /** * Check if the queue is empty at a given time. @@ -183,24 +184,74 @@ class Queue : public event::EventEntity { */ container_t container; - iterator last_front; + /** + * The time of the last access to the queue. + */ + time::time_t last_pop; }; template -const T &Queue::front(const time::time_t &t) const { - return this->begin(t).value(); +const T &Queue::front(const time::time_t &time) const { + if (this->empty(time)) [[unlikely]] { + throw Error{MSG(err) << "Tried accessing front at " + << time << " but queue is empty."}; + } + + // search for the first element that is after the given time + auto it = this->container.begin(); + ++it; + while (it != this->container.end() and it->time() <= time) { + ++it; + } + + // get the last element before the given time + --it; + + return it->value; } template -bool Queue::empty(const time::time_t &time) const { - return this->last_front == this->begin(time).get_base(); +const T Queue::pop_front(const time::time_t &time) { + if (this->empty(time)) [[unlikely]] { + throw Error{MSG(err) << "Tried accessing front at " + << time << " but queue is empty."}; + } + + // search for the first element that is after the given time + auto it = this->container.begin(); + ++it; + while (it->time() <= time and it != this->container.end()) { + ++it; + } + + auto to = it->time(); + auto from = time; + + // get the last element inserted before the given time + --it; + auto val = it->value; + + // erase the element + // TODO: We should be able to reinsert elements + auto filter_iterator = QueueFilterIterator>(it, this, to, from); + this->erase(filter_iterator); + + this->last_pop = time; + + return val; } template -inline const T &Queue::pop_front(const time::time_t &time) { - this->last_front = this->begin(time).get_base(); - return this->front(time); +bool Queue::empty(const time::time_t &time) const { + if (this->container.empty()) { + return true; + } + + // search for the first element that is after the given time + auto begin = this->begin(time).get_base(); + + return begin == this->container.begin() and begin->time() > time; } template @@ -230,15 +281,14 @@ QueueFilterIterator> Queue::end(const time::time_t &t) const { template -QueueFilterIterator> Queue::between( - const time::time_t &begin, - const time::time_t &end) const { +QueueFilterIterator> Queue::between(const time::time_t &begin, + const time::time_t &end) const { auto it = QueueFilterIterator>( container.begin(), this, begin, end); - if (!container.empty() && !it.valid()) { + if (not it.valid()) { ++it; } return it; @@ -253,9 +303,8 @@ void Queue::erase(const CurveIterator> &it) { template -QueueFilterIterator> Queue::insert( - const time::time_t &time, - const T &e) { +QueueFilterIterator> Queue::insert(const time::time_t &time, + const T &e) { const_iterator insertion_point = this->container.end(); for (auto it = this->container.begin(); it != this->container.end(); ++it) { if (time < it->time()) { diff --git a/libopenage/curve/tests/container.cpp b/libopenage/curve/tests/container.cpp index a5efbf2887..9787243db3 100644 --- a/libopenage/curve/tests/container.cpp +++ b/libopenage/curve/tests/container.cpp @@ -149,11 +149,25 @@ void test_queue() { auto loop = std::make_shared(); Queue q{loop, 0}; - q.insert(0, 1); + + TESTEQUALS(q.empty(0), true); + TESTEQUALS(q.empty(1), true); + TESTEQUALS(q.empty(100001), true); + q.insert(2, 2); q.insert(4, 3); q.insert(10, 4); q.insert(100001, 5); + q.insert(100001, 6); + + TESTEQUALS(q.empty(0), true); + TESTEQUALS(q.empty(1), true); + TESTEQUALS(q.empty(2), false); + TESTEQUALS(q.empty(100001), false); + TESTEQUALS(q.empty(100002), false); + + q.insert(0, 1); + TESTEQUALS(*q.begin(0), 1); TESTEQUALS(*q.begin(1), 2); TESTEQUALS(*q.begin(2), 2); @@ -164,6 +178,17 @@ void test_queue() { TESTEQUALS(*q.begin(12), 5); TESTEQUALS(*q.begin(100000), 5); + TESTEQUALS(q.front(0), 1); + TESTEQUALS(q.front(1), 1); + TESTEQUALS(q.front(2), 2); + TESTEQUALS(q.front(3), 2); + TESTEQUALS(q.front(4), 3); + TESTEQUALS(q.front(5), 3); + TESTEQUALS(q.front(10), 4); + TESTEQUALS(q.front(12), 4); + TESTEQUALS(q.front(100000), 4); + TESTEQUALS(q.front(100001), 6); + { std::unordered_set reference = {1, 2, 3}; for (auto it = q.between(0, 6); it != q.end(); ++it) { @@ -204,6 +229,19 @@ void test_queue() { } TESTEQUALS(reference.empty(), true); } + + + TESTEQUALS(q.pop_front(0), 1); + TESTEQUALS(q.empty(0), true); + + TESTEQUALS(q.pop_front(12), 4); + TESTEQUALS(q.empty(12), false); + + TESTEQUALS(q.pop_front(12), 3); + TESTEQUALS(q.empty(12), false); + + TESTEQUALS(q.pop_front(12), 2); + TESTEQUALS(q.empty(12), true); } diff --git a/libopenage/gamestate/component/internal/command_queue.cpp b/libopenage/gamestate/component/internal/command_queue.cpp index b1e8ce1d47..b22d9fe834 100644 --- a/libopenage/gamestate/component/internal/command_queue.cpp +++ b/libopenage/gamestate/component/internal/command_queue.cpp @@ -26,7 +26,7 @@ const curve::Queue> &CommandQueue::get_queue() return this->command_queue; } -const std::shared_ptr &CommandQueue::pop_command(const time::time_t &time) { +const std::shared_ptr CommandQueue::pop_command(const time::time_t &time) { return this->command_queue.pop_front(time); } diff --git a/libopenage/gamestate/component/internal/command_queue.h b/libopenage/gamestate/component/internal/command_queue.h index 4c63327a3b..f0725a686d 100644 --- a/libopenage/gamestate/component/internal/command_queue.h +++ b/libopenage/gamestate/component/internal/command_queue.h @@ -53,7 +53,7 @@ class CommandQueue : public InternalComponent { * * @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 pop_command(const time::time_t &time); private: /** From c269a67c0d70b9cf308e8405905ac7dc342f145a Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 21:57:27 +0100 Subject: [PATCH 135/771] convert: Fix game entity activity not idling after move. --- openage/convert/processor/conversion/aoc/pregen_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openage/convert/processor/conversion/aoc/pregen_processor.py b/openage/convert/processor/conversion/aoc/pregen_processor.py index ba41e11d86..db92c0fa8a 100644 --- a/openage/convert/processor/conversion/aoc/pregen_processor.py +++ b/openage/convert/processor/conversion/aoc/pregen_processor.py @@ -334,7 +334,7 @@ def generate_activities( wait_command = api_objects["engine.util.activity.event.type.CommandInQueue"] wait_raw_api_object.add_raw_member("next", { - wait_finish: queue_forward_ref, + 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 From c2244c4888d2e1da254f59aed4803ad168773e21 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 22:11:12 +0100 Subject: [PATCH 136/771] convert: Import aliases for nyan API activities. --- .../convert/processor/conversion/aoc/modpack_subprocessor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openage/convert/processor/conversion/aoc/modpack_subprocessor.py b/openage/convert/processor/conversion/aoc/modpack_subprocessor.py index 29764c5823..1c823c3ba6 100644 --- a/openage/convert/processor/conversion/aoc/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/modpack_subprocessor.py @@ -151,6 +151,11 @@ def _set_static_aliases(modpack: Modpack, import_tree: ImportTree) -> None: import_tree.add_alias(("engine", "ability", "property", "type"), "ability_prop") # Auxiliary objects + import_tree.add_alias( + ("engine", "util", "activity", "condition", "type"), "activity_condition" + ) + import_tree.add_alias(("engine", "util", "activity", "event", "type"), "activity_event") + import_tree.add_alias(("engine", "util", "activity", "node", "type"), "activity_node") import_tree.add_alias(("engine", "util", "accuracy"), "accuracy") import_tree.add_alias( ("engine", "util", "animation_override"), "animation_override" From d035829d47dc8f2ea986f10c1e6c5565f3220708 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 22:11:33 +0100 Subject: [PATCH 137/771] convert: Warn when alias cannot be added to import tree. --- openage/nyan/import_tree.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openage/nyan/import_tree.py b/openage/nyan/import_tree.py index 8a3880ff8d..5e61feb55c 100644 --- a/openage/nyan/import_tree.py +++ b/openage/nyan/import_tree.py @@ -7,6 +7,8 @@ from enum import Enum import typing +from openage.log import warn + if typing.TYPE_CHECKING: from openage.convert.entity_object.export.formats.nyan_file import NyanFile from openage.nyan.nyan_structs import NyanObject @@ -161,7 +163,9 @@ def add_alias(self, fqon: tuple[str], alias: str) -> None: current_node = current_node.get_child(node_str) except KeyError: # as err: - # TODO: Do not silently fail + # TODO: Fail when the fqon is not found in the tree + warn(f"fqon '{'.'.join(fqon)}' " + "could not be found in import tree") return # raise KeyError(f"fqon '{'.'.join(fqon)}' " # "could not be found in import tree") from err From c31e8615fdb3eda4a80c35df9d06e7c89741a6f6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 22:15:17 +0100 Subject: [PATCH 138/771] doc: Replace activity example in docs with current default activity. --- doc/code/game_simulation/images/activity_graph.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/code/game_simulation/images/activity_graph.svg b/doc/code/game_simulation/images/activity_graph.svg index bf87f779fa..4044797dc2 100644 --- a/doc/code/game_simulation/images/activity_graph.svg +++ b/doc/code/game_simulation/images/activity_graph.svg @@ -1,4 +1,4 @@ -IdleMoveStartMoveable?Can MoveCan't MoveWait for commandWait for Move to FinishEnd \ No newline at end of file +Idlecommand in queue?wait for commandbranch on commandGatherAttackMovemove finishednew commandcommand receivedMove \ No newline at end of file From 737e2b29065120477f9784d9fab4733719c58fea Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Dec 2023 22:52:19 +0100 Subject: [PATCH 139/771] gamestate: Cache created activities. --- libopenage/gamestate/entity_factory.cpp | 11 +++++++++++ libopenage/gamestate/entity_factory.h | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index cc9009b7b9..c9ab7ab472 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -219,6 +219,16 @@ void EntityFactory::init_activity(const std::shared_ptr &entity, const nyan::Object &ability) { nyan::Object graph = ability.get_object("Activity.graph"); + + // Check if the activity is already exists in the cache + if (this->activity_cache.contains(graph.get_name())) { + auto activity = this->activity_cache.at(graph.get_name()); + auto component = std::make_shared(loop, activity); + entity->add_component(component); + + return; + } + auto start_obj = api::APIActivity::get_start(graph); size_t node_id = 0; @@ -337,6 +347,7 @@ void EntityFactory::init_activity(const std::shared_ptr(0, start_node, graph.get_name()); + this->activity_cache.insert({graph.get_name(), activity}); auto component = std::make_shared(loop, activity); entity->add_component(component); diff --git a/libopenage/gamestate/entity_factory.h b/libopenage/gamestate/entity_factory.h index 728a91b820..1a12ece80b 100644 --- a/libopenage/gamestate/entity_factory.h +++ b/libopenage/gamestate/entity_factory.h @@ -21,6 +21,11 @@ class RenderFactory; } namespace gamestate { + +namespace activity { +class Activity; +} // namespace activity + class GameEntity; class GameState; class Player; @@ -128,6 +133,11 @@ class EntityFactory { // TODO: Cache created game entities. + /** + * Cache for activities. + */ + std::unordered_map> activity_cache; + /** * Mutex for thread safety. */ From 2457ed8314215d4f8ee3e25a856ce433483757da Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Dec 2023 18:57:41 +0100 Subject: [PATCH 140/771] gamestate: Make command queue getter non-const. --- libopenage/gamestate/activity/event/command_in_queue.cpp | 2 +- libopenage/gamestate/component/internal/command_queue.cpp | 2 +- libopenage/gamestate/component/internal/command_queue.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libopenage/gamestate/activity/event/command_in_queue.cpp b/libopenage/gamestate/activity/event/command_in_queue.cpp index 51a82750d0..0168e2bf83 100644 --- a/libopenage/gamestate/activity/event/command_in_queue.cpp +++ b/libopenage/gamestate/activity/event/command_in_queue.cpp @@ -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 = const_cast> &>(entity_queue->get_queue()); + auto &queue = entity_queue->get_queue(); queue.add_dependent(ev); return ev; diff --git a/libopenage/gamestate/component/internal/command_queue.cpp b/libopenage/gamestate/component/internal/command_queue.cpp index b22d9fe834..4c6963a75d 100644 --- a/libopenage/gamestate/component/internal/command_queue.cpp +++ b/libopenage/gamestate/component/internal/command_queue.cpp @@ -22,7 +22,7 @@ void CommandQueue::add_command(const time::time_t &time, this->command_queue.insert(time, command); } -const curve::Queue> &CommandQueue::get_queue() const { +curve::Queue> &CommandQueue::get_queue() { return this->command_queue; } diff --git a/libopenage/gamestate/component/internal/command_queue.h b/libopenage/gamestate/component/internal/command_queue.h index f0725a686d..ea0c26502a 100644 --- a/libopenage/gamestate/component/internal/command_queue.h +++ b/libopenage/gamestate/component/internal/command_queue.h @@ -44,7 +44,7 @@ class CommandQueue : public InternalComponent { * * @return Command queue. */ - const curve::Queue> &get_queue() const; + curve::Queue> &get_queue(); /** * Get the command in the front of the queue. From ab12e98d28d454365db95570c560a4c49be4fee5 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Dec 2023 19:05:27 +0100 Subject: [PATCH 141/771] time: Constants for min, max, zero. --- libopenage/time/time.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/libopenage/time/time.h b/libopenage/time/time.h index a6851f679c..ceac0c6365 100644 --- a/libopenage/time/time.h +++ b/libopenage/time/time.h @@ -15,4 +15,19 @@ namespace openage::time { */ using time_t = util::FixedPoint; +/** + * Minimum time value. + */ +static constexpr time_t TIME_MIN = std::numeric_limits::min(); + +/** + * Maximum time value. + */ +static constexpr time_t TIME_MAX = std::numeric_limits::max(); + +/** + * Zero time value (start of simulation). + */ +static constexpr time_t TIME_ZERO = time_t::zero(); + } // namespace openage::time From 26b2f97925a8f9f3c38bcf0ba77917286d0c85de Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Dec 2023 19:11:21 +0100 Subject: [PATCH 142/771] curve: Replace time numeric limit with constants. --- libopenage/curve/base_curve.h | 6 +++--- libopenage/curve/iterator.h | 4 ++-- libopenage/curve/keyframe.h | 2 +- libopenage/curve/keyframe_container.h | 8 ++++---- libopenage/curve/map.h | 12 ++++++------ libopenage/curve/queue.h | 16 ++++++++-------- libopenage/curve/tests/curve_types.cpp | 4 ++-- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/libopenage/curve/base_curve.h b/libopenage/curve/base_curve.h index 8c56586637..bd5b6003ae 100644 --- a/libopenage/curve/base_curve.h +++ b/libopenage/curve/base_curve.h @@ -115,7 +115,7 @@ class BaseCurve : public event::EventEntity { * the keyframes of \p other. */ void sync(const BaseCurve &other, - const time::time_t &start = std::numeric_limits::min()); + const time::time_t &start = time::TIME_MIN); /** * Copy keyframes from another curve (with a different element type) to this curve. @@ -134,7 +134,7 @@ class BaseCurve : public event::EventEntity { template void sync(const BaseCurve &other, const std::function &converter, - const time::time_t &start = std::numeric_limits::min()); + const time::time_t &start = time::TIME_MIN); /** * Get the identifier of this curve. @@ -270,7 +270,7 @@ std::string BaseCurve::str() const { template void BaseCurve::check_integrity() const { - time::time_t last_time = std::numeric_limits::min(); + time::time_t last_time = time::TIME_MIN; for (const auto &keyframe : this->container) { if (keyframe.time < last_time) { throw Error{MSG(err) << "curve is broken after t=" << last_time << ": " << this->str()}; diff --git a/libopenage/curve/iterator.h b/libopenage/curve/iterator.h index 2500e083cd..1062ddcc1e 100644 --- a/libopenage/curve/iterator.h +++ b/libopenage/curve/iterator.h @@ -33,8 +33,8 @@ class CurveIterator { explicit CurveIterator(const container_t *c) : base{}, container{c}, - from{-std::numeric_limits::max()}, - to{+std::numeric_limits::max()} {} + from{-time::TIME_MAX}, + to{+time::TIME_MAX} {} protected: /** diff --git a/libopenage/curve/keyframe.h b/libopenage/curve/keyframe.h index d6b90af4b5..82e36ed147 100644 --- a/libopenage/curve/keyframe.h +++ b/libopenage/curve/keyframe.h @@ -33,7 +33,7 @@ class Keyframe { time{time}, value{value} {} - const time::time_t time = std::numeric_limits::min(); + const time::time_t time = time::TIME_MIN; T value = T{}; }; diff --git a/libopenage/curve/keyframe_container.h b/libopenage/curve/keyframe_container.h index 1d7079498b..13b50bd04f 100644 --- a/libopenage/curve/keyframe_container.h +++ b/libopenage/curve/keyframe_container.h @@ -281,7 +281,7 @@ class KeyframeContainer { * the keyframes of \p other. */ iterator sync(const KeyframeContainer &other, - const time::time_t &start = std::numeric_limits::min()); + const time::time_t &start = time::TIME_MIN); /** * Copy keyframes from another container (with a different element type) to this container. @@ -298,7 +298,7 @@ class KeyframeContainer { template iterator sync(const KeyframeContainer &other, const std::function &converter, - const time::time_t &start = std::numeric_limits::min()); + const time::time_t &start = time::TIME_MIN); /** * Debugging method to be used from gdb to understand bugs better. @@ -328,7 +328,7 @@ 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(std::numeric_limits::min(), T())); + this->container.push_back(keyframe_t(time::TIME_MIN, T())); } @@ -336,7 +336,7 @@ 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(std::numeric_limits::min(), defaultval)); + this->container.push_back(keyframe_t(time::TIME_MIN, defaultval)); } diff --git a/libopenage/curve/map.h b/libopenage/curve/map.h index 3a8374a128..9712c89470 100644 --- a/libopenage/curve/map.h +++ b/libopenage/curve/map.h @@ -48,10 +48,10 @@ class UnorderedMap { at(const time::time_t &, const key_t &) const; MapFilterIterator - begin(const time::time_t &e = std::numeric_limits::max()) const; + begin(const time::time_t &e = time::TIME_MAX) const; MapFilterIterator - end(const time::time_t &e = std::numeric_limits::max()) const; + end(const time::time_t &e = time::TIME_MAX) const; MapFilterIterator insert(const time::time_t &birth, const key_t &, const val_t &); @@ -100,7 +100,7 @@ UnorderedMap::at(const time::time_t &time, e, this, time, - std::numeric_limits::max()); + time::TIME_MAX); } else { return {}; @@ -114,7 +114,7 @@ UnorderedMap::begin(const time::time_t &time) const { this->container.begin(), this, time, - std::numeric_limits::max()); + time::TIME_MAX); } template @@ -123,7 +123,7 @@ UnorderedMap::end(const time::time_t &time) const { return MapFilterIterator>( this->container.end(), this, - -std::numeric_limits::max(), + -time::TIME_MAX, time); } @@ -149,7 +149,7 @@ UnorderedMap::insert(const time::time_t &alive, const val_t &value) { return this->insert( alive, - std::numeric_limits::max(), + time::TIME_MAX, key, value); } diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index a6a0a142ef..de6807b5ab 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -53,7 +53,7 @@ class Queue : public event::EventEntity { EventEntity{loop}, _id{id}, _idstr{idstr}, - last_pop{time::time_t::zero()} {} + last_pop{time::TIME_ZERO} {} // prevent accidental copy of queue Queue(const Queue &) = delete; @@ -93,7 +93,7 @@ class Queue : public event::EventEntity { * @return Iterator to the first element. */ QueueFilterIterator> begin( - const time::time_t &t = -std::numeric_limits::max()) const; + const time::time_t &t = -time::TIME_MAX) const; /** * Get an iterator to the last element in the queue at the given time. @@ -102,7 +102,7 @@ class Queue : public event::EventEntity { * @return Iterator to the last element. */ QueueFilterIterator> end( - const time::time_t &t = std::numeric_limits::max()) const; + const time::time_t &t = time::TIME_MAX) const; /** * Get an iterator to elements that are in the queue between two time frames. @@ -112,8 +112,8 @@ class Queue : public event::EventEntity { * @return Iterator to the first element in the time frame. */ QueueFilterIterator> between( - const time::time_t &begin = std::numeric_limits::max(), - const time::time_t &end = std::numeric_limits::max()) const; + const time::time_t &begin = time::TIME_MAX, + const time::time_t &end = time::TIME_MAX) const; /** * Erase an element from the queue. @@ -262,7 +262,7 @@ QueueFilterIterator> Queue::begin(const time::time_t &t) const { it, this, t, - std::numeric_limits::max()); + time::TIME_MAX); } } @@ -276,7 +276,7 @@ QueueFilterIterator> Queue::end(const time::time_t &t) const { container.end(), this, t, - std::numeric_limits::max()); + time::TIME_MAX); } @@ -321,7 +321,7 @@ QueueFilterIterator> Queue::insert(const time::time_t &time, insertion_point, this, time, - std::numeric_limits::max()); + time::TIME_MAX); if (!ct.valid()) { ++ct; diff --git a/libopenage/curve/tests/curve_types.cpp b/libopenage/curve/tests/curve_types.cpp index cab8473bc3..40f20395ee 100644 --- a/libopenage/curve/tests/curve_types.cpp +++ b/libopenage/curve/tests/curve_types.cpp @@ -37,7 +37,7 @@ void curve_types() { { auto it = c.begin(); TESTEQUALS(it->value, 0); - TESTEQUALS(it->time, std::numeric_limits::min()); + TESTEQUALS(it->time, time::TIME_MIN); TESTEQUALS((++it)->time, 0); TESTEQUALS(it->value, 0); TESTEQUALS((++it)->time, 1); @@ -140,7 +140,7 @@ void curve_types() { { auto it = c.begin(); - TESTEQUALS(it->time, std::numeric_limits::min()); + TESTEQUALS(it->time, time::TIME_MIN); TESTEQUALS(it->value, 0); TESTEQUALS((++it)->time, 0); From 6f0117b768f0d086cefbe21e6955b6732baa00d3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Dec 2023 19:11:29 +0100 Subject: [PATCH 143/771] event: Replace time numeric limit with constants. --- libopenage/event/demo/physics.cpp | 4 ++-- libopenage/event/event_loop.cpp | 4 ++-- libopenage/event/eventqueue.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libopenage/event/demo/physics.cpp b/libopenage/event/demo/physics.cpp index d43365f042..e27a51d962 100644 --- a/libopenage/event/demo/physics.cpp +++ b/libopenage/event/demo/physics.cpp @@ -104,7 +104,7 @@ class BallReflectWall : public DependencyEventHandler { auto pos = positioncurve->get(now); if (speed[1] == 0) { - return std::numeric_limits::max(); + return time::TIME_MAX; } time::time_t ty = 0; @@ -227,7 +227,7 @@ class BallReflectPanel : public DependencyEventHandler { auto pos = positioncurve->get(now); if (speed[0] == 0) - return std::numeric_limits::max(); + return time::TIME_MAX; time::time_t ty = 0; diff --git a/libopenage/event/event_loop.cpp b/libopenage/event/event_loop.cpp index eb9e8ca6f1..26bf176978 100644 --- a/libopenage/event/event_loop.cpp +++ b/libopenage/event/event_loop.cpp @@ -151,7 +151,7 @@ int EventLoop::execute_events(const time::time_t &time_until, time::time_t new_time = event->get_eventhandler()->predict_invoke_time( target, state, event->get_time()); - if (new_time != std::numeric_limits::min()) { + if (new_time != time::TIME_MIN) { event->set_time(new_time); log::log(DBG << "Loop: repeating event \"" << event->get_eventhandler()->id() @@ -204,7 +204,7 @@ void EventLoop::update_changes(const std::shared_ptr &state) { time::time_t new_time = evnt->get_eventhandler() ->predict_invoke_time(entity, state, change.time); - if (new_time != std::numeric_limits::min()) { + if (new_time != time::TIME_MIN) { log::log(DBG << "Loop: due to a change, rescheduling event of '" << evnt->get_eventhandler()->id() << "' on entity '" << entity->idstr() diff --git a/libopenage/event/eventqueue.cpp b/libopenage/event/eventqueue.cpp index 83a96e6e16..51a0c2fdaa 100644 --- a/libopenage/event/eventqueue.cpp +++ b/libopenage/event/eventqueue.cpp @@ -35,7 +35,7 @@ std::shared_ptr EventQueue::create_event(const std::shared_ptrset_time(event->get_eventhandler() ->predict_invoke_time(trgt, state, reference_time)); - if (event->get_time() == std::numeric_limits::min()) { + if (event->get_time() == time::TIME_MIN) { log::log(DBG << "Queue: ignoring insertion of event " << event->get_eventhandler()->id() << " because no execution was scheduled."); From 1f54b5df5b370addd17500f9be4576bde507c75a Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Dec 2023 19:11:43 +0100 Subject: [PATCH 144/771] pong: Replace time numeric limit with constants. --- libopenage/main/demo/pong/aicontroller.cpp | 4 ++-- libopenage/main/demo/pong/physics.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libopenage/main/demo/pong/aicontroller.cpp b/libopenage/main/demo/pong/aicontroller.cpp index bdd1b7df3a..fd635d2a21 100644 --- a/libopenage/main/demo/pong/aicontroller.cpp +++ b/libopenage/main/demo/pong/aicontroller.cpp @@ -34,7 +34,7 @@ std::vector get_ai_inputs(const std::shared_ptr &player, time::time_t ty_hit = 0, tx_hit = 0; if (speed[0] == 0) { - tx_hit = std::numeric_limits::max(); + tx_hit = time::TIME_MAX; } else if (speed[0] > 0) { tx_hit = time::time_t::from_double((area_width - ball_pos[0]) / speed[0]); @@ -44,7 +44,7 @@ std::vector get_ai_inputs(const std::shared_ptr &player, } if (speed[1] == 0) { - ty_hit = std::numeric_limits::max(); + ty_hit = time::TIME_MAX; } else if (speed[1] > 0) { ty_hit = time::time_t::from_double((area_height - ball_pos[1]) / speed[1]); diff --git a/libopenage/main/demo/pong/physics.cpp b/libopenage/main/demo/pong/physics.cpp index 72a1f65c96..e2aa12e28e 100644 --- a/libopenage/main/demo/pong/physics.cpp +++ b/libopenage/main/demo/pong/physics.cpp @@ -75,7 +75,7 @@ class BallReflectWall : public event::DependencyEventHandler { auto screen_size = state->area_size->get(now); if (speed[1] == 0) { - return std::numeric_limits::max(); + return time::TIME_MAX; } time::time_t ty = 0; @@ -199,7 +199,7 @@ class BallReflectPanel : public event::DependencyEventHandler { auto screen_size = state->area_size->get(now); if (speed[0] == 0) - return std::numeric_limits::max(); + return time::TIME_MAX; time::time_t ty = 0; From 0e7178f6c069ff9823941cbf89c32184eb2defae Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Dec 2023 19:11:50 +0100 Subject: [PATCH 145/771] gamestate: Replace time numeric limit with constants. --- libopenage/gamestate/activity/event/command_in_queue.cpp | 2 +- libopenage/gamestate/entity_factory.cpp | 2 +- libopenage/gamestate/terrain.cpp | 2 +- libopenage/gamestate/terrain_factory.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libopenage/gamestate/activity/event/command_in_queue.cpp b/libopenage/gamestate/activity/event/command_in_queue.cpp index 0168e2bf83..af57692078 100644 --- a/libopenage/gamestate/activity/event/command_in_queue.cpp +++ b/libopenage/gamestate/activity/event/command_in_queue.cpp @@ -22,7 +22,7 @@ std::shared_ptr primer_command_in_queue(const time::time_ entity->get_manager(), state, // event is not executed until a command is available - std::numeric_limits::max(), + time::TIME_MAX, params); auto entity_queue = std::dynamic_pointer_cast( entity->get_component(component::component_t::COMMANDQUEUE)); diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index c9ab7ab472..831527c1ec 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -191,7 +191,7 @@ void EntityFactory::init_components(const std::shared_ptradd_attribute(std::numeric_limits::min(), + live->add_attribute(time::TIME_MIN, attribute.get_name(), std::make_shared>(loop, 0, diff --git a/libopenage/gamestate/terrain.cpp b/libopenage/gamestate/terrain.cpp index f31fc648b8..9e40230ffe 100644 --- a/libopenage/gamestate/terrain.cpp +++ b/libopenage/gamestate/terrain.cpp @@ -32,7 +32,7 @@ void Terrain::attach_renderer(const std::shared_ptr &re chunk->get_offset()); chunk->set_render_entity(render_entity); - chunk->render_update(time::time_t::zero()); + chunk->render_update(time::TIME_ZERO); } } diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp index 3a46773429..7276d73d57 100644 --- a/libopenage/gamestate/terrain_factory.cpp +++ b/libopenage/gamestate/terrain_factory.cpp @@ -131,7 +131,7 @@ std::shared_ptr TerrainFactory::add_chunk(const std::shared_ptrrender_factory->add_terrain_render_entity(size, offset); chunk->set_render_entity(render_entity); - chunk->render_update(time::time_t::zero(), + chunk->render_update(time::TIME_ZERO, test_texture_path); } From 73b97d4f4b09d80a37d2e5d41d35e65494b68b9b Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Dec 2023 19:26:01 +0100 Subject: [PATCH 146/771] curve: Search queue from the end instead from begin. --- libopenage/curve/queue.h | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index de6807b5ab..32675a72d3 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -198,15 +198,12 @@ const T &Queue::front(const time::time_t &time) const { << time << " but queue is empty."}; } - // search for the first element that is after the given time - auto it = this->container.begin(); - ++it; - while (it != this->container.end() and it->time() <= time) { - ++it; - } - - // get the last element before the given time + // search for the last element before the given time + auto it = this->container.end(); --it; + while (it->time() > time and it != this->container.begin()) { + --it; + } return it->value; } @@ -218,19 +215,20 @@ const T Queue::pop_front(const time::time_t &time) { << time << " but queue is empty."}; } - // search for the first element that is after the given time - auto it = this->container.begin(); - ++it; - while (it->time() <= time and it != this->container.end()) { - ++it; + // search for the last element before the given time + auto it = this->container.end(); + --it; + while (it->time() > time and it != this->container.begin()) { + --it; } - auto to = it->time(); - auto from = time; - // get the last element inserted before the given time + auto val = std::move(it->value); + + // get the time span between current time and the next element + auto to = (++it)->time(); --it; - auto val = it->value; + auto from = time; // erase the element // TODO: We should be able to reinsert elements From 6d41917e9932581d336844cf87ca59311e170f37 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Dec 2023 19:36:15 +0100 Subject: [PATCH 147/771] gamestate: Remove unused variable. --- libopenage/gamestate/api/activity.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libopenage/gamestate/api/activity.cpp b/libopenage/gamestate/api/activity.cpp index 63dea05e6e..edd596abe8 100644 --- a/libopenage/gamestate/api/activity.cpp +++ b/libopenage/gamestate/api/activity.cpp @@ -85,7 +85,6 @@ std::vector APIActivityNode::get_next(const nyan::Object &node) { system::system_id_t APIActivityNode::get_system_id(const nyan::Object &ability_node) { auto ability = ability_node.get("Ability.ability"); - std::shared_ptr db_view = ability_node.get_view(); if (not ACTIVITY_TASK_SYSTEM_DEFS.contains(ability->get_name())) [[unlikely]] { throw Error(MSG(err) << "Ability '" << ability->get_name() << "' has no associated system defined."); From ec381dfe5ea2bd38433e92d138d58e7975fa247a Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 2 Jan 2024 19:39:51 +0100 Subject: [PATCH 148/771] convert: Clarify modpack version/versionstr difference. --- .../entity_object/export/formats/modpack_info.py | 12 ++++++------ openage/convert/tool/api_export.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openage/convert/entity_object/export/formats/modpack_info.py b/openage/convert/entity_object/export/formats/modpack_info.py index 1a6017ce4e..0df5f220ce 100644 --- a/openage/convert/entity_object/export/formats/modpack_info.py +++ b/openage/convert/entity_object/export/formats/modpack_info.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-instance-attributes,too-many-arguments @@ -153,7 +153,7 @@ def add_dependency(self, modpack_id: str) -> None: def set_info( self, packagename: str, - version: str, + modpack_version: str, versionstr: str = None, repo: str = None, alias: str = None, @@ -168,9 +168,9 @@ def set_info( :param packagename: Name of the modpack. :type packagename: str - :param version: Internal version number. Must have semver format. - :type version: str - :param versionstr: Human-readable version number. + :param modpack_version: Internal version number. Must have semver format. + :type modpack_version: str + :param versionstr: Human-readable version number. Can be anything. :type versionstr: str :param repo: Name of the repo where the package is hosted. :type repo: str @@ -188,7 +188,7 @@ def set_info( :type licenses: list """ self.packagename = packagename - self.version = version + self.version = modpack_version if versionstr: self.extra_info["versionstr"] = versionstr diff --git a/openage/convert/tool/api_export.py b/openage/convert/tool/api_export.py index 0e35ec4602..1124da8771 100644 --- a/openage/convert/tool/api_export.py +++ b/openage/convert/tool/api_export.py @@ -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. """ Export tool for dumping the nyan API of the engine from the converter. @@ -76,7 +76,7 @@ def create_modpack() -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("engine", "0.4.1", "0.4.1", repo="openage") + mod_def.set_info("engine", modpack_version="0.4.1", versionstr="0.4.1", repo="openage") mod_def.add_include("**") From 7b74280be03bffce6c7ed79285078f48cc80cdd0 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 19 Nov 2023 01:05:09 +0100 Subject: [PATCH 149/771] renderer: HUD renderer. --- libopenage/renderer/stages/CMakeLists.txt | 1 + libopenage/renderer/stages/hud/CMakeLists.txt | 5 + libopenage/renderer/stages/hud/hud_object.cpp | 71 +++++++++ libopenage/renderer/stages/hud/hud_object.h | 136 +++++++++++++++++ .../renderer/stages/hud/hud_render_entity.cpp | 39 +++++ .../renderer/stages/hud/hud_render_entity.h | 63 ++++++++ .../renderer/stages/hud/hud_renderer.cpp | 99 ++++++++++++ libopenage/renderer/stages/hud/hud_renderer.h | 144 ++++++++++++++++++ .../stages/world/world_render_entity.h | 2 +- .../renderer/stages/world/world_renderer.h | 7 + 10 files changed, 566 insertions(+), 1 deletion(-) create mode 100644 libopenage/renderer/stages/hud/CMakeLists.txt create mode 100644 libopenage/renderer/stages/hud/hud_object.cpp create mode 100644 libopenage/renderer/stages/hud/hud_object.h create mode 100644 libopenage/renderer/stages/hud/hud_render_entity.cpp create mode 100644 libopenage/renderer/stages/hud/hud_render_entity.h create mode 100644 libopenage/renderer/stages/hud/hud_renderer.cpp create mode 100644 libopenage/renderer/stages/hud/hud_renderer.h diff --git a/libopenage/renderer/stages/CMakeLists.txt b/libopenage/renderer/stages/CMakeLists.txt index 5be8fb465b..c2ae5bf781 100644 --- a/libopenage/renderer/stages/CMakeLists.txt +++ b/libopenage/renderer/stages/CMakeLists.txt @@ -1,4 +1,5 @@ add_subdirectory(camera/) +add_subdirectory(hud/) add_subdirectory(screen/) add_subdirectory(skybox/) add_subdirectory(terrain/) diff --git a/libopenage/renderer/stages/hud/CMakeLists.txt b/libopenage/renderer/stages/hud/CMakeLists.txt new file mode 100644 index 0000000000..ce084b5a22 --- /dev/null +++ b/libopenage/renderer/stages/hud/CMakeLists.txt @@ -0,0 +1,5 @@ +add_sources(libopenage + hud_object.cpp + hud_render_entity.cpp + hud_renderer.cpp +) diff --git a/libopenage/renderer/stages/hud/hud_object.cpp b/libopenage/renderer/stages/hud/hud_object.cpp new file mode 100644 index 0000000000..c923adbda0 --- /dev/null +++ b/libopenage/renderer/stages/hud/hud_object.cpp @@ -0,0 +1,71 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "hud_object.h" + +#include "renderer/stages/hud/hud_render_entity.h" + + +namespace openage::renderer::hud { + +HudObject::HudObject(const std::shared_ptr &asset_manager) : + require_renderable{true}, + changed{false}, + camera{nullptr}, + asset_manager{asset_manager}, + render_entity{nullptr}, + uniforms{nullptr}, + last_update{0.0} { +} + +void HudObject::set_render_entity(const std::shared_ptr &entity) { + this->render_entity = entity; + this->fetch_updates(); +} + +void HudObject::set_camera(const std::shared_ptr &camera) { + this->camera = camera; +} + +void HudObject::fetch_updates(const time::time_t &time) { + if (not this->render_entity->is_changed()) { + // exit early because there is nothing to do + return; + } + // Get data from render entity + // TODO + + // 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; +} + +void HudObject::update_uniforms(const time::time_t &time) { + // TODO: Only update uniforms that changed since last update + if (this->uniforms == nullptr) [[unlikely]] { + return; + } + // TODO +} + +bool HudObject::requires_renderable() { + return this->require_renderable; +} + +void HudObject::clear_requires_renderable() { + this->require_renderable = false; +} + +bool HudObject::is_changed() { + return this->changed; +} + +void HudObject::clear_changed_flag() { + this->changed = false; +} + +void HudObject::set_uniforms(const std::shared_ptr &uniforms) { + this->uniforms = uniforms; +} + +} // namespace openage::renderer::hud diff --git a/libopenage/renderer/stages/hud/hud_object.h b/libopenage/renderer/stages/hud/hud_object.h new file mode 100644 index 0000000000..8de81df591 --- /dev/null +++ b/libopenage/renderer/stages/hud/hud_object.h @@ -0,0 +1,136 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include +#include + +#include "time/time.h" + + +namespace openage::renderer { +class UniformInput; + +namespace camera { +class Camera; +} + +namespace resources { +class AssetManager; +class Animation2dInfo; +} // namespace resources + +namespace hud { +class HudRenderEntity; + +class HudObject { +public: + HudObject(const std::shared_ptr &asset_manager); + ~HudObject() = default; + + /** + * Set the world render entity. + * + * @param entity New world render entity. + */ + void set_render_entity(const std::shared_ptr &entity); + + /** + * Set the current camera of the scene. + * + * @param camera Camera object viewing the scene. + */ + void set_camera(const std::shared_ptr &camera); + + /** + * Fetch updates from the render entity. + * + * @param time Current simulation time. + */ + void fetch_updates(const time::time_t &time = 0.0); + + /** + * Update the uniforms of the renderable associated with this object. + * + * @param time Current simulation time. + */ + void update_uniforms(const time::time_t &time = 0.0); + + /** + * Check whether a new renderable needs to be created for this mesh. + * + * If true, the old renderable should be removed from the render pass. + * The updated uniforms and geometry should be passed to this mesh. + * Afterwards, clear the requirement flag with \p clear_requires_renderable(). + * + * @return true if a new renderable is required, else false. + */ + bool requires_renderable(); + + /** + * Indicate to this mesh that a new renderable has been created. + */ + void clear_requires_renderable(); + + /** + * Check whether the object was changed by \p update(). + * + * @return true if changes were made, else false. + */ + bool is_changed(); + + /** + * Clear the update flag by setting it to false. + */ + void clear_changed_flag(); + + /** + * Set the reference to the uniform inputs of the renderable + * associated with this object. Relevant uniforms are updated + * when calling \p update(). + * + * @param uniforms Uniform inputs of this object's renderable. + */ + void set_uniforms(const std::shared_ptr &uniforms); + +private: + /** + * Stores whether a new renderable for this object needs to be created + * for the render pass. + */ + bool require_renderable; + + /** + * Stores whether the \p update() call changed the object. + */ + bool changed; + + /** + * Camera for model uniforms. + */ + std::shared_ptr camera; + + /** + * Asset manager for central accessing and loading asset resources. + */ + std::shared_ptr asset_manager; + + /** + * Source for positional and texture data. + */ + std::shared_ptr render_entity; + + /** + * Shader uniforms for the renderable in the terrain render pass. + */ + std::shared_ptr uniforms; + + /** + * Time of the last update call. + */ + time::time_t last_update; +}; +} // namespace hud +} // namespace openage::renderer diff --git a/libopenage/renderer/stages/hud/hud_render_entity.cpp b/libopenage/renderer/stages/hud/hud_render_entity.cpp new file mode 100644 index 0000000000..b43f25bfa7 --- /dev/null +++ b/libopenage/renderer/stages/hud/hud_render_entity.cpp @@ -0,0 +1,39 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "hud_render_entity.h" + +#include + + +namespace openage::renderer::hud { + +HudRenderEntity::HudRenderEntity() : + changed{false}, + last_update{0.0} { +} + +void HudRenderEntity::update(const time::time_t time) { + std::unique_lock lock{this->mutex}; + + // TODO +} + +time::time_t HudRenderEntity::get_update_time() { + std::shared_lock lock{this->mutex}; + + return this->last_update; +} + +bool HudRenderEntity::is_changed() { + std::shared_lock lock{this->mutex}; + + return this->changed; +} + +void HudRenderEntity::clear_changed_flag() { + std::unique_lock lock{this->mutex}; + + this->changed = false; +} + +} // namespace openage::renderer::hud diff --git a/libopenage/renderer/stages/hud/hud_render_entity.h b/libopenage/renderer/stages/hud/hud_render_entity.h new file mode 100644 index 0000000000..e5b227a944 --- /dev/null +++ b/libopenage/renderer/stages/hud/hud_render_entity.h @@ -0,0 +1,63 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include +#include + +#include "time/time.h" + + +namespace openage::renderer::hud { + +class HudRenderEntity { +public: + HudRenderEntity(); + ~HudRenderEntity() = default; + + /** + * TODO: Update the render entity with information from the gamestate. + */ + void update(const time::time_t time = 0.0); + + /** + * Get the time of the last update. + * + * @return Time of last update. + */ + time::time_t get_update_time(); + + /** + * Check whether the render entity has received new updates from the + * gamestate. + * + * @return true if updates have been received, else false. + */ + bool is_changed(); + + /** + * Clear the update flag by setting it to false. + */ + void clear_changed_flag(); + +private: + /** + * Flag for determining if the render entity has been updated by the + * corresponding gamestate entity. Set to true every time \p update() + * is called. + */ + bool changed; + + /** + * Time of the last update call. + */ + time::time_t last_update; + + /** + * Mutex for protecting threaded access. + */ + std::shared_mutex mutex; +}; +} // namespace openage::renderer::hud diff --git a/libopenage/renderer/stages/hud/hud_renderer.cpp b/libopenage/renderer/stages/hud/hud_renderer.cpp new file mode 100644 index 0000000000..295c222230 --- /dev/null +++ b/libopenage/renderer/stages/hud/hud_renderer.cpp @@ -0,0 +1,99 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "hud_renderer.h" + +#include "renderer/camera/camera.h" +#include "renderer/opengl/context.h" +#include "renderer/resources/assets/asset_manager.h" +#include "renderer/resources/shader_source.h" +#include "renderer/resources/texture_info.h" +#include "renderer/shader_program.h" +#include "renderer/stages/hud/hud_object.h" +#include "renderer/texture.h" +#include "renderer/window.h" +#include "time/clock.h" + + +namespace openage::renderer::hud { + +HudRenderer::HudRenderer(const std::shared_ptr &window, + const std::shared_ptr &renderer, + const std::shared_ptr &camera, + const util::Path &shaderdir, + const std::shared_ptr &asset_manager, + const std::shared_ptr clock) : + renderer{renderer}, + camera{camera}, + asset_manager{asset_manager}, + render_objects{}, + clock{clock} { + renderer::opengl::GlContext::check_error(); + + auto size = window->get_size(); + this->initialize_render_pass(size[0], size[1], shaderdir); + + window->add_resize_callback([this](size_t width, size_t height, double /*scale*/) { + this->resize(width, height); + }); + + log::log(INFO << "Created render stage 'HUD'"); +} + +std::shared_ptr HudRenderer::get_render_pass() { + return this->render_pass; +} + +void HudRenderer::add_render_entity(const std::shared_ptr entity) { + std::unique_lock lock{this->mutex}; + + auto hud_object = std::make_shared(this->asset_manager); + hud_object->set_render_entity(entity); + hud_object->set_camera(this->camera); + this->render_objects.push_back(hud_object); +} + +void HudRenderer::update() { + std::unique_lock lock{this->mutex}; + auto current_time = this->clock->get_real_time(); + for (auto &obj : this->render_objects) { + // TODO + } +} + +void HudRenderer::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)); + + auto fbo = this->renderer->create_texture_target({this->output_texture, this->depth_texture}); + this->render_pass->set_target(fbo); +} + +void HudRenderer::initialize_render_pass(size_t width, + size_t height, + const util::Path &shaderdir) { + // ASDF: add vertex shader + auto vert_shader_file = (shaderdir / "world2d.vert.glsl").open(); + auto vert_shader_src = renderer::resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::vertex, + vert_shader_file.read()); + vert_shader_file.close(); + + // ASDF: add fragment shader + auto frag_shader_file = (shaderdir / "world2d.frag.glsl").open(); + auto frag_shader_src = renderer::resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::fragment, + frag_shader_file.read()); + frag_shader_file.close(); + + 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->display_shader = this->renderer->add_shader({vert_shader_src, frag_shader_src}); + + auto fbo = this->renderer->create_texture_target({this->output_texture, this->depth_texture}); + this->render_pass = this->renderer->add_render_pass({}, fbo); +} + +} // namespace openage::renderer::hud diff --git a/libopenage/renderer/stages/hud/hud_renderer.h b/libopenage/renderer/stages/hud/hud_renderer.h new file mode 100644 index 0000000000..f085e825f8 --- /dev/null +++ b/libopenage/renderer/stages/hud/hud_renderer.h @@ -0,0 +1,144 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include + +#include "util/path.h" + +namespace openage { + +namespace time { +class Clock; +} + +namespace renderer { +class Renderer; +class RenderPass; +class ShaderProgram; +class Texture2d; +class Window; + +namespace camera { +class Camera; +} + +namespace resources { +class AssetManager; +} + +namespace hud { +class HudObject; +class HudRenderEntity; + +/** + * Renderer for the "Heads-Up Display" (HUD). + * Draws UI elements that are not part of the GUI, e.g. health bars, selection boxes, minimap, etc. + */ +class HudRenderer { +public: + HudRenderer(const std::shared_ptr &window, + const std::shared_ptr &renderer, + const std::shared_ptr &camera, + const util::Path &shaderdir, + const std::shared_ptr &asset_manager, + const std::shared_ptr clock); + ~HudRenderer() = default; + + /** + * Get the render pass of the HUD renderer. + * + * @return Render pass for HUD drawing. + */ + std::shared_ptr get_render_pass(); + + /** + * Add a new render entity of the HUD renderer. + * + * @param render_entity New render entity. + */ + void add_render_entity(const std::shared_ptr entity); + + /** + * Update the render entities and render positions. + */ + void update(); + + /** + * Resize the FBO for the HUD rendering. This basically updates the output + * texture size. + * + * @param width New width of the FBO. + * @param height New height of the FBO. + */ + void resize(size_t width, size_t height); + +private: + /** + * Create the render pass for HUD drawing. + * + * Called during initialization of the HUD renderer. + * + * @param width Width of the FBO. + * @param height Height of the FBO. + * @param shaderdir Directory containg the shader source files. + */ + void initialize_render_pass(size_t width, + size_t height, + const util::Path &shaderdir); + + /** + * Reference to the openage renderer. + */ + std::shared_ptr renderer; + + /** + * Camera for model uniforms. + */ + std::shared_ptr camera; + + /** + * Texture manager for loading assets. + */ + std::shared_ptr asset_manager; + + /** + * Render pass for the HUD drawing. + */ + std::shared_ptr render_pass; + + /** + * Render entities requested by the game simulation or input system. + */ + std::vector> render_objects; + + /** + * Shader for rendering the HUD objects. + */ + std::shared_ptr display_shader; + + /** + * Simulation clock for timing animations. + */ + std::shared_ptr clock; + + /** + * Output texture. + */ + std::shared_ptr output_texture; + + /** + * Depth texture. + */ + std::shared_ptr depth_texture; + + /** + * Mutex for protecting threaded access. + */ + std::shared_mutex mutex; +}; +} // namespace hud +} // namespace renderer +} // namespace openage diff --git a/libopenage/renderer/stages/world/world_render_entity.h b/libopenage/renderer/stages/world/world_render_entity.h index ae656da20d..74ce51661f 100644 --- a/libopenage/renderer/stages/world/world_render_entity.h +++ b/libopenage/renderer/stages/world/world_render_entity.h @@ -40,7 +40,7 @@ class WorldRenderEntity { const time::time_t time = 0.0); /** - * Thus function is for DEBUGGING and should not be used. + * This function is for DEBUGGING and should not be used. * * Update the render entity with information from the gamestate. * diff --git a/libopenage/renderer/stages/world/world_renderer.h b/libopenage/renderer/stages/world/world_renderer.h index e7e9e0c596..1aa7575d46 100644 --- a/libopenage/renderer/stages/world/world_renderer.h +++ b/libopenage/renderer/stages/world/world_renderer.h @@ -89,6 +89,13 @@ class WorldRenderer { size_t height, const util::Path &shaderdir); + /** + * Fetch the uniform IDs for the uniforms of the world shader from OpenGL + * and assign them to the WorldObject class. + * + * This method must be called after the shader program has been created but + * before any uniforms are set. + */ void init_uniform_ids(); /** From a64c2c9bdb8f39e18846d1805c13c8261b822a8e Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 20 Nov 2023 03:21:39 +0100 Subject: [PATCH 150/771] refactor: Solve weird tab/space mixup. --- .../input/controller/camera/controller.h | 14 ++-- libopenage/input/controller/game/controller.h | 71 +++++++++---------- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/libopenage/input/controller/camera/controller.h b/libopenage/input/controller/camera/controller.h index bdc64df28c..b668c18823 100644 --- a/libopenage/input/controller/camera/controller.h +++ b/libopenage/input/controller/camera/controller.h @@ -6,6 +6,7 @@ #include "input/event.h" + namespace openage { namespace renderer::camera { @@ -27,12 +28,13 @@ class Controller { ~Controller() = default; /** - * Process an input event from the input manager. - * - * @param ev Input event and arguments. - * - * @return true if the event is accepted, else false. - */ + * Process an input event from the input manager. + * + * @param ev Input event and arguments. + * @param ctx Binding context that maps input events to camera actions. + * + * @return true if the event is accepted, else false. + */ bool process(const event_arguments &ev_args, const std::shared_ptr &ctx); }; diff --git a/libopenage/input/controller/game/controller.h b/libopenage/input/controller/game/controller.h index 847a5999f5..5eae0949f5 100644 --- a/libopenage/input/controller/game/controller.h +++ b/libopenage/input/controller/game/controller.h @@ -10,6 +10,7 @@ #include "gamestate/types.h" #include "input/event.h" + namespace openage { namespace gamestate { @@ -30,8 +31,6 @@ class BindingContext; * Controllers handle inputs from outside of a game (e.g. GUI, AI, scripts, ...) * and pass the resulting events to game entities. They also act as a form of * access control for using in-game functionality of game entities. - * - * TODO: Connection to engine */ class Controller : public std::enable_shared_from_this { public: @@ -41,68 +40,68 @@ class Controller : public std::enable_shared_from_this { ~Controller() = default; /** - * Switch the actively controlled faction by the controller. - * The ID must be in the list of controlled factions. - * - * @param faction_id ID of the new active faction. - */ + * Switch the actively controlled faction by the controller. + * The ID must be in the list of controlled factions. + * + * @param faction_id ID of the new active faction. + */ void set_control(size_t faction_id); /** - * Get the ID of the faction actively controlled by the controller. - * - * @return ID of the active faction. - */ + * Get the ID of the faction actively controlled by the controller. + * + * @return ID of the active faction. + */ size_t get_controlled() const; /** - * Get the currently selected entities. - * - * @return Selected entities. - */ + * Get the currently selected entities. + * + * @return Selected entities. + */ const std::vector &get_selected() const; /** - * Set the currently selected entities. - * - * @param ids Selected entities. - */ + * Set the currently selected entities. + * + * @param ids Selected entities. + */ void set_selected(std::vector ids); /** - * Process an input event from the input manager. - * - * @param ev_args Input event and arguments. - * @param ctx Binding context for looking up the event transformation. - * - * @return true if the event is accepted, else false. - */ + * Process an input event from the input manager. + * + * @param ev_args Input event and arguments. + * @param ctx Binding context for looking up the event transformation. + * + * @return true if the event is accepted, else false. + */ bool process(const event_arguments &ev_args, const std::shared_ptr &ctx); private: /** - * List of factions controllable by this controller. - */ + * Factions controllable by this controller. + */ std::unordered_set controlled_factions; /** - * ID of the currently active faction. - */ + * ID of the currently active faction. + */ size_t active_faction_id; /** - * Currently selected entities. - */ + * Currently selected entities. + */ std::vector selected; /** - * Queue for gamestate events generated from inputs. - */ + * Queue for gamestate events generated from inputs. + */ std::vector> outqueue; /** - * Mutex for threaded access. - */ + * Mutex for threaded access. + */ mutable std::recursive_mutex mutex; }; From 449df59d393313e5895bbfc7209566598468abd3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 20 Nov 2023 03:42:27 +0100 Subject: [PATCH 151/771] input: Change entity creation to 'CTRL+LMB'. --- libopenage/input/controller/game/controller.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/libopenage/input/controller/game/controller.cpp b/libopenage/input/controller/game/controller.cpp index 7a17de186a..d5f04c9afd 100644 --- a/libopenage/input/controller/game/controller.cpp +++ b/libopenage/input/controller/game/controller.cpp @@ -112,7 +112,11 @@ void setup_defaults(const std::shared_ptr &ctx, }}; binding_action create_entity_action{forward_action_t::SEND, create_entity_event}; - Event ev_mouse_lmb{event_class::MOUSE_BUTTON, Qt::MouseButton::LeftButton, Qt::NoModifier, QEvent::MouseButtonRelease}; + Event ev_mouse_lmb{ + event_class::MOUSE_BUTTON, + Qt::MouseButton::LeftButton, + Qt::KeyboardModifier::ControlModifier, + QEvent::MouseButtonRelease}; ctx->bind(ev_mouse_lmb, create_entity_action); @@ -135,7 +139,11 @@ void setup_defaults(const std::shared_ptr &ctx, }}; binding_action move_entity_action{forward_action_t::SEND, move_entity}; - Event ev_mouse_rmb{event_class::MOUSE_BUTTON, Qt::MouseButton::RightButton, Qt::NoModifier, QEvent::MouseButtonRelease}; + Event ev_mouse_rmb{ + event_class::MOUSE_BUTTON, + Qt::MouseButton::RightButton, + Qt::KeyboardModifier::NoModifier, + QEvent::MouseButtonRelease}; ctx->bind(ev_mouse_rmb, move_entity_action); } From 542ff6bb3120c5e9758a03d471656d3877a2e5c5 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 20 Nov 2023 03:44:16 +0100 Subject: [PATCH 152/771] input: Rename engine controller to game controller. --- libopenage/input/action.h | 2 +- libopenage/input/controller/game/binding.cpp | 2 +- .../input/controller/game/binding_context.cpp | 2 +- .../input/controller/game/binding_context.h | 2 +- libopenage/input/controller/game/controller.h | 3 ++- libopenage/input/input_context.cpp | 8 +++---- libopenage/input/input_context.h | 8 +++---- libopenage/input/input_manager.cpp | 21 +++++++++++-------- libopenage/input/input_manager.h | 10 ++++----- libopenage/presenter/presenter.cpp | 6 +++--- 10 files changed, 34 insertions(+), 30 deletions(-) diff --git a/libopenage/input/action.h b/libopenage/input/action.h index 4bad86c412..facd02bdca 100644 --- a/libopenage/input/action.h +++ b/libopenage/input/action.h @@ -23,7 +23,7 @@ enum class input_action_t { PUSH_CONTEXT, POP_CONTEXT, REMOVE_CONTEXT, - ENGINE, + GAME, CAMERA, GUI, CUSTOM, diff --git a/libopenage/input/controller/game/binding.cpp b/libopenage/input/controller/game/binding.cpp index abfc8b39b8..17f90f942a 100644 --- a/libopenage/input/controller/game/binding.cpp +++ b/libopenage/input/controller/game/binding.cpp @@ -4,4 +4,4 @@ namespace openage::input::game { -} // namespace openage::input::engine +} // namespace openage::input::game diff --git a/libopenage/input/controller/game/binding_context.cpp b/libopenage/input/controller/game/binding_context.cpp index 98a1a3d60e..9f963f4a11 100644 --- a/libopenage/input/controller/game/binding_context.cpp +++ b/libopenage/input/controller/game/binding_context.cpp @@ -36,4 +36,4 @@ const binding_action &BindingContext::lookup(const Event &ev) const { throw Error{MSG(err) << "Event is not bound in binding_action context."}; } -} // namespace openage::input::engine +} // namespace openage::input::game diff --git a/libopenage/input/controller/game/binding_context.h b/libopenage/input/controller/game/binding_context.h index d63b6e4c8c..84b331c690 100644 --- a/libopenage/input/controller/game/binding_context.h +++ b/libopenage/input/controller/game/binding_context.h @@ -69,4 +69,4 @@ class BindingContext { std::unordered_map by_class; }; -} // namespace openage::input::engine +} // namespace openage::input::game diff --git a/libopenage/input/controller/game/controller.h b/libopenage/input/controller/game/controller.h index 5eae0949f5..a75483492c 100644 --- a/libopenage/input/controller/game/controller.h +++ b/libopenage/input/controller/game/controller.h @@ -108,7 +108,8 @@ class Controller : public std::enable_shared_from_this { /** * Setup default controller action bindings: * - * - Mouse click: Create game entity. + * - CTRL + Left Mouse click: Create game entity. + * - Right Mouse click: Move game entity. * * @param ctx Binding context the actions are added to. * @param time_loop Time loop for getting simulation time. diff --git a/libopenage/input/input_context.cpp b/libopenage/input/input_context.cpp index ef733b0e98..ce36ddc419 100644 --- a/libopenage/input/input_context.cpp +++ b/libopenage/input/input_context.cpp @@ -15,16 +15,16 @@ const std::string &InputContext::get_id() { return this->id; } -void InputContext::set_engine_bindings(const std::shared_ptr &bindings) { - this->engine_bindings = bindings; +void InputContext::set_game_bindings(const std::shared_ptr &bindings) { + this->game_bindings = bindings; } void InputContext::set_camera_bindings(const std::shared_ptr &bindings) { this->camera_bindings = bindings; } -const std::shared_ptr &InputContext::get_engine_bindings() { - return this->engine_bindings; +const std::shared_ptr &InputContext::get_game_bindings() { + return this->game_bindings; } const std::shared_ptr &InputContext::get_camera_bindings() { diff --git a/libopenage/input/input_context.h b/libopenage/input/input_context.h index f3d2d41903..cec9ba9d72 100644 --- a/libopenage/input/input_context.h +++ b/libopenage/input/input_context.h @@ -47,7 +47,7 @@ class InputContext { * * @param bindings Binding context for gamestate events. */ - void set_engine_bindings(const std::shared_ptr &bindings); + void set_game_bindings(const std::shared_ptr &bindings); /** * Set the associated context for binding input events to camera actions. @@ -61,7 +61,7 @@ class InputContext { * * @return Binding context of the input context. */ - const std::shared_ptr &get_engine_bindings(); + const std::shared_ptr &get_game_bindings(); /** * Get the associated context for binding input events to camera actions. @@ -138,9 +138,9 @@ class InputContext { std::unordered_map by_class; /** - * Additional context for engine events. + * Additional context for game simulation events. */ - std::shared_ptr engine_bindings; + std::shared_ptr game_bindings; /** * Additional context for camera actions. diff --git a/libopenage/input/input_manager.cpp b/libopenage/input/input_manager.cpp index 47ff5c1834..49127b093c 100644 --- a/libopenage/input/input_manager.cpp +++ b/libopenage/input/input_manager.cpp @@ -14,7 +14,7 @@ InputManager::InputManager() : global_context{std::make_shared("main")}, active_contexts{}, available_contexts{}, - engine_controller{nullptr}, + game_controller{nullptr}, camera_controller{nullptr}, gui_input{nullptr} { } @@ -27,8 +27,8 @@ void InputManager::set_camera_controller(const std::shared_ptrcamera_controller = controller; } -void InputManager::set_engine_controller(const std::shared_ptr &controller) { - this->engine_controller = controller; +void InputManager::set_game_controller(const std::shared_ptr &controller) { + this->game_controller = controller; } const std::shared_ptr &InputManager::get_global_context() { @@ -173,8 +173,8 @@ void InputManager::process_action(const input::Event &ev, this->pop_context(ctx_id); break; } - case input_action_t::ENGINE: - this->engine_controller->process(args, ctx->get_engine_bindings()); + case input_action_t::GAME: + this->game_controller->process(args, ctx->get_game_bindings()); break; case input_action_t::CAMERA: @@ -214,14 +214,17 @@ void setup_defaults(const std::shared_ptr &ctx) { ctx->bind(ev_wheel_down, camera_action); ctx->bind(event_class::MOUSE_MOVE, camera_action); - // engine - input_action engine_action{input_action_t::ENGINE}; + // game + input_action game_action{input_action_t::GAME}; Event ev_mouse_lmb{event_class::MOUSE_BUTTON, Qt::LeftButton, Qt::NoModifier, QEvent::MouseButtonRelease}; Event ev_mouse_rmb{event_class::MOUSE_BUTTON, Qt::RightButton, Qt::NoModifier, QEvent::MouseButtonRelease}; - ctx->bind(ev_mouse_lmb, engine_action); - ctx->bind(ev_mouse_rmb, engine_action); + ctx->bind(ev_mouse_lmb, game_action); + ctx->bind(ev_mouse_rmb, game_action); + + // also forward all other mouse button events + ctx->bind(event_class::MOUSE_BUTTON, game_action); } diff --git a/libopenage/input/input_manager.h b/libopenage/input/input_manager.h index f81a5a5bc4..189acf07e0 100644 --- a/libopenage/input/input_manager.h +++ b/libopenage/input/input_manager.h @@ -53,11 +53,11 @@ class InputManager { void set_camera_controller(const std::shared_ptr &controller); /** - * Set the controller for the engine. + * Set the controller for the game simulation. * - * @param controller Engine controller. + * @param controller Game controller. */ - void set_engine_controller(const std::shared_ptr &controller); + void set_game_controller(const std::shared_ptr &controller); /** * returns the global keybind context. @@ -170,9 +170,9 @@ class InputManager { std::unordered_map> available_contexts; /** - * Interface to the engine. + * Interface to the game simulation. */ - std::shared_ptr engine_controller; + std::shared_ptr game_controller; /** * Interface to the camera. diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index 06498e51c6..8fe6315d4a 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -215,12 +215,12 @@ void Presenter::init_input() { log::log(INFO << "Loading game simulation controls"); // TODO: Remove hardcoding - auto engine_controller = std::make_shared( + auto game_controller = std::make_shared( std::unordered_set{0, 1, 2, 3}, 0); auto engine_context = std::make_shared(); input::game::setup_defaults(engine_context, this->time_loop, this->simulation, this->camera); - this->input_manager->set_engine_controller(engine_controller); - input_ctx->set_engine_bindings(engine_context); + this->input_manager->set_game_controller(game_controller); + input_ctx->set_game_bindings(engine_context); } // attach GUI if it's initialized From ad992b4d7e0205613b034892359b262a1df5284f Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 20 Nov 2023 04:06:30 +0100 Subject: [PATCH 153/771] input: Event bindings for drag selection box. --- .../input/controller/game/controller.cpp | 56 ++++++++++++++++++- libopenage/input/controller/game/controller.h | 21 +++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/libopenage/input/controller/game/controller.cpp b/libopenage/input/controller/game/controller.cpp index d5f04c9afd..6005398333 100644 --- a/libopenage/input/controller/game/controller.cpp +++ b/libopenage/input/controller/game/controller.cpp @@ -2,6 +2,8 @@ #include "controller.h" +#include "log/log.h" + #include "event/event_loop.h" #include "event/evententity.h" #include "event/state.h" @@ -48,6 +50,8 @@ const std::vector &Controller::get_selected() const { void Controller::set_selected(std::vector ids) { std::unique_lock lock{this->mutex}; + log::log(DBG << "Selected " << ids.size() << " entities"); + this->selected = ids; } @@ -86,6 +90,24 @@ bool Controller::process(const event_arguments &ev_args, const std::shared_ptrmutex}; + + log::log(DBG << "Drag select start at " << start); + + this->drag_select_start = start; +} + +void Controller::drag_select(const coord::input &end) { + std::unique_lock lock{this->mutex}; + + log::log(DBG << "Drag select end at " << end); + + // TODO + + this->drag_select_start = std::nullopt; +} + void setup_defaults(const std::shared_ptr &ctx, const std::shared_ptr &time_loop, const std::shared_ptr &simulation, @@ -112,13 +134,13 @@ void setup_defaults(const std::shared_ptr &ctx, }}; binding_action create_entity_action{forward_action_t::SEND, create_entity_event}; - Event ev_mouse_lmb{ + Event ev_mouse_lmb_ctrl{ event_class::MOUSE_BUTTON, Qt::MouseButton::LeftButton, Qt::KeyboardModifier::ControlModifier, QEvent::MouseButtonRelease}; - ctx->bind(ev_mouse_lmb, create_entity_action); + ctx->bind(ev_mouse_lmb_ctrl, create_entity_action); binding_func_t move_entity{[&](const event_arguments &args, const std::shared_ptr controller) { @@ -146,6 +168,36 @@ void setup_defaults(const std::shared_ptr &ctx, QEvent::MouseButtonRelease}; ctx->bind(ev_mouse_rmb, move_entity_action); + + binding_func_t init_drag_selection{[&](const event_arguments &args, + const std::shared_ptr controller) { + controller->set_drag_select_start(args.mouse); + return nullptr; + }}; + + binding_action init_drag_selection_action{forward_action_t::CLEAR, init_drag_selection}; + Event ev_mouse_lmb_press{ + event_class::MOUSE_BUTTON, + Qt::MouseButton::LeftButton, + Qt::KeyboardModifier::NoModifier, + QEvent::MouseButtonPress}; + + ctx->bind(ev_mouse_lmb_press, init_drag_selection_action); + + binding_func_t drag_selection{[&](const event_arguments &args, + const std::shared_ptr controller) { + controller->drag_select(args.mouse); + return nullptr; + }}; + + binding_action drag_selection_action{forward_action_t::CLEAR, drag_selection}; + Event ev_mouse_lmb_release{ + event_class::MOUSE_BUTTON, + Qt::MouseButton::LeftButton, + Qt::KeyboardModifier::NoModifier, + QEvent::MouseButtonRelease}; + + ctx->bind(ev_mouse_lmb_release, drag_selection_action); } diff --git a/libopenage/input/controller/game/controller.h b/libopenage/input/controller/game/controller.h index a75483492c..0578e712b1 100644 --- a/libopenage/input/controller/game/controller.h +++ b/libopenage/input/controller/game/controller.h @@ -4,8 +4,10 @@ #include #include +#include #include +#include "coord/pixel.h" #include "curve/discrete.h" #include "gamestate/types.h" #include "input/event.h" @@ -78,6 +80,18 @@ class Controller : public std::enable_shared_from_this { */ bool process(const event_arguments &ev_args, const std::shared_ptr &ctx); + /** + * Set the start position of a drag selection. + */ + void set_drag_select_start(const coord::input &start); + + /** + * Process a drag selection. + * + * @param end End position of the drag selection. + */ + void drag_select(const coord::input &end); + private: /** * Factions controllable by this controller. @@ -99,6 +113,13 @@ class Controller : public std::enable_shared_from_this { */ std::vector> outqueue; + /** + * Start position of a drag selection. + * + * TODO: Move this into an input event. + */ + std::optional drag_select_start; + /** * Mutex for threaded access. */ From 3e16e5d102aedcd791a3a0bca2ad7b5fef4fd794 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 22 Nov 2023 04:02:20 +0100 Subject: [PATCH 154/771] gamestate: Drag selection as event. --- libopenage/gamestate/event/CMakeLists.txt | 1 + libopenage/gamestate/event/drag_select.cpp | 105 ++++++++++++++++++ libopenage/gamestate/event/drag_select.h | 46 ++++++++ libopenage/gamestate/event/spawn_entity.cpp | 6 +- libopenage/gamestate/simulation.cpp | 3 + .../input/controller/game/controller.cpp | 58 +++++++--- libopenage/input/controller/game/controller.h | 12 +- 7 files changed, 211 insertions(+), 20 deletions(-) create mode 100644 libopenage/gamestate/event/drag_select.cpp create mode 100644 libopenage/gamestate/event/drag_select.h diff --git a/libopenage/gamestate/event/CMakeLists.txt b/libopenage/gamestate/event/CMakeLists.txt index 309ba96909..4c885d82b1 100644 --- a/libopenage/gamestate/event/CMakeLists.txt +++ b/libopenage/gamestate/event/CMakeLists.txt @@ -1,4 +1,5 @@ add_sources(libopenage + drag_select.cpp process_command.cpp send_command.cpp spawn_entity.cpp diff --git a/libopenage/gamestate/event/drag_select.cpp b/libopenage/gamestate/event/drag_select.cpp new file mode 100644 index 0000000000..740affeefb --- /dev/null +++ b/libopenage/gamestate/event/drag_select.cpp @@ -0,0 +1,105 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "drag_select.h" + +#include "coord/phys.h" +#include "curve/discrete.h" +#include "gamestate/component/internal/ownership.h" +#include "gamestate/component/internal/position.h" +#include "gamestate/game_entity.h" +#include "gamestate/game_state.h" +#include "gamestate/types.h" + + +namespace openage::gamestate::event { + +DragSelectHandler::DragSelectHandler() : + OnceEventHandler{"game.drag_select"} {} + +void DragSelectHandler::setup_event(const std::shared_ptr & /* event */, + const std::shared_ptr & /* state */) { + // TODO +} + +void DragSelectHandler::invoke(openage::event::EventLoop & /* loop */, + const std::shared_ptr & /* target */, + const std::shared_ptr &state, + const time::time_t &time, + const param_map ¶ms) { + auto gstate = std::dynamic_pointer_cast(state); + + size_t controlled_id = params.get("controlled", 0); + + // Start and end of the drag select rectangle + coord::phys3 start = params.get("drag_start", coord::phys3{0, 0, 0}); + coord::phys3 end = params.get("drag_end", coord::phys3{0, 0, 0}); + coord::phys3 corner0 = params.get("drag_corner0", coord::phys3{0, 0, 0}); + coord::phys3 corner1 = params.get("drag_corner1", coord::phys3{0, 0, 0}); + + // Check which coordinate is the top left and which is the bottom right + coord::phys3 top_left = coord::phys3{0, 0, 0}; + coord::phys3 bottom_right = coord::phys3{0, 0, 0}; + if (start.se < end.se) { + top_left = start; + bottom_right = end; + } + else { + top_left = end; + bottom_right = start; + } + + // Get the other two corners of the rectangle + coord::phys3 top_right = coord::phys3{0, 0, 0}; + coord::phys3 bottom_left = coord::phys3{0, 0, 0}; + if (corner0.ne < corner1.ne) { + top_right = corner1; + bottom_left = corner0; + } + else { + top_right = corner0; + bottom_left = corner1; + } + + log::log(DBG << "Drag select rectangle:"); + log::log(DBG << "\tTop left: " << top_left); + log::log(DBG << "\tTop right: " << top_right); + log::log(DBG << "\tBottom left: " << bottom_left); + log::log(DBG << "\tBottom right: " << bottom_right); + + // Check which entities are in the rectangle + std::vector selected; + for (auto entity : gstate->get_game_entities()) { + auto owner = std::dynamic_pointer_cast( + entity.second->get_component(component::component_t::OWNERSHIP)); + if (owner->get_owners().get(time) != controlled_id) { + // only select entities of the controlled player + continue; + } + + auto pos = std::dynamic_pointer_cast( + entity.second->get_component(component::component_t::POSITION)); + auto current_pos = pos->get_positions().get(time); + if (current_pos.ne <= top_right.ne + and current_pos.ne >= bottom_left.ne + and current_pos.se <= bottom_right.se + and current_pos.se >= top_left.se) { + // check if the entity is in the rectangle + selected.push_back(entity.first); + } + } + + // Select the units + auto select_cb = params.get("select_cb", + std::function ids)>{ + [](const std::vector /* ids */) {}}); + select_cb(selected); +} + +time::time_t DragSelectHandler::predict_invoke_time(const std::shared_ptr & /* target */, + const std::shared_ptr & /* state */, + const time::time_t &at) { + return at; +} + + +} // namespace openage::gamestate::event diff --git a/libopenage/gamestate/event/drag_select.h b/libopenage/gamestate/event/drag_select.h new file mode 100644 index 0000000000..29a7338f27 --- /dev/null +++ b/libopenage/gamestate/event/drag_select.h @@ -0,0 +1,46 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include + +#include "event/evententity.h" +#include "event/eventhandler.h" + + +namespace openage { + +namespace event { +class EventLoop; +class Event; +class State; +} // namespace event + +namespace gamestate::event { + +/** + * Drag select game entities. + */ +class DragSelectHandler : public openage::event::OnceEventHandler { +public: + DragSelectHandler(); + ~DragSelectHandler() = default; + + void setup_event(const std::shared_ptr &event, + const std::shared_ptr &state) override; + + void invoke(openage::event::EventLoop &loop, + const std::shared_ptr &target, + const std::shared_ptr &state, + const time::time_t &time, + const param_map ¶ms) override; + + time::time_t predict_invoke_time(const std::shared_ptr &target, + const std::shared_ptr &state, + const time::time_t &at) override; +}; + +} // namespace gamestate::event +} // namespace openage diff --git a/libopenage/gamestate/event/spawn_entity.cpp b/libopenage/gamestate/event/spawn_entity.cpp index 155441445f..25ceed3431 100644 --- a/libopenage/gamestate/event/spawn_entity.cpp +++ b/libopenage/gamestate/event/spawn_entity.cpp @@ -198,8 +198,10 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, // TODO: Select the unit when it's created // very dumb but it gets the job done - auto select_cb = params.get("select_cb", std::function{[](entity_id_t /* id */) {}}); - select_cb(entity->get_id()); + auto select_cb = params.get("select_cb", + std::function ids)>{ + [](const std::vector /* ids */) {}}); + select_cb({entity->get_id()}); gstate->add_game_entity(entity); } diff --git a/libopenage/gamestate/simulation.cpp b/libopenage/gamestate/simulation.cpp index 4af77d2815..e883bd56f7 100644 --- a/libopenage/gamestate/simulation.cpp +++ b/libopenage/gamestate/simulation.cpp @@ -5,6 +5,7 @@ #include "assets/mod_manager.h" #include "event/event_loop.h" #include "gamestate/entity_factory.h" +#include "gamestate/event/drag_select.h" #include "gamestate/event/process_command.h" #include "gamestate/event/send_command.h" #include "gamestate/event/spawn_entity.h" @@ -133,11 +134,13 @@ void GameSimulation::set_modpacks(const std::vector &modpacks) { } void GameSimulation::init_event_handlers() { + auto drag_select_handler = std::make_shared(); auto spawn_handler = std::make_shared(this->event_loop, this->entity_factory); auto command_handler = std::make_shared(); auto manager_handler = std::make_shared(); auto wait_handler = std::make_shared(); + this->event_loop->add_event_handler(drag_select_handler); this->event_loop->add_event_handler(spawn_handler); this->event_loop->add_event_handler(command_handler); this->event_loop->add_event_handler(manager_handler); diff --git a/libopenage/input/controller/game/controller.cpp b/libopenage/input/controller/game/controller.cpp index 6005398333..cd0f0833a5 100644 --- a/libopenage/input/controller/game/controller.cpp +++ b/libopenage/input/controller/game/controller.cpp @@ -47,7 +47,7 @@ const std::vector &Controller::get_selected() const { return this->selected; } -void Controller::set_selected(std::vector ids) { +void Controller::set_selected(const std::vector ids) { std::unique_lock lock{this->mutex}; log::log(DBG << "Selected " << ids.size() << " entities"); @@ -90,22 +90,27 @@ bool Controller::process(const event_arguments &ev_args, const std::shared_ptr &start) { std::unique_lock lock{this->mutex}; - log::log(DBG << "Drag select start at " << start); + if (start.has_value()) { + log::log(DBG << "Drag select start at " << start.value()); + } + else { + log::log(DBG << "Drag select start cleared"); + } this->drag_select_start = start; } -void Controller::drag_select(const coord::input &end) { +const coord::input Controller::get_drag_select_start() const { std::unique_lock lock{this->mutex}; - log::log(DBG << "Drag select end at " << end); - - // TODO + if (not this->drag_select_start.has_value()) { + throw Error{ERR << "Drag select start not set."}; + } - this->drag_select_start = std::nullopt; + return this->drag_select_start.value(); } void setup_defaults(const std::shared_ptr &ctx, @@ -119,9 +124,12 @@ void setup_defaults(const std::shared_ptr &ctx, {"position", mouse_pos}, {"owner", controller->get_controlled()}, // TODO: Remove - {"select_cb", std::function{[controller](gamestate::entity_id_t id) { - controller->set_selected({id}); - }}}, + {"select_cb", + std::function ids)>{ + [controller]( + const std::vector ids) { + controller->set_selected(ids); + }}}, }; auto event = simulation->get_event_loop()->create_event( @@ -186,8 +194,32 @@ void setup_defaults(const std::shared_ptr &ctx, binding_func_t drag_selection{[&](const event_arguments &args, const std::shared_ptr controller) { - controller->drag_select(args.mouse); - return nullptr; + event::EventHandler::param_map::map_t params{ + {"controlled", controller->get_controlled()}, + {"drag_start", controller->get_drag_select_start().to_phys3(camera)}, + {"drag_end", args.mouse.to_phys3(camera)}, + {"drag_corner0", coord::input{args.mouse.x, controller->get_drag_select_start().y}.to_phys3(camera)}, + {"drag_corner1", coord::input{controller->get_drag_select_start().x, args.mouse.y}.to_phys3(camera)}, + // TODO: Remove + {"select_cb", + std::function ids)>{ + [controller]( + const std::vector ids) { + controller->set_selected(ids); + }}}, + }; + + auto event = simulation->get_event_loop()->create_event( + "game.drag_select", + simulation->get_commander(), + simulation->get_game()->get_state(), + time_loop->get_clock()->get_time(), + params); + + // Reset drag selection start + controller->set_drag_select_start(std::nullopt); + + return event; }}; binding_action drag_selection_action{forward_action_t::CLEAR, drag_selection}; diff --git a/libopenage/input/controller/game/controller.h b/libopenage/input/controller/game/controller.h index 0578e712b1..c825b6e6a9 100644 --- a/libopenage/input/controller/game/controller.h +++ b/libopenage/input/controller/game/controller.h @@ -68,7 +68,7 @@ class Controller : public std::enable_shared_from_this { * * @param ids Selected entities. */ - void set_selected(std::vector ids); + void set_selected(const std::vector ids); /** * Process an input event from the input manager. @@ -82,15 +82,17 @@ class Controller : public std::enable_shared_from_this { /** * Set the start position of a drag selection. + * + * @param start Start position of the drag selection. */ - void set_drag_select_start(const coord::input &start); + void set_drag_select_start(const std::optional &start); /** - * Process a drag selection. + * Get the start position of a drag selection. * - * @param end End position of the drag selection. + * @return Start position of the drag selection. */ - void drag_select(const coord::input &end); + const coord::input get_drag_select_start() const; private: /** From 20f870916246ca445c30e3968fd45da034b528f4 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 22 Nov 2023 05:08:32 +0100 Subject: [PATCH 155/771] input: HUD controller. --- libopenage/input/controller/CMakeLists.txt | 1 + .../input/controller/hud/CMakeLists.txt | 5 ++ libopenage/input/controller/hud/binding.cpp | 7 ++ libopenage/input/controller/hud/binding.h | 28 ++++++++ .../input/controller/hud/binding_context.cpp | 39 ++++++++++ .../input/controller/hud/binding_context.h | 72 +++++++++++++++++++ .../input/controller/hud/controller.cpp | 30 ++++++++ libopenage/input/controller/hud/controller.h | 42 +++++++++++ libopenage/presenter/presenter.cpp | 1 + 9 files changed, 225 insertions(+) create mode 100644 libopenage/input/controller/hud/CMakeLists.txt create mode 100644 libopenage/input/controller/hud/binding.cpp create mode 100644 libopenage/input/controller/hud/binding.h create mode 100644 libopenage/input/controller/hud/binding_context.cpp create mode 100644 libopenage/input/controller/hud/binding_context.h create mode 100644 libopenage/input/controller/hud/controller.cpp create mode 100644 libopenage/input/controller/hud/controller.h diff --git a/libopenage/input/controller/CMakeLists.txt b/libopenage/input/controller/CMakeLists.txt index fd940942a7..eab16635b6 100644 --- a/libopenage/input/controller/CMakeLists.txt +++ b/libopenage/input/controller/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory("camera") add_subdirectory("game") +add_subdirectory("hud") diff --git a/libopenage/input/controller/hud/CMakeLists.txt b/libopenage/input/controller/hud/CMakeLists.txt new file mode 100644 index 0000000000..0d348caf31 --- /dev/null +++ b/libopenage/input/controller/hud/CMakeLists.txt @@ -0,0 +1,5 @@ +add_sources(libopenage + binding_context.cpp + binding.cpp + controller.cpp +) diff --git a/libopenage/input/controller/hud/binding.cpp b/libopenage/input/controller/hud/binding.cpp new file mode 100644 index 0000000000..1c886a3d43 --- /dev/null +++ b/libopenage/input/controller/hud/binding.cpp @@ -0,0 +1,7 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "binding.h" + +namespace openage::input::hud { + +} // namespace openage::input::hud diff --git a/libopenage/input/controller/hud/binding.h b/libopenage/input/controller/hud/binding.h new file mode 100644 index 0000000000..a899ffbaf2 --- /dev/null +++ b/libopenage/input/controller/hud/binding.h @@ -0,0 +1,28 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "event/event.h" +#include "input/event.h" + +namespace openage::input::hud { + +using binding_flags_t = std::unordered_map; +using binding_func_t = std::function; + + +/** + * Action taken by the controller when receiving an input. + * + * @param action Maps an input event to a camera action. + * @param flags Additional parameters for the transformation. + */ +struct binding_action { + const binding_func_t action; + const binding_flags_t flags = {}; +}; + +} // namespace openage::input::hud diff --git a/libopenage/input/controller/hud/binding_context.cpp b/libopenage/input/controller/hud/binding_context.cpp new file mode 100644 index 0000000000..fb1d7a5e25 --- /dev/null +++ b/libopenage/input/controller/hud/binding_context.cpp @@ -0,0 +1,39 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "binding_context.h" + + +namespace openage::input::hud { + +BindingContext::BindingContext() : + by_event{} {} + +void BindingContext::bind(const Event &ev, const binding_action bind) { + this->by_event.emplace(std::make_pair(ev, bind)); +} + +void BindingContext::bind(const event_class &cl, const binding_action bind) { + this->by_class.emplace(std::make_pair(cl, bind)); +} + +bool BindingContext::is_bound(const Event &ev) const { + return this->by_event.contains(ev) || this->by_class.contains(ev.cc.cl); +} + +const binding_action &BindingContext::lookup(const Event &ev) const { + auto event_lookup = this->by_event.find(ev); + if (event_lookup != std::end(this->by_event)) { + return (*event_lookup).second; + } + + for (auto eclass : ev.cc.get_classes()) { + auto class_lookup = this->by_class.find(eclass); + if (class_lookup != std::end(this->by_class)) { + return (*class_lookup).second; + } + } + + throw Error{MSG(err) << "Event is not bound in binding_action context."}; +} + +} // namespace openage::input::hud diff --git a/libopenage/input/controller/hud/binding_context.h b/libopenage/input/controller/hud/binding_context.h new file mode 100644 index 0000000000..fac8283df6 --- /dev/null +++ b/libopenage/input/controller/hud/binding_context.h @@ -0,0 +1,72 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include "input/controller/hud/binding.h" +#include "input/event.h" + +namespace openage::input::hud { + +/** + * Maps input events to HUD actons. + */ +class BindingContext { +public: + /** + * Create a new binding context. + */ + BindingContext(); + + ~BindingContext() = default; + + /** + * Bind a specific key combination to a binding. + * + * This is the first matching priority. + * + * @param ev Input event triggering the action. + * @param bind Binding for the event. + */ + void bind(const Event &ev, const binding_action bind); + + /** + * Bind an event class to an action. + * + * This is the second matching priority. + * + * @param ev Input event triggering the action. + * @param bind Binding for the event. + */ + void bind(const event_class &cl, const binding_action bind); + + /** + * Check whether a specific key event is bound in this context. + * + * @param ev Input event. + * + * @return true if event is bound, else false. + */ + bool is_bound(const Event &ev) const; + + /** + * Get the bindings for a specific event. + * + * @param ev Input event mapped to the binding. + */ + const binding_action &lookup(const Event &ev) const; + +private: + /** + * Maps specific input events to bindings. + */ + std::unordered_map by_event; + + /** + * Maps event classes to bindings. + */ + std::unordered_map by_class; +}; + +} // namespace openage::input::hud diff --git a/libopenage/input/controller/hud/controller.cpp b/libopenage/input/controller/hud/controller.cpp new file mode 100644 index 0000000000..c130856b9b --- /dev/null +++ b/libopenage/input/controller/hud/controller.cpp @@ -0,0 +1,30 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "controller.h" + +#include + +#include "input/controller/hud/binding_context.h" + + +namespace openage::input::hud { + +Controller::Controller() {} + +bool Controller::process(const event_arguments &ev_args, + const std::shared_ptr &ctx) { + if (not ctx->is_bound(ev_args.e)) { + return false; + } + + auto bind = ctx->lookup(ev_args.e); + bind.action(ev_args); + + return true; +} + +void setup_defaults(const std::shared_ptr &ctx) { + // TODO +} + +} // namespace openage::input::hud diff --git a/libopenage/input/controller/hud/controller.h b/libopenage/input/controller/hud/controller.h new file mode 100644 index 0000000000..5ca5d31ced --- /dev/null +++ b/libopenage/input/controller/hud/controller.h @@ -0,0 +1,42 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include "input/event.h" + + +namespace openage::input::hud { +class BindingContext; + +/** + * Control a camera with input from the input manager. + */ +class Controller { +public: + Controller(); + + ~Controller() = default; + + /** + * Process an input event from the input manager. + * + * @param ev Input event and arguments. + * @param ctx Binding context that maps input events to HUD actions. + * + * @return true if the event is accepted, else false. + */ + bool process(const event_arguments &ev_args, const std::shared_ptr &ctx); +}; + +/** + * Setup default HUD action bindings: + * + * - Mouse drag: selectION BOX + * + * TODO: Make this configurable. + * + * @param ctx Binding context the actions are added to. + */ +void setup_defaults(const std::shared_ptr &ctx); + +} // namespace openage::input::hud diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index 8fe6315d4a..f576560c97 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -269,6 +269,7 @@ void Presenter::init_final_render_pass() { } void Presenter::render() { + // TODO: Pass current time to update() instead of fetching it in renderer this->camera_manager->update(); this->terrain_renderer->update(); this->world_renderer->update(); From c8657df6387313d03bd97ec6c698d2fa1ab808c0 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 23 Nov 2023 08:50:16 +0100 Subject: [PATCH 156/771] assets: Shader for drag select rectangle. --- assets/shaders/hud_drag_select.frag.glsl | 10 ++++++++++ assets/shaders/hud_drag_select.vert.glsl | 7 +++++++ 2 files changed, 17 insertions(+) create mode 100644 assets/shaders/hud_drag_select.frag.glsl create mode 100644 assets/shaders/hud_drag_select.vert.glsl diff --git a/assets/shaders/hud_drag_select.frag.glsl b/assets/shaders/hud_drag_select.frag.glsl new file mode 100644 index 0000000000..a6a36e5a1d --- /dev/null +++ b/assets/shaders/hud_drag_select.frag.glsl @@ -0,0 +1,10 @@ +#version 330 + +// Color of the drag rectangle +uniform vec4 in_col; + +layout(location=0) out vec4 out_col; + +void main() { + out_col = in_col; +} diff --git a/assets/shaders/hud_drag_select.vert.glsl b/assets/shaders/hud_drag_select.vert.glsl new file mode 100644 index 0000000000..527e0bb652 --- /dev/null +++ b/assets/shaders/hud_drag_select.vert.glsl @@ -0,0 +1,7 @@ +#version 330 + +layout(location=0) in vec2 position; + +void main() { + gl_Position = vec4(position, 0.0, 1.0); +} From d2f85389a2ce581017f0d7c261bbdaf5eba3f254 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 25 Nov 2023 05:12:50 +0100 Subject: [PATCH 157/771] input: Select with viewport transformation. --- libopenage/gamestate/event/drag_select.cpp | 68 ++++++++----------- .../input/controller/game/controller.cpp | 61 +++++++++-------- 2 files changed, 60 insertions(+), 69 deletions(-) diff --git a/libopenage/gamestate/event/drag_select.cpp b/libopenage/gamestate/event/drag_select.cpp index 740affeefb..c4d03ea1f8 100644 --- a/libopenage/gamestate/event/drag_select.cpp +++ b/libopenage/gamestate/event/drag_select.cpp @@ -2,7 +2,11 @@ #include "drag_select.h" +#include + #include "coord/phys.h" +#include "coord/pixel.h" +#include "coord/scene.h" #include "curve/discrete.h" #include "gamestate/component/internal/ownership.h" #include "gamestate/component/internal/position.h" @@ -30,45 +34,25 @@ void DragSelectHandler::invoke(openage::event::EventLoop & /* loop */, size_t controlled_id = params.get("controlled", 0); - // Start and end of the drag select rectangle - coord::phys3 start = params.get("drag_start", coord::phys3{0, 0, 0}); - coord::phys3 end = params.get("drag_end", coord::phys3{0, 0, 0}); - coord::phys3 corner0 = params.get("drag_corner0", coord::phys3{0, 0, 0}); - coord::phys3 corner1 = params.get("drag_corner1", coord::phys3{0, 0, 0}); - - // Check which coordinate is the top left and which is the bottom right - coord::phys3 top_left = coord::phys3{0, 0, 0}; - coord::phys3 bottom_right = coord::phys3{0, 0, 0}; - if (start.se < end.se) { - top_left = start; - bottom_right = end; - } - else { - top_left = end; - bottom_right = start; - } + Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity(); + Eigen::Matrix4f cam_matrix = params.get("camera_matrix", id_matrix); + Eigen::Vector2f drag_start = params.get("drag_start", Eigen::Vector2f{0, 0}); + Eigen::Vector2f drag_end = params.get("drag_end", Eigen::Vector2f{0, 0}); - // Get the other two corners of the rectangle - coord::phys3 top_right = coord::phys3{0, 0, 0}; - coord::phys3 bottom_left = coord::phys3{0, 0, 0}; - if (corner0.ne < corner1.ne) { - top_right = corner1; - bottom_left = corner0; - } - else { - top_right = corner0; - bottom_left = corner1; - } + // Boundaries of the rectangle + float top = std::max(drag_start.y(), drag_end.y()); + float bottom = std::min(drag_start.y(), drag_end.y()); + float left = std::min(drag_start.x(), drag_end.x()); + float right = std::max(drag_start.x(), drag_end.x()); - log::log(DBG << "Drag select rectangle:"); - log::log(DBG << "\tTop left: " << top_left); - log::log(DBG << "\tTop right: " << top_right); - log::log(DBG << "\tBottom left: " << bottom_left); - log::log(DBG << "\tBottom right: " << bottom_right); + log::log(DBG << "Drag select rectangle (NDC):"); + log::log(DBG << "\tTop: " << top); + log::log(DBG << "\tBottom: " << bottom); + log::log(DBG << "\tLeft: " << left); + log::log(DBG << "\tRight: " << right); - // Check which entities are in the rectangle std::vector selected; - for (auto entity : gstate->get_game_entities()) { + for (auto &entity : gstate->get_game_entities()) { auto owner = std::dynamic_pointer_cast( entity.second->get_component(component::component_t::OWNERSHIP)); if (owner->get_owners().get(time) != controlled_id) { @@ -76,14 +60,18 @@ void DragSelectHandler::invoke(openage::event::EventLoop & /* loop */, continue; } + // Get the position of the entity in the viewport auto pos = std::dynamic_pointer_cast( entity.second->get_component(component::component_t::POSITION)); auto current_pos = pos->get_positions().get(time); - if (current_pos.ne <= top_right.ne - and current_pos.ne >= bottom_left.ne - and current_pos.se <= bottom_right.se - and current_pos.se >= top_left.se) { - // check if the entity is in the rectangle + auto world_pos = current_pos.to_scene3().to_world_space(); + Eigen::Vector4f clip_pos = cam_matrix * Eigen::Vector4f{world_pos.x(), world_pos.y(), world_pos.z(), 1}; + + // Check if the entity is in the rectangle + if (clip_pos.x() > left + and clip_pos.x() < right + and clip_pos.y() > bottom + and clip_pos.y() < top) { selected.push_back(entity.first); } } diff --git a/libopenage/input/controller/game/controller.cpp b/libopenage/input/controller/game/controller.cpp index cd0f0833a5..2cc318acd3 100644 --- a/libopenage/input/controller/game/controller.cpp +++ b/libopenage/input/controller/game/controller.cpp @@ -18,6 +18,8 @@ #include "time/time_loop.h" #include "coord/phys.h" +#include "renderer/camera/camera.h" + namespace openage::input::game { @@ -192,35 +194,36 @@ void setup_defaults(const std::shared_ptr &ctx, ctx->bind(ev_mouse_lmb_press, init_drag_selection_action); - binding_func_t drag_selection{[&](const event_arguments &args, - const std::shared_ptr controller) { - event::EventHandler::param_map::map_t params{ - {"controlled", controller->get_controlled()}, - {"drag_start", controller->get_drag_select_start().to_phys3(camera)}, - {"drag_end", args.mouse.to_phys3(camera)}, - {"drag_corner0", coord::input{args.mouse.x, controller->get_drag_select_start().y}.to_phys3(camera)}, - {"drag_corner1", coord::input{controller->get_drag_select_start().x, args.mouse.y}.to_phys3(camera)}, - // TODO: Remove - {"select_cb", - std::function ids)>{ - [controller]( - const std::vector ids) { - controller->set_selected(ids); - }}}, - }; - - auto event = simulation->get_event_loop()->create_event( - "game.drag_select", - simulation->get_commander(), - simulation->get_game()->get_state(), - time_loop->get_clock()->get_time(), - params); - - // Reset drag selection start - controller->set_drag_select_start(std::nullopt); - - return event; - }}; + binding_func_t drag_selection{ + [&](const event_arguments &args, + const std::shared_ptr controller) { + Eigen::Matrix4f cam_matrix = camera->get_projection_matrix() * camera->get_view_matrix(); + event::EventHandler::param_map::map_t params{ + {"controlled", controller->get_controlled()}, + {"drag_start", controller->get_drag_select_start().to_viewport(camera).to_ndc_space(camera)}, + {"drag_end", args.mouse.to_viewport(camera).to_ndc_space(camera)}, + {"camera_matrix", cam_matrix}, + // TODO: Remove + {"select_cb", + std::function ids)>{ + [controller]( + const std::vector ids) { + controller->set_selected(ids); + }}}, + }; + + auto event = simulation->get_event_loop()->create_event( + "game.drag_select", + simulation->get_commander(), + simulation->get_game()->get_state(), + time_loop->get_clock()->get_time(), + params); + + // Reset drag selection start + controller->set_drag_select_start(std::nullopt); + + return event; + }}; binding_action drag_selection_action{forward_action_t::CLEAR, drag_selection}; Event ev_mouse_lmb_release{ From 05b1fbabf51795d07fe0c0267b1244a6877b130a Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 25 Nov 2023 05:47:44 +0100 Subject: [PATCH 158/771] gamestate: Add 'Selectable' component. --- .../gamestate/component/api/CMakeLists.txt | 1 + .../gamestate/component/api/selectable.cpp | 12 +++++++++++ .../gamestate/component/api/selectable.h | 20 +++++++++++++++++++ libopenage/gamestate/component/types.h | 1 + 4 files changed, 34 insertions(+) create mode 100644 libopenage/gamestate/component/api/selectable.cpp create mode 100644 libopenage/gamestate/component/api/selectable.h diff --git a/libopenage/gamestate/component/api/CMakeLists.txt b/libopenage/gamestate/component/api/CMakeLists.txt index 16947be1ba..8588909bf2 100644 --- a/libopenage/gamestate/component/api/CMakeLists.txt +++ b/libopenage/gamestate/component/api/CMakeLists.txt @@ -2,5 +2,6 @@ add_sources(libopenage idle.cpp live.cpp move.cpp + selectable.cpp turn.cpp ) diff --git a/libopenage/gamestate/component/api/selectable.cpp b/libopenage/gamestate/component/api/selectable.cpp new file mode 100644 index 0000000000..c397a146de --- /dev/null +++ b/libopenage/gamestate/component/api/selectable.cpp @@ -0,0 +1,12 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "selectable.h" + + +namespace openage::gamestate::component { + +component_t Selectable::get_type() const { + return component_t::SELECTABLE; +} + +} // namespace openage::gamestate::component diff --git a/libopenage/gamestate/component/api/selectable.h b/libopenage/gamestate/component/api/selectable.h new file mode 100644 index 0000000000..d12e1522a7 --- /dev/null +++ b/libopenage/gamestate/component/api/selectable.h @@ -0,0 +1,20 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include "gamestate/component/api_component.h" +#include "gamestate/component/types.h" + + +namespace openage::gamestate::component { + +class Selectable : public APIComponent { +public: + using APIComponent::APIComponent; + + component_t get_type() const override; +}; + +} // namespace openage::gamestate::component diff --git a/libopenage/gamestate/component/types.h b/libopenage/gamestate/component/types.h index 500b42692d..5e87b8ed23 100644 --- a/libopenage/gamestate/component/types.h +++ b/libopenage/gamestate/component/types.h @@ -19,6 +19,7 @@ enum class component_t { IDLE, TURN, MOVE, + SELECTABLE, LIVE }; From a5322558fc8a8a387e6805ae4dcb2e78ca0a4092 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 25 Nov 2023 05:53:02 +0100 Subject: [PATCH 159/771] gamestate: Assign 'Selectable' component when configured in nyan. --- libopenage/gamestate/entity_factory.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index 831527c1ec..f2f489206c 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -26,6 +26,7 @@ #include "gamestate/component/api/idle.h" #include "gamestate/component/api/live.h" #include "gamestate/component/api/move.h" +#include "gamestate/component/api/selectable.h" #include "gamestate/component/api/turn.h" #include "gamestate/component/internal/activity.h" #include "gamestate/component/internal/command_queue.h" @@ -203,6 +204,10 @@ void EntityFactory::init_components(const std::shared_ptr(loop, ability_obj); + entity->add_component(selectable); + } } if (activity_ability) { From 16ef5e7caa82e328109d8bd374f2ab330f4e38d7 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 25 Nov 2023 05:57:27 +0100 Subject: [PATCH 160/771] gamestate: Check if an entity is selectable when drag selecting. --- libopenage/gamestate/event/drag_select.cpp | 7 +++++++ libopenage/gamestate/game_entity.h | 2 ++ 2 files changed, 9 insertions(+) diff --git a/libopenage/gamestate/event/drag_select.cpp b/libopenage/gamestate/event/drag_select.cpp index c4d03ea1f8..373bfbfe2d 100644 --- a/libopenage/gamestate/event/drag_select.cpp +++ b/libopenage/gamestate/event/drag_select.cpp @@ -53,6 +53,13 @@ void DragSelectHandler::invoke(openage::event::EventLoop & /* loop */, std::vector selected; for (auto &entity : gstate->get_game_entities()) { + if (not entity.second->has_component(component::component_t::SELECTABLE)) { + // skip entities that are not selectable + continue; + } + + // Check if the entity is owned by the controlled player + // TODO: Check this using Selectable diplomatic property auto owner = std::dynamic_pointer_cast( entity.second->get_component(component::component_t::OWNERSHIP)); if (owner->get_owners().get(time) != controlled_id) { diff --git a/libopenage/gamestate/game_entity.h b/libopenage/gamestate/game_entity.h index d4b61202e9..f72544427d 100644 --- a/libopenage/gamestate/game_entity.h +++ b/libopenage/gamestate/game_entity.h @@ -134,6 +134,8 @@ class GameEntity { /** * Data components. + * + * TODO: Multiple components of the same type. */ std::unordered_map> components; From 4247e3caeef18d9f6e3797facf5525bb6c9422d2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 25 Nov 2023 05:58:06 +0100 Subject: [PATCH 161/771] gamestate: Remove auto-selection on spawning. --- libopenage/gamestate/event/spawn_entity.cpp | 7 ------- libopenage/input/controller/game/controller.cpp | 8 -------- 2 files changed, 15 deletions(-) diff --git a/libopenage/gamestate/event/spawn_entity.cpp b/libopenage/gamestate/event/spawn_entity.cpp index 25ceed3431..9a728a71ef 100644 --- a/libopenage/gamestate/event/spawn_entity.cpp +++ b/libopenage/gamestate/event/spawn_entity.cpp @@ -196,13 +196,6 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, activity->init(time); entity->get_manager()->run_activity_system(time); - // TODO: Select the unit when it's created - // very dumb but it gets the job done - auto select_cb = params.get("select_cb", - std::function ids)>{ - [](const std::vector /* ids */) {}}); - select_cb({entity->get_id()}); - gstate->add_game_entity(entity); } diff --git a/libopenage/input/controller/game/controller.cpp b/libopenage/input/controller/game/controller.cpp index 2cc318acd3..f53caeeda7 100644 --- a/libopenage/input/controller/game/controller.cpp +++ b/libopenage/input/controller/game/controller.cpp @@ -125,13 +125,6 @@ void setup_defaults(const std::shared_ptr &ctx, event::EventHandler::param_map::map_t params{ {"position", mouse_pos}, {"owner", controller->get_controlled()}, - // TODO: Remove - {"select_cb", - std::function ids)>{ - [controller]( - const std::vector ids) { - controller->set_selected(ids); - }}}, }; auto event = simulation->get_event_loop()->create_event( @@ -203,7 +196,6 @@ void setup_defaults(const std::shared_ptr &ctx, {"drag_start", controller->get_drag_select_start().to_viewport(camera).to_ndc_space(camera)}, {"drag_end", args.mouse.to_viewport(camera).to_ndc_space(camera)}, {"camera_matrix", cam_matrix}, - // TODO: Remove {"select_cb", std::function ids)>{ [controller]( From 522bd56ca0282bf78ebc2bff2ac1f545e89ae6f4 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 25 Nov 2023 06:13:26 +0100 Subject: [PATCH 162/771] input: Separate method for resetting drag select. --- .../input/controller/game/controller.cpp | 19 ++++++++++--------- libopenage/input/controller/game/controller.h | 7 ++++++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/libopenage/input/controller/game/controller.cpp b/libopenage/input/controller/game/controller.cpp index f53caeeda7..96ccf53429 100644 --- a/libopenage/input/controller/game/controller.cpp +++ b/libopenage/input/controller/game/controller.cpp @@ -92,16 +92,10 @@ bool Controller::process(const event_arguments &ev_args, const std::shared_ptr &start) { +void Controller::set_drag_select_start(const coord::input &start) { std::unique_lock lock{this->mutex}; - if (start.has_value()) { - log::log(DBG << "Drag select start at " << start.value()); - } - else { - log::log(DBG << "Drag select start cleared"); - } - + log::log(DBG << "Drag select start at " << start); this->drag_select_start = start; } @@ -115,6 +109,13 @@ const coord::input Controller::get_drag_select_start() const { return this->drag_select_start.value(); } +void Controller::reset_drag_select() { + std::unique_lock lock{this->mutex}; + + log::log(DBG << "Drag select start reset"); + this->drag_select_start = std::nullopt; +} + void setup_defaults(const std::shared_ptr &ctx, const std::shared_ptr &time_loop, const std::shared_ptr &simulation, @@ -212,7 +213,7 @@ void setup_defaults(const std::shared_ptr &ctx, params); // Reset drag selection start - controller->set_drag_select_start(std::nullopt); + controller->reset_drag_select(); return event; }}; diff --git a/libopenage/input/controller/game/controller.h b/libopenage/input/controller/game/controller.h index c825b6e6a9..a9d690f622 100644 --- a/libopenage/input/controller/game/controller.h +++ b/libopenage/input/controller/game/controller.h @@ -85,7 +85,7 @@ class Controller : public std::enable_shared_from_this { * * @param start Start position of the drag selection. */ - void set_drag_select_start(const std::optional &start); + void set_drag_select_start(const coord::input &start); /** * Get the start position of a drag selection. @@ -94,6 +94,11 @@ class Controller : public std::enable_shared_from_this { */ const coord::input get_drag_select_start() const; + /** + * Reset the drag select start position. + */ + void reset_drag_select(); + private: /** * Factions controllable by this controller. From b6d33a540bdbe813691cf98201e39d061975e01d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 25 Nov 2023 21:43:27 +0100 Subject: [PATCH 163/771] renderer: Add drag select in HUD renderer. --- libopenage/renderer/stages/hud/hud_object.cpp | 71 +++++++++++++++---- libopenage/renderer/stages/hud/hud_object.h | 48 +++++++++++-- .../renderer/stages/hud/hud_render_entity.cpp | 28 ++++++-- .../renderer/stages/hud/hud_render_entity.h | 46 ++++++++++-- .../renderer/stages/hud/hud_renderer.cpp | 53 ++++++++++---- libopenage/renderer/stages/hud/hud_renderer.h | 23 +++--- 6 files changed, 217 insertions(+), 52 deletions(-) diff --git a/libopenage/renderer/stages/hud/hud_object.cpp b/libopenage/renderer/stages/hud/hud_object.cpp index c923adbda0..87b5fc6ffb 100644 --- a/libopenage/renderer/stages/hud/hud_object.cpp +++ b/libopenage/renderer/stages/hud/hud_object.cpp @@ -2,37 +2,43 @@ #include "hud_object.h" +#include "renderer/geometry.h" #include "renderer/stages/hud/hud_render_entity.h" namespace openage::renderer::hud { -HudObject::HudObject(const std::shared_ptr &asset_manager) : +HudDragObject::HudDragObject(const std::shared_ptr &asset_manager) : require_renderable{true}, changed{false}, camera{nullptr}, asset_manager{asset_manager}, render_entity{nullptr}, + drag_pos{nullptr, 0, "", nullptr, {0, 0}}, + drag_start{0, 0}, uniforms{nullptr}, + geometry{nullptr}, last_update{0.0} { } -void HudObject::set_render_entity(const std::shared_ptr &entity) { +void HudDragObject::set_render_entity(const std::shared_ptr &entity) { this->render_entity = entity; this->fetch_updates(); } -void HudObject::set_camera(const std::shared_ptr &camera) { +void HudDragObject::set_camera(const std::shared_ptr &camera) { this->camera = camera; } -void HudObject::fetch_updates(const time::time_t &time) { +void HudDragObject::fetch_updates(const time::time_t &time) { if (not this->render_entity->is_changed()) { // exit early because there is nothing to do return; } + // Get data from render entity - // TODO + this->drag_start = this->render_entity->get_drag_start(); + 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; @@ -40,32 +46,73 @@ void HudObject::fetch_updates(const time::time_t &time) { this->last_update = time; } -void HudObject::update_uniforms(const time::time_t &time) { +void HudDragObject::update_uniforms(const time::time_t & /* time */) { // TODO: Only update uniforms that changed since last update if (this->uniforms == nullptr) [[unlikely]] { return; } - // TODO + + // TODO: Do something with the uniforms } -bool HudObject::requires_renderable() { +void HudDragObject::update_geometry(const time::time_t &time) { + // TODO: Only update geometry that changed since last update + if (this->geometry == nullptr) [[unlikely]] { + return; + } + + auto drag_start_ndc = this->drag_start.to_viewport(this->camera).to_ndc_space(this->camera); + auto drag_pos_ndc = this->drag_pos.get(time).to_viewport(this->camera).to_ndc_space(this->camera); + + float top = std::max(drag_start_ndc.y(), drag_pos_ndc.y()); + float bottom = std::min(drag_start_ndc.y(), drag_pos_ndc.y()); + float left = std::min(drag_start_ndc.x(), drag_pos_ndc.x()); + float right = std::max(drag_start_ndc.x(), drag_pos_ndc.x()); + + std::array quad_vertices{ + top, left, 0.0f, 1.0f, // top left corner + bottom, + left, + 0.0f, + 0.0f, // bottom left corner + top, + right, + 1.0f, + 1.0f, // top right corner + bottom, + right, + 1.0f, + 0.0f // bottom right corner + }; + + std::vector vertex_data(quad_vertices.size() * sizeof(float)); + std::memcpy(vertex_data.data(), quad_vertices.data(), vertex_data.size()); + + this->geometry->update_verts(vertex_data); +} + +bool HudDragObject::requires_renderable() { return this->require_renderable; } -void HudObject::clear_requires_renderable() { +void HudDragObject::clear_requires_renderable() { this->require_renderable = false; } -bool HudObject::is_changed() { +bool HudDragObject::is_changed() { return this->changed; } -void HudObject::clear_changed_flag() { +void HudDragObject::clear_changed_flag() { this->changed = false; } -void HudObject::set_uniforms(const std::shared_ptr &uniforms) { +void HudDragObject::set_uniforms(const std::shared_ptr &uniforms) { this->uniforms = uniforms; } +void HudDragObject::set_geometry(const std::shared_ptr &geometry) { + this->geometry = geometry; +} + } // namespace openage::renderer::hud diff --git a/libopenage/renderer/stages/hud/hud_object.h b/libopenage/renderer/stages/hud/hud_object.h index 8de81df591..0da212aa29 100644 --- a/libopenage/renderer/stages/hud/hud_object.h +++ b/libopenage/renderer/stages/hud/hud_object.h @@ -7,10 +7,13 @@ #include #include +#include "coord/pixel.h" +#include "curve/continuous.h" #include "time/time.h" namespace openage::renderer { +class Geometry; class UniformInput; namespace camera { @@ -23,19 +26,19 @@ class Animation2dInfo; } // namespace resources namespace hud { -class HudRenderEntity; +class HudDragRenderEntity; -class HudObject { +class HudDragObject { public: - HudObject(const std::shared_ptr &asset_manager); - ~HudObject() = default; + HudDragObject(const std::shared_ptr &asset_manager); + ~HudDragObject() = default; /** * Set the world render entity. * * @param entity New world render entity. */ - void set_render_entity(const std::shared_ptr &entity); + void set_render_entity(const std::shared_ptr &entity); /** * Set the current camera of the scene. @@ -58,6 +61,13 @@ class HudObject { */ void update_uniforms(const time::time_t &time = 0.0); + /** + * Update the geometry of the renderable associated with this object. + * + * @param time Current simulation time. + */ + void update_geometry(const time::time_t &time = 0.0); + /** * Check whether a new renderable needs to be created for this mesh. * @@ -95,6 +105,15 @@ class HudObject { */ void set_uniforms(const std::shared_ptr &uniforms); + /** + * Set the geometry of the renderable associated with this object. + * + * The geometry is updated when calling \p update(). + * + * @param geometry Geometry of this object's renderable. + */ + void set_geometry(const std::shared_ptr &geometry); + private: /** * Stores whether a new renderable for this object needs to be created @@ -120,13 +139,28 @@ class HudObject { /** * Source for positional and texture data. */ - std::shared_ptr render_entity; + std::shared_ptr render_entity; /** - * Shader uniforms for the renderable in the terrain render pass. + * Position of the dragged corner. + */ + curve::Continuous drag_pos; + + /** + * Position of the start corner. + */ + coord::input drag_start; + + /** + * Shader uniforms for the renderable in the HUD render pass. */ std::shared_ptr uniforms; + /** + * Geometry of the renderable in the HUD render pass. + */ + std::shared_ptr geometry; + /** * Time of the last update call. */ diff --git a/libopenage/renderer/stages/hud/hud_render_entity.cpp b/libopenage/renderer/stages/hud/hud_render_entity.cpp index b43f25bfa7..b57e7f911e 100644 --- a/libopenage/renderer/stages/hud/hud_render_entity.cpp +++ b/libopenage/renderer/stages/hud/hud_render_entity.cpp @@ -7,30 +7,44 @@ namespace openage::renderer::hud { -HudRenderEntity::HudRenderEntity() : +HudDragRenderEntity::HudDragRenderEntity(const coord::input drag_start) : changed{false}, - last_update{0.0} { + last_update{0.0}, + drag_pos{nullptr, 0, "", nullptr, drag_start}, + drag_start{drag_start} { } -void HudRenderEntity::update(const time::time_t time) { +void HudDragRenderEntity::update(const coord::input drag_pos, + const time::time_t time) { std::unique_lock lock{this->mutex}; - // TODO + this->drag_pos.set_insert(time, drag_pos); + + this->last_update = time; + this->changed = true; } -time::time_t HudRenderEntity::get_update_time() { +time::time_t HudDragRenderEntity::get_update_time() { std::shared_lock lock{this->mutex}; return this->last_update; } -bool HudRenderEntity::is_changed() { +const curve::Continuous &HudDragRenderEntity::get_drag_pos() { + return this->drag_pos; +} + +const coord::input &HudDragRenderEntity::get_drag_start() { + return this->drag_start; +} + +bool HudDragRenderEntity::is_changed() { std::shared_lock lock{this->mutex}; return this->changed; } -void HudRenderEntity::clear_changed_flag() { +void HudDragRenderEntity::clear_changed_flag() { std::unique_lock lock{this->mutex}; this->changed = false; diff --git a/libopenage/renderer/stages/hud/hud_render_entity.h b/libopenage/renderer/stages/hud/hud_render_entity.h index e5b227a944..5b35690907 100644 --- a/libopenage/renderer/stages/hud/hud_render_entity.h +++ b/libopenage/renderer/stages/hud/hud_render_entity.h @@ -3,24 +3,34 @@ #pragma once #include -#include #include #include +#include "coord/pixel.h" +#include "curve/continuous.h" #include "time/time.h" namespace openage::renderer::hud { -class HudRenderEntity { +class HudDragRenderEntity { public: - HudRenderEntity(); - ~HudRenderEntity() = default; + /** + * Create a new render entity for drag selection in the HUD. + * + * @param drag_start Position of the start corner. + */ + HudDragRenderEntity(const coord::input drag_start); + ~HudDragRenderEntity() = default; /** - * TODO: Update the render entity with information from the gamestate. + * Update the render entity with information from the gamestate. + * + * @param drag_pos Position of the dragged corner. + * @param time Current simulation time. */ - void update(const time::time_t time = 0.0); + void update(const coord::input drag_pos, + const time::time_t time = 0.0); /** * Get the time of the last update. @@ -29,6 +39,20 @@ class HudRenderEntity { */ time::time_t get_update_time(); + /** + * Get the position of the dragged corner. + * + * @return Coordinates of the dragged corner. + */ + const curve::Continuous &get_drag_pos(); + + /** + * Get the position of the start corner. + * + * @return Coordinates of the start corner. + */ + const coord::input &get_drag_start(); + /** * Check whether the render entity has received new updates from the * gamestate. @@ -55,6 +79,16 @@ class HudRenderEntity { */ time::time_t last_update; + /** + * Position of the dragged corner. + */ + curve::Continuous drag_pos; + + /** + * Position of the start corner. + */ + coord::input drag_start; + /** * Mutex for protecting threaded access. */ diff --git a/libopenage/renderer/stages/hud/hud_renderer.cpp b/libopenage/renderer/stages/hud/hud_renderer.cpp index 295c222230..6b8827339f 100644 --- a/libopenage/renderer/stages/hud/hud_renderer.cpp +++ b/libopenage/renderer/stages/hud/hud_renderer.cpp @@ -25,7 +25,7 @@ HudRenderer::HudRenderer(const std::shared_ptr &window, renderer{renderer}, camera{camera}, asset_manager{asset_manager}, - render_objects{}, + drag_object{nullptr}, clock{clock} { renderer::opengl::GlContext::check_error(); @@ -43,20 +43,49 @@ std::shared_ptr HudRenderer::get_render_pass() { return this->render_pass; } -void HudRenderer::add_render_entity(const std::shared_ptr entity) { +void HudRenderer::add_drag_entity(const std::shared_ptr entity) { std::unique_lock lock{this->mutex}; - auto hud_object = std::make_shared(this->asset_manager); + auto hud_object = std::make_shared(this->asset_manager); hud_object->set_render_entity(entity); hud_object->set_camera(this->camera); - this->render_objects.push_back(hud_object); + this->drag_object = hud_object; +} + +void HudRenderer::remove_drag_entity() { + std::unique_lock lock{this->mutex}; + + this->drag_object = nullptr; + this->render_pass->clear_renderables(); } void HudRenderer::update() { std::unique_lock lock{this->mutex}; auto current_time = this->clock->get_real_time(); - for (auto &obj : this->render_objects) { - // TODO + + if (this->drag_object) { + this->drag_object->fetch_updates(current_time); + if (this->drag_object->requires_renderable()) { + auto geometry = this->renderer->add_bufferless_quad(); + auto transform_unifs = this->drag_select_shader->new_uniform_input( + "in_col", + Eigen::Vector4f{1.0f, 1.0f, 1.0f, 0.2f}); + + Renderable display_obj{ + transform_unifs, + geometry, + true, + true, + }; + + this->render_pass->add_renderables(display_obj); + this->drag_object->clear_requires_renderable(); + + this->drag_object->set_uniforms(transform_unifs); + this->drag_object->set_geometry(geometry); + } + this->drag_object->update_uniforms(current_time); + this->drag_object->update_geometry(current_time); } } @@ -71,27 +100,27 @@ void HudRenderer::resize(size_t width, size_t height) { void HudRenderer::initialize_render_pass(size_t width, size_t height, const util::Path &shaderdir) { - // ASDF: add vertex shader - auto vert_shader_file = (shaderdir / "world2d.vert.glsl").open(); + // Drag select shader + auto vert_shader_file = (shaderdir / "hud_drag_select.vert.glsl").open(); auto vert_shader_src = renderer::resources::ShaderSource( resources::shader_lang_t::glsl, resources::shader_stage_t::vertex, vert_shader_file.read()); vert_shader_file.close(); - // ASDF: add fragment shader - auto frag_shader_file = (shaderdir / "world2d.frag.glsl").open(); + auto frag_shader_file = (shaderdir / "hud_drag_select.frag.glsl").open(); auto frag_shader_src = renderer::resources::ShaderSource( resources::shader_lang_t::glsl, resources::shader_stage_t::fragment, frag_shader_file.read()); frag_shader_file.close(); + this->drag_select_shader = this->renderer->add_shader({vert_shader_src, frag_shader_src}); + + // Texture targets 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->display_shader = this->renderer->add_shader({vert_shader_src, frag_shader_src}); - auto fbo = this->renderer->create_texture_target({this->output_texture, this->depth_texture}); this->render_pass = this->renderer->add_render_pass({}, fbo); } diff --git a/libopenage/renderer/stages/hud/hud_renderer.h b/libopenage/renderer/stages/hud/hud_renderer.h index f085e825f8..0373757adb 100644 --- a/libopenage/renderer/stages/hud/hud_renderer.h +++ b/libopenage/renderer/stages/hud/hud_renderer.h @@ -30,8 +30,8 @@ class AssetManager; } namespace hud { -class HudObject; -class HudRenderEntity; +class HudDragObject; +class HudDragRenderEntity; /** * Renderer for the "Heads-Up Display" (HUD). @@ -55,11 +55,18 @@ class HudRenderer { std::shared_ptr get_render_pass(); /** - * Add a new render entity of the HUD renderer. + * Add a new render entity for drag selection. * * @param render_entity New render entity. */ - void add_render_entity(const std::shared_ptr entity); + void add_drag_entity(const std::shared_ptr entity); + + /** + * Remove the render object for drag selection. + * + * @param render_entity Render entity to remove. + */ + void remove_drag_entity(); /** * Update the render entities and render positions. @@ -110,14 +117,14 @@ class HudRenderer { std::shared_ptr render_pass; /** - * Render entities requested by the game simulation or input system. + * Render object for the drag select rectangle. */ - std::vector> render_objects; + std::shared_ptr drag_object; /** - * Shader for rendering the HUD objects. + * Shader for rendering the drag select rectangle. */ - std::shared_ptr display_shader; + std::shared_ptr drag_select_shader; /** * Simulation clock for timing animations. From f834b05da01f358f0bef9c02d7625f549be36fa3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 Nov 2023 00:20:48 +0100 Subject: [PATCH 164/771] presenter: Add HUD renderer. --- libopenage/presenter/presenter.cpp | 21 +++++++++++++++------ libopenage/presenter/presenter.h | 9 +++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index f576560c97..3d0530bf18 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -23,6 +23,7 @@ #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" #include "renderer/stages/camera/manager.h" +#include "renderer/stages/hud/hud_renderer.h" #include "renderer/stages/screen/screen_renderer.h" #include "renderer/stages/skybox/skybox_renderer.h" #include "renderer/stages/terrain/terrain_renderer.h" @@ -75,8 +76,7 @@ void Presenter::run(bool debug_graphics) { void Presenter::set_simulation(const std::shared_ptr &simulation) { this->simulation = simulation; - auto render_factory = std::make_shared(this->terrain_renderer, - this->world_renderer); + auto render_factory = std::make_shared(this->terrain_renderer, this->world_renderer); this->simulation->attach_renderer(render_factory); } @@ -103,8 +103,7 @@ void Presenter::init_graphics(bool debug) { this->asset_manager->set_placeholder_animation(missing_tex); // Camera - this->camera = std::make_shared(this->renderer, - this->window->get_size()); + this->camera = std::make_shared(this->renderer, this->window->get_size()); this->window->add_resize_callback([this](size_t w, size_t h, double /*scale*/) { this->camera->resize(w, h); }); @@ -139,12 +138,21 @@ void Presenter::init_graphics(bool debug) { this->time_loop->get_clock()); this->render_passes.push_back(this->world_renderer->get_render_pass()); + // HUD + this->hud_renderer = std::make_shared( + this->window, + this->renderer, + this->camera, + this->root_dir["assets"]["shaders"], + this->asset_manager, + this->time_loop->get_clock()); + this->render_passes.push_back(this->hud_renderer->get_render_pass()); + this->init_gui(); this->init_final_render_pass(); if (this->simulation) { - auto render_factory = std::make_shared(this->terrain_renderer, - this->world_renderer); + auto render_factory = std::make_shared(this->terrain_renderer, this->world_renderer); this->simulation->attach_renderer(render_factory); } @@ -273,6 +281,7 @@ void Presenter::render() { this->camera_manager->update(); this->terrain_renderer->update(); this->world_renderer->update(); + this->hud_renderer->update(); this->gui->render(); for (auto &pass : this->render_passes) { diff --git a/libopenage/presenter/presenter.h b/libopenage/presenter/presenter.h index 6e7817ed2d..f3abcb95c7 100644 --- a/libopenage/presenter/presenter.h +++ b/libopenage/presenter/presenter.h @@ -41,6 +41,10 @@ namespace gui { class GUI; } +namespace hud { +class HudRenderer; +} + namespace screen { class ScreenRenderer; } @@ -190,6 +194,11 @@ class Presenter { */ std::shared_ptr world_renderer; + /** + * Graphics output for the HUD. + */ + std::shared_ptr hud_renderer; + /** * Final graphics output to the window screen. */ From 6acc008fe08afce4f1a060a23dd3ba3cc6a8818e Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 Nov 2023 00:21:10 +0100 Subject: [PATCH 165/771] input: Default actions for HUD controller. --- libopenage/input/controller/hud/binding.h | 4 +- .../input/controller/hud/controller.cpp | 64 +++++++++++++++++-- libopenage/input/controller/hud/controller.h | 47 ++++++++++++-- 3 files changed, 103 insertions(+), 12 deletions(-) diff --git a/libopenage/input/controller/hud/binding.h b/libopenage/input/controller/hud/binding.h index a899ffbaf2..716a7e4f39 100644 --- a/libopenage/input/controller/hud/binding.h +++ b/libopenage/input/controller/hud/binding.h @@ -9,9 +9,11 @@ #include "input/event.h" namespace openage::input::hud { +class Controller; using binding_flags_t = std::unordered_map; -using binding_func_t = std::function; +using binding_func_t = std::function)>; /** diff --git a/libopenage/input/controller/hud/controller.cpp b/libopenage/input/controller/hud/controller.cpp index c130856b9b..ff1630886e 100644 --- a/libopenage/input/controller/hud/controller.cpp +++ b/libopenage/input/controller/hud/controller.cpp @@ -6,10 +6,14 @@ #include "input/controller/hud/binding_context.h" +#include "renderer/stages/hud/hud_render_entity.h" +#include "renderer/stages/hud/hud_renderer.h" + namespace openage::input::hud { -Controller::Controller() {} +Controller::Controller() : + drag_entity{nullptr} {} bool Controller::process(const event_arguments &ev_args, const std::shared_ptr &ctx) { @@ -18,13 +22,65 @@ bool Controller::process(const event_arguments &ev_args, } auto bind = ctx->lookup(ev_args.e); - bind.action(ev_args); + bind.action(ev_args, this->shared_from_this()); return true; } -void setup_defaults(const std::shared_ptr &ctx) { - // TODO +void Controller::set_drag_entity(const std::shared_ptr &entity) { + this->drag_entity = entity; +} + +const std::shared_ptr &Controller::get_drag_entity() const { + return this->drag_entity; +} + +void setup_defaults(const std::shared_ptr &ctx, + const std::shared_ptr &hud_renderer) { + binding_func_t drag_selection_init{[&](const event_arguments &args, + const std::shared_ptr controller) { + auto render_entity = std::make_shared(args.mouse); + hud_renderer->add_drag_entity(render_entity); + controller->set_drag_entity(render_entity); + }}; + + binding_action drag_selection_init_action{drag_selection_init}; + Event ev_mouse_lmb_press{ + event_class::MOUSE_BUTTON, + Qt::MouseButton::LeftButton, + Qt::KeyboardModifier::NoModifier, + QEvent::MouseButtonPress}; + + ctx->bind(ev_mouse_lmb_press, drag_selection_init_action); + + binding_func_t drag_selection_move{[&](const event_arguments &args, + const std::shared_ptr controller) { + controller->get_drag_entity()->update(args.mouse); + }}; + + binding_action drag_selection_move_action{drag_selection_move}; + Event ev_mouse_move{ + event_class::MOUSE_MOVE, + Qt::MouseButton::NoButton, + Qt::KeyboardModifier::NoModifier, + QEvent::MouseMove}; + + ctx->bind(ev_mouse_move, drag_selection_move_action); + + binding_func_t drag_selection_end{[&](const event_arguments & /* args */, + const std::shared_ptr controller) { + hud_renderer->remove_drag_entity(); + controller->set_drag_entity(nullptr); + }}; + + binding_action drag_selection_end_action{drag_selection_end}; + Event ev_mouse_lmb_release{ + event_class::MOUSE_BUTTON, + Qt::MouseButton::LeftButton, + Qt::KeyboardModifier::NoModifier, + QEvent::MouseButtonRelease}; + + ctx->bind(ev_mouse_lmb_release, drag_selection_end_action); } } // namespace openage::input::hud diff --git a/libopenage/input/controller/hud/controller.h b/libopenage/input/controller/hud/controller.h index 5ca5d31ced..b72e12b021 100644 --- a/libopenage/input/controller/hud/controller.h +++ b/libopenage/input/controller/hud/controller.h @@ -2,16 +2,25 @@ #pragma once +#include + #include "input/event.h" -namespace openage::input::hud { +namespace openage { + +namespace renderer::hud { +class HudDragRenderEntity; +class HudRenderer; +} // namespace renderer::hud + +namespace input::hud { class BindingContext; /** - * Control a camera with input from the input manager. + * Control athe HUD with input from the input manager. */ -class Controller { +class Controller : public std::enable_shared_from_this { public: Controller(); @@ -25,18 +34,42 @@ class Controller { * * @return true if the event is accepted, else false. */ - bool process(const event_arguments &ev_args, const std::shared_ptr &ctx); + bool process(const event_arguments &ev_args, + const std::shared_ptr &ctx); + + /** + * Set the render entity for the selection box. + * + * @param entity New render entity. + */ + void set_drag_entity(const std::shared_ptr &entity); + + /** + * Get the render entity for the selection box. + * + * @return Render entity for the selection box. + */ + const std::shared_ptr &get_drag_entity() const; + +private: + /** + * Render entity for the selection box. + */ + std::shared_ptr drag_entity; }; /** * Setup default HUD action bindings: * - * - Mouse drag: selectION BOX + * - Mouse drag: selection box draw * * TODO: Make this configurable. * * @param ctx Binding context the actions are added to. + * @param hud_renderer HUD render stage that is used to render the selection box. */ -void setup_defaults(const std::shared_ptr &ctx); +void setup_defaults(const std::shared_ptr &ctx, + const std::shared_ptr &hud_renderer); -} // namespace openage::input::hud +} // namespace input::hud +} // namespace openage From 248dba61faefea44d7bf052b1e1a921b994e372a Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 Nov 2023 00:51:40 +0100 Subject: [PATCH 166/771] input: Allow multiple input actions per event. --- libopenage/input/input_context.cpp | 16 +++++++++++--- libopenage/input/input_context.h | 35 ++++++++++++++++++++++++------ libopenage/input/input_manager.cpp | 14 +++++++----- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/libopenage/input/input_context.cpp b/libopenage/input/input_context.cpp index ce36ddc419..bcb3f5807f 100644 --- a/libopenage/input/input_context.cpp +++ b/libopenage/input/input_context.cpp @@ -32,18 +32,28 @@ const std::shared_ptr &InputContext::get_camera_bindings } void InputContext::bind(const Event &ev, const input_action act) { - this->by_event.emplace(std::make_pair(ev, act)); + std::vector actions{act}; + this->by_event.emplace(std::make_pair(ev, actions)); } void InputContext::bind(const event_class &cl, const input_action act) { - this->by_class.emplace(std::make_pair(cl, act)); + std::vector actions{act}; + this->by_class.emplace(std::make_pair(cl, actions)); +} + +void InputContext::bind(const Event &ev, const std::vector &&acts) { + this->by_event.emplace(std::make_pair(ev, acts)); +} + +void InputContext::bind(const event_class &cl, const std::vector &&acts) { + this->by_class.emplace(std::make_pair(cl, acts)); } bool InputContext::is_bound(const Event &ev) const { return this->by_event.contains(ev) || this->by_class.contains(ev.cc.cl); } -const input_action &InputContext::lookup(const Event &ev) const { +const std::vector &InputContext::lookup(const Event &ev) const { auto event_lookup = this->by_event.find(ev); if (event_lookup != std::end(this->by_event)) { return (*event_lookup).second; diff --git a/libopenage/input/input_context.h b/libopenage/input/input_context.h index cec9ba9d72..f2d5f82aee 100644 --- a/libopenage/input/input_context.h +++ b/libopenage/input/input_context.h @@ -3,6 +3,7 @@ #pragma once #include +#include #include "input/action.h" #include "input/event.h" @@ -71,25 +72,45 @@ class InputContext { const std::shared_ptr &get_camera_bindings(); /** - * Bind a specific key combination to an action. + * Bind a specific key combination to a single action. * * This is the first matching priority. * * @param ev Input event triggering the action. - * @param act Function executing the action. + * @param act Action executed by the event. */ void bind(const Event &ev, const input_action act); /** - * Bind an event class to an action. + * Bind an event class to a single action. * * This is the second matching priority. * * @param ev Input event triggering the action. - * @param act Function executing the action. + * @param act Action executed by the event. */ void bind(const event_class &cl, const input_action act); + /** + * Bind a specific key combination to a list of actions. + * + * This is the first matching priority. + * + * @param ev Input event triggering the action. + * @param act Actions executed by the event. + */ + void bind(const Event &ev, const std::vector &&acts); + + /** + * Bind an event class to a list of actions. + * + * This is the second matching priority. + * + * @param ev Input event triggering the action. + * @param act Actions executed by the event. + */ + void bind(const event_class &cl, const std::vector &&acts); + /** * Check whether a specific key event is bound in this context. * @@ -104,7 +125,7 @@ class InputContext { * * @param ev Input event triggering the action. */ - const input_action &lookup(const Event &ev) const; + const std::vector &lookup(const Event &ev) const; /** * Get all event->action bindings in this context. @@ -130,12 +151,12 @@ class InputContext { /** * Maps specific input events to actions. */ - std::unordered_map by_event; + std::unordered_map, event_hash> by_event; /** * Maps event classes to actions. */ - std::unordered_map by_class; + std::unordered_map, event_class_hash> by_class; /** * Additional context for game simulation events. diff --git a/libopenage/input/input_manager.cpp b/libopenage/input/input_manager.cpp index 49127b093c..c152ee5ab1 100644 --- a/libopenage/input/input_manager.cpp +++ b/libopenage/input/input_manager.cpp @@ -127,18 +127,20 @@ bool InputManager::process(const QEvent &ev) { // Check context list on top of the stack (most recent bound first) for (auto const &ctx : this->active_contexts) { if (ctx->is_bound(input_ev)) { - this->process_action(input_ev, - ctx->lookup(input_ev), - ctx); + auto &actions = ctx->lookup(input_ev); + for (auto const &action : actions) { + this->process_action(input_ev, action, ctx); + } return true; } } // If no local keybinds were bound, check the global keybinds if (this->global_context->is_bound(input_ev)) { - this->process_action(input_ev, - this->global_context->lookup(input_ev), - this->global_context); + auto &actions = this->global_context->lookup(input_ev); + for (auto const &action : actions) { + this->process_action(input_ev, action, this->global_context); + } return true; } From 664475f015ca66ba37f2ff08c7d5f25360b93572 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 Nov 2023 15:40:32 +0100 Subject: [PATCH 167/771] input: Forward actions to HuD controller. --- libopenage/input/action.h | 1 + .../input/controller/hud/controller.cpp | 4 +++- libopenage/input/input_context.cpp | 8 +++++++ libopenage/input/input_context.h | 23 +++++++++++++++++++ libopenage/input/input_manager.cpp | 21 +++++++++++++---- libopenage/input/input_manager.h | 16 +++++++++++++ libopenage/presenter/presenter.cpp | 12 ++++++++++ 7 files changed, 80 insertions(+), 5 deletions(-) diff --git a/libopenage/input/action.h b/libopenage/input/action.h index facd02bdca..f6ca5296d3 100644 --- a/libopenage/input/action.h +++ b/libopenage/input/action.h @@ -25,6 +25,7 @@ enum class input_action_t { REMOVE_CONTEXT, GAME, CAMERA, + HUD, GUI, CUSTOM, }; diff --git a/libopenage/input/controller/hud/controller.cpp b/libopenage/input/controller/hud/controller.cpp index ff1630886e..5d8576b9c4 100644 --- a/libopenage/input/controller/hud/controller.cpp +++ b/libopenage/input/controller/hud/controller.cpp @@ -55,7 +55,9 @@ void setup_defaults(const std::shared_ptr &ctx, binding_func_t drag_selection_move{[&](const event_arguments &args, const std::shared_ptr controller) { - controller->get_drag_entity()->update(args.mouse); + if (controller->get_drag_entity()) { + controller->get_drag_entity()->update(args.mouse); + } }}; binding_action drag_selection_move_action{drag_selection_move}; diff --git a/libopenage/input/input_context.cpp b/libopenage/input/input_context.cpp index bcb3f5807f..bbbdfc99d3 100644 --- a/libopenage/input/input_context.cpp +++ b/libopenage/input/input_context.cpp @@ -23,6 +23,10 @@ void InputContext::set_camera_bindings(const std::shared_ptrcamera_bindings = bindings; } +void InputContext::set_hud_bindings(const std::shared_ptr &bindings) { + this->hud_bindings = bindings; +} + const std::shared_ptr &InputContext::get_game_bindings() { return this->game_bindings; } @@ -31,6 +35,10 @@ const std::shared_ptr &InputContext::get_camera_bindings return this->camera_bindings; } +const std::shared_ptr &InputContext::get_hud_bindings() { + return this->hud_bindings; +} + void InputContext::bind(const Event &ev, const input_action act) { std::vector actions{act}; this->by_event.emplace(std::make_pair(ev, actions)); diff --git a/libopenage/input/input_context.h b/libopenage/input/input_context.h index f2d5f82aee..55e3cd2974 100644 --- a/libopenage/input/input_context.h +++ b/libopenage/input/input_context.h @@ -18,6 +18,10 @@ namespace game { class BindingContext; } +namespace hud { +class BindingContext; +} + /** * An input context contains all keybindings and actions * active in e.g. the HUD only. @@ -57,6 +61,13 @@ class InputContext { */ void set_camera_bindings(const std::shared_ptr &bindings); + /** + * Set the associated context for binding input events to HUD actions. + * + * @param bindings Binding context for HUD actions. + */ + void set_hud_bindings(const std::shared_ptr &bindings); + /** * Get the associated context for binding input events to game events. * @@ -71,6 +82,13 @@ class InputContext { */ const std::shared_ptr &get_camera_bindings(); + /** + * Get the associated context for binding input events to HUD actions. + * + * @return Binding context of the input context. + */ + const std::shared_ptr &get_hud_bindings(); + /** * Bind a specific key combination to a single action. * @@ -167,6 +185,11 @@ class InputContext { * Additional context for camera actions. */ std::shared_ptr camera_bindings; + + /** + * Additional context for HUD actions. + */ + std::shared_ptr hud_bindings; }; } // namespace openage::input diff --git a/libopenage/input/input_manager.cpp b/libopenage/input/input_manager.cpp index c152ee5ab1..8517dfefd6 100644 --- a/libopenage/input/input_manager.cpp +++ b/libopenage/input/input_manager.cpp @@ -4,6 +4,7 @@ #include "input/controller/camera/controller.h" #include "input/controller/game/controller.h" +#include "input/controller/hud/controller.h" #include "input/event.h" #include "input/input_context.h" #include "renderer/gui/guisys/public/gui_input.h" @@ -16,6 +17,7 @@ InputManager::InputManager() : available_contexts{}, game_controller{nullptr}, camera_controller{nullptr}, + hud_controller{nullptr}, gui_input{nullptr} { } @@ -31,6 +33,10 @@ void InputManager::set_game_controller(const std::shared_ptr & this->game_controller = controller; } +void InputManager::set_hud_controller(const std::shared_ptr controller) { + this->hud_controller = controller; +} + const std::shared_ptr &InputManager::get_global_context() { return this->global_context; } @@ -183,6 +189,10 @@ void InputManager::process_action(const input::Event &ev, this->camera_controller->process(args, ctx->get_camera_bindings()); break; + case input_action_t::HUD: + this->hud_controller->process(args, ctx->get_hud_bindings()); + break; + case input_action_t::GUI: this->gui_input->process(args.e.get_event()); break; @@ -198,6 +208,9 @@ void InputManager::process_action(const input::Event &ev, void setup_defaults(const std::shared_ptr &ctx) { + // hud + input_action hud_action{input_action_t::HUD}; + // camera input_action camera_action{input_action_t::CAMERA}; @@ -214,7 +227,7 @@ void setup_defaults(const std::shared_ptr &ctx) { ctx->bind(ev_down, camera_action); ctx->bind(ev_wheel_up, camera_action); ctx->bind(ev_wheel_down, camera_action); - ctx->bind(event_class::MOUSE_MOVE, camera_action); + ctx->bind(event_class::MOUSE_MOVE, {/* camera_action, ASDF */ hud_action}); // game input_action game_action{input_action_t::GAME}; @@ -222,11 +235,11 @@ void setup_defaults(const std::shared_ptr &ctx) { Event ev_mouse_lmb{event_class::MOUSE_BUTTON, Qt::LeftButton, Qt::NoModifier, QEvent::MouseButtonRelease}; Event ev_mouse_rmb{event_class::MOUSE_BUTTON, Qt::RightButton, Qt::NoModifier, QEvent::MouseButtonRelease}; - ctx->bind(ev_mouse_lmb, game_action); - ctx->bind(ev_mouse_rmb, game_action); + ctx->bind(ev_mouse_lmb, {game_action, hud_action}); + ctx->bind(ev_mouse_rmb, {game_action, hud_action}); // also forward all other mouse button events - ctx->bind(event_class::MOUSE_BUTTON, game_action); + ctx->bind(event_class::MOUSE_BUTTON, {game_action, hud_action}); } diff --git a/libopenage/input/input_manager.h b/libopenage/input/input_manager.h index 189acf07e0..36bd158d59 100644 --- a/libopenage/input/input_manager.h +++ b/libopenage/input/input_manager.h @@ -26,6 +26,10 @@ namespace game { class Controller; } // namespace game +namespace hud { +class Controller; +} // namespace hud + class InputContext; /** @@ -59,6 +63,13 @@ class InputManager { */ void set_game_controller(const std::shared_ptr &controller); + /** + * Set the controller for the HUD. + * + * @param controller HUD controller. + */ + void set_hud_controller(const std::shared_ptr controller); + /** * returns the global keybind context. * actions bound here will be retained even when override_context is called. @@ -179,6 +190,11 @@ class InputManager { */ std::shared_ptr camera_controller; + /** + * Interface to the HUD. + */ + std::shared_ptr hud_controller; + /** * Interface to the GUI. */ diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index 3d0530bf18..095fbaece0 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -12,6 +12,8 @@ #include "input/controller/camera/controller.h" #include "input/controller/game/binding_context.h" #include "input/controller/game/controller.h" +#include "input/controller/hud/binding_context.h" +#include "input/controller/hud/controller.h" #include "input/input_context.h" #include "input/input_manager.h" #include "log/log.h" @@ -247,6 +249,16 @@ void Presenter::init_input() { input_ctx->set_camera_bindings(camera_context); } + // setup HUD controls + if (this->hud_renderer) { + log::log(INFO << "Loading HUD controls"); + auto hud_controller = std::make_shared(); + auto hud_context = std::make_shared(); + input::hud::setup_defaults(hud_context, this->hud_renderer); + this->input_manager->set_hud_controller(hud_controller); + input_ctx->set_hud_bindings(hud_context); + } + log::log(INFO << "Presenter: Input subsystem initialized"); } From 386eb8c2cb372197d4291f4c42a2017de9325940 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 Nov 2023 15:41:26 +0100 Subject: [PATCH 168/771] renderer: Fix drag selection rectable vertices. --- libopenage/renderer/stages/hud/hud_object.cpp | 15 ++++++++++----- libopenage/renderer/stages/hud/hud_renderer.cpp | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/libopenage/renderer/stages/hud/hud_object.cpp b/libopenage/renderer/stages/hud/hud_object.cpp index 87b5fc6ffb..523fe66442 100644 --- a/libopenage/renderer/stages/hud/hud_object.cpp +++ b/libopenage/renderer/stages/hud/hud_object.cpp @@ -38,7 +38,7 @@ void HudDragObject::fetch_updates(const time::time_t &time) { // Get data from render entity this->drag_start = this->render_entity->get_drag_start(); - this->drag_pos.sync(this->render_entity->get_drag_pos(), this->last_update); + 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; @@ -69,18 +69,23 @@ void HudDragObject::update_geometry(const time::time_t &time) { float left = std::min(drag_start_ndc.x(), drag_pos_ndc.x()); float right = std::max(drag_start_ndc.x(), drag_pos_ndc.x()); + log::log(SPAM << "top: " << top + << ", bottom: " << bottom + << ", left: " << left + << ", right: " << right); + std::array quad_vertices{ - top, left, 0.0f, 1.0f, // top left corner - bottom, + left, top, 0.0f, 1.0f, // top left corner left, + bottom, 0.0f, 0.0f, // bottom left corner - top, right, + top, 1.0f, 1.0f, // top right corner - bottom, right, + bottom, 1.0f, 0.0f // bottom right corner }; diff --git a/libopenage/renderer/stages/hud/hud_renderer.cpp b/libopenage/renderer/stages/hud/hud_renderer.cpp index 6b8827339f..938eb5bdb4 100644 --- a/libopenage/renderer/stages/hud/hud_renderer.cpp +++ b/libopenage/renderer/stages/hud/hud_renderer.cpp @@ -66,10 +66,10 @@ void HudRenderer::update() { if (this->drag_object) { this->drag_object->fetch_updates(current_time); if (this->drag_object->requires_renderable()) { - auto geometry = this->renderer->add_bufferless_quad(); + auto geometry = this->renderer->add_mesh_geometry(resources::MeshData::make_quad()); auto transform_unifs = this->drag_select_shader->new_uniform_input( "in_col", - Eigen::Vector4f{1.0f, 1.0f, 1.0f, 0.2f}); + Eigen::Vector4f{0.0f, 0.0f, 0.0f, 0.5f}); Renderable display_obj{ transform_unifs, From 016d8742cb7234cca266fd2c8da1057a5ff7580e Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 Nov 2023 21:14:05 +0100 Subject: [PATCH 169/771] renderer: Add missing docstrings. --- libopenage/renderer/stages/hud/hud_object.h | 8 ++++++++ libopenage/renderer/stages/hud/hud_render_entity.h | 3 +++ libopenage/renderer/stages/hud/hud_renderer.h | 12 ++++++++++++ libopenage/renderer/stages/screen/screen_renderer.h | 7 +++++++ libopenage/renderer/stages/skybox/skybox_renderer.h | 7 +++++++ libopenage/renderer/stages/terrain/terrain_chunk.h | 3 +++ libopenage/renderer/stages/terrain/terrain_model.h | 5 +++++ .../renderer/stages/terrain/terrain_render_entity.h | 4 +++- .../renderer/stages/terrain/terrain_renderer.h | 10 ++++++++++ libopenage/renderer/stages/world/world_object.h | 8 ++++++++ .../renderer/stages/world/world_render_entity.h | 3 +++ libopenage/renderer/stages/world/world_renderer.h | 10 ++++++++++ 12 files changed, 79 insertions(+), 1 deletion(-) diff --git a/libopenage/renderer/stages/hud/hud_object.h b/libopenage/renderer/stages/hud/hud_object.h index 0da212aa29..b116821067 100644 --- a/libopenage/renderer/stages/hud/hud_object.h +++ b/libopenage/renderer/stages/hud/hud_object.h @@ -28,8 +28,16 @@ class Animation2dInfo; namespace hud { class HudDragRenderEntity; +/** + * Stores the state of a renderable object in the HUD render stage. + */ class HudDragObject { public: + /** + * Create a new object for the HUD render stage. + * + * @param asset_manager Asset manager for loading resources. + */ HudDragObject(const std::shared_ptr &asset_manager); ~HudDragObject() = default; diff --git a/libopenage/renderer/stages/hud/hud_render_entity.h b/libopenage/renderer/stages/hud/hud_render_entity.h index 5b35690907..00439b5cc6 100644 --- a/libopenage/renderer/stages/hud/hud_render_entity.h +++ b/libopenage/renderer/stages/hud/hud_render_entity.h @@ -13,6 +13,9 @@ namespace openage::renderer::hud { +/** + * Render entity for pushing drag selection updates to the HUD renderer. + */ class HudDragRenderEntity { public: /** diff --git a/libopenage/renderer/stages/hud/hud_renderer.h b/libopenage/renderer/stages/hud/hud_renderer.h index 0373757adb..d9bed7529f 100644 --- a/libopenage/renderer/stages/hud/hud_renderer.h +++ b/libopenage/renderer/stages/hud/hud_renderer.h @@ -36,9 +36,21 @@ class HudDragRenderEntity; /** * Renderer for the "Heads-Up Display" (HUD). * Draws UI elements that are not part of the GUI, e.g. health bars, selection boxes, minimap, etc. + * + * TODO: Currently only supports drag selection. */ class HudRenderer { public: + /** + * Create a new render stage for the HUD. + * + * @param window openage window targeted for rendering. + * @param renderer openage low-level renderer. + * @param camera Camera used for the rendered scene. + * @param shaderdir Directory containing the shader source files. + * @param asset_manager Asset manager for loading resources. + * @param clock Simulation clock for timing animations. + */ HudRenderer(const std::shared_ptr &window, const std::shared_ptr &renderer, const std::shared_ptr &camera, diff --git a/libopenage/renderer/stages/screen/screen_renderer.h b/libopenage/renderer/stages/screen/screen_renderer.h index 7aae82a4be..1b1c7dcaa7 100644 --- a/libopenage/renderer/stages/screen/screen_renderer.h +++ b/libopenage/renderer/stages/screen/screen_renderer.h @@ -24,6 +24,13 @@ namespace screen { */ class ScreenRenderer { public: + /** + * Create a new render stage for drawing to the screen. + * + * @param window openage window targeted for rendering. + * @param renderer openage low-level renderer. + * @param shaderdir Directory containing the shader source files. + */ ScreenRenderer(const std::shared_ptr &window, const std::shared_ptr &renderer, const util::Path &shaderdir); diff --git a/libopenage/renderer/stages/skybox/skybox_renderer.h b/libopenage/renderer/stages/skybox/skybox_renderer.h index 910b971ccd..95155e13b1 100644 --- a/libopenage/renderer/stages/skybox/skybox_renderer.h +++ b/libopenage/renderer/stages/skybox/skybox_renderer.h @@ -26,6 +26,13 @@ namespace skybox { */ class SkyboxRenderer { public: + /** + * Create a new render stage for the skybox. + * + * @param window openage window targeted for rendering. + * @param renderer openage low-level renderer. + * @param shaderdir Directory containing the shader source files. + */ SkyboxRenderer(const std::shared_ptr &window, const std::shared_ptr &renderer, const util::Path &shaderdir); diff --git a/libopenage/renderer/stages/terrain/terrain_chunk.h b/libopenage/renderer/stages/terrain/terrain_chunk.h index 4e96619865..4506877230 100644 --- a/libopenage/renderer/stages/terrain/terrain_chunk.h +++ b/libopenage/renderer/stages/terrain/terrain_chunk.h @@ -21,6 +21,9 @@ namespace terrain { class TerrainRenderMesh; class TerrainRenderEntity; +/** + * Stores the state of a terrain chunk in the terrain render stage. + */ class TerrainChunk { public: /** diff --git a/libopenage/renderer/stages/terrain/terrain_model.h b/libopenage/renderer/stages/terrain/terrain_model.h index 6677f3c4c5..a24441f191 100644 --- a/libopenage/renderer/stages/terrain/terrain_model.h +++ b/libopenage/renderer/stages/terrain/terrain_model.h @@ -31,6 +31,11 @@ class TerrainChunk; */ class TerrainRenderModel { public: + /** + * Create a new model for the terrain. + * + * @param asset_manager Asset manager for loading resources. + */ TerrainRenderModel(const std::shared_ptr &asset_manager); ~TerrainRenderModel() = default; diff --git a/libopenage/renderer/stages/terrain/terrain_render_entity.h b/libopenage/renderer/stages/terrain/terrain_render_entity.h index a7b044b874..dd95edda73 100644 --- a/libopenage/renderer/stages/terrain/terrain_render_entity.h +++ b/libopenage/renderer/stages/terrain/terrain_render_entity.h @@ -16,7 +16,9 @@ namespace openage::renderer::terrain { - +/** + * Render entity for pushing updates to the Terrain renderer. + */ class TerrainRenderEntity { public: TerrainRenderEntity(); diff --git a/libopenage/renderer/stages/terrain/terrain_renderer.h b/libopenage/renderer/stages/terrain/terrain_renderer.h index 4986509c95..7177a8f3be 100644 --- a/libopenage/renderer/stages/terrain/terrain_renderer.h +++ b/libopenage/renderer/stages/terrain/terrain_renderer.h @@ -41,6 +41,16 @@ class TerrainRenderModel; */ class TerrainRenderer { public: + /** + * Create a new render stage for the terrain. + * + * @param window openage window targeted for rendering. + * @param renderer openage low-level renderer. + * @param camera Camera used for the rendered scene. + * @param shaderdir Directory containing the shader source files. + * @param asset_manager Asset manager for loading resources. + * @param clock Simulation clock for timing animations. + */ TerrainRenderer(const std::shared_ptr &window, const std::shared_ptr &renderer, const std::shared_ptr &camera, diff --git a/libopenage/renderer/stages/world/world_object.h b/libopenage/renderer/stages/world/world_object.h index 2893084ed3..c558e4f93f 100644 --- a/libopenage/renderer/stages/world/world_object.h +++ b/libopenage/renderer/stages/world/world_object.h @@ -31,8 +31,16 @@ class Animation2dInfo; namespace world { class WorldRenderEntity; +/** + * Stores the state of a renderable object in the World render stage. + */ class WorldObject { public: + /** + * Create a new object for the World render stage. + * + * @param asset_manager Asset manager for loading resources. + */ WorldObject(const std::shared_ptr &asset_manager); ~WorldObject() = default; diff --git a/libopenage/renderer/stages/world/world_render_entity.h b/libopenage/renderer/stages/world/world_render_entity.h index 74ce51661f..b1ca52c0d3 100644 --- a/libopenage/renderer/stages/world/world_render_entity.h +++ b/libopenage/renderer/stages/world/world_render_entity.h @@ -19,6 +19,9 @@ namespace openage::renderer::world { +/** + * Render entity for pushing updates to the World renderer. + */ class WorldRenderEntity { public: WorldRenderEntity(); diff --git a/libopenage/renderer/stages/world/world_renderer.h b/libopenage/renderer/stages/world/world_renderer.h index 1aa7575d46..4206cc3b87 100644 --- a/libopenage/renderer/stages/world/world_renderer.h +++ b/libopenage/renderer/stages/world/world_renderer.h @@ -39,6 +39,16 @@ class WorldObject; */ class WorldRenderer { public: + /** + * Create a new render stage for the game world. + * + * @param window openage window targeted for rendering. + * @param renderer openage low-level renderer. + * @param camera Camera used for the rendered scene. + * @param shaderdir Directory containing the shader source files. + * @param asset_manager Asset manager for loading resources. + * @param clock Simulation clock for timing animations. + */ WorldRenderer(const std::shared_ptr &window, const std::shared_ptr &renderer, const std::shared_ptr &camera, From f7ce0335d92c70f4d1a63686a6b4b2146242c71d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 Nov 2023 21:21:38 +0100 Subject: [PATCH 170/771] renderer: Rename high-level renderers to 'render stage'. --- .../input/controller/hud/controller.cpp | 2 +- libopenage/input/controller/hud/controller.h | 4 +-- libopenage/presenter/presenter.cpp | 10 +++---- libopenage/presenter/presenter.h | 20 ++++++------- libopenage/renderer/demo/demo_3.cpp | 8 +++--- libopenage/renderer/demo/stresstest_0.cpp | 8 +++--- libopenage/renderer/render_factory.cpp | 4 +-- libopenage/renderer/render_factory.h | 12 ++++---- .../renderer/stages/hud/hud_render_entity.h | 6 ++-- .../renderer/stages/hud/hud_renderer.cpp | 28 +++++++++---------- libopenage/renderer/stages/hud/hud_renderer.h | 16 +++++------ .../stages/screen/screen_renderer.cpp | 14 +++++----- .../renderer/stages/screen/screen_renderer.h | 10 +++---- .../renderer/stages/screen/screenshot.cpp | 2 +- .../renderer/stages/screen/screenshot.h | 6 ++-- .../stages/skybox/skybox_renderer.cpp | 16 +++++------ .../renderer/stages/skybox/skybox_renderer.h | 10 +++---- .../stages/terrain/terrain_renderer.cpp | 12 ++++---- .../stages/terrain/terrain_renderer.h | 16 +++++------ .../renderer/stages/world/world_renderer.cpp | 14 +++++----- .../renderer/stages/world/world_renderer.h | 16 +++++------ 21 files changed, 117 insertions(+), 117 deletions(-) diff --git a/libopenage/input/controller/hud/controller.cpp b/libopenage/input/controller/hud/controller.cpp index 5d8576b9c4..a26f8cc4a1 100644 --- a/libopenage/input/controller/hud/controller.cpp +++ b/libopenage/input/controller/hud/controller.cpp @@ -36,7 +36,7 @@ const std::shared_ptr &Controller::get_drag_ } void setup_defaults(const std::shared_ptr &ctx, - const std::shared_ptr &hud_renderer) { + const std::shared_ptr &hud_renderer) { binding_func_t drag_selection_init{[&](const event_arguments &args, const std::shared_ptr controller) { auto render_entity = std::make_shared(args.mouse); diff --git a/libopenage/input/controller/hud/controller.h b/libopenage/input/controller/hud/controller.h index b72e12b021..e7c391846e 100644 --- a/libopenage/input/controller/hud/controller.h +++ b/libopenage/input/controller/hud/controller.h @@ -11,7 +11,7 @@ namespace openage { namespace renderer::hud { class HudDragRenderEntity; -class HudRenderer; +class HudRenderStage; } // namespace renderer::hud namespace input::hud { @@ -69,7 +69,7 @@ class Controller : public std::enable_shared_from_this { * @param hud_renderer HUD render stage that is used to render the selection box. */ void setup_defaults(const std::shared_ptr &ctx, - const std::shared_ptr &hud_renderer); + const std::shared_ptr &hud_renderer); } // namespace input::hud } // namespace openage diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index 095fbaece0..f8885c7ce9 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -113,7 +113,7 @@ void Presenter::init_graphics(bool debug) { this->camera_manager = std::make_shared(this->camera); // Skybox - this->skybox_renderer = std::make_shared( + this->skybox_renderer = std::make_shared( this->window, this->renderer, this->root_dir["assets"]["shaders"]); @@ -121,7 +121,7 @@ void Presenter::init_graphics(bool debug) { this->render_passes.push_back(this->skybox_renderer->get_render_pass()); // Terrain - this->terrain_renderer = std::make_shared( + this->terrain_renderer = std::make_shared( this->window, this->renderer, this->camera, @@ -131,7 +131,7 @@ void Presenter::init_graphics(bool debug) { this->render_passes.push_back(this->terrain_renderer->get_render_pass()); // Units/buildings - this->world_renderer = std::make_shared( + this->world_renderer = std::make_shared( this->window, this->renderer, this->camera, @@ -141,7 +141,7 @@ void Presenter::init_graphics(bool debug) { this->render_passes.push_back(this->world_renderer->get_render_pass()); // HUD - this->hud_renderer = std::make_shared( + this->hud_renderer = std::make_shared( this->window, this->renderer, this->camera, @@ -264,7 +264,7 @@ void Presenter::init_input() { void Presenter::init_final_render_pass() { // Final output to window - this->screen_renderer = std::make_shared( + this->screen_renderer = std::make_shared( this->window, this->renderer, this->root_dir["assets"]["shaders"]); diff --git a/libopenage/presenter/presenter.h b/libopenage/presenter/presenter.h index f3abcb95c7..db28522a7c 100644 --- a/libopenage/presenter/presenter.h +++ b/libopenage/presenter/presenter.h @@ -42,23 +42,23 @@ class GUI; } namespace hud { -class HudRenderer; +class HudRenderStage; } namespace screen { -class ScreenRenderer; +class ScreenRenderStage; } namespace skybox { -class SkyboxRenderer; +class SkyboxRenderStage; } namespace terrain { -class TerrainRenderer; +class TerrainRenderStage; } namespace world { -class WorldRenderer; +class WorldRenderStage; } namespace resources { @@ -182,27 +182,27 @@ class Presenter { /** * Graphics output for the map background. */ - std::shared_ptr skybox_renderer; + std::shared_ptr skybox_renderer; /** * Graphics output for terrain. */ - std::shared_ptr terrain_renderer; + std::shared_ptr terrain_renderer; /** * Graphics output for units/buildings. */ - std::shared_ptr world_renderer; + std::shared_ptr world_renderer; /** * Graphics output for the HUD. */ - std::shared_ptr hud_renderer; + std::shared_ptr hud_renderer; /** * Final graphics output to the window screen. */ - std::shared_ptr screen_renderer; + std::shared_ptr screen_renderer; /** * Manager for loading/storing asset resources. diff --git a/libopenage/renderer/demo/demo_3.cpp b/libopenage/renderer/demo/demo_3.cpp index 9dbaa9b93e..16f391344b 100644 --- a/libopenage/renderer/demo/demo_3.cpp +++ b/libopenage/renderer/demo/demo_3.cpp @@ -58,14 +58,14 @@ void renderer_demo_3(const util::Path &path) { path["assets"]["test"]); // Renders the background - auto skybox_renderer = std::make_shared( + auto skybox_renderer = std::make_shared( window, renderer, path["assets"]["shaders"]); skybox_renderer->set_color(1.0f, 0.5f, 0.0f, 1.0f); // orange color // Renders the terrain in 3D - auto terrain_renderer = std::make_shared( + auto terrain_renderer = std::make_shared( window, renderer, camera, @@ -74,7 +74,7 @@ void renderer_demo_3(const util::Path &path) { clock); // Renders units/buildings/other objects - auto world_renderer = std::make_shared( + auto world_renderer = std::make_shared( window, renderer, camera, @@ -92,7 +92,7 @@ void renderer_demo_3(const util::Path &path) { // Final output on screen has its own subrenderer // It takes the outputs of all previous render passes // and blends them together - auto screen_renderer = std::make_shared( + auto screen_renderer = std::make_shared( window, renderer, path["assets"]["shaders"]); diff --git a/libopenage/renderer/demo/stresstest_0.cpp b/libopenage/renderer/demo/stresstest_0.cpp index b33abb9685..ee9500178f 100644 --- a/libopenage/renderer/demo/stresstest_0.cpp +++ b/libopenage/renderer/demo/stresstest_0.cpp @@ -59,14 +59,14 @@ void renderer_stresstest_0(const util::Path &path) { path["assets"]["test"]); // Renders the background - auto skybox_renderer = std::make_shared( + auto skybox_renderer = std::make_shared( window, renderer, path["assets"]["shaders"]); skybox_renderer->set_color(1.0f, 0.5f, 0.0f, 1.0f); // orange color // Renders the terrain in 3D - auto terrain_renderer = std::make_shared( + auto terrain_renderer = std::make_shared( window, renderer, camera, @@ -75,7 +75,7 @@ void renderer_stresstest_0(const util::Path &path) { clock); // Renders units/buildings/other objects - auto world_renderer = std::make_shared( + auto world_renderer = std::make_shared( window, renderer, camera, @@ -93,7 +93,7 @@ void renderer_stresstest_0(const util::Path &path) { // Final output on screen has its own subrenderer // It takes the outputs of all previous render passes // and blends them together - auto screen_renderer = std::make_shared( + auto screen_renderer = std::make_shared( window, renderer, path["assets"]["shaders"]); diff --git a/libopenage/renderer/render_factory.cpp b/libopenage/renderer/render_factory.cpp index c40cc46508..e9ac9ca4e1 100644 --- a/libopenage/renderer/render_factory.cpp +++ b/libopenage/renderer/render_factory.cpp @@ -9,8 +9,8 @@ #include "renderer/stages/world/world_renderer.h" namespace openage::renderer { -RenderFactory::RenderFactory(const std::shared_ptr terrain_renderer, - const std::shared_ptr world_renderer) : +RenderFactory::RenderFactory(const std::shared_ptr terrain_renderer, + const std::shared_ptr world_renderer) : terrain_renderer{terrain_renderer}, world_renderer{world_renderer} { } diff --git a/libopenage/renderer/render_factory.h b/libopenage/renderer/render_factory.h index 5a2a36de59..113c9ac966 100644 --- a/libopenage/renderer/render_factory.h +++ b/libopenage/renderer/render_factory.h @@ -10,12 +10,12 @@ namespace openage::renderer { namespace terrain { -class TerrainRenderer; +class TerrainRenderStage; class TerrainRenderEntity; } // namespace terrain namespace world { -class WorldRenderer; +class WorldRenderStage; class WorldRenderEntity; } // namespace world @@ -32,8 +32,8 @@ class RenderFactory { * @param terrain_renderer Terrain renderer. * @param world_renderer World renderer. */ - RenderFactory(const std::shared_ptr terrain_renderer, - const std::shared_ptr world_renderer); + RenderFactory(const std::shared_ptr terrain_renderer, + const std::shared_ptr world_renderer); ~RenderFactory() = default; /** @@ -61,12 +61,12 @@ class RenderFactory { /** * Render stage for terrain drawing. */ - std::shared_ptr terrain_renderer; + std::shared_ptr terrain_renderer; /** * Render stage for game entity drawing. */ - std::shared_ptr world_renderer; + std::shared_ptr world_renderer; }; } // namespace openage::renderer diff --git a/libopenage/renderer/stages/hud/hud_render_entity.h b/libopenage/renderer/stages/hud/hud_render_entity.h index 00439b5cc6..7bbdbd7366 100644 --- a/libopenage/renderer/stages/hud/hud_render_entity.h +++ b/libopenage/renderer/stages/hud/hud_render_entity.h @@ -27,7 +27,8 @@ class HudDragRenderEntity { ~HudDragRenderEntity() = default; /** - * Update the render entity with information from the gamestate. + * Update the render entity with information from the gamestate + * or input system. * * @param drag_pos Position of the dragged corner. * @param time Current simulation time. @@ -57,8 +58,7 @@ class HudDragRenderEntity { const coord::input &get_drag_start(); /** - * Check whether the render entity has received new updates from the - * gamestate. + * Check whether the render entity has received new updates. * * @return true if updates have been received, else false. */ diff --git a/libopenage/renderer/stages/hud/hud_renderer.cpp b/libopenage/renderer/stages/hud/hud_renderer.cpp index 938eb5bdb4..590885681f 100644 --- a/libopenage/renderer/stages/hud/hud_renderer.cpp +++ b/libopenage/renderer/stages/hud/hud_renderer.cpp @@ -16,12 +16,12 @@ namespace openage::renderer::hud { -HudRenderer::HudRenderer(const std::shared_ptr &window, - const std::shared_ptr &renderer, - const std::shared_ptr &camera, - const util::Path &shaderdir, - const std::shared_ptr &asset_manager, - const std::shared_ptr clock) : +HudRenderStage::HudRenderStage(const std::shared_ptr &window, + const std::shared_ptr &renderer, + const std::shared_ptr &camera, + const util::Path &shaderdir, + const std::shared_ptr &asset_manager, + const std::shared_ptr clock) : renderer{renderer}, camera{camera}, asset_manager{asset_manager}, @@ -39,11 +39,11 @@ HudRenderer::HudRenderer(const std::shared_ptr &window, log::log(INFO << "Created render stage 'HUD'"); } -std::shared_ptr HudRenderer::get_render_pass() { +std::shared_ptr HudRenderStage::get_render_pass() { return this->render_pass; } -void HudRenderer::add_drag_entity(const std::shared_ptr entity) { +void HudRenderStage::add_drag_entity(const std::shared_ptr entity) { std::unique_lock lock{this->mutex}; auto hud_object = std::make_shared(this->asset_manager); @@ -52,14 +52,14 @@ void HudRenderer::add_drag_entity(const std::shared_ptr ent this->drag_object = hud_object; } -void HudRenderer::remove_drag_entity() { +void HudRenderStage::remove_drag_entity() { std::unique_lock lock{this->mutex}; this->drag_object = nullptr; this->render_pass->clear_renderables(); } -void HudRenderer::update() { +void HudRenderStage::update() { std::unique_lock lock{this->mutex}; auto current_time = this->clock->get_real_time(); @@ -89,7 +89,7 @@ void HudRenderer::update() { } } -void HudRenderer::resize(size_t width, size_t height) { +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)); @@ -97,9 +97,9 @@ void HudRenderer::resize(size_t width, size_t height) { this->render_pass->set_target(fbo); } -void HudRenderer::initialize_render_pass(size_t width, - size_t height, - const util::Path &shaderdir) { +void HudRenderStage::initialize_render_pass(size_t width, + size_t height, + const util::Path &shaderdir) { // Drag select shader auto vert_shader_file = (shaderdir / "hud_drag_select.vert.glsl").open(); auto vert_shader_src = renderer::resources::ShaderSource( diff --git a/libopenage/renderer/stages/hud/hud_renderer.h b/libopenage/renderer/stages/hud/hud_renderer.h index d9bed7529f..6d3b68e9f6 100644 --- a/libopenage/renderer/stages/hud/hud_renderer.h +++ b/libopenage/renderer/stages/hud/hud_renderer.h @@ -39,7 +39,7 @@ class HudDragRenderEntity; * * TODO: Currently only supports drag selection. */ -class HudRenderer { +class HudRenderStage { public: /** * Create a new render stage for the HUD. @@ -51,13 +51,13 @@ class HudRenderer { * @param asset_manager Asset manager for loading resources. * @param clock Simulation clock for timing animations. */ - HudRenderer(const std::shared_ptr &window, - const std::shared_ptr &renderer, - const std::shared_ptr &camera, - const util::Path &shaderdir, - const std::shared_ptr &asset_manager, - const std::shared_ptr clock); - ~HudRenderer() = default; + HudRenderStage(const std::shared_ptr &window, + const std::shared_ptr &renderer, + const std::shared_ptr &camera, + const util::Path &shaderdir, + const std::shared_ptr &asset_manager, + const std::shared_ptr clock); + ~HudRenderStage() = default; /** * Get the render pass of the HUD renderer. diff --git a/libopenage/renderer/stages/screen/screen_renderer.cpp b/libopenage/renderer/stages/screen/screen_renderer.cpp index 1fa045abcf..71be26b451 100644 --- a/libopenage/renderer/stages/screen/screen_renderer.cpp +++ b/libopenage/renderer/stages/screen/screen_renderer.cpp @@ -15,9 +15,9 @@ namespace openage::renderer::screen { -ScreenRenderer::ScreenRenderer(const std::shared_ptr & /* window */, - const std::shared_ptr &renderer, - const util::Path &shaderdir) : +ScreenRenderStage::ScreenRenderStage(const std::shared_ptr & /* window */, + const std::shared_ptr &renderer, + const util::Path &shaderdir) : renderer{renderer}, render_targets{}, pass_outputs{} { @@ -28,16 +28,16 @@ ScreenRenderer::ScreenRenderer(const std::shared_ptr & /* window */, log::log(INFO << "Created render stage 'Screen'"); } -std::shared_ptr ScreenRenderer::get_render_pass() { +std::shared_ptr ScreenRenderStage::get_render_pass() { return this->render_pass; } -void ScreenRenderer::set_render_targets(const std::vector> &targets) { +void ScreenRenderStage::set_render_targets(const std::vector> &targets) { this->render_targets = targets; this->update_render_pass(); } -void ScreenRenderer::initialize_render_pass(const util::Path &shaderdir) { +void ScreenRenderStage::initialize_render_pass(const util::Path &shaderdir) { auto vert_shader_file = (shaderdir / "final.vert.glsl").open(); auto vert_shader_src = renderer::resources::ShaderSource( resources::shader_lang_t::glsl, @@ -59,7 +59,7 @@ void ScreenRenderer::initialize_render_pass(const util::Path &shaderdir) { this->render_pass = renderer->add_render_pass({}, renderer->get_display_target()); } -void ScreenRenderer::update_render_pass() { +void ScreenRenderStage::update_render_pass() { auto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); std::vector output_layers{}; diff --git a/libopenage/renderer/stages/screen/screen_renderer.h b/libopenage/renderer/stages/screen/screen_renderer.h index 1b1c7dcaa7..f528850784 100644 --- a/libopenage/renderer/stages/screen/screen_renderer.h +++ b/libopenage/renderer/stages/screen/screen_renderer.h @@ -22,7 +22,7 @@ namespace screen { * (i.e. the renderers display target). This should always be the * last render stage. */ -class ScreenRenderer { +class ScreenRenderStage { public: /** * Create a new render stage for drawing to the screen. @@ -31,10 +31,10 @@ class ScreenRenderer { * @param renderer openage low-level renderer. * @param shaderdir Directory containing the shader source files. */ - ScreenRenderer(const std::shared_ptr &window, - const std::shared_ptr &renderer, - const util::Path &shaderdir); - ~ScreenRenderer() = default; + ScreenRenderStage(const std::shared_ptr &window, + const std::shared_ptr &renderer, + const util::Path &shaderdir); + ~ScreenRenderStage() = default; /** * Get the render pass of the screen renderer. diff --git a/libopenage/renderer/stages/screen/screenshot.cpp b/libopenage/renderer/stages/screen/screenshot.cpp index e73c875a11..03c1df7902 100644 --- a/libopenage/renderer/stages/screen/screenshot.cpp +++ b/libopenage/renderer/stages/screen/screenshot.cpp @@ -22,7 +22,7 @@ namespace openage::renderer::screen { -ScreenshotManager::ScreenshotManager(std::shared_ptr &renderer, +ScreenshotManager::ScreenshotManager(std::shared_ptr &renderer, util::Path &outdir, std::shared_ptr &job_mgr) : outdir{outdir}, diff --git a/libopenage/renderer/stages/screen/screenshot.h b/libopenage/renderer/stages/screen/screenshot.h index df4ebcee48..718eabdf0d 100644 --- a/libopenage/renderer/stages/screen/screenshot.h +++ b/libopenage/renderer/stages/screen/screenshot.h @@ -16,7 +16,7 @@ class JobManager; } namespace renderer::screen { -class ScreenRenderer; +class ScreenRenderStage; /** * Takes screenshots, duh. @@ -30,7 +30,7 @@ class ScreenshotManager { * @param outdir Directory where the screenshots are saved. * @param job_mgr Job manager to use for writing the screenshot to disk. */ - ScreenshotManager(std::shared_ptr &renderer, + ScreenshotManager(std::shared_ptr &renderer, util::Path &outdir, std::shared_ptr &job_mgr); @@ -68,7 +68,7 @@ class ScreenshotManager { /** * Screen render stage to take the screenshot from. */ - std::shared_ptr renderer; + std::shared_ptr renderer; /** * Job manager to use for writing the screenshot to disk. diff --git a/libopenage/renderer/stages/skybox/skybox_renderer.cpp b/libopenage/renderer/stages/skybox/skybox_renderer.cpp index cc2777d324..0107d68f17 100644 --- a/libopenage/renderer/stages/skybox/skybox_renderer.cpp +++ b/libopenage/renderer/stages/skybox/skybox_renderer.cpp @@ -14,7 +14,7 @@ namespace openage::renderer::skybox { -SkyboxRenderer::SkyboxRenderer(const std::shared_ptr &window, +SkyboxRenderStage::SkyboxRenderStage(const std::shared_ptr &window, const std::shared_ptr &renderer, const util::Path &shaderdir) : renderer{renderer}, @@ -32,11 +32,11 @@ SkyboxRenderer::SkyboxRenderer(const std::shared_ptr &window, log::log(INFO << "Created render stage 'Skybox'"); } -std::shared_ptr SkyboxRenderer::get_render_pass() { +std::shared_ptr SkyboxRenderStage::get_render_pass() { return this->render_pass; } -void SkyboxRenderer::set_color(const Eigen::Vector4i col) { +void SkyboxRenderStage::set_color(const Eigen::Vector4i col) { this->bg_color = Eigen::Vector4f( col[0] / 255, col[1] / 255, @@ -45,7 +45,7 @@ void SkyboxRenderer::set_color(const Eigen::Vector4i col) { this->color_unif->update("in_col", this->bg_color); } -void SkyboxRenderer::set_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { +void SkyboxRenderStage::set_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { this->bg_color = Eigen::Vector4f( r / 255, g / 255, @@ -54,24 +54,24 @@ void SkyboxRenderer::set_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { this->color_unif->update("in_col", this->bg_color); } -void SkyboxRenderer::set_color(const Eigen::Vector4f col) { +void SkyboxRenderStage::set_color(const Eigen::Vector4f col) { this->bg_color = col; this->color_unif->update("in_col", this->bg_color); } -void SkyboxRenderer::set_color(float r, float g, float b, float a) { +void SkyboxRenderStage::set_color(float r, float g, float b, float a) { this->bg_color = Eigen::Vector4f(r, g, b, a); this->color_unif->update("in_col", this->bg_color); } -void SkyboxRenderer::resize(size_t width, size_t height) { +void SkyboxRenderStage::resize(size_t width, size_t height) { this->output_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::rgba8)); auto fbo = this->renderer->create_texture_target({this->output_texture}); this->render_pass->set_target(fbo); } -void SkyboxRenderer::initialize_render_pass(size_t width, +void SkyboxRenderStage::initialize_render_pass(size_t width, size_t height, const util::Path &shaderdir) { auto vert_shader_file = (shaderdir / "skybox.vert.glsl").open(); diff --git a/libopenage/renderer/stages/skybox/skybox_renderer.h b/libopenage/renderer/stages/skybox/skybox_renderer.h index 95155e13b1..fc2deb0a65 100644 --- a/libopenage/renderer/stages/skybox/skybox_renderer.h +++ b/libopenage/renderer/stages/skybox/skybox_renderer.h @@ -24,7 +24,7 @@ namespace skybox { * a black devilish void of nothingness. Maybe "hellbox" is more * appropriate.) */ -class SkyboxRenderer { +class SkyboxRenderStage { public: /** * Create a new render stage for the skybox. @@ -33,10 +33,10 @@ class SkyboxRenderer { * @param renderer openage low-level renderer. * @param shaderdir Directory containing the shader source files. */ - SkyboxRenderer(const std::shared_ptr &window, - const std::shared_ptr &renderer, - const util::Path &shaderdir); - ~SkyboxRenderer() = default; + SkyboxRenderStage(const std::shared_ptr &window, + const std::shared_ptr &renderer, + const util::Path &shaderdir); + ~SkyboxRenderStage() = default; /** * Get the render pass of the skybox renderer. diff --git a/libopenage/renderer/stages/terrain/terrain_renderer.cpp b/libopenage/renderer/stages/terrain/terrain_renderer.cpp index 92b5933d4f..78f2711690 100644 --- a/libopenage/renderer/stages/terrain/terrain_renderer.cpp +++ b/libopenage/renderer/stages/terrain/terrain_renderer.cpp @@ -17,7 +17,7 @@ namespace openage::renderer::terrain { -TerrainRenderer::TerrainRenderer(const std::shared_ptr &window, +TerrainRenderStage::TerrainRenderStage(const std::shared_ptr &window, const std::shared_ptr &renderer, const std::shared_ptr &camera, const util::Path &shaderdir, @@ -42,11 +42,11 @@ TerrainRenderer::TerrainRenderer(const std::shared_ptr &window, log::log(INFO << "Created render stage 'Terrain'"); } -std::shared_ptr TerrainRenderer::get_render_pass() { +std::shared_ptr TerrainRenderStage::get_render_pass() { return this->render_pass; } -void TerrainRenderer::add_render_entity(const std::shared_ptr entity, +void TerrainRenderStage::add_render_entity(const std::shared_ptr entity, const util::Vector2s chunk_size, const coord::scene2_delta chunk_offset) { std::unique_lock lock{this->mutex}; @@ -56,7 +56,7 @@ void TerrainRenderer::add_render_entity(const std::shared_ptrupdate(); } -void TerrainRenderer::update() { +void TerrainRenderStage::update() { this->model->fetch_updates(); auto current_time = this->clock->get_real_time(); for (auto &chunk : this->model->get_chunks()) { @@ -85,7 +85,7 @@ void TerrainRenderer::update() { this->model->update_uniforms(current_time); } -void TerrainRenderer::resize(size_t width, size_t height) { +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)); @@ -93,7 +93,7 @@ void TerrainRenderer::resize(size_t width, size_t height) { this->render_pass->set_target(fbo); } -void TerrainRenderer::initialize_render_pass(size_t width, +void TerrainRenderStage::initialize_render_pass(size_t width, size_t height, const util::Path &shaderdir) { auto vert_shader_file = (shaderdir / "terrain.vert.glsl").open(); diff --git a/libopenage/renderer/stages/terrain/terrain_renderer.h b/libopenage/renderer/stages/terrain/terrain_renderer.h index 7177a8f3be..d8d1659329 100644 --- a/libopenage/renderer/stages/terrain/terrain_renderer.h +++ b/libopenage/renderer/stages/terrain/terrain_renderer.h @@ -39,7 +39,7 @@ class TerrainRenderModel; /** * Manage and render terrain geometry and graphics. */ -class TerrainRenderer { +class TerrainRenderStage { public: /** * Create a new render stage for the terrain. @@ -51,13 +51,13 @@ class TerrainRenderer { * @param asset_manager Asset manager for loading resources. * @param clock Simulation clock for timing animations. */ - TerrainRenderer(const std::shared_ptr &window, - const std::shared_ptr &renderer, - const std::shared_ptr &camera, - const util::Path &shaderdir, - const std::shared_ptr &asset_manager, - const std::shared_ptr &clock); - ~TerrainRenderer() = default; + TerrainRenderStage(const std::shared_ptr &window, + const std::shared_ptr &renderer, + const std::shared_ptr &camera, + const util::Path &shaderdir, + const std::shared_ptr &asset_manager, + const std::shared_ptr &clock); + ~TerrainRenderStage() = default; /** * Get the render pass of the terrain renderer. diff --git a/libopenage/renderer/stages/world/world_renderer.cpp b/libopenage/renderer/stages/world/world_renderer.cpp index 69bc67b54c..65a9c77dc4 100644 --- a/libopenage/renderer/stages/world/world_renderer.cpp +++ b/libopenage/renderer/stages/world/world_renderer.cpp @@ -16,7 +16,7 @@ namespace openage::renderer::world { -WorldRenderer::WorldRenderer(const std::shared_ptr &window, +WorldRenderStage::WorldRenderStage(const std::shared_ptr &window, const std::shared_ptr &renderer, const std::shared_ptr &camera, const util::Path &shaderdir, @@ -41,11 +41,11 @@ WorldRenderer::WorldRenderer(const std::shared_ptr &window, log::log(INFO << "Created render stage 'World'"); } -std::shared_ptr WorldRenderer::get_render_pass() { +std::shared_ptr WorldRenderStage::get_render_pass() { return this->render_pass; } -void WorldRenderer::add_render_entity(const std::shared_ptr entity) { +void WorldRenderStage::add_render_entity(const std::shared_ptr entity) { std::unique_lock lock{this->mutex}; auto world_object = std::make_shared(this->asset_manager); @@ -54,7 +54,7 @@ void WorldRenderer::add_render_entity(const std::shared_ptr e this->render_objects.push_back(world_object); } -void WorldRenderer::update() { +void WorldRenderStage::update() { std::unique_lock lock{this->mutex}; auto current_time = this->clock->get_real_time(); for (auto &obj : this->render_objects) { @@ -92,7 +92,7 @@ void WorldRenderer::update() { } } -void WorldRenderer::resize(size_t width, size_t height) { +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)); @@ -101,7 +101,7 @@ void WorldRenderer::resize(size_t width, size_t height) { this->render_pass->set_target(fbo); } -void WorldRenderer::initialize_render_pass(size_t width, +void WorldRenderStage::initialize_render_pass(size_t width, size_t height, const util::Path &shaderdir) { auto vert_shader_file = (shaderdir / "world2d.vert.glsl").open(); @@ -129,7 +129,7 @@ void WorldRenderer::initialize_render_pass(size_t width, this->render_pass = this->renderer->add_render_pass({}, fbo); } -void WorldRenderer::init_uniform_ids() { +void WorldRenderStage::init_uniform_ids() { WorldObject::obj_world_position = this->display_shader->get_uniform_id("obj_world_position"); WorldObject::flip_x = this->display_shader->get_uniform_id("flip_x"); WorldObject::flip_y = this->display_shader->get_uniform_id("flip_y"); diff --git a/libopenage/renderer/stages/world/world_renderer.h b/libopenage/renderer/stages/world/world_renderer.h index 4206cc3b87..fe3cf1970b 100644 --- a/libopenage/renderer/stages/world/world_renderer.h +++ b/libopenage/renderer/stages/world/world_renderer.h @@ -37,7 +37,7 @@ class WorldObject; /** * Renderer for drawing and displaying entities in the game world (units, buildings, etc.) */ -class WorldRenderer { +class WorldRenderStage { public: /** * Create a new render stage for the game world. @@ -49,13 +49,13 @@ class WorldRenderer { * @param asset_manager Asset manager for loading resources. * @param clock Simulation clock for timing animations. */ - WorldRenderer(const std::shared_ptr &window, - const std::shared_ptr &renderer, - const std::shared_ptr &camera, - const util::Path &shaderdir, - const std::shared_ptr &asset_manager, - const std::shared_ptr clock); - ~WorldRenderer() = default; + WorldRenderStage(const std::shared_ptr &window, + const std::shared_ptr &renderer, + const std::shared_ptr &camera, + const util::Path &shaderdir, + const std::shared_ptr &asset_manager, + const std::shared_ptr clock); + ~WorldRenderStage() = default; /** * Get the render pass of the world renderer. From 7b027b0879b5621110e6cbe8d04c7aa9151c9724 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 Nov 2023 21:28:46 +0100 Subject: [PATCH 171/771] renderer: Refactor filenames for render stages. --- libopenage/gamestate/game_entity.cpp | 2 +- libopenage/gamestate/terrain_chunk.h | 2 +- libopenage/gamestate/terrain_factory.cpp | 2 +- .../input/controller/hud/controller.cpp | 4 +-- libopenage/presenter/presenter.cpp | 10 +++---- libopenage/renderer/demo/demo_3.cpp | 12 ++++----- libopenage/renderer/demo/stresstest_0.cpp | 12 ++++----- libopenage/renderer/render_factory.cpp | 8 +++--- libopenage/renderer/stages/hud/CMakeLists.txt | 6 ++--- .../stages/hud/{hud_object.cpp => object.cpp} | 4 +-- .../stages/hud/{hud_object.h => object.h} | 0 ...ud_render_entity.cpp => render_entity.cpp} | 2 +- .../{hud_render_entity.h => render_entity.h} | 0 .../{hud_renderer.cpp => render_stage.cpp} | 4 +-- .../hud/{hud_renderer.h => render_stage.h} | 0 .../renderer/stages/screen/CMakeLists.txt | 2 +- .../{screen_renderer.cpp => render_stage.cpp} | 2 +- .../{screen_renderer.h => render_stage.h} | 0 .../renderer/stages/screen/screenshot.cpp | 2 +- .../renderer/stages/skybox/CMakeLists.txt | 2 +- .../{skybox_renderer.cpp => render_stage.cpp} | 10 +++---- .../{skybox_renderer.h => render_stage.h} | 0 .../renderer/stages/terrain/CMakeLists.txt | 10 +++---- .../terrain/{terrain_chunk.cpp => chunk.cpp} | 6 ++--- .../terrain/{terrain_chunk.h => chunk.h} | 0 .../terrain/{terrain_mesh.cpp => mesh.cpp} | 2 +- .../stages/terrain/{terrain_mesh.h => mesh.h} | 0 .../terrain/{terrain_model.cpp => model.cpp} | 6 ++--- .../terrain/{terrain_model.h => model.h} | 0 ...in_render_entity.cpp => render_entity.cpp} | 2 +- ...errain_render_entity.h => render_entity.h} | 0 ...{terrain_renderer.cpp => render_stage.cpp} | 26 +++++++++---------- .../{terrain_renderer.h => render_stage.h} | 0 .../renderer/stages/world/CMakeLists.txt | 6 ++--- .../world/{world_object.cpp => object.cpp} | 4 +-- .../stages/world/{world_object.h => object.h} | 0 ...ld_render_entity.cpp => render_entity.cpp} | 2 +- ...{world_render_entity.h => render_entity.h} | 0 .../{world_renderer.cpp => render_stage.cpp} | 18 ++++++------- .../{world_renderer.h => render_stage.h} | 0 40 files changed, 84 insertions(+), 84 deletions(-) rename libopenage/renderer/stages/hud/{hud_object.cpp => object.cpp} (97%) rename libopenage/renderer/stages/hud/{hud_object.h => object.h} (100%) rename libopenage/renderer/stages/hud/{hud_render_entity.cpp => render_entity.cpp} (97%) rename libopenage/renderer/stages/hud/{hud_render_entity.h => render_entity.h} (100%) rename libopenage/renderer/stages/hud/{hud_renderer.cpp => render_stage.cpp} (98%) rename libopenage/renderer/stages/hud/{hud_renderer.h => render_stage.h} (100%) rename libopenage/renderer/stages/screen/{screen_renderer.cpp => render_stage.cpp} (99%) rename libopenage/renderer/stages/screen/{screen_renderer.h => render_stage.h} (100%) rename libopenage/renderer/stages/skybox/{skybox_renderer.cpp => render_stage.cpp} (90%) rename libopenage/renderer/stages/skybox/{skybox_renderer.h => render_stage.h} (100%) rename libopenage/renderer/stages/terrain/{terrain_chunk.cpp => chunk.cpp} (96%) rename libopenage/renderer/stages/terrain/{terrain_chunk.h => chunk.h} (100%) rename libopenage/renderer/stages/terrain/{terrain_mesh.cpp => mesh.cpp} (99%) rename libopenage/renderer/stages/terrain/{terrain_mesh.h => mesh.h} (100%) rename libopenage/renderer/stages/terrain/{terrain_model.cpp => model.cpp} (91%) rename libopenage/renderer/stages/terrain/{terrain_model.h => model.h} (100%) rename libopenage/renderer/stages/terrain/{terrain_render_entity.cpp => render_entity.cpp} (99%) rename libopenage/renderer/stages/terrain/{terrain_render_entity.h => render_entity.h} (100%) rename libopenage/renderer/stages/terrain/{terrain_renderer.cpp => render_stage.cpp} (81%) rename libopenage/renderer/stages/terrain/{terrain_renderer.h => render_stage.h} (100%) rename libopenage/renderer/stages/world/{world_object.cpp => object.cpp} (98%) rename libopenage/renderer/stages/world/{world_object.h => object.h} (100%) rename libopenage/renderer/stages/world/{world_render_entity.cpp => render_entity.cpp} (98%) rename libopenage/renderer/stages/world/{world_render_entity.h => render_entity.h} (100%) rename libopenage/renderer/stages/world/{world_renderer.cpp => render_stage.cpp} (88%) rename libopenage/renderer/stages/world/{world_renderer.h => render_stage.h} (100%) diff --git a/libopenage/gamestate/game_entity.cpp b/libopenage/gamestate/game_entity.cpp index e8b7a9e086..a4e18a29e3 100644 --- a/libopenage/gamestate/game_entity.cpp +++ b/libopenage/gamestate/game_entity.cpp @@ -9,7 +9,7 @@ #include "gamestate/component/api/move.h" #include "gamestate/component/base_component.h" #include "gamestate/component/internal/position.h" -#include "renderer/stages/world/world_render_entity.h" +#include "renderer/stages/world/render_entity.h" namespace openage::gamestate { diff --git a/libopenage/gamestate/terrain_chunk.h b/libopenage/gamestate/terrain_chunk.h index df9091a321..e7abce08f5 100644 --- a/libopenage/gamestate/terrain_chunk.h +++ b/libopenage/gamestate/terrain_chunk.h @@ -6,7 +6,7 @@ #include "coord/tile.h" #include "gamestate/terrain_tile.h" -#include "renderer/stages/terrain/terrain_render_entity.h" +#include "renderer/stages/terrain/render_entity.h" #include "time/time.h" #include "util/vector.h" diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp index 7276d73d57..128b92a043 100644 --- a/libopenage/gamestate/terrain_factory.cpp +++ b/libopenage/gamestate/terrain_factory.cpp @@ -11,7 +11,7 @@ #include "gamestate/terrain_chunk.h" #include "gamestate/terrain_tile.h" #include "renderer/render_factory.h" -#include "renderer/stages/terrain/terrain_render_entity.h" +#include "renderer/stages/terrain/render_entity.h" #include "time/time.h" #include "assets/mod_manager.h" diff --git a/libopenage/input/controller/hud/controller.cpp b/libopenage/input/controller/hud/controller.cpp index a26f8cc4a1..b423ef79ae 100644 --- a/libopenage/input/controller/hud/controller.cpp +++ b/libopenage/input/controller/hud/controller.cpp @@ -6,8 +6,8 @@ #include "input/controller/hud/binding_context.h" -#include "renderer/stages/hud/hud_render_entity.h" -#include "renderer/stages/hud/hud_renderer.h" +#include "renderer/stages/hud/render_entity.h" +#include "renderer/stages/hud/render_stage.h" namespace openage::input::hud { diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index f8885c7ce9..b8c034987a 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -25,11 +25,11 @@ #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" #include "renderer/stages/camera/manager.h" -#include "renderer/stages/hud/hud_renderer.h" -#include "renderer/stages/screen/screen_renderer.h" -#include "renderer/stages/skybox/skybox_renderer.h" -#include "renderer/stages/terrain/terrain_renderer.h" -#include "renderer/stages/world/world_renderer.h" +#include "renderer/stages/hud/render_stage.h" +#include "renderer/stages/screen/render_stage.h" +#include "renderer/stages/skybox/render_stage.h" +#include "renderer/stages/terrain/render_stage.h" +#include "renderer/stages/world/render_stage.h" #include "renderer/window.h" #include "time/time_loop.h" #include "util/path.h" diff --git a/libopenage/renderer/demo/demo_3.cpp b/libopenage/renderer/demo/demo_3.cpp index 16f391344b..835c13a364 100644 --- a/libopenage/renderer/demo/demo_3.cpp +++ b/libopenage/renderer/demo/demo_3.cpp @@ -13,12 +13,12 @@ #include "renderer/resources/assets/asset_manager.h" #include "renderer/resources/shader_source.h" #include "renderer/stages/camera/manager.h" -#include "renderer/stages/screen/screen_renderer.h" -#include "renderer/stages/skybox/skybox_renderer.h" -#include "renderer/stages/terrain/terrain_render_entity.h" -#include "renderer/stages/terrain/terrain_renderer.h" -#include "renderer/stages/world/world_render_entity.h" -#include "renderer/stages/world/world_renderer.h" +#include "renderer/stages/screen/render_stage.h" +#include "renderer/stages/skybox/render_stage.h" +#include "renderer/stages/terrain/render_entity.h" +#include "renderer/stages/terrain/render_stage.h" +#include "renderer/stages/world/render_entity.h" +#include "renderer/stages/world/render_stage.h" #include "renderer/uniform_buffer.h" #include "time/clock.h" diff --git a/libopenage/renderer/demo/stresstest_0.cpp b/libopenage/renderer/demo/stresstest_0.cpp index ee9500178f..7167150ac9 100644 --- a/libopenage/renderer/demo/stresstest_0.cpp +++ b/libopenage/renderer/demo/stresstest_0.cpp @@ -12,12 +12,12 @@ #include "renderer/resources/assets/asset_manager.h" #include "renderer/resources/shader_source.h" #include "renderer/stages/camera/manager.h" -#include "renderer/stages/screen/screen_renderer.h" -#include "renderer/stages/skybox/skybox_renderer.h" -#include "renderer/stages/terrain/terrain_render_entity.h" -#include "renderer/stages/terrain/terrain_renderer.h" -#include "renderer/stages/world/world_render_entity.h" -#include "renderer/stages/world/world_renderer.h" +#include "renderer/stages/screen/render_stage.h" +#include "renderer/stages/skybox/render_stage.h" +#include "renderer/stages/terrain/render_entity.h" +#include "renderer/stages/terrain/render_stage.h" +#include "renderer/stages/world/render_entity.h" +#include "renderer/stages/world/render_stage.h" #include "renderer/uniform_buffer.h" #include "time/clock.h" #include "util/fps.h" diff --git a/libopenage/renderer/render_factory.cpp b/libopenage/renderer/render_factory.cpp index e9ac9ca4e1..6d5e276479 100644 --- a/libopenage/renderer/render_factory.cpp +++ b/libopenage/renderer/render_factory.cpp @@ -3,10 +3,10 @@ #include "render_factory.h" #include "coord/phys.h" -#include "renderer/stages/terrain/terrain_render_entity.h" -#include "renderer/stages/terrain/terrain_renderer.h" -#include "renderer/stages/world/world_render_entity.h" -#include "renderer/stages/world/world_renderer.h" +#include "renderer/stages/terrain/render_entity.h" +#include "renderer/stages/terrain/render_stage.h" +#include "renderer/stages/world/render_entity.h" +#include "renderer/stages/world/render_stage.h" namespace openage::renderer { RenderFactory::RenderFactory(const std::shared_ptr terrain_renderer, diff --git a/libopenage/renderer/stages/hud/CMakeLists.txt b/libopenage/renderer/stages/hud/CMakeLists.txt index ce084b5a22..811fd929ed 100644 --- a/libopenage/renderer/stages/hud/CMakeLists.txt +++ b/libopenage/renderer/stages/hud/CMakeLists.txt @@ -1,5 +1,5 @@ add_sources(libopenage - hud_object.cpp - hud_render_entity.cpp - hud_renderer.cpp + object.cpp + render_entity.cpp + render_stage.cpp ) diff --git a/libopenage/renderer/stages/hud/hud_object.cpp b/libopenage/renderer/stages/hud/object.cpp similarity index 97% rename from libopenage/renderer/stages/hud/hud_object.cpp rename to libopenage/renderer/stages/hud/object.cpp index 523fe66442..2b53bd1b3b 100644 --- a/libopenage/renderer/stages/hud/hud_object.cpp +++ b/libopenage/renderer/stages/hud/object.cpp @@ -1,9 +1,9 @@ // Copyright 2023-2023 the openage authors. See copying.md for legal info. -#include "hud_object.h" +#include "object.h" #include "renderer/geometry.h" -#include "renderer/stages/hud/hud_render_entity.h" +#include "renderer/stages/hud/render_entity.h" namespace openage::renderer::hud { diff --git a/libopenage/renderer/stages/hud/hud_object.h b/libopenage/renderer/stages/hud/object.h similarity index 100% rename from libopenage/renderer/stages/hud/hud_object.h rename to libopenage/renderer/stages/hud/object.h diff --git a/libopenage/renderer/stages/hud/hud_render_entity.cpp b/libopenage/renderer/stages/hud/render_entity.cpp similarity index 97% rename from libopenage/renderer/stages/hud/hud_render_entity.cpp rename to libopenage/renderer/stages/hud/render_entity.cpp index b57e7f911e..f8cf19e692 100644 --- a/libopenage/renderer/stages/hud/hud_render_entity.cpp +++ b/libopenage/renderer/stages/hud/render_entity.cpp @@ -1,6 +1,6 @@ // Copyright 2023-2023 the openage authors. See copying.md for legal info. -#include "hud_render_entity.h" +#include "render_entity.h" #include diff --git a/libopenage/renderer/stages/hud/hud_render_entity.h b/libopenage/renderer/stages/hud/render_entity.h similarity index 100% rename from libopenage/renderer/stages/hud/hud_render_entity.h rename to libopenage/renderer/stages/hud/render_entity.h diff --git a/libopenage/renderer/stages/hud/hud_renderer.cpp b/libopenage/renderer/stages/hud/render_stage.cpp similarity index 98% rename from libopenage/renderer/stages/hud/hud_renderer.cpp rename to libopenage/renderer/stages/hud/render_stage.cpp index 590885681f..ebf6dcfd93 100644 --- a/libopenage/renderer/stages/hud/hud_renderer.cpp +++ b/libopenage/renderer/stages/hud/render_stage.cpp @@ -1,6 +1,6 @@ // Copyright 2023-2023 the openage authors. See copying.md for legal info. -#include "hud_renderer.h" +#include "render_stage.h" #include "renderer/camera/camera.h" #include "renderer/opengl/context.h" @@ -8,7 +8,7 @@ #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" #include "renderer/shader_program.h" -#include "renderer/stages/hud/hud_object.h" +#include "renderer/stages/hud/object.h" #include "renderer/texture.h" #include "renderer/window.h" #include "time/clock.h" diff --git a/libopenage/renderer/stages/hud/hud_renderer.h b/libopenage/renderer/stages/hud/render_stage.h similarity index 100% rename from libopenage/renderer/stages/hud/hud_renderer.h rename to libopenage/renderer/stages/hud/render_stage.h diff --git a/libopenage/renderer/stages/screen/CMakeLists.txt b/libopenage/renderer/stages/screen/CMakeLists.txt index 59529a556b..589edbe681 100644 --- a/libopenage/renderer/stages/screen/CMakeLists.txt +++ b/libopenage/renderer/stages/screen/CMakeLists.txt @@ -1,4 +1,4 @@ add_sources(libopenage - screen_renderer.cpp + render_stage.cpp screenshot.cpp ) diff --git a/libopenage/renderer/stages/screen/screen_renderer.cpp b/libopenage/renderer/stages/screen/render_stage.cpp similarity index 99% rename from libopenage/renderer/stages/screen/screen_renderer.cpp rename to libopenage/renderer/stages/screen/render_stage.cpp index 71be26b451..786c547f62 100644 --- a/libopenage/renderer/stages/screen/screen_renderer.cpp +++ b/libopenage/renderer/stages/screen/render_stage.cpp @@ -1,6 +1,6 @@ // Copyright 2022-2023 the openage authors. See copying.md for legal info. -#include "screen_renderer.h" +#include "render_stage.h" #include "renderer/opengl/context.h" #include "renderer/renderer.h" diff --git a/libopenage/renderer/stages/screen/screen_renderer.h b/libopenage/renderer/stages/screen/render_stage.h similarity index 100% rename from libopenage/renderer/stages/screen/screen_renderer.h rename to libopenage/renderer/stages/screen/render_stage.h diff --git a/libopenage/renderer/stages/screen/screenshot.cpp b/libopenage/renderer/stages/screen/screenshot.cpp index 03c1df7902..e910fb34ec 100644 --- a/libopenage/renderer/stages/screen/screenshot.cpp +++ b/libopenage/renderer/stages/screen/screenshot.cpp @@ -15,7 +15,7 @@ #include "log/log.h" #include "renderer/renderer.h" #include "renderer/resources/texture_data.h" -#include "renderer/stages/screen/screen_renderer.h" +#include "renderer/stages/screen/render_stage.h" #include "util/strings.h" diff --git a/libopenage/renderer/stages/skybox/CMakeLists.txt b/libopenage/renderer/stages/skybox/CMakeLists.txt index 0bafbc06db..2da05f4873 100644 --- a/libopenage/renderer/stages/skybox/CMakeLists.txt +++ b/libopenage/renderer/stages/skybox/CMakeLists.txt @@ -1,3 +1,3 @@ add_sources(libopenage - skybox_renderer.cpp + render_stage.cpp ) diff --git a/libopenage/renderer/stages/skybox/skybox_renderer.cpp b/libopenage/renderer/stages/skybox/render_stage.cpp similarity index 90% rename from libopenage/renderer/stages/skybox/skybox_renderer.cpp rename to libopenage/renderer/stages/skybox/render_stage.cpp index 0107d68f17..c8b2fe76d2 100644 --- a/libopenage/renderer/stages/skybox/skybox_renderer.cpp +++ b/libopenage/renderer/stages/skybox/render_stage.cpp @@ -1,6 +1,6 @@ // Copyright 2022-2023 the openage authors. See copying.md for legal info. -#include "skybox_renderer.h" +#include "render_stage.h" #include "renderer/opengl/context.h" #include "renderer/renderer.h" @@ -15,8 +15,8 @@ namespace openage::renderer::skybox { SkyboxRenderStage::SkyboxRenderStage(const std::shared_ptr &window, - const std::shared_ptr &renderer, - const util::Path &shaderdir) : + const std::shared_ptr &renderer, + const util::Path &shaderdir) : renderer{renderer}, bg_color{0.0, 0.0, 0.0, 1.0} // black { @@ -72,8 +72,8 @@ void SkyboxRenderStage::resize(size_t width, size_t height) { } void SkyboxRenderStage::initialize_render_pass(size_t width, - size_t height, - const util::Path &shaderdir) { + size_t height, + const util::Path &shaderdir) { auto vert_shader_file = (shaderdir / "skybox.vert.glsl").open(); auto vert_shader_src = renderer::resources::ShaderSource( resources::shader_lang_t::glsl, diff --git a/libopenage/renderer/stages/skybox/skybox_renderer.h b/libopenage/renderer/stages/skybox/render_stage.h similarity index 100% rename from libopenage/renderer/stages/skybox/skybox_renderer.h rename to libopenage/renderer/stages/skybox/render_stage.h diff --git a/libopenage/renderer/stages/terrain/CMakeLists.txt b/libopenage/renderer/stages/terrain/CMakeLists.txt index f3d10e1354..7a55cd28ed 100644 --- a/libopenage/renderer/stages/terrain/CMakeLists.txt +++ b/libopenage/renderer/stages/terrain/CMakeLists.txt @@ -1,7 +1,7 @@ add_sources(libopenage - terrain_chunk.cpp - terrain_mesh.cpp - terrain_model.cpp - terrain_render_entity.cpp - terrain_renderer.cpp + chunk.cpp + mesh.cpp + model.cpp + render_entity.cpp + render_stage.cpp ) diff --git a/libopenage/renderer/stages/terrain/terrain_chunk.cpp b/libopenage/renderer/stages/terrain/chunk.cpp similarity index 96% rename from libopenage/renderer/stages/terrain/terrain_chunk.cpp rename to libopenage/renderer/stages/terrain/chunk.cpp index dcea61585e..2bb836a673 100644 --- a/libopenage/renderer/stages/terrain/terrain_chunk.cpp +++ b/libopenage/renderer/stages/terrain/chunk.cpp @@ -1,11 +1,11 @@ // Copyright 2023-2023 the openage authors. See copying.md for legal info. -#include "terrain_chunk.h" +#include "chunk.h" #include "renderer/resources/assets/asset_manager.h" #include "renderer/resources/mesh_data.h" -#include "renderer/stages/terrain/terrain_mesh.h" -#include "renderer/stages/terrain/terrain_render_entity.h" +#include "renderer/stages/terrain/mesh.h" +#include "renderer/stages/terrain/render_entity.h" namespace openage::renderer::terrain { diff --git a/libopenage/renderer/stages/terrain/terrain_chunk.h b/libopenage/renderer/stages/terrain/chunk.h similarity index 100% rename from libopenage/renderer/stages/terrain/terrain_chunk.h rename to libopenage/renderer/stages/terrain/chunk.h diff --git a/libopenage/renderer/stages/terrain/terrain_mesh.cpp b/libopenage/renderer/stages/terrain/mesh.cpp similarity index 99% rename from libopenage/renderer/stages/terrain/terrain_mesh.cpp rename to libopenage/renderer/stages/terrain/mesh.cpp index b02f867050..ca8b19937c 100644 --- a/libopenage/renderer/stages/terrain/terrain_mesh.cpp +++ b/libopenage/renderer/stages/terrain/mesh.cpp @@ -1,6 +1,6 @@ // Copyright 2022-2023 the openage authors. See copying.md for legal info. -#include "terrain_mesh.h" +#include "mesh.h" #include #include diff --git a/libopenage/renderer/stages/terrain/terrain_mesh.h b/libopenage/renderer/stages/terrain/mesh.h similarity index 100% rename from libopenage/renderer/stages/terrain/terrain_mesh.h rename to libopenage/renderer/stages/terrain/mesh.h diff --git a/libopenage/renderer/stages/terrain/terrain_model.cpp b/libopenage/renderer/stages/terrain/model.cpp similarity index 91% rename from libopenage/renderer/stages/terrain/terrain_model.cpp rename to libopenage/renderer/stages/terrain/model.cpp index 329968f858..8d8cc4c727 100644 --- a/libopenage/renderer/stages/terrain/terrain_model.cpp +++ b/libopenage/renderer/stages/terrain/model.cpp @@ -1,6 +1,6 @@ // Copyright 2022-2023 the openage authors. See copying.md for legal info. -#include "terrain_model.h" +#include "model.h" #include #include @@ -12,8 +12,8 @@ #include "coord/scene.h" #include "renderer/resources/assets/asset_manager.h" #include "renderer/resources/mesh_data.h" -#include "renderer/stages/terrain/terrain_chunk.h" -#include "renderer/stages/terrain/terrain_render_entity.h" +#include "renderer/stages/terrain/chunk.h" +#include "renderer/stages/terrain/render_entity.h" #include "util/fixed_point.h" #include "util/vector.h" diff --git a/libopenage/renderer/stages/terrain/terrain_model.h b/libopenage/renderer/stages/terrain/model.h similarity index 100% rename from libopenage/renderer/stages/terrain/terrain_model.h rename to libopenage/renderer/stages/terrain/model.h diff --git a/libopenage/renderer/stages/terrain/terrain_render_entity.cpp b/libopenage/renderer/stages/terrain/render_entity.cpp similarity index 99% rename from libopenage/renderer/stages/terrain/terrain_render_entity.cpp rename to libopenage/renderer/stages/terrain/render_entity.cpp index 2d83b018f7..fdfd7e486d 100644 --- a/libopenage/renderer/stages/terrain/terrain_render_entity.cpp +++ b/libopenage/renderer/stages/terrain/render_entity.cpp @@ -1,6 +1,6 @@ // Copyright 2022-2023 the openage authors. See copying.md for legal info. -#include "terrain_render_entity.h" +#include "render_entity.h" #include #include diff --git a/libopenage/renderer/stages/terrain/terrain_render_entity.h b/libopenage/renderer/stages/terrain/render_entity.h similarity index 100% rename from libopenage/renderer/stages/terrain/terrain_render_entity.h rename to libopenage/renderer/stages/terrain/render_entity.h diff --git a/libopenage/renderer/stages/terrain/terrain_renderer.cpp b/libopenage/renderer/stages/terrain/render_stage.cpp similarity index 81% rename from libopenage/renderer/stages/terrain/terrain_renderer.cpp rename to libopenage/renderer/stages/terrain/render_stage.cpp index 78f2711690..518ebd7c9b 100644 --- a/libopenage/renderer/stages/terrain/terrain_renderer.cpp +++ b/libopenage/renderer/stages/terrain/render_stage.cpp @@ -1,6 +1,6 @@ // Copyright 2022-2023 the openage authors. See copying.md for legal info. -#include "terrain_renderer.h" +#include "render_stage.h" #include "renderer/camera/camera.h" #include "renderer/opengl/context.h" @@ -8,9 +8,9 @@ #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" #include "renderer/shader_program.h" -#include "renderer/stages/terrain/terrain_chunk.h" -#include "renderer/stages/terrain/terrain_mesh.h" -#include "renderer/stages/terrain/terrain_model.h" +#include "renderer/stages/terrain/chunk.h" +#include "renderer/stages/terrain/mesh.h" +#include "renderer/stages/terrain/model.h" #include "renderer/window.h" #include "time/clock.h" @@ -18,11 +18,11 @@ namespace openage::renderer::terrain { TerrainRenderStage::TerrainRenderStage(const std::shared_ptr &window, - const std::shared_ptr &renderer, - const std::shared_ptr &camera, - const util::Path &shaderdir, - const std::shared_ptr &asset_manager, - const std::shared_ptr &clock) : + const std::shared_ptr &renderer, + const std::shared_ptr &camera, + const util::Path &shaderdir, + const std::shared_ptr &asset_manager, + const std::shared_ptr &clock) : renderer{renderer}, camera{camera}, render_entity{nullptr}, @@ -47,8 +47,8 @@ std::shared_ptr TerrainRenderStage::get_render_pass() { } void TerrainRenderStage::add_render_entity(const std::shared_ptr entity, - const util::Vector2s chunk_size, - const coord::scene2_delta chunk_offset) { + const util::Vector2s chunk_size, + const coord::scene2_delta chunk_offset) { std::unique_lock lock{this->mutex}; this->render_entity = entity; @@ -94,8 +94,8 @@ void TerrainRenderStage::resize(size_t width, size_t height) { } void TerrainRenderStage::initialize_render_pass(size_t width, - size_t height, - const util::Path &shaderdir) { + size_t height, + const util::Path &shaderdir) { auto vert_shader_file = (shaderdir / "terrain.vert.glsl").open(); auto vert_shader_src = renderer::resources::ShaderSource( resources::shader_lang_t::glsl, diff --git a/libopenage/renderer/stages/terrain/terrain_renderer.h b/libopenage/renderer/stages/terrain/render_stage.h similarity index 100% rename from libopenage/renderer/stages/terrain/terrain_renderer.h rename to libopenage/renderer/stages/terrain/render_stage.h diff --git a/libopenage/renderer/stages/world/CMakeLists.txt b/libopenage/renderer/stages/world/CMakeLists.txt index 1bf4758b19..811fd929ed 100644 --- a/libopenage/renderer/stages/world/CMakeLists.txt +++ b/libopenage/renderer/stages/world/CMakeLists.txt @@ -1,5 +1,5 @@ add_sources(libopenage - world_object.cpp - world_render_entity.cpp - world_renderer.cpp + object.cpp + render_entity.cpp + render_stage.cpp ) diff --git a/libopenage/renderer/stages/world/world_object.cpp b/libopenage/renderer/stages/world/object.cpp similarity index 98% rename from libopenage/renderer/stages/world/world_object.cpp rename to libopenage/renderer/stages/world/object.cpp index 79960d236c..23915d420f 100644 --- a/libopenage/renderer/stages/world/world_object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -1,6 +1,6 @@ // Copyright 2022-2023 the openage authors. See copying.md for legal info. -#include "world_object.h" +#include "object.h" #include #include @@ -22,7 +22,7 @@ #include "renderer/resources/mesh_data.h" #include "renderer/resources/texture_info.h" #include "renderer/resources/texture_subinfo.h" -#include "renderer/stages/world/world_render_entity.h" +#include "renderer/stages/world/render_entity.h" #include "renderer/uniform_input.h" #include "util/fixed_point.h" #include "util/vector.h" diff --git a/libopenage/renderer/stages/world/world_object.h b/libopenage/renderer/stages/world/object.h similarity index 100% rename from libopenage/renderer/stages/world/world_object.h rename to libopenage/renderer/stages/world/object.h diff --git a/libopenage/renderer/stages/world/world_render_entity.cpp b/libopenage/renderer/stages/world/render_entity.cpp similarity index 98% rename from libopenage/renderer/stages/world/world_render_entity.cpp rename to libopenage/renderer/stages/world/render_entity.cpp index 8cba90ca40..6909a6554a 100644 --- a/libopenage/renderer/stages/world/world_render_entity.cpp +++ b/libopenage/renderer/stages/world/render_entity.cpp @@ -1,6 +1,6 @@ // Copyright 2022-2023 the openage authors. See copying.md for legal info. -#include "world_render_entity.h" +#include "render_entity.h" #include #include diff --git a/libopenage/renderer/stages/world/world_render_entity.h b/libopenage/renderer/stages/world/render_entity.h similarity index 100% rename from libopenage/renderer/stages/world/world_render_entity.h rename to libopenage/renderer/stages/world/render_entity.h diff --git a/libopenage/renderer/stages/world/world_renderer.cpp b/libopenage/renderer/stages/world/render_stage.cpp similarity index 88% rename from libopenage/renderer/stages/world/world_renderer.cpp rename to libopenage/renderer/stages/world/render_stage.cpp index 65a9c77dc4..85e5da7048 100644 --- a/libopenage/renderer/stages/world/world_renderer.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -1,6 +1,6 @@ // Copyright 2022-2023 the openage authors. See copying.md for legal info. -#include "world_renderer.h" +#include "render_stage.h" #include "renderer/camera/camera.h" #include "renderer/opengl/context.h" @@ -8,7 +8,7 @@ #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" #include "renderer/shader_program.h" -#include "renderer/stages/world/world_object.h" +#include "renderer/stages/world/object.h" #include "renderer/texture.h" #include "renderer/window.h" #include "time/clock.h" @@ -17,11 +17,11 @@ namespace openage::renderer::world { WorldRenderStage::WorldRenderStage(const std::shared_ptr &window, - const std::shared_ptr &renderer, - const std::shared_ptr &camera, - const util::Path &shaderdir, - const std::shared_ptr &asset_manager, - const std::shared_ptr clock) : + const std::shared_ptr &renderer, + const std::shared_ptr &camera, + const util::Path &shaderdir, + const std::shared_ptr &asset_manager, + const std::shared_ptr clock) : renderer{renderer}, camera{camera}, asset_manager{asset_manager}, @@ -102,8 +102,8 @@ void WorldRenderStage::resize(size_t width, size_t height) { } void WorldRenderStage::initialize_render_pass(size_t width, - size_t height, - const util::Path &shaderdir) { + size_t height, + const util::Path &shaderdir) { auto vert_shader_file = (shaderdir / "world2d.vert.glsl").open(); auto vert_shader_src = renderer::resources::ShaderSource( resources::shader_lang_t::glsl, diff --git a/libopenage/renderer/stages/world/world_renderer.h b/libopenage/renderer/stages/world/render_stage.h similarity index 100% rename from libopenage/renderer/stages/world/world_renderer.h rename to libopenage/renderer/stages/world/render_stage.h From 4ad0fb9b03fc9f97347bdef275c597dc0d27a5bc Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 Nov 2023 21:39:01 +0100 Subject: [PATCH 172/771] input: Reactivate camera actions. --- libopenage/input/input_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/input/input_manager.cpp b/libopenage/input/input_manager.cpp index 8517dfefd6..58bca7fb9e 100644 --- a/libopenage/input/input_manager.cpp +++ b/libopenage/input/input_manager.cpp @@ -227,7 +227,7 @@ void setup_defaults(const std::shared_ptr &ctx) { ctx->bind(ev_down, camera_action); ctx->bind(ev_wheel_up, camera_action); ctx->bind(ev_wheel_down, camera_action); - ctx->bind(event_class::MOUSE_MOVE, {/* camera_action, ASDF */ hud_action}); + ctx->bind(event_class::MOUSE_MOVE, {camera_action, hud_action}); // game input_action game_action{input_action_t::GAME}; From b40817896ccea06c4565185c69207f711e4760c3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 Nov 2023 21:46:10 +0100 Subject: [PATCH 173/771] doc: Add HudRenderer description to renderer docs. --- doc/code/renderer/level2.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/code/renderer/level2.md b/doc/code/renderer/level2.md index f7bf8bbe2a..cec5950bfd 100644 --- a/doc/code/renderer/level2.md +++ b/doc/code/renderer/level2.md @@ -4,21 +4,21 @@ High-level renderer for transforming data from the gamestate to render objects f ## Overview -1. [Level 2](#level-2) - 1. [Overview](#overview) - 2. [Stages](#stages) - 1. [Updating Render Stages from the Gamestate](#updating-render-stages-from-the-gamestate) - 3. [Camera](#camera) +1. [Overview](#overview) +2. [Stages](#stages) + 1. [Updating Render Stages from the Gamestate](#updating-render-stages-from-the-gamestate) +3. [Camera](#camera) ## Stages Every stage has its own subrenderer that manages a `RenderPass` from the level 1 renderer and updates it with `Renderable`s created using update information from the gamestate. Stages also store the vertex and fragment shaders used for drawing the renderable objects. -There are currently 5 stages in the level 2 rendering pipeline: +There are currently 6 stages in the level 2 rendering pipeline: 1. `SkyboxRenderer`: Draws the background behind the terrain (as a single color). 1. `TerrainRenderer`: Draws the terrain. Terrains are handled as textured 3D meshes. 1. `WorldRenderer`: Draws animations and sprites for units/buildings and other 2D ingame objects. +1. `HudRenderer`: Draws "Head-Up Display" elements like health bars, selection boxes, and others. 1. `GuiRenderer`: Draws the GUI overlay. The drawing part in this stage is actually done by Qt, while the level 1 renderer only provides the framebuffer. 1. `ScreenRenderer`: Alpha composites the framebuffer data of previous stages and draws them onto the screen (i.e. it overlays the outputs from the other stages). From 136471d972dba0634388857e43bec7031af1dac2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 Nov 2023 22:43:17 +0100 Subject: [PATCH 174/771] renderer: Make sure that alpha is 1 for blend results. --- libopenage/renderer/opengl/renderer.cpp | 12 ++++++++++-- libopenage/renderer/stages/hud/render_stage.cpp | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/libopenage/renderer/opengl/renderer.cpp b/libopenage/renderer/opengl/renderer.cpp index 91cc5b35ee..a06cc16edf 100644 --- a/libopenage/renderer/opengl/renderer.cpp +++ b/libopenage/renderer/opengl/renderer.cpp @@ -27,8 +27,17 @@ GlRenderer::GlRenderer(const std::shared_ptr &ctx, display{std::make_shared(ctx, viewport_size[0], viewport_size[1])} { + // color used to clear the color buffers + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + // global GL alpha blending settings - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + // glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBlendFuncSeparate( + GL_SRC_ALPHA, // source (overlaying) RGB factor + GL_ONE_MINUS_SRC_ALPHA, // destination (underlying) RGB factor + GL_ONE, // source (overlaying) alpha factor + GL_ONE_MINUS_SRC_ALPHA // destination (underlying) alpha factor + ); // global GL depth testing settings glDepthFunc(GL_LEQUAL); @@ -157,7 +166,6 @@ void GlRenderer::render(const std::shared_ptr &pass) { auto gl_target = std::dynamic_pointer_cast(pass->get_target()); gl_target->bind_write(); - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // TODO: Option for face culling diff --git a/libopenage/renderer/stages/hud/render_stage.cpp b/libopenage/renderer/stages/hud/render_stage.cpp index ebf6dcfd93..b7ecaef31e 100644 --- a/libopenage/renderer/stages/hud/render_stage.cpp +++ b/libopenage/renderer/stages/hud/render_stage.cpp @@ -69,7 +69,7 @@ void HudRenderStage::update() { auto geometry = this->renderer->add_mesh_geometry(resources::MeshData::make_quad()); auto transform_unifs = this->drag_select_shader->new_uniform_input( "in_col", - Eigen::Vector4f{0.0f, 0.0f, 0.0f, 0.5f}); + Eigen::Vector4f{0.0f, 0.0f, 0.0f, 0.2f}); Renderable display_obj{ transform_unifs, From 4ff5e6d0b63bd6926ad57751a52e07b213d5ae53 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Dec 2023 19:45:28 +0100 Subject: [PATCH 175/771] gamestate: Suppress less useful drag select log messages. --- libopenage/gamestate/event/drag_select.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libopenage/gamestate/event/drag_select.cpp b/libopenage/gamestate/event/drag_select.cpp index 373bfbfe2d..efb43b0ab1 100644 --- a/libopenage/gamestate/event/drag_select.cpp +++ b/libopenage/gamestate/event/drag_select.cpp @@ -45,11 +45,11 @@ void DragSelectHandler::invoke(openage::event::EventLoop & /* loop */, float left = std::min(drag_start.x(), drag_end.x()); float right = std::max(drag_start.x(), drag_end.x()); - log::log(DBG << "Drag select rectangle (NDC):"); - log::log(DBG << "\tTop: " << top); - log::log(DBG << "\tBottom: " << bottom); - log::log(DBG << "\tLeft: " << left); - log::log(DBG << "\tRight: " << right); + log::log(SPAM << "Drag select rectangle (NDC):"); + log::log(SPAM << "\tTop: " << top); + log::log(SPAM << "\tBottom: " << bottom); + log::log(SPAM << "\tLeft: " << left); + log::log(SPAM << "\tRight: " << right); std::vector selected; for (auto &entity : gstate->get_game_entities()) { From a51d2c3ef91028b1ff1ce3691a8a3a41e0c73da1 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Dec 2023 19:59:17 +0100 Subject: [PATCH 176/771] input: Fix missing std::move. --- libopenage/input/input_context.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/input/input_context.cpp b/libopenage/input/input_context.cpp index bbbdfc99d3..bdedf60f37 100644 --- a/libopenage/input/input_context.cpp +++ b/libopenage/input/input_context.cpp @@ -50,11 +50,11 @@ void InputContext::bind(const event_class &cl, const input_action act) { } void InputContext::bind(const Event &ev, const std::vector &&acts) { - this->by_event.emplace(std::make_pair(ev, acts)); + this->by_event.emplace(std::make_pair(ev, std::move(acts))); } void InputContext::bind(const event_class &cl, const std::vector &&acts) { - this->by_class.emplace(std::make_pair(cl, acts)); + this->by_class.emplace(std::make_pair(cl, std::move(acts))); } bool InputContext::is_bound(const Event &ev) const { From c937e30d3955244452506489925e4969ac8ce59a Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 17 Jan 2024 01:46:56 +0100 Subject: [PATCH 177/771] convert: Log processor stages execution times. --- openage/convert/tool/driver.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openage/convert/tool/driver.py b/openage/convert/tool/driver.py index d7d664d16e..ba974f42e8 100644 --- a/openage/convert/tool/driver.py +++ b/openage/convert/tool/driver.py @@ -1,4 +1,4 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-return-statements @@ -65,7 +65,9 @@ def convert_metadata(args: Namespace) -> None: gamedata_path = args.targetdir.joinpath('gamedata') if gamedata_path.exists(): gamedata_path.removerecursive() + read_start = timeit.default_timer() + # Read .dat debug_gamedata_format(args.debugdir, args.debug_info, args.game_version) gamespec = get_gamespec(args.srcdir, args.game_version, not args.flag("no_pickle_cache")) @@ -86,6 +88,7 @@ def convert_metadata(args: Namespace) -> None: debug_registered_graphics(args.debugdir, args.debug_info, existing_graphics) read_end = timeit.default_timer() + info("Finished metadata read (%.2f seconds)", read_end - read_start) conversion_start = timeit.default_timer() # Convert @@ -95,6 +98,7 @@ def convert_metadata(args: Namespace) -> None: existing_graphics) conversion_end = timeit.default_timer() + info("Finished data conversion (%.2f seconds)", conversion_end - conversion_start) export_start = timeit.default_timer() for modpack in modpacks: @@ -102,6 +106,7 @@ def convert_metadata(args: Namespace) -> None: debug_modpack(args.debugdir, args.debug_info, modpack) export_end = timeit.default_timer() + info("Finished modpack export (%.2f seconds)", export_end - export_start) stages_time = { "read": read_end - read_start, From 11b5d71975edf25775f2c59d4cc3e8af2f328756 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 17 Jan 2024 01:47:30 +0100 Subject: [PATCH 178/771] convert: Remove parent relationship for 'Texture'. --- .../convert/entity_object/export/texture.py | 34 ++++--------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/openage/convert/entity_object/export/texture.py b/openage/convert/entity_object/export/texture.py index 5039abfeb3..885f5dbfaa 100644 --- a/openage/convert/entity_object/export/texture.py +++ b/openage/convert/entity_object/export/texture.py @@ -1,4 +1,4 @@ -# Copyright 2014-2023 the openage authors. See copying.md for legal info. +# Copyright 2014-2024 the openage authors. See copying.md for legal info. """ Routines for texture generation etc """ @@ -14,7 +14,6 @@ from ....log import spam from ...value_object.read.media.blendomatic import BlendingMode from ...value_object.read.media.hardcoded.terrain_tile_size import TILE_HALFSIZE -from ...value_object.read.genie_structure import GenieStructure if typing.TYPE_CHECKING: from openage.convert.value_object.read.media.colortable import ColorTable @@ -65,17 +64,13 @@ def get_data(self) -> numpy.ndarray: return self.data -class Texture(GenieStructure): - image_format = "png" +class Texture: + """ + one sprite, as part of a texture atlas. - name_struct = "subtexture" - name_struct_file = "texture" - struct_description = ( - "one sprite, as part of a texture atlas.\n" - "\n" - "this struct stores information about positions and sizes\n" - "of sprites included in the 'big texture'." - ) + stores information about positions and sizes + of sprites included in the 'big texture'. + """ def __init__( self, @@ -180,18 +175,3 @@ def get_cache_params(self) -> tuple[tuple, tuple]: - PNG compression parameters (compression level + deflate params) """ return self.best_packer_hints, self.best_compr - - @classmethod - def get_data_format_members(cls, game_version) -> tuple: - """ - Return the members in this struct. - """ - data_format = ( - (True, "x", None, "int32_t"), - (True, "y", None, "int32_t"), - (True, "w", None, "int32_t"), - (True, "h", None, "int32_t"), - (True, "cx", None, "int32_t"), - (True, "cy", None, "int32_t"), - ) - return data_format From fecdddccfd623257a96a8d28bbe385caaa1e0189 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 18 Jan 2024 03:44:01 +0100 Subject: [PATCH 179/771] convert: Write to PNG with thread pool. --- .../processor/export/media_exporter.py | 109 ++++++++++++++++-- 1 file changed, 99 insertions(+), 10 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 7005e262c9..36475e9e80 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -1,4 +1,4 @@ -# Copyright 2021-2023 the openage authors. See copying.md for legal info. +# Copyright 2021-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-arguments,too-many-locals """ @@ -9,7 +9,7 @@ import logging import os - +from multiprocessing import Pool from openage.convert.entity_object.export.texture import Texture from openage.convert.service import debug_info @@ -59,6 +59,8 @@ def export( if args.game_version.edition.media_cache: cache_info = load_media_cache(args.game_version.edition.media_cache) + textures = [] + for media_type in export_requests.keys(): cur_export_requests = export_requests[media_type] @@ -79,6 +81,28 @@ def export( export_func = MediaExporter._export_graphics info("-- Exporting graphics files...") + for count, request in enumerate(cur_export_requests, start = 1): + texture = MediaExporter._export_graphics( + request, + sourcedir, + exportdir, + args.palettes, + args.compression_level, + cache_info + ) + textures.append(( + texture, + # exportdir[request.targetdir], + exportdir[request.targetdir][request.target_filename].resolve_native_path(), + args.compression_level, + cache_info + )) + + with Pool() as pool: + pool.starmap(save_png, textures) + + continue + elif media_type is MediaType.SOUNDS: kwargs["loglevel"] = args.debug_info kwargs["debugdir"] = args.debugdir @@ -172,7 +196,7 @@ def _export_graphics( palettes: dict[int, ColorTable], compression_level: int, cache_info: dict = None - ) -> None: + ) -> Texture: """ Convert and export a graphics file. @@ -245,13 +269,13 @@ def _export_graphics( texture = Texture(image, palettes) merge_frames(texture, cache=packer_cache) - MediaExporter.save_png( - texture, - exportdir[export_request.targetdir], - export_request.target_filename, - compression_level=compression_level, - cache=compr_cache - ) + # MediaExporter.save_png( + # texture, + # exportdir[export_request.targetdir], + # export_request.target_filename, + # compression_level=compression_level, + # cache=compr_cache + # ) metadata = {export_request.target_filename: texture.get_metadata()} export_request.set_changed() export_request.notify_observers(metadata) @@ -263,6 +287,8 @@ def _export_graphics( exportdir[export_request.targetdir, export_request.target_filename] ) + return texture + @staticmethod def _export_interface( export_request: MediaExportRequest, @@ -582,3 +608,66 @@ def log_fileinfo( f"{(target_size / source_size * 100) - 100:+.1f}%)") dbg(log) + + +def save_png( + texture: Texture, + # targetdir: Path, + filename: str, + compression_level: int = 1, + cache: dict = None, + dry_run: bool = False +) -> None: + """ + Store the image data into the target directory path, + with given filename="dir/out.png". + + :param texture: Texture with an image atlas. + :param targetdir: Directory where the image file is created. + :param filename: Name of the resulting image file. + :param compression_level: PNG compression level used for the resulting image file. + :param dry_run: If True, create the PNG but don't save it as a file. + :type texture: Texture + :type targetdir: Directory + :type filename: str + :type compression_level: int + :type dry_run: bool + """ + from ...service.export.png import png_create + + compression_levels = { + 0: png_create.CompressionMethod.COMPR_NONE, + 1: png_create.CompressionMethod.COMPR_DEFAULT, + 2: png_create.CompressionMethod.COMPR_OPTI, + 3: png_create.CompressionMethod.COMPR_GREEDY, + 4: png_create.CompressionMethod.COMPR_AGGRESSIVE, + } + + if not dry_run: + _, ext = os.path.splitext(filename) + + # only allow png + if ext != b".png": + raise ValueError("Filename invalid, a texture must be saved" + f" as '*.png', not '*.{ext}'") + + compression_method = compression_levels.get( + compression_level, + png_create.CompressionMethod.COMPR_DEFAULT + ) + png_data, compr_params = png_create.save( + texture.image_data.data, + compression_method, + cache + ) + + if not dry_run: + with open(filename, "wb") as imagefile: + imagefile.write(png_data) + + # if not dry_run: + # with targetdir[filename].open("wb") as imagefile: + # imagefile.write(png_data) + + if compr_params: + texture.best_compr = (compression_level, *compr_params) From e1762eccca94800f73eac9a9131361bce5db950f Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 19 Jan 2024 23:13:59 +0100 Subject: [PATCH 180/771] convert: Graphics conversion inside thread pool. --- .../processor/export/media_exporter.py | 179 +++++++++++++----- 1 file changed, 134 insertions(+), 45 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 36475e9e80..7fc296ed33 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -9,7 +9,7 @@ import logging import os -from multiprocessing import Pool +import multiprocessing from openage.convert.entity_object.export.texture import Texture from openage.convert.service import debug_info @@ -59,8 +59,6 @@ def export( if args.game_version.edition.media_cache: cache_info = load_media_cache(args.game_version.edition.media_cache) - textures = [] - for media_type in export_requests.keys(): cur_export_requests = export_requests[media_type] @@ -81,25 +79,46 @@ def export( export_func = MediaExporter._export_graphics info("-- Exporting graphics files...") - for count, request in enumerate(cur_export_requests, start = 1): - texture = MediaExporter._export_graphics( - request, - sourcedir, - exportdir, - args.palettes, - args.compression_level, - cache_info - ) - textures.append(( - texture, - # exportdir[request.targetdir], - exportdir[request.targetdir][request.target_filename].resolve_native_path(), - args.compression_level, - cache_info - )) - - with Pool() as pool: - pool.starmap(save_png, textures) + with multiprocessing.Manager() as manager: + outqueue = manager.Queue() + with multiprocessing.Pool() as pool: + for idx, request in enumerate(cur_export_requests): + source_file = sourcedir[request.get_type().value][request.source_filename] + if not source_file.exists(): + if source_file.suffix.lower() in (".smx", ".sld"): + # Rename extension to SMP and try again + other_filename = request.source_filename[:-3] + "smp" + source_file = sourcedir[ + request.get_type().value, + other_filename + ] + request.set_source_filename(other_filename) + + target_path = exportdir[request.targetdir][request.target_filename].resolve_native_path() + func_args = ( + idx, + source_file.open("rb").read(), + outqueue, + request.source_filename, + target_path, + kwargs["palettes"], + kwargs["compression_level"], + cache_info + ) + pool.apply_async( + _export_texture, + func_args + ) + + pool.close() + pool.join() + + while not outqueue.empty(): + idx, metadata = outqueue.get() + update_data = {cur_export_requests[idx].target_filename: metadata} + cur_export_requests[idx].set_changed() + cur_export_requests[idx].notify_observers(update_data) + cur_export_requests[idx].clear_changed() continue @@ -269,13 +288,13 @@ def _export_graphics( texture = Texture(image, palettes) merge_frames(texture, cache=packer_cache) - # MediaExporter.save_png( - # texture, - # exportdir[export_request.targetdir], - # export_request.target_filename, - # compression_level=compression_level, - # cache=compr_cache - # ) + MediaExporter.save_png( + texture, + exportdir[export_request.targetdir], + export_request.target_filename, + compression_level=compression_level, + cache=compr_cache + ) metadata = {export_request.target_filename: texture.get_metadata()} export_request.set_changed() export_request.notify_observers(metadata) @@ -287,8 +306,6 @@ def _export_graphics( exportdir[export_request.targetdir, export_request.target_filename] ) - return texture - @staticmethod def _export_interface( export_request: MediaExportRequest, @@ -610,26 +627,102 @@ def log_fileinfo( dbg(log) -def save_png( +def _export_texture( + export_request_idx: int, + graphics_data: bytes, + outqueue: multiprocessing.Queue, + source_filename: str, + target_path: str, + palettes: dict[int, ColorTable], + compression_level: int, + cache_info: dict = None +) -> None: + """ + Convert and export a graphics file to a PNG texture. + + :param export_request: Export request for a graphics file. + :param sourcedir: Directory where all media assets are mounted. Source subfolder and + source filename should be stored in the export request. + :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder + and target filename should be stored in the export request. + :param palettes: Palettes used by the game. + :param compression_level: PNG compression level for the resulting image file. + :param cache_info: Media cache information with compression parameters from a previous run. + :type export_request: MediaExportRequest + :type sourcedir: Path + :type exportdir: Path + :type palettes: dict + :type compression_level: int + :type cache_info: tuple + """ + file_ext = source_filename.split('.')[-1].lower() + if file_ext == "slp": + from ...value_object.read.media.slp import SLP + image = SLP(graphics_data) + + elif file_ext == "smp": + from ...value_object.read.media.smp import SMP + image = SMP(graphics_data) + + elif file_ext == "smx": + from ...value_object.read.media.smx import SMX + image = SMX(graphics_data) + + elif file_ext == "sld": + from ...value_object.read.media.sld import SLD + image = SLD(graphics_data) + + else: + raise SyntaxError(f"Source file {source_filename} has an unrecognized extension: " + f"{file_ext}") + + packer_cache = None + compr_cache = None + if cache_info: + cache_params = cache_info.get(source_filename, None) + + if cache_params: + packer_cache = cache_params["packer_settings"] + compression_level = cache_params["compr_settings"][0] + compr_cache = cache_params["compr_settings"][1:] + + from .texture_merge import merge_frames + + texture = Texture(image, palettes) + merge_frames(texture, cache=packer_cache) + _save_png( + texture, + target_path, + compression_level=compression_level, + cache=compr_cache + ) + metadata = (export_request_idx, texture.get_metadata().copy()) + outqueue.put(metadata) + + # if get_loglevel() <= logging.DEBUG: + # MediaExporter.log_fileinfo( + # source_file, + # exportdir[export_request.targetdir, export_request.target_filename] + # ) + + +def _save_png( texture: Texture, - # targetdir: Path, - filename: str, + target_path: str, compression_level: int = 1, cache: dict = None, dry_run: bool = False ) -> None: """ Store the image data into the target directory path, - with given filename="dir/out.png". + with given target_path="dir/out.png". :param texture: Texture with an image atlas. - :param targetdir: Directory where the image file is created. - :param filename: Name of the resulting image file. + :param target_path: Path to the resulting image file. :param compression_level: PNG compression level used for the resulting image file. :param dry_run: If True, create the PNG but don't save it as a file. :type texture: Texture - :type targetdir: Directory - :type filename: str + :type target_path: str :type compression_level: int :type dry_run: bool """ @@ -644,7 +737,7 @@ def save_png( } if not dry_run: - _, ext = os.path.splitext(filename) + _, ext = os.path.splitext(target_path) # only allow png if ext != b".png": @@ -662,12 +755,8 @@ def save_png( ) if not dry_run: - with open(filename, "wb") as imagefile: + with open(target_path, "wb") as imagefile: imagefile.write(png_data) - # if not dry_run: - # with targetdir[filename].open("wb") as imagefile: - # imagefile.write(png_data) - if compr_params: texture.best_compr = (compression_level, *compr_params) From ff59db03d1be4cc58b138524b6b735323d4c384c Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 19 Jan 2024 23:48:52 +0100 Subject: [PATCH 181/771] convert: Switch graphics export function to multi-threading. --- .../processor/export/media_exporter.py | 216 ++++++++---------- 1 file changed, 94 insertions(+), 122 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 7fc296ed33..7b859762a3 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -73,53 +73,15 @@ def export( info("-- Exporting terrain files...") elif media_type is MediaType.GRAPHICS: - kwargs["palettes"] = args.palettes - kwargs["compression_level"] = args.compression_level - kwargs["cache_info"] = cache_info - export_func = MediaExporter._export_graphics info("-- Exporting graphics files...") - - with multiprocessing.Manager() as manager: - outqueue = manager.Queue() - with multiprocessing.Pool() as pool: - for idx, request in enumerate(cur_export_requests): - source_file = sourcedir[request.get_type().value][request.source_filename] - if not source_file.exists(): - if source_file.suffix.lower() in (".smx", ".sld"): - # Rename extension to SMP and try again - other_filename = request.source_filename[:-3] + "smp" - source_file = sourcedir[ - request.get_type().value, - other_filename - ] - request.set_source_filename(other_filename) - - target_path = exportdir[request.targetdir][request.target_filename].resolve_native_path() - func_args = ( - idx, - source_file.open("rb").read(), - outqueue, - request.source_filename, - target_path, - kwargs["palettes"], - kwargs["compression_level"], - cache_info - ) - pool.apply_async( - _export_texture, - func_args - ) - - pool.close() - pool.join() - - while not outqueue.empty(): - idx, metadata = outqueue.get() - update_data = {cur_export_requests[idx].target_filename: metadata} - cur_export_requests[idx].set_changed() - cur_export_requests[idx].notify_observers(update_data) - cur_export_requests[idx].clear_changed() - + MediaExporter._export_graphics( + cur_export_requests, + sourcedir, + exportdir, + args.palettes, + args.compression_level, + cache_info + ) continue elif media_type is MediaType.SOUNDS: @@ -209,17 +171,17 @@ def _export_blend( @staticmethod def _export_graphics( - export_request: MediaExportRequest, + requests: list[MediaExportRequest], sourcedir: Path, exportdir: Path, palettes: dict[int, ColorTable], compression_level: int, cache_info: dict = None - ) -> Texture: + ) -> None: """ - Convert and export a graphics file. + Convert and export graphics file requests (multi-threaded). - :param export_request: Export request for a graphics file. + :param requests: Export requests for graphics files. :param sourcedir: Directory where all media assets are mounted. Source subfolder and source filename should be stored in the export request. :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder @@ -227,84 +189,94 @@ def _export_graphics( :param palettes: Palettes used by the game. :param compression_level: PNG compression level for the resulting image file. :param cache_info: Media cache information with compression parameters from a previous run. - :type export_request: MediaExportRequest + :type requests: list[MediaExportRequest] :type sourcedir: Path :type exportdir: Path :type palettes: dict :type compression_level: int :type cache_info: tuple """ - source_file = sourcedir[ - export_request.get_type().value, - export_request.source_filename - ] - - try: - media_file = source_file.open("rb") - - except FileNotFoundError: - if source_file.suffix.lower() in (".smx", ".sld"): - # Rename extension to SMP and try again - other_filename = export_request.source_filename[:-3] + "smp" - source_file = sourcedir[ - export_request.get_type().value, - other_filename - ] - export_request.set_source_filename(other_filename) - - media_file = source_file.open("rb") - - if source_file.suffix.lower() == ".slp": - from ...value_object.read.media.slp import SLP - image = SLP(media_file.read()) - - elif source_file.suffix.lower() == ".smp": - from ...value_object.read.media.smp import SMP - image = SMP(media_file.read()) - - elif source_file.suffix.lower() == ".smx": - from ...value_object.read.media.smx import SMX - image = SMX(media_file.read()) - - elif source_file.suffix.lower() == ".sld": - from ...value_object.read.media.sld import SLD - image = SLD(media_file.read()) - - else: - raise SyntaxError(f"Source file {source_file.name} has an unrecognized extension: " - f"{source_file.suffix.lower()}") - - packer_cache = None - compr_cache = None - if cache_info: - cache_params = cache_info.get(export_request.source_filename, None) - - if cache_params: - packer_cache = cache_params["packer_settings"] - compression_level = cache_params["compr_settings"][0] - compr_cache = cache_params["compr_settings"][1:] - - from .texture_merge import merge_frames - - texture = Texture(image, palettes) - merge_frames(texture, cache=packer_cache) - MediaExporter.save_png( - texture, - exportdir[export_request.targetdir], - export_request.target_filename, - compression_level=compression_level, - cache=compr_cache - ) - metadata = {export_request.target_filename: texture.get_metadata()} - export_request.set_changed() - export_request.notify_observers(metadata) - export_request.clear_changed() - - if get_loglevel() <= logging.DEBUG: - MediaExporter.log_fileinfo( - source_file, - exportdir[export_request.targetdir, export_request.target_filename] - ) + # Create a manager for sharing data between the workers and main process + with multiprocessing.Manager() as manager: + # Workers write the image metadata to this queue + # so that it can be forwarded to the export requests + # + # we cannot do this in a worker process directly + # because the export requests cannot be pickled + outqueue = manager.Queue() + + # Create a pool of workers + with multiprocessing.Pool() as pool: + for idx, request in enumerate(requests): + # Feed the worker with the source file data (bytes) from the + # main process + # + # This is necessary because some image files are inside an + # archive and cannot be accessed asynchronously + source_file = sourcedir[request.get_type().value, + request.source_filename] + if not source_file.exists(): + if source_file.suffix.lower() in (".smx", ".sld"): + # Some DE2 graphics files have the wrong extension + # Fall back to the SMP (beta) extension + other_filename = request.source_filename[:-3] + "smp" + source_file = sourcedir[ + request.get_type().value, + other_filename + ] + request.set_source_filename(other_filename) + + # The target path must be native + target_path = exportdir[request.targetdir, + request.target_filename].resolve_native_path() + + # Start an export call in a worker process + # The call is asynchronous, so the next worker can be + # started immediately + pool.apply_async( + _export_texture, + args=( + idx, + source_file.open("rb").read(), + outqueue, + request.source_filename, + target_path, + palettes, + compression_level, + cache_info + ) + ) + + # Log file information + if get_loglevel() <= logging.DEBUG: + MediaExporter.log_fileinfo( + source_file, + exportdir[request.targetdir, request.target_filename] + ) + + # Show progress + print(f"-- Files done: {format_progress(outqueue.qsize(), len(requests))}", + end = "\r", flush = True) + + # Close the pool since all workers have been started + pool.close() + + # Show progress for remaining workers + while outqueue.qsize() < len(requests): + print(f"-- Files done: {format_progress(outqueue.qsize(), len(requests))}", + end = "\r", flush = True) + + # Wait for all workers to finish + pool.join() + + # Collect the metadata from the workers and forward it to the + # export requests + while not outqueue.empty(): + idx, metadata = outqueue.get() + update_data = {requests[idx].target_filename: metadata} + requests[idx].set_changed() + requests[idx].notify_observers(update_data) + requests[idx].clear_changed() @staticmethod def _export_interface( From 2592ea9b7974ff8bbd1a94db306891be517e1ade Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 20 Jan 2024 00:07:53 +0100 Subject: [PATCH 182/771] convert: Switch terrain export to multi-threading. --- .../processor/export/media_exporter.py | 218 ++++++++++++------ 1 file changed, 152 insertions(+), 66 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 7b859762a3..d4792763a6 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -65,12 +65,16 @@ def export( export_func = None kwargs = {} if media_type is MediaType.TERRAIN: - # Game version and palettes - kwargs["game_version"] = args.game_version - kwargs["palettes"] = args.palettes - kwargs["compression_level"] = args.compression_level - export_func = MediaExporter._export_terrain info("-- Exporting terrain files...") + MediaExporter._export_terrains( + cur_export_requests, + sourcedir, + exportdir, + args.palettes, + args.game_version, + args.compression_level + ) + continue elif media_type is MediaType.GRAPHICS: info("-- Exporting graphics files...") @@ -355,8 +359,8 @@ def _export_sound( ) @staticmethod - def _export_terrain( - export_request: MediaExportRequest, + def _export_terrains( + requests: list[MediaExportRequest], sourcedir: Path, exportdir: Path, palettes: dict[int, ColorTable], @@ -364,9 +368,9 @@ def _export_terrain( compression_level: int ) -> None: """ - Convert and export a terrain graphics file. + Convert and export terrain graphics files (multi-threaded). - :param export_request: Export request for a terrain graphics file. + :param requests: Export requests for terrain graphics files. :param sourcedir: Directory where all media assets are mounted. Source subfolder and source filename should be stored in the export request. :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder @@ -374,61 +378,82 @@ def _export_terrain( :param game_version: Game edition and expansion info. :param palettes: Palettes used by the game. :param compression_level: PNG compression level for the resulting image file. - :type export_request: MediaExportRequest + :type requests: list[MediaExportRequest] :type sourcedir: Directory :type exportdir: Directory :type palettes: dict :type game_version: GameVersion :type compression_level: int """ - source_file = sourcedir[ - export_request.get_type().value, - export_request.source_filename - ] + # Create a manager for sharing data between the workers and main process + with multiprocessing.Manager() as manager: + # Create a queue for data sharing + # it's not actually used for passing data, only for counting + # finished tasks + outqueue = manager.Queue() - if source_file.suffix.lower() == ".slp": - from ...value_object.read.media.slp import SLP - media_file = source_file.open("rb") - image = SLP(media_file.read()) + # Create a pool of workers + with multiprocessing.Pool() as pool: + for request in requests: + # Feed the worker with the source file data (bytes) from the + # main process + # + # This is necessary because some image files are inside an + # archive and cannot be accessed asynchronously + source_file = sourcedir[request.get_type().value, + request.source_filename] + if not source_file.exists(): + if source_file.suffix.lower() in (".smx", ".sld"): + # Some DE2 graphics files have the wrong extension + # Fall back to the SMP (beta) extension + other_filename = request.source_filename[:-3] + "smp" + source_file = sourcedir[ + request.get_type().value, + other_filename + ] + request.set_source_filename(other_filename) - elif source_file.suffix.lower() == ".dds": - # TODO: Implement - pass + # The target path must be native + target_path = exportdir[request.targetdir, + request.target_filename].resolve_native_path() - elif source_file.suffix.lower() == ".png": - from shutil import copyfileobj - src_path = source_file.open('rb') - dst_path = exportdir[export_request.targetdir, - export_request.target_filename].open('wb') - copyfileobj(src_path, dst_path) - return + # Start an export call in a worker process + # The call is asynchronous, so the next worker can be + # started immediately + pool.apply_async( + _export_terrain, + args=( + source_file.open("rb").read(), + outqueue, + request.source_filename, + target_path, + palettes, + compression_level, + game_version + ) + ) - else: - raise SyntaxError(f"Source file {source_file.name} has an unrecognized extension: " - f"{source_file.suffix.lower()}") + # Log file information + if get_loglevel() <= logging.DEBUG: + MediaExporter.log_fileinfo( + source_file, + exportdir[request.targetdir, request.target_filename] + ) - if game_version.edition.game_id in ("AOC", "SWGB"): - from .terrain_merge import merge_terrain - texture = Texture(image, palettes) - merge_terrain(texture) + # Show progress + print(f"-- Files done: {format_progress(outqueue.qsize(), len(requests))}", + end = "\r", flush = True) - else: - from .texture_merge import merge_frames - texture = Texture(image, palettes) - merge_frames(texture) + # Close the pool since all workers have been started + pool.close() - MediaExporter.save_png( - texture, - exportdir[export_request.targetdir], - export_request.target_filename, - compression_level, - ) + # Show progress for remaining workers + while outqueue.qsize() < len(requests): + print(f"-- Files done: {format_progress(outqueue.qsize(), len(requests))}", + end = "\r", flush = True) - if get_loglevel() <= logging.DEBUG: - MediaExporter.log_fileinfo( - source_file, - exportdir[export_request.targetdir, export_request.target_filename] - ) + # Wait for all workers to finish + pool.join() @staticmethod def _get_media_cache( @@ -599,8 +624,73 @@ def log_fileinfo( dbg(log) +def _export_terrain( + graphics_data: bytes, + outqueue: multiprocessing.Queue, + source_filename: str, + target_path: str, + palettes: dict[int, ColorTable], + compression_level: int, + game_version: GameVersion +) -> None: + """ + Convert and export a terrain graphics file. + + :param graphics_data: Raw file data of the graphics file. + :param outqueue: Queue for passing the image metadata to the main process. + :param source_filename: Filename of the source file. + :param target_path: Path to the resulting image file. + :param palettes: Palettes used by the game. + :param compression_level: PNG compression level for the resulting image file. + :param game_version: Game edition and expansion info. + :type graphics_data: bytes + :type outqueue: multiprocessing.Queue + :type source_filename: str + :type target_path: str + :type palettes: dict + :type compression_level: int + :type game_version: GameVersion + """ + file_ext = source_filename.split('.')[-1].lower() + if file_ext == "slp": + from ...value_object.read.media.slp import SLP + image = SLP(graphics_data) + + elif file_ext == "dds": + # TODO: Implement + pass + + elif file_ext == "png": + with open(target_path, "wb") as imagefile: + imagefile.write(graphics_data) + + return + + else: + raise SyntaxError(f"Source file {source_filename} has an unrecognized extension: " + f"{file_ext}") + + if game_version.edition.game_id in ("AOC", "SWGB"): + from .terrain_merge import merge_terrain + texture = Texture(image, palettes) + merge_terrain(texture) + + else: + from .texture_merge import merge_frames + texture = Texture(image, palettes) + merge_frames(texture) + + _save_png( + texture, + target_path, + compression_level=compression_level + ) + + outqueue.put(0) + + def _export_texture( - export_request_idx: int, + export_request_id: int, graphics_data: bytes, outqueue: multiprocessing.Queue, source_filename: str, @@ -612,17 +702,19 @@ def _export_texture( """ Convert and export a graphics file to a PNG texture. - :param export_request: Export request for a graphics file. - :param sourcedir: Directory where all media assets are mounted. Source subfolder and - source filename should be stored in the export request. - :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder - and target filename should be stored in the export request. + :param export_request_id: ID of the export request. + :param graphics_data: Raw file data of the graphics file. + :param outqueue: Queue for passing the image metadata to the main process. + :param source_filename: Filename of the source file. + :param target_path: Path to the resulting image file. :param palettes: Palettes used by the game. :param compression_level: PNG compression level for the resulting image file. :param cache_info: Media cache information with compression parameters from a previous run. - :type export_request: MediaExportRequest - :type sourcedir: Path - :type exportdir: Path + :type export_request_id: int + :type graphics_data: bytes + :type outqueue: multiprocessing.Queue + :type source_filename: str + :type target_path: str :type palettes: dict :type compression_level: int :type cache_info: tuple @@ -668,15 +760,9 @@ def _export_texture( compression_level=compression_level, cache=compr_cache ) - metadata = (export_request_idx, texture.get_metadata().copy()) + metadata = (export_request_id, texture.get_metadata().copy()) outqueue.put(metadata) - # if get_loglevel() <= logging.DEBUG: - # MediaExporter.log_fileinfo( - # source_file, - # exportdir[export_request.targetdir, export_request.target_filename] - # ) - def _save_png( texture: Texture, From 34ec340c84ed6607beeb01ed1a4543498c6d82d4 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 20 Jan 2024 00:22:47 +0100 Subject: [PATCH 183/771] convert: Switch sound export to multi-threading. --- .../processor/export/media_exporter.py | 131 +++++++++++++----- 1 file changed, 99 insertions(+), 32 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index d4792763a6..fcee47a7ef 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -89,10 +89,16 @@ def export( continue elif media_type is MediaType.SOUNDS: - kwargs["loglevel"] = args.debug_info kwargs["debugdir"] = args.debugdir - export_func = MediaExporter._export_sound + kwargs["loglevel"] = args.debug_info info("-- Exporting sound files...") + MediaExporter._export_sound( + cur_export_requests, + sourcedir, + exportdir, + **kwargs + ) + continue elif media_type is MediaType.BLEND: kwargs["blend_mode_count"] = args.blend_mode_count @@ -306,57 +312,91 @@ def _export_palette( @staticmethod def _export_sound( - export_request: MediaExportRequest, + requests: list[MediaExportRequest], sourcedir: Path, exportdir: Path, **kwargs ) -> None: """ - Convert and export a sound file. + Convert and export sound files (multi-threaded). - :param export_request: Export request for a sound file. + :param requests: Export requests for sound files. :param sourcedir: Directory where all media assets are mounted. Source subfolder and source filename should be stored in the export request. :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder and target filename should be stored in the export request. - :type export_request: MediaExportRequest + :type requests: list[MediaExportRequest] :type sourcedir: Path :type exportdir: Path """ - source_file = sourcedir[ - export_request.get_type().value, - export_request.source_filename - ] + # Create a manager for sharing data between the workers and main process + with multiprocessing.Manager() as manager: + # Create a queue for data sharing + # it's not actually used for passing data, only for counting + # finished tasks + outqueue = manager.Queue() + + # Create a pool of workers + with multiprocessing.Pool() as pool: + sound_count = len(requests) + for request in requests: + # Feed the worker with the source file data (bytes) from the + # main process + # + # This is necessary because some image files are inside an + # archive and cannot be accessed asynchronously + source_file = sourcedir[request.get_type().value, + request.source_filename] - if source_file.is_file(): - with source_file.open_r() as infile: - media_file = infile.read() + if source_file.is_file(): + with source_file.open_r() as infile: + media_file = infile.read() - else: - # TODO: Filter files that do not exist out sooner - debug_info.debug_not_found_sounds(kwargs["debugdir"], kwargs["loglevel"], source_file) - return + else: + # TODO: Filter files that do not exist out sooner + debug_info.debug_not_found_sounds(kwargs["debugdir"], + kwargs["loglevel"], + source_file) + sound_count -= 1 + continue + + # The target path must be native + target_path = exportdir[request.targetdir, + request.target_filename].resolve_native_path() - from ...service.export.opus.opusenc import encode + # Start an export call in a worker process + # The call is asynchronous, so the next worker can be + # started immediately + pool.apply_async( + _export_sound, + args=( + media_file, + outqueue, + target_path + ) + ) - soundata = encode(media_file) + # Log file information + if get_loglevel() <= logging.DEBUG: + MediaExporter.log_fileinfo( + source_file, + exportdir[request.targetdir, request.target_filename] + ) - if isinstance(soundata, (str, int)): - raise RuntimeError(f"opusenc failed: {soundata}") + # Show progress + print(f"-- Files done: {format_progress(outqueue.qsize(), sound_count)}", + end = "\r", flush = True) - export_file = exportdir[ - export_request.targetdir, - export_request.target_filename - ] + # Close the pool since all workers have been started + pool.close() - with export_file.open_w() as outfile: - outfile.write(soundata) + # Show progress for remaining workers + while outqueue.qsize() < sound_count: + print(f"-- Files done: {format_progress(outqueue.qsize(), sound_count)}", + end = "\r", flush = True) - if get_loglevel() <= logging.DEBUG: - MediaExporter.log_fileinfo( - source_file, - exportdir[export_request.targetdir, export_request.target_filename] - ) + # Wait for all workers to finish + pool.join() @staticmethod def _export_terrains( @@ -624,6 +664,33 @@ def log_fileinfo( dbg(log) +def _export_sound( + sound_data: bytes, + outqueue: multiprocessing.Queue, + target_path: str +) -> None: + """ + Convert and export a sound file. + + :param sound_data: Raw file data of the sound file. + :param outqueue: Queue for passing metadata to the main process. + :param target_path: Path to the resulting sound file. + :type sound_data: bytes + :type outqueue: multiprocessing.Queue + :type target_path: str + """ + from ...service.export.opus.opusenc import encode + encoded = encode(sound_data) + + if isinstance(encoded, (str, int)): + raise RuntimeError(f"opusenc failed: {encoded}") + + with open(target_path, "wb") as outfile: + outfile.write(encoded) + + outqueue.put(0) + + def _export_terrain( graphics_data: bytes, outqueue: multiprocessing.Queue, From 1e08bf2006af11dfdd1e50e4b6110c90648c8ee9 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 20 Jan 2024 00:42:55 +0100 Subject: [PATCH 184/771] convert: Switch blend export to multi-threading. --- .../processor/export/media_exporter.py | 127 +++++++++++++----- 1 file changed, 94 insertions(+), 33 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index fcee47a7ef..3270536f2f 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -62,7 +62,6 @@ def export( for media_type in export_requests.keys(): cur_export_requests = export_requests[media_type] - export_func = None kwargs = {} if media_type is MediaType.TERRAIN: info("-- Exporting terrain files...") @@ -74,7 +73,6 @@ def export( args.game_version, args.compression_level ) - continue elif media_type is MediaType.GRAPHICS: info("-- Exporting graphics files...") @@ -86,7 +84,6 @@ def export( args.compression_level, cache_info ) - continue elif media_type is MediaType.SOUNDS: kwargs["debugdir"] = args.debugdir @@ -98,18 +95,15 @@ def export( exportdir, **kwargs ) - continue elif media_type is MediaType.BLEND: - kwargs["blend_mode_count"] = args.blend_mode_count - export_func = MediaExporter._export_blend info("-- Exporting blend files...") - - total_count = len(cur_export_requests) - for count, request in enumerate(cur_export_requests, start = 1): - export_func(request, sourcedir, exportdir, **kwargs) - print(f"-- Files done: {format_progress(count, total_count)}", - end = "\r", flush = True) + MediaExporter._export_blend( + cur_export_requests, + sourcedir, + exportdir, + args.blend_mode_count + ) if args.debug_info > 5: cachedata = {} @@ -137,7 +131,7 @@ def export( @staticmethod def _export_blend( - export_request: MediaExportRequest, + requests: list[MediaExportRequest], sourcedir: Path, exportdir: Path, blend_mode_count: int = None @@ -145,39 +139,73 @@ def _export_blend( """ Convert and export a blending mode. - :param export_request: Export request for a blending mask. + :param requests: Export requests for blending masks. :param sourcedir: Directory where all media assets are mounted. Source subfolder and source filename should be stored in the export request. :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder and target filename should be stored in the export request. :param blend_mode_count: Number of blending modes extracted from the source file. - :type export_request: MediaExportRequest + :type requests: list[MediaExportRequest] :type sourcedir: Path :type exportdir: Path :type blend_mode_count: int """ - source_file = sourcedir.joinpath(export_request.source_filename) + # Create a manager for sharing data between the workers and main process + with multiprocessing.Manager() as manager: + # Create a queue for data sharing + # it's not actually used for passing data, only for counting + # finished tasks + outqueue = manager.Queue() - media_file = source_file.open("rb") - blend_data = Blendomatic(media_file, blend_mode_count) + # Create a pool of workers + with multiprocessing.Pool() as pool: + for request in requests: + # Feed the worker with the source file data (bytes) from the + # main process + # + # This is necessary because some image files are inside an + # archive and cannot be accessed asynchronously + source_file = sourcedir[request.get_type().value, + request.source_filename] - from .texture_merge import merge_frames + # The target path must be native + target_path = exportdir[request.targetdir, + request.target_filename].resolve_native_path() - textures = blend_data.get_textures() - for idx, texture in enumerate(textures): - merge_frames(texture) - MediaExporter.save_png( - texture, - exportdir[export_request.targetdir], - f"{export_request.target_filename}{idx}.png" - ) + # Start an export call in a worker process + # The call is asynchronous, so the next worker can be + # started immediately + pool.apply_async( + _export_blend, + args=( + source_file.open("rb").read(), + outqueue, + target_path, + blend_mode_count + ) + ) - if get_loglevel() <= logging.DEBUG: - MediaExporter.log_fileinfo( - source_file, - exportdir[export_request.targetdir, - f"{export_request.target_filename}{idx}.png"] - ) + # Log file information + if get_loglevel() <= logging.DEBUG: + MediaExporter.log_fileinfo( + source_file, + exportdir[request.targetdir, request.target_filename] + ) + + # Show progress + print(f"-- Files done: {format_progress(outqueue.qsize(), len(requests))}", + end = "\r", flush = True) + + # Close the pool since all workers have been started + pool.close() + + # Show progress for remaining workers + while outqueue.qsize() < len(requests): + print(f"-- Files done: {format_progress(outqueue.qsize(), len(requests))}", + end = "\r", flush = True) + + # Wait for all workers to finish + pool.join() @staticmethod def _export_graphics( @@ -664,6 +692,39 @@ def log_fileinfo( dbg(log) +def _export_blend( + blendfile_data: bytes, + outqueue: multiprocessing.Queue, + target_path: str, + blend_mode_count: int = None +) -> None: + """ + Convert and export a blending mode. + + :param blendfile_data: Raw file data of the blending mask. + :param outqueue: Queue for passing metadata to the main process. + :param target_path: Path to the resulting image file. + :param blend_mode_count: Number of blending modes extracted from the source file. + :type blendfile_data: bytes + :type outqueue: multiprocessing.Queue + :type target_path: str + :type blend_mode_count: int + """ + blend_data = Blendomatic(blendfile_data, blend_mode_count) + + from .texture_merge import merge_frames + + textures = blend_data.get_textures() + for idx, texture in enumerate(textures): + merge_frames(texture) + _save_png( + texture, + f"{target_path}{idx}.png" + ) + + outqueue.put(0) + + def _export_sound( sound_data: bytes, outqueue: multiprocessing.Queue, From d891e0e7060d0135c28102f1f6d2bc9c64bc6db4 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 20 Jan 2024 00:55:55 +0100 Subject: [PATCH 185/771] convert: Reorder media type checks in export(). --- .../processor/export/media_exporter.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 3270536f2f..d019be4f00 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -63,15 +63,14 @@ def export( cur_export_requests = export_requests[media_type] kwargs = {} - if media_type is MediaType.TERRAIN: - info("-- Exporting terrain files...") - MediaExporter._export_terrains( + + if media_type is MediaType.BLEND: + info("-- Exporting blend files...") + MediaExporter._export_blend( cur_export_requests, sourcedir, exportdir, - args.palettes, - args.game_version, - args.compression_level + args.blend_mode_count ) elif media_type is MediaType.GRAPHICS: @@ -96,13 +95,15 @@ def export( **kwargs ) - elif media_type is MediaType.BLEND: - info("-- Exporting blend files...") - MediaExporter._export_blend( + elif media_type is MediaType.TERRAIN: + info("-- Exporting terrain files...") + MediaExporter._export_terrains( cur_export_requests, sourcedir, exportdir, - args.blend_mode_count + args.palettes, + args.game_version, + args.compression_level ) if args.debug_info > 5: From 55f4b026146b34de7ff15cdb531d5e4afa09cc04 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 20 Jan 2024 01:59:01 +0100 Subject: [PATCH 186/771] convert: Fix queue not being fed when a terrain texture is copied. --- openage/convert/processor/export/media_exporter.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index d019be4f00..60ead219e4 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -471,16 +471,6 @@ def _export_terrains( # archive and cannot be accessed asynchronously source_file = sourcedir[request.get_type().value, request.source_filename] - if not source_file.exists(): - if source_file.suffix.lower() in (".smx", ".sld"): - # Some DE2 graphics files have the wrong extension - # Fall back to the SMP (beta) extension - other_filename = request.source_filename[:-3] + "smp" - source_file = sourcedir[ - request.get_type().value, - other_filename - ] - request.set_source_filename(other_filename) # The target path must be native target_path = exportdir[request.targetdir, @@ -793,6 +783,7 @@ def _export_terrain( with open(target_path, "wb") as imagefile: imagefile.write(graphics_data) + outqueue.put(0) return else: From 9e518721e6d8abb856aa8678281875bb6249a510 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Jan 2024 01:57:25 +0100 Subject: [PATCH 187/771] convert: Remove synchronizer lock for export dir. --- openage/convert/main.py | 7 ++- .../processor/export/media_exporter.py | 45 +++++++++++-------- openage/convert/tool/api_export.py | 5 +-- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/openage/convert/main.py b/openage/convert/main.py index a0bd7a927b..9bf3f920ba 100644 --- a/openage/convert/main.py +++ b/openage/convert/main.py @@ -1,4 +1,4 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-branches """ @@ -64,7 +64,7 @@ def convert_assets( # add a dir for debug info debug_log_path = converted_path / "debug" / datetime.now().strftime("%Y-%m-%d-%H-%M-%S") debugdir = DirectoryCreator(debug_log_path).root - args.debugdir = AccessSynchronizer(debugdir).root + args.debugdir = debugdir # Create CLI args info debug_cli_args(args.debugdir, args.debug_info, args) @@ -93,9 +93,8 @@ def convert_assets( if not data_dir: return None - # make srcdir and targetdir safe for threaded conversion args.srcdir = AccessSynchronizer(data_dir).root - args.targetdir = AccessSynchronizer(targetdir).root + args.targetdir = targetdir # Create mountpoint info debug_mounts(args.debugdir, args.debug_info, args) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 60ead219e4..9aade3e96a 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -266,24 +266,33 @@ def _export_graphics( request.set_source_filename(other_filename) # The target path must be native - target_path = exportdir[request.targetdir, - request.target_filename].resolve_native_path() + target_path = exportdir[request.targetdir, request.target_filename] # Start an export call in a worker process # The call is asynchronous, so the next worker can be # started immediately - pool.apply_async( - _export_texture, - args=( - idx, - source_file.open("rb").read(), - outqueue, - request.source_filename, - target_path, - palettes, - compression_level, - cache_info - ) + # pool.apply_async( + # _export_texture, + # args=( + # idx, + # source_file.open("rb").read(), + # outqueue, + # request.source_filename, + # target_path, + # palettes, + # compression_level, + # cache_info + # ) + # ) + _export_texture( + idx, + source_file.open("rb").read(), + outqueue, + request.source_filename, + target_path, + palettes, + compression_level, + cache_info ) # Log file information @@ -915,12 +924,12 @@ def _save_png( } if not dry_run: - _, ext = os.path.splitext(target_path) + ext = target_path.suffix.lower() # only allow png - if ext != b".png": + if ext != ".png": raise ValueError("Filename invalid, a texture must be saved" - f" as '*.png', not '*.{ext}'") + f" as '*.png', not '*{ext}'") compression_method = compression_levels.get( compression_level, @@ -933,7 +942,7 @@ def _save_png( ) if not dry_run: - with open(target_path, "wb") as imagefile: + with target_path.open("wb") as imagefile: imagefile.write(png_data) if compr_params: diff --git a/openage/convert/tool/api_export.py b/openage/convert/tool/api_export.py index 1124da8771..b1cd767898 100644 --- a/openage/convert/tool/api_export.py +++ b/openage/convert/tool/api_export.py @@ -12,8 +12,7 @@ from openage.nyan.import_tree import ImportTree from openage.util.fslike.directory import Directory from openage.util.fslike.union import Union, UnionPath -from openage.util.fslike.wrapper import (DirectoryCreator, - Synchronizer as AccessSynchronizer) +from openage.util.fslike.wrapper import DirectoryCreator from ...log import info @@ -50,7 +49,7 @@ def export_api(exportdir: UnionPath) -> None: info("Dumping info file...") targetdir = DirectoryCreator(exportdir).root - outdir = AccessSynchronizer(targetdir).root / "engine" + outdir = targetdir / "engine" # Modpack info file DataExporter.export([modpack.info], outdir) From 1e7e25cda0313e4f86201baa726ed49cd53483df Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Jan 2024 02:10:31 +0100 Subject: [PATCH 188/771] convert: Use fslike paths for export again. --- .../processor/export/media_exporter.py | 74 ++++++++----------- 1 file changed, 32 insertions(+), 42 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 9aade3e96a..0976980411 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -170,8 +170,8 @@ def _export_blend( request.source_filename] # The target path must be native - target_path = exportdir[request.targetdir, - request.target_filename].resolve_native_path() + targetdir = exportdir[request.targetdir] + target_filename = request.target_filename # Start an export call in a worker process # The call is asynchronous, so the next worker can be @@ -181,7 +181,8 @@ def _export_blend( args=( source_file.open("rb").read(), outqueue, - target_path, + targetdir, + target_filename, blend_mode_count ) ) @@ -271,28 +272,18 @@ def _export_graphics( # Start an export call in a worker process # The call is asynchronous, so the next worker can be # started immediately - # pool.apply_async( - # _export_texture, - # args=( - # idx, - # source_file.open("rb").read(), - # outqueue, - # request.source_filename, - # target_path, - # palettes, - # compression_level, - # cache_info - # ) - # ) - _export_texture( - idx, - source_file.open("rb").read(), - outqueue, - request.source_filename, - target_path, - palettes, - compression_level, - cache_info + pool.apply_async( + _export_texture, + args=( + idx, + source_file.open("rb").read(), + outqueue, + request.source_filename, + target_path, + palettes, + compression_level, + cache_info + ) ) # Log file information @@ -399,8 +390,7 @@ def _export_sound( continue # The target path must be native - target_path = exportdir[request.targetdir, - request.target_filename].resolve_native_path() + target_path = exportdir[request.targetdir, request.target_filename] # Start an export call in a worker process # The call is asynchronous, so the next worker can be @@ -482,8 +472,7 @@ def _export_terrains( request.source_filename] # The target path must be native - target_path = exportdir[request.targetdir, - request.target_filename].resolve_native_path() + target_path = exportdir[request.targetdir, request.target_filename] # Start an export call in a worker process # The call is asynchronous, so the next worker can be @@ -695,7 +684,8 @@ def log_fileinfo( def _export_blend( blendfile_data: bytes, outqueue: multiprocessing.Queue, - target_path: str, + targetdir: Path, + target_filename: str, blend_mode_count: int = None ) -> None: """ @@ -707,7 +697,7 @@ def _export_blend( :param blend_mode_count: Number of blending modes extracted from the source file. :type blendfile_data: bytes :type outqueue: multiprocessing.Queue - :type target_path: str + :type target_path: openage.util.fslike.path.Path :type blend_mode_count: int """ blend_data = Blendomatic(blendfile_data, blend_mode_count) @@ -719,7 +709,7 @@ def _export_blend( merge_frames(texture) _save_png( texture, - f"{target_path}{idx}.png" + targetdir.joinpath(f"{target_filename}_{idx}.png") ) outqueue.put(0) @@ -728,7 +718,7 @@ def _export_blend( def _export_sound( sound_data: bytes, outqueue: multiprocessing.Queue, - target_path: str + target_path: Path ) -> None: """ Convert and export a sound file. @@ -738,7 +728,7 @@ def _export_sound( :param target_path: Path to the resulting sound file. :type sound_data: bytes :type outqueue: multiprocessing.Queue - :type target_path: str + :type target_path: openage.util.fslike.path.Path """ from ...service.export.opus.opusenc import encode encoded = encode(sound_data) @@ -746,7 +736,7 @@ def _export_sound( if isinstance(encoded, (str, int)): raise RuntimeError(f"opusenc failed: {encoded}") - with open(target_path, "wb") as outfile: + with target_path.open("wb") as outfile: outfile.write(encoded) outqueue.put(0) @@ -756,7 +746,7 @@ def _export_terrain( graphics_data: bytes, outqueue: multiprocessing.Queue, source_filename: str, - target_path: str, + target_path: Path, palettes: dict[int, ColorTable], compression_level: int, game_version: GameVersion @@ -774,7 +764,7 @@ def _export_terrain( :type graphics_data: bytes :type outqueue: multiprocessing.Queue :type source_filename: str - :type target_path: str + :type target_path: openage.util.fslike.path.Path :type palettes: dict :type compression_level: int :type game_version: GameVersion @@ -789,7 +779,7 @@ def _export_terrain( pass elif file_ext == "png": - with open(target_path, "wb") as imagefile: + with target_path.open("wb") as imagefile: imagefile.write(graphics_data) outqueue.put(0) @@ -823,7 +813,7 @@ def _export_texture( graphics_data: bytes, outqueue: multiprocessing.Queue, source_filename: str, - target_path: str, + target_path: Path, palettes: dict[int, ColorTable], compression_level: int, cache_info: dict = None @@ -843,7 +833,7 @@ def _export_texture( :type graphics_data: bytes :type outqueue: multiprocessing.Queue :type source_filename: str - :type target_path: str + :type target_path: openage.util.fslike.path.Path :type palettes: dict :type compression_level: int :type cache_info: tuple @@ -895,7 +885,7 @@ def _export_texture( def _save_png( texture: Texture, - target_path: str, + target_path: Path, compression_level: int = 1, cache: dict = None, dry_run: bool = False @@ -909,7 +899,7 @@ def _save_png( :param compression_level: PNG compression level used for the resulting image file. :param dry_run: If True, create the PNG but don't save it as a file. :type texture: Texture - :type target_path: str + :type target_path: openage.util.fslike.path.Path :type compression_level: int :type dry_run: bool """ From ff651a7772697d2106223d9be8f99739ee57e51f Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Jan 2024 02:16:32 +0100 Subject: [PATCH 189/771] convert: Allow specifying number of workers in pool. --- .../processor/export/media_exporter.py | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 0976980411..23ac515900 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -135,7 +135,8 @@ def _export_blend( requests: list[MediaExportRequest], sourcedir: Path, exportdir: Path, - blend_mode_count: int = None + blend_mode_count: int = None, + jobs: int = None ) -> None: """ Convert and export a blending mode. @@ -146,10 +147,12 @@ def _export_blend( :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder and target filename should be stored in the export request. :param blend_mode_count: Number of blending modes extracted from the source file. + :param jobs: Number of worker processes to use (default: number of CPU cores). :type requests: list[MediaExportRequest] :type sourcedir: Path :type exportdir: Path :type blend_mode_count: int + :type jobs: int """ # Create a manager for sharing data between the workers and main process with multiprocessing.Manager() as manager: @@ -159,7 +162,7 @@ def _export_blend( outqueue = manager.Queue() # Create a pool of workers - with multiprocessing.Pool() as pool: + with multiprocessing.Pool(jobs) as pool: for request in requests: # Feed the worker with the source file data (bytes) from the # main process @@ -216,7 +219,8 @@ def _export_graphics( exportdir: Path, palettes: dict[int, ColorTable], compression_level: int, - cache_info: dict = None + cache_info: dict = None, + jobs: int = None ) -> None: """ Convert and export graphics file requests (multi-threaded). @@ -229,12 +233,14 @@ def _export_graphics( :param palettes: Palettes used by the game. :param compression_level: PNG compression level for the resulting image file. :param cache_info: Media cache information with compression parameters from a previous run. + :param jobs: Number of worker processes to use (default: number of CPU cores). :type requests: list[MediaExportRequest] :type sourcedir: Path :type exportdir: Path :type palettes: dict :type compression_level: int :type cache_info: tuple + :type jobs: int """ # Create a manager for sharing data between the workers and main process with multiprocessing.Manager() as manager: @@ -246,7 +252,7 @@ def _export_graphics( outqueue = manager.Queue() # Create a pool of workers - with multiprocessing.Pool() as pool: + with multiprocessing.Pool(jobs) as pool: for idx, request in enumerate(requests): # Feed the worker with the source file data (bytes) from the # main process @@ -344,6 +350,7 @@ def _export_sound( requests: list[MediaExportRequest], sourcedir: Path, exportdir: Path, + jobs: int = None, **kwargs ) -> None: """ @@ -354,9 +361,11 @@ def _export_sound( source filename should be stored in the export request. :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder and target filename should be stored in the export request. + :param jobs: Number of worker processes to use (default: number of CPU cores). :type requests: list[MediaExportRequest] :type sourcedir: Path :type exportdir: Path + :type jobs: int """ # Create a manager for sharing data between the workers and main process with multiprocessing.Manager() as manager: @@ -366,7 +375,7 @@ def _export_sound( outqueue = manager.Queue() # Create a pool of workers - with multiprocessing.Pool() as pool: + with multiprocessing.Pool(jobs) as pool: sound_count = len(requests) for request in requests: # Feed the worker with the source file data (bytes) from the @@ -433,7 +442,8 @@ def _export_terrains( exportdir: Path, palettes: dict[int, ColorTable], game_version: GameVersion, - compression_level: int + compression_level: int, + jobs: int = None ) -> None: """ Convert and export terrain graphics files (multi-threaded). @@ -446,12 +456,14 @@ def _export_terrains( :param game_version: Game edition and expansion info. :param palettes: Palettes used by the game. :param compression_level: PNG compression level for the resulting image file. + :param jobs: Number of worker processes to use (default: number of CPU cores). :type requests: list[MediaExportRequest] :type sourcedir: Directory :type exportdir: Directory :type palettes: dict :type game_version: GameVersion :type compression_level: int + :type jobs: int """ # Create a manager for sharing data between the workers and main process with multiprocessing.Manager() as manager: @@ -461,7 +473,7 @@ def _export_terrains( outqueue = manager.Queue() # Create a pool of workers - with multiprocessing.Pool() as pool: + with multiprocessing.Pool(jobs) as pool: for request in requests: # Feed the worker with the source file data (bytes) from the # main process From 20b4cdb2e1bb31e31eee3790518fc43b47e69aa2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Jan 2024 04:55:32 +0100 Subject: [PATCH 190/771] convert: Move pool into main loop. Reduce code duplication. --- .../processor/export/media_exporter.py | 275 +++++++++++++++--- 1 file changed, 237 insertions(+), 38 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 23ac515900..a6e959f796 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -31,6 +31,8 @@ class MediaExporter: """ Provides functions for converting media files and writing them to a targetdir. + + TODO: Avoid code duplication in the export functions. """ @staticmethod @@ -62,49 +64,108 @@ def export( for media_type in export_requests.keys(): cur_export_requests = export_requests[media_type] + read_data_func = None + export_func = None + handle_outqueue_func = None kwargs = {} - if media_type is MediaType.BLEND: + read_data_func = MediaExporter._get_blend_data + export_func = _export_blend + itargs = (sourcedir, exportdir) + kwargs["blend_mode_count"] = args.blend_mode_count info("-- Exporting blend files...") - MediaExporter._export_blend( - cur_export_requests, - sourcedir, - exportdir, - args.blend_mode_count - ) elif media_type is MediaType.GRAPHICS: + read_data_func = MediaExporter._get_graphics_data + export_func = _export_texture + handle_outqueue_func = MediaExporter._handle_graphics_outqueue + itargs = (args.palettes, args.compression_level) + kwargs["cache_info"] = cache_info info("-- Exporting graphics files...") - MediaExporter._export_graphics( - cur_export_requests, - sourcedir, - exportdir, - args.palettes, - args.compression_level, - cache_info - ) elif media_type is MediaType.SOUNDS: + read_data_func = MediaExporter._get_sound_data + export_func = _export_sound + itargs = tuple() kwargs["debugdir"] = args.debugdir kwargs["loglevel"] = args.debug_info info("-- Exporting sound files...") - MediaExporter._export_sound( - cur_export_requests, - sourcedir, - exportdir, - **kwargs - ) elif media_type is MediaType.TERRAIN: + read_data_func = MediaExporter._get_terrain_data + export_func = _export_terrain + itargs = (args.palettes, args.compression_level, args.game_version) info("-- Exporting terrain files...") - MediaExporter._export_terrains( - cur_export_requests, - sourcedir, - exportdir, - args.palettes, - args.game_version, - args.compression_level - ) + + # Create a manager for sharing data between the workers and main process + with multiprocessing.Manager() as manager: + # Workers write the image metadata to this queue + # so that it can be forwarded to the export requests + # + # we cannot do this in a worker process directly + # because the export requests cannot be pickled + outqueue = manager.Queue() + + expected_size = len(cur_export_requests) + + worker_count = args.jobs + if worker_count is None: + worker_count = min(multiprocessing.cpu_count(), expected_size) + + # Create a pool of workers + with multiprocessing.Pool(worker_count) as pool: + for idx, request in enumerate(cur_export_requests): + # Feed the worker with the source file data (bytes) from the + # main process + # + # This is necessary because some image files are inside an + # archive and cannot be accessed asynchronously + source_data = read_data_func(request, sourcedir, **kwargs) + if source_data is None: + expected_size -= 1 + continue + + # The target path must be native + target_path = exportdir[request.targetdir, request.target_filename] + + # Start an export call in a worker process + # The call is asynchronous, so the next worker can be + # started immediately + pool.apply_async( + export_func, + args=( + idx, + source_data, + outqueue, + request.source_filename, + target_path, + *itargs + ), + kwds=kwargs + ) + + # Log file information + if get_loglevel() <= logging.DEBUG: + MediaExporter.log_fileinfo( + sourcedir[request.get_type().value, request.source_filename], + exportdir[request.targetdir, request.target_filename] + ) + + # Show progress + MediaExporter._show_progress(outqueue.qsize(), expected_size) + + # Close the pool since all workers have been started + pool.close() + + # Show progress for remaining workers + while outqueue.qsize() < expected_size: + MediaExporter._show_progress(outqueue.qsize(), expected_size) + + # Wait for all workers to finish + pool.join() + + if handle_outqueue_func: + handle_outqueue_func(outqueue, cur_export_requests) if args.debug_info > 5: cachedata = {} @@ -130,6 +191,138 @@ def export( args.game_version ) + @staticmethod + def _get_blend_data( + request: MediaExportRequest, + sourcedir: Path, + **kwargs # pylint: disable=unused-argument + ) -> bytes: + """ + Get the raw file data of a blending mask. + + :param request: Export request for a blending mask. + :param sourcedir: Directory where all media assets are mounted. + :type request: MediaExportRequest + :type sourcedir: Path + """ + source_file = sourcedir[request.get_type().value, + request.source_filename] + + return source_file.open("rb").read() + + @staticmethod + def _get_graphics_data( + request: MediaExportRequest, + sourcedir: Path, + **kwargs # pylint: disable=unused-argument + ) -> bytes: + """ + Get the raw file data of a graphics file. + + :param request: Export request for a graphics file. + :param sourcedir: Directory where all media assets are mounted. + :type request: MediaExportRequest + :type sourcedir: Path + """ + source_file = sourcedir[request.get_type().value, + request.source_filename] + if not source_file.exists(): + if source_file.suffix.lower() in (".smx", ".sld"): + # Some DE2 graphics files have the wrong extension + # Fall back to the SMP (beta) extension + other_filename = request.source_filename[:-3] + "smp" + source_file = sourcedir[ + request.get_type().value, + other_filename + ] + request.set_source_filename(other_filename) + + return source_file.open("rb").read() + + @staticmethod + def _get_sound_data( + request: MediaExportRequest, + sourcedir: Path, + **kwargs + ) -> bytes | None: + """ + Get the raw file data of a sound file. + + :param request: Export request for a sound file. + :param sourcedir: Directory where all media assets are mounted. + :type request: MediaExportRequest + :type sourcedir: Path + """ + source_file = sourcedir[request.get_type().value, + request.source_filename] + + if not source_file.is_file(): + # TODO: Filter files that do not exist out sooner + debug_info.debug_not_found_sounds(kwargs["debugdir"], + kwargs["loglevel"], + source_file) + return None + + return source_file.open("rb").read() + + @staticmethod + def _get_terrain_data( + request: MediaExportRequest, + sourcedir: Path, + **kwargs # pylint: disable=unused-argument + ) -> bytes: + """ + Get the raw file data of a terrain graphics file. + + :param request: Export request for a terrain graphics file. + :param sourcedir: Directory where all media assets are mounted. + :type request: MediaExportRequest + :type sourcedir: Path + """ + source_file = sourcedir[request.get_type().value, + request.source_filename] + + return source_file.open("rb").read() + + @staticmethod + def _handle_graphics_outqueue( + outqueue: multiprocessing.Queue, + requests: list[MediaExportRequest] + ): + """ + Collect the metadata from the workers and forward it to the + export requests. + + This must be called before the manager of the queue is shutdown! + + :param outqueue: Queue for passing metadata to the main process. + :param requests: Export requests for graphics files. + :type outqueue: multiprocessing.Queue + :type requests: list[MediaExportRequest] + """ + while not outqueue.empty(): + idx, metadata = outqueue.get() + update_data = {requests[idx].target_filename: metadata} + requests[idx].set_changed() + requests[idx].notify_observers(update_data) + requests[idx].clear_changed() + + @staticmethod + def _show_progress( + current_size: int, + total_size: int, + ): + """ + Show the progress of the export process. + + :param current_size: Number of files that have been exported. + :param total_size: Total number of files to export. + :type current_size: int + :type total_size: int + """ + print(f"-- Files done: {format_progress(current_size, total_size)}", + end = "\r", flush = True) + @staticmethod def _export_blend( requests: list[MediaExportRequest], @@ -694,8 +887,10 @@ def log_fileinfo( def _export_blend( + request_id: int, blendfile_data: bytes, outqueue: multiprocessing.Queue, + source_filename: str, # pylint: disable=unused-argument targetdir: Path, target_filename: str, blend_mode_count: int = None @@ -724,13 +919,16 @@ def _export_blend( targetdir.joinpath(f"{target_filename}_{idx}.png") ) - outqueue.put(0) + outqueue.put(request_id) def _export_sound( + request_id: int, sound_data: bytes, outqueue: multiprocessing.Queue, - target_path: Path + source_filename: str, # pylint: disable=unused-argument + target_path: Path, + **kwargs # pylint: disable=unused-argument ) -> None: """ Convert and export a sound file. @@ -751,10 +949,11 @@ def _export_sound( with target_path.open("wb") as outfile: outfile.write(encoded) - outqueue.put(0) + outqueue.put(request_id) def _export_terrain( + request_id: int, graphics_data: bytes, outqueue: multiprocessing.Queue, source_filename: str, @@ -794,7 +993,7 @@ def _export_terrain( with target_path.open("wb") as imagefile: imagefile.write(graphics_data) - outqueue.put(0) + outqueue.put(request_id) return else: @@ -817,11 +1016,11 @@ def _export_terrain( compression_level=compression_level ) - outqueue.put(0) + outqueue.put(request_id) def _export_texture( - export_request_id: int, + request_id: int, graphics_data: bytes, outqueue: multiprocessing.Queue, source_filename: str, @@ -833,7 +1032,7 @@ def _export_texture( """ Convert and export a graphics file to a PNG texture. - :param export_request_id: ID of the export request. + :param request_id: ID of the export request. :param graphics_data: Raw file data of the graphics file. :param outqueue: Queue for passing the image metadata to the main process. :param source_filename: Filename of the source file. @@ -841,7 +1040,7 @@ def _export_texture( :param palettes: Palettes used by the game. :param compression_level: PNG compression level for the resulting image file. :param cache_info: Media cache information with compression parameters from a previous run. - :type export_request_id: int + :type request_id: int :type graphics_data: bytes :type outqueue: multiprocessing.Queue :type source_filename: str @@ -891,7 +1090,7 @@ def _export_texture( compression_level=compression_level, cache=compr_cache ) - metadata = (export_request_id, texture.get_metadata().copy()) + metadata = (request_id, texture.get_metadata().copy()) outqueue.put(metadata) From 2473a5901fba74570e63ca5f7719a9ba3a88d410 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Jan 2024 04:58:17 +0100 Subject: [PATCH 191/771] refactor: Remove obsolete export methods. --- .../processor/export/media_exporter.py | 394 ------------------ 1 file changed, 394 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index a6e959f796..275f1e0708 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -323,400 +323,6 @@ def _show_progress( print(f"-- Files done: {format_progress(current_size, total_size)}", end = "\r", flush = True) - @staticmethod - def _export_blend( - requests: list[MediaExportRequest], - sourcedir: Path, - exportdir: Path, - blend_mode_count: int = None, - jobs: int = None - ) -> None: - """ - Convert and export a blending mode. - - :param requests: Export requests for blending masks. - :param sourcedir: Directory where all media assets are mounted. Source subfolder and - source filename should be stored in the export request. - :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder - and target filename should be stored in the export request. - :param blend_mode_count: Number of blending modes extracted from the source file. - :param jobs: Number of worker processes to use (default: number of CPU cores). - :type requests: list[MediaExportRequest] - :type sourcedir: Path - :type exportdir: Path - :type blend_mode_count: int - :type jobs: int - """ - # Create a manager for sharing data between the workers and main process - with multiprocessing.Manager() as manager: - # Create a queue for data sharing - # it's not actually used for passing data, only for counting - # finished tasks - outqueue = manager.Queue() - - # Create a pool of workers - with multiprocessing.Pool(jobs) as pool: - for request in requests: - # Feed the worker with the source file data (bytes) from the - # main process - # - # This is necessary because some image files are inside an - # archive and cannot be accessed asynchronously - source_file = sourcedir[request.get_type().value, - request.source_filename] - - # The target path must be native - targetdir = exportdir[request.targetdir] - target_filename = request.target_filename - - # Start an export call in a worker process - # The call is asynchronous, so the next worker can be - # started immediately - pool.apply_async( - _export_blend, - args=( - source_file.open("rb").read(), - outqueue, - targetdir, - target_filename, - blend_mode_count - ) - ) - - # Log file information - if get_loglevel() <= logging.DEBUG: - MediaExporter.log_fileinfo( - source_file, - exportdir[request.targetdir, request.target_filename] - ) - - # Show progress - print(f"-- Files done: {format_progress(outqueue.qsize(), len(requests))}", - end = "\r", flush = True) - - # Close the pool since all workers have been started - pool.close() - - # Show progress for remaining workers - while outqueue.qsize() < len(requests): - print(f"-- Files done: {format_progress(outqueue.qsize(), len(requests))}", - end = "\r", flush = True) - - # Wait for all workers to finish - pool.join() - - @staticmethod - def _export_graphics( - requests: list[MediaExportRequest], - sourcedir: Path, - exportdir: Path, - palettes: dict[int, ColorTable], - compression_level: int, - cache_info: dict = None, - jobs: int = None - ) -> None: - """ - Convert and export graphics file requests (multi-threaded). - - :param requests: Export requests for graphics files. - :param sourcedir: Directory where all media assets are mounted. Source subfolder and - source filename should be stored in the export request. - :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder - and target filename should be stored in the export request. - :param palettes: Palettes used by the game. - :param compression_level: PNG compression level for the resulting image file. - :param cache_info: Media cache information with compression parameters from a previous run. - :param jobs: Number of worker processes to use (default: number of CPU cores). - :type requests: list[MediaExportRequest] - :type sourcedir: Path - :type exportdir: Path - :type palettes: dict - :type compression_level: int - :type cache_info: tuple - :type jobs: int - """ - # Create a manager for sharing data between the workers and main process - with multiprocessing.Manager() as manager: - # Workers write the image metadata to this queue - # so that it can be forwarded to the export requests - # - # we cannot do this in a worker process directly - # because the export requests cannot be pickled - outqueue = manager.Queue() - - # Create a pool of workers - with multiprocessing.Pool(jobs) as pool: - for idx, request in enumerate(requests): - # Feed the worker with the source file data (bytes) from the - # main process - # - # This is necessary because some image files are inside an - # archive and cannot be accessed asynchronously - source_file = sourcedir[request.get_type().value, - request.source_filename] - if not source_file.exists(): - if source_file.suffix.lower() in (".smx", ".sld"): - # Some DE2 graphics files have the wrong extension - # Fall back to the SMP (beta) extension - other_filename = request.source_filename[:-3] + "smp" - source_file = sourcedir[ - request.get_type().value, - other_filename - ] - request.set_source_filename(other_filename) - - # The target path must be native - target_path = exportdir[request.targetdir, request.target_filename] - - # Start an export call in a worker process - # The call is asynchronous, so the next worker can be - # started immediately - pool.apply_async( - _export_texture, - args=( - idx, - source_file.open("rb").read(), - outqueue, - request.source_filename, - target_path, - palettes, - compression_level, - cache_info - ) - ) - - # Log file information - if get_loglevel() <= logging.DEBUG: - MediaExporter.log_fileinfo( - source_file, - exportdir[request.targetdir, request.target_filename] - ) - - # Show progress - print(f"-- Files done: {format_progress(outqueue.qsize(), len(requests))}", - end = "\r", flush = True) - - # Close the pool since all workers have been started - pool.close() - - # Show progress for remaining workers - while outqueue.qsize() < len(requests): - print(f"-- Files done: {format_progress(outqueue.qsize(), len(requests))}", - end = "\r", flush = True) - - # Wait for all workers to finish - pool.join() - - # Collect the metadata from the workers and forward it to the - # export requests - while not outqueue.empty(): - idx, metadata = outqueue.get() - update_data = {requests[idx].target_filename: metadata} - requests[idx].set_changed() - requests[idx].notify_observers(update_data) - requests[idx].clear_changed() - - @staticmethod - def _export_interface( - export_request: MediaExportRequest, - sourcedir: Path, - **kwargs - ) -> None: - """ - Convert and export a sprite file. - """ - # TODO: Implement - - @staticmethod - def _export_palette( - export_request: MediaExportRequest, - sourcedir: Path, - **kwargs - ) -> None: - """ - Convert and export a palette file. - """ - # TODO: Implement - - @staticmethod - def _export_sound( - requests: list[MediaExportRequest], - sourcedir: Path, - exportdir: Path, - jobs: int = None, - **kwargs - ) -> None: - """ - Convert and export sound files (multi-threaded). - - :param requests: Export requests for sound files. - :param sourcedir: Directory where all media assets are mounted. Source subfolder and - source filename should be stored in the export request. - :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder - and target filename should be stored in the export request. - :param jobs: Number of worker processes to use (default: number of CPU cores). - :type requests: list[MediaExportRequest] - :type sourcedir: Path - :type exportdir: Path - :type jobs: int - """ - # Create a manager for sharing data between the workers and main process - with multiprocessing.Manager() as manager: - # Create a queue for data sharing - # it's not actually used for passing data, only for counting - # finished tasks - outqueue = manager.Queue() - - # Create a pool of workers - with multiprocessing.Pool(jobs) as pool: - sound_count = len(requests) - for request in requests: - # Feed the worker with the source file data (bytes) from the - # main process - # - # This is necessary because some image files are inside an - # archive and cannot be accessed asynchronously - source_file = sourcedir[request.get_type().value, - request.source_filename] - - if source_file.is_file(): - with source_file.open_r() as infile: - media_file = infile.read() - - else: - # TODO: Filter files that do not exist out sooner - debug_info.debug_not_found_sounds(kwargs["debugdir"], - kwargs["loglevel"], - source_file) - sound_count -= 1 - continue - - # The target path must be native - target_path = exportdir[request.targetdir, request.target_filename] - - # Start an export call in a worker process - # The call is asynchronous, so the next worker can be - # started immediately - pool.apply_async( - _export_sound, - args=( - media_file, - outqueue, - target_path - ) - ) - - # Log file information - if get_loglevel() <= logging.DEBUG: - MediaExporter.log_fileinfo( - source_file, - exportdir[request.targetdir, request.target_filename] - ) - - # Show progress - print(f"-- Files done: {format_progress(outqueue.qsize(), sound_count)}", - end = "\r", flush = True) - - # Close the pool since all workers have been started - pool.close() - - # Show progress for remaining workers - while outqueue.qsize() < sound_count: - print(f"-- Files done: {format_progress(outqueue.qsize(), sound_count)}", - end = "\r", flush = True) - - # Wait for all workers to finish - pool.join() - - @staticmethod - def _export_terrains( - requests: list[MediaExportRequest], - sourcedir: Path, - exportdir: Path, - palettes: dict[int, ColorTable], - game_version: GameVersion, - compression_level: int, - jobs: int = None - ) -> None: - """ - Convert and export terrain graphics files (multi-threaded). - - :param requests: Export requests for terrain graphics files. - :param sourcedir: Directory where all media assets are mounted. Source subfolder and - source filename should be stored in the export request. - :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder - and target filename should be stored in the export request. - :param game_version: Game edition and expansion info. - :param palettes: Palettes used by the game. - :param compression_level: PNG compression level for the resulting image file. - :param jobs: Number of worker processes to use (default: number of CPU cores). - :type requests: list[MediaExportRequest] - :type sourcedir: Directory - :type exportdir: Directory - :type palettes: dict - :type game_version: GameVersion - :type compression_level: int - :type jobs: int - """ - # Create a manager for sharing data between the workers and main process - with multiprocessing.Manager() as manager: - # Create a queue for data sharing - # it's not actually used for passing data, only for counting - # finished tasks - outqueue = manager.Queue() - - # Create a pool of workers - with multiprocessing.Pool(jobs) as pool: - for request in requests: - # Feed the worker with the source file data (bytes) from the - # main process - # - # This is necessary because some image files are inside an - # archive and cannot be accessed asynchronously - source_file = sourcedir[request.get_type().value, - request.source_filename] - - # The target path must be native - target_path = exportdir[request.targetdir, request.target_filename] - - # Start an export call in a worker process - # The call is asynchronous, so the next worker can be - # started immediately - pool.apply_async( - _export_terrain, - args=( - source_file.open("rb").read(), - outqueue, - request.source_filename, - target_path, - palettes, - compression_level, - game_version - ) - ) - - # Log file information - if get_loglevel() <= logging.DEBUG: - MediaExporter.log_fileinfo( - source_file, - exportdir[request.targetdir, request.target_filename] - ) - - # Show progress - print(f"-- Files done: {format_progress(outqueue.qsize(), len(requests))}", - end = "\r", flush = True) - - # Close the pool since all workers have been started - pool.close() - - # Show progress for remaining workers - while outqueue.qsize() < len(requests): - print(f"-- Files done: {format_progress(outqueue.qsize(), len(requests))}", - end = "\r", flush = True) - - # Wait for all workers to finish - pool.join() - @staticmethod def _get_media_cache( export_request: MediaExportRequest, From 477557051aa067a516655dacf4fd7955d60b4d43 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Jan 2024 05:54:26 +0100 Subject: [PATCH 192/771] convert: Fix job count not in args when started from 'main' entrypoint. --- openage/convert/main.py | 4 ++++ openage/convert/processor/export/media_exporter.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/openage/convert/main.py b/openage/convert/main.py index 9bf3f920ba..f89356402f 100644 --- a/openage/convert/main.py +++ b/openage/convert/main.py @@ -53,6 +53,10 @@ def convert_assets( if "compression_level" not in vars(args): args.compression_level = 1 + # Set worker count for multi-threading if it was not set + if "jobs" not in vars(args): + args.jobs = None + # Set verbosity for debug output if "debug_info" not in vars(args) or not args.debug_info: if args.devmode: diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 275f1e0708..c4a14b1ab9 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -64,9 +64,15 @@ def export( for media_type in export_requests.keys(): cur_export_requests = export_requests[media_type] + # Function for reading the source file data read_data_func = None + + # Multi-threaded function for exporting the source file data export_func = None + + # Optional function for handling data in the outqueue handle_outqueue_func = None + kwargs = {} if media_type is MediaType.BLEND: read_data_func = MediaExporter._get_blend_data From 4dbab823bbeb899a0ad60850d472dd4796cea170 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Jan 2024 07:10:08 +0100 Subject: [PATCH 193/771] convert: Allow both multi-threaded and single-threaded export. --- .../processor/export/media_exporter.py | 260 +++++++++++++----- 1 file changed, 192 insertions(+), 68 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index c4a14b1ab9..1d824a532e 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -10,6 +10,7 @@ import logging import os import multiprocessing +import queue from openage.convert.entity_object.export.texture import Texture from openage.convert.service import debug_info @@ -103,75 +104,30 @@ def export( itargs = (args.palettes, args.compression_level, args.game_version) info("-- Exporting terrain files...") - # Create a manager for sharing data between the workers and main process - with multiprocessing.Manager() as manager: - # Workers write the image metadata to this queue - # so that it can be forwarded to the export requests - # - # we cannot do this in a worker process directly - # because the export requests cannot be pickled - outqueue = manager.Queue() - - expected_size = len(cur_export_requests) - - worker_count = args.jobs - if worker_count is None: - worker_count = min(multiprocessing.cpu_count(), expected_size) - - # Create a pool of workers - with multiprocessing.Pool(worker_count) as pool: - for idx, request in enumerate(cur_export_requests): - # Feed the worker with the source file data (bytes) from the - # main process - # - # This is necessary because some image files are inside an - # archive and cannot be accessed asynchronously - source_data = read_data_func(request, sourcedir, **kwargs) - if source_data is None: - expected_size -= 1 - continue - - # The target path must be native - target_path = exportdir[request.targetdir, request.target_filename] - - # Start an export call in a worker process - # The call is asynchronous, so the next worker can be - # started immediately - pool.apply_async( - export_func, - args=( - idx, - source_data, - outqueue, - request.source_filename, - target_path, - *itargs - ), - kwds=kwargs - ) - - # Log file information - if get_loglevel() <= logging.DEBUG: - MediaExporter.log_fileinfo( - sourcedir[request.get_type().value, request.source_filename], - exportdir[request.targetdir, request.target_filename] - ) - - # Show progress - MediaExporter._show_progress(outqueue.qsize(), expected_size) - - # Close the pool since all workers have been started - pool.close() - - # Show progress for remaining workers - while outqueue.qsize() < expected_size: - MediaExporter._show_progress(outqueue.qsize(), expected_size) - - # Wait for all workers to finish - pool.join() + if args.jobs == 1: + MediaExporter._export_singlethreaded( + cur_export_requests, + sourcedir, + exportdir, + read_data_func, + export_func, + handle_outqueue_func, + itargs, + kwargs + ) - if handle_outqueue_func: - handle_outqueue_func(outqueue, cur_export_requests) + else: + MediaExporter._export_multithreaded( + cur_export_requests, + sourcedir, + exportdir, + read_data_func, + export_func, + handle_outqueue_func, + itargs, + kwargs, + args.jobs + ) if args.debug_info > 5: cachedata = {} @@ -197,6 +153,174 @@ def export( args.game_version ) + @staticmethod + def _export_singlethreaded( + requests: list[MediaExportRequest], + sourcedir: Path, + exportdir: Path, + read_data_func: typing.Callable, + export_func: typing.Callable, + handle_outqueue_func: typing.Callable | None, + itargs: tuple, + kwargs: dict + ): + """ + Export media files in a single thread. + + :param requests: Export requests for media files. + :param sourcedir: Directory where all media assets are mounted. Source subfolder and + source filename should be stored in the export request. + :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder + and target filename should be stored in the export request. + :param read_data_func: Function for reading the source file data. + :param export_func: Function for exporting media files. + :param handle_outqueue_func: Optional function for handling data in the outqueue. + :param itargs: Arguments for the export function. + :param kwargs: Keyword arguments for the export function. + :type requests: list[MediaExportRequest] + :type sourcedir: Path + :type exportdir: Path + :type read_data_func: typing.Callable + :type export_func: typing.Callable + :type handle_outqueue_func: typing.Callable + :type itargs: tuple + :type kwargs: dict + """ + single_queue = queue.Queue() + for idx, request in enumerate(requests): + source_data = read_data_func(request, sourcedir, **kwargs) + if source_data is None: + continue + + target_path = exportdir[request.targetdir, request.target_filename] + + export_func( + idx, + source_data, + single_queue, + request.source_filename, + target_path, + *itargs, + **kwargs + ) + + if get_loglevel() <= logging.DEBUG: + MediaExporter.log_fileinfo( + sourcedir[request.get_type().value, request.source_filename], + exportdir[request.targetdir, request.target_filename] + ) + + MediaExporter._show_progress(idx + 1, len(requests)) + + if handle_outqueue_func: + handle_outqueue_func(single_queue, requests) + + @staticmethod + def _export_multithreaded( + requests: list[MediaExportRequest], + sourcedir: Path, + exportdir: Path, + read_data_func: typing.Callable, + export_func: typing.Callable, + handle_outqueue_func: typing.Callable | None, + itargs: tuple, + kwargs: dict, + job_count: int = None + ): + """ + Export media files in multiple threads. + + :param requests: Export requests for media files. + :param sourcedir: Directory where all media assets are mounted. Source subfolder and + source filename should be stored in the export request. + :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder + and target filename should be stored in the export request. + :param read_data_func: Function for reading the source file data. + :param export_func: Function for exporting media files. + :param handle_outqueue_func: Optional function for handling data in the outqueue. + :param itargs: Arguments for the export function. + :param kwargs: Keyword arguments for the export function. + :param job_count: Number of worker processes to use. + :type requests: list[MediaExportRequest] + :type sourcedir: Path + :type exportdir: Path + :type read_data_func: typing.Callable + :type export_func: typing.Callable + :type handle_outqueue_func: typing.Callable + :type itargs: tuple + :type kwargs: dict + :type job_count: int + """ + worker_count = job_count + if worker_count is None: + # Small optimization that saves some time for small exports + worker_count = min(multiprocessing.cpu_count(), len(requests)) + + # Create a manager for sharing data between the workers and main process + with multiprocessing.Manager() as manager: + # Workers write the image metadata to this queue + # so that it can be forwarded to the export requests + # + # we cannot do this in a worker process directly + # because the export requests cannot be pickled + outqueue = manager.Queue() + + expected_size = len(requests) + + # Create a pool of workers + with multiprocessing.Pool(worker_count) as pool: + for idx, request in enumerate(requests): + # Feed the worker with the source file data (bytes) from the + # main process + # + # This is necessary because some image files are inside an + # archive and cannot be accessed asynchronously + source_data = read_data_func(request, sourcedir, **kwargs) + if source_data is None: + expected_size -= 1 + continue + + target_path = exportdir[request.targetdir, request.target_filename] + + # Start an export call in a worker process + # The call is asynchronous, so the next worker can be + # started immediately + pool.apply_async( + export_func, + args=( + idx, + source_data, + outqueue, + request.source_filename, + target_path, + *itargs + ), + kwds=kwargs + ) + + # Log file information + if get_loglevel() <= logging.DEBUG: + MediaExporter.log_fileinfo( + sourcedir[request.get_type().value, request.source_filename], + exportdir[request.targetdir, request.target_filename] + ) + + # Show progress + MediaExporter._show_progress(outqueue.qsize(), expected_size) + + # Close the pool since all workers have been started + pool.close() + + # Show progress for remaining workers + while outqueue.qsize() < expected_size: + MediaExporter._show_progress(outqueue.qsize(), expected_size) + + # Wait for all workers to finish + pool.join() + + if handle_outqueue_func: + handle_outqueue_func(outqueue, requests) + @staticmethod def _get_blend_data( request: MediaExportRequest, From 97a67c7d83b0ed41ea2a3764e29725926e739acb Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Jan 2024 15:03:04 +0100 Subject: [PATCH 194/771] util: Replace file collection lambdas with file entry objects. --- openage/cabextract/cab.py | 43 +++++++---- .../convert/value_object/read/media/drs.py | 32 +++++--- openage/util/fslike/filecollection.py | 76 +++++++++++++------ 3 files changed, 103 insertions(+), 48 deletions(-) diff --git a/openage/cabextract/cab.py b/openage/cabextract/cab.py index 3f04d45947..dde6d09aac 100644 --- a/openage/cabextract/cab.py +++ b/openage/cabextract/cab.py @@ -1,4 +1,4 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. """ Provides CABFile, an extractor for the MSCAB format. @@ -18,7 +18,7 @@ from ..util.filelike.readonly import PosSavingReadOnlyFileLikeObject from ..util.filelike.stream import StreamFragment from ..util.files import read_guaranteed, read_nullterminated_string -from ..util.fslike.filecollection import FileCollection +from ..util.fslike.filecollection import FileCollection, FileEntry from ..util.math import INF from ..util.strings import try_decode from ..util.struct import NamedStruct, Flags @@ -216,6 +216,28 @@ def verify_checksum(self) -> Union[None, NoReturn]: raise ValueError("checksum error in MSCAB data block") +class CABEntry(FileEntry): + """ + Entry in a CAB file. + """ + + def __init__(self, fileobj: CFFile): + self.fileobj = fileobj + + def open_r(self): + return StreamFragment( + self.fileobj.folder.plain_stream, + self.fileobj.pos, + self.fileobj.size + ) + + def size(self) -> int: + return self.fileobj.size + + def mtime(self) -> float: + return self.fileobj.timestamp + + class CABFile(FileCollection): """ The actual file system-like CAB object. @@ -275,20 +297,9 @@ def __init__(self, cab: FileLikeObject, offset: int = 0): "CABFile has multiple entries with the same path: " + b'/'.join(fileobj.path).decode()) - def open_r(fileobj=fileobj): - """ Returns a opened ('rb') file-like object for fileobj. """ - return StreamFragment( - fileobj.folder.plain_stream, - fileobj.pos, - fileobj.size - ) - - self.add_fileentry(fileobj.path, ( - open_r, - None, - lambda fileobj=fileobj: fileobj.size, - lambda fileobj=fileobj: fileobj.timestamp - )) + file_entry = CABEntry(fileobj) + + self.add_fileentry(fileobj.path, file_entry) def __repr__(self): return "CABFile" diff --git a/openage/convert/value_object/read/media/drs.py b/openage/convert/value_object/read/media/drs.py index 72bf6a0679..8e3410f614 100644 --- a/openage/convert/value_object/read/media/drs.py +++ b/openage/convert/value_object/read/media/drs.py @@ -1,4 +1,4 @@ -# Copyright 2013-2022 the openage authors. See copying.md for legal info. +# Copyright 2013-2024 the openage authors. See copying.md for legal info. """ Code for reading Genie .DRS archives. @@ -12,7 +12,7 @@ from .....log import spam, dbg from .....util.filelike.stream import StreamFragment -from .....util.fslike.filecollection import FileCollection +from .....util.fslike.filecollection import FileCollection, FileEntry from .....util.strings import decode_until_null from .....util.struct import NamedStruct @@ -87,6 +87,23 @@ class DRSFileInfo(NamedStruct): file_size = "i" +class DRSEntry(FileEntry): + """ + Entry in a DRS archive. + """ + + def __init__(self, fileobj: GuardedFile, offset: int, size: int): + self.fileobj = fileobj + self.offset = offset + self.entry_size = size + + def open_r(self): + return StreamFragment(self.fileobj, self.offset, self.entry_size) + + def size(self) -> int: + return self.entry_size + + class DRS(FileCollection): """ represents a file archive in DRS format. @@ -133,14 +150,9 @@ def __init__(self, fileobj: GuardedFile, game_version: GameVersion): self.tables.append(table_header) for filename, offset, size in self.read_tables(): - def open_r(offset=offset, size=size): - """ Returns a opened ('rb') file-like object for fileobj. """ - return StreamFragment(self.fileobj, offset, size) - - self.add_fileentry( - [filename.encode()], - (open_r, None, lambda size=size: size, None) - ) + file_entry = DRSEntry(self.fileobj, offset, size) + + self.add_fileentry([filename.encode()], file_entry) def read_tables(self) -> typing.Generator[tuple[str, str, str], None, None]: """ diff --git a/openage/util/fslike/filecollection.py b/openage/util/fslike/filecollection.py index 86df01533b..f77b034099 100644 --- a/openage/util/fslike/filecollection.py +++ b/openage/util/fslike/filecollection.py @@ -1,9 +1,11 @@ -# Copyright 2015-2022 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. """ Provides Filecollection, a utility class for combining multiple file-like objects to a FSLikeObject. """ +from __future__ import annotations +import typing from collections import OrderedDict from io import UnsupportedOperation @@ -12,6 +14,9 @@ from .abstract import FSLikeObject from .path import Path +if typing.TYPE_CHECKING: + from openage.util.filelike.stream import StreamFragment + class FileCollection(FSLikeObject): """ @@ -59,14 +64,12 @@ def get_direntries(self, parts=None, create: bool = False) -> tuple[OrderedDict, return entries - def add_fileentry(self, parts, fileentry): + def add_fileentry(self, parts, fileentry: FileEntry): """ Adds a file entry (and parent directory entries, if needed). This method should not be called directly; instead, use the add_file method of Path objects that were obtained from this. - - fileentry must be open_r, open_w, size, mtime. """ if not parts: raise IsADirectoryError("FileCollection.root is a directory") @@ -79,11 +82,9 @@ def add_fileentry(self, parts, fileentry): entries[0][name] = fileentry - def get_fileentry(self, parts): + def get_fileentry(self, parts) -> FileEntry: """ Gets a file entry. Helper method for internal use. - - Returns open_r, open_w, size, mtime """ if not parts: raise IsADirectoryError( @@ -101,24 +102,30 @@ def get_fileentry(self, parts): return entries[0][name] - def open_r(self, parts) -> None: - open_r, _, _, _ = self.get_fileentry(parts) + def open_r(self, parts: list[bytes]) -> StreamFragment: + entry = self.get_fileentry(parts) + + open_r = entry.open_r() if open_r is None: raise UnsupportedOperation( "not readable: " + b"/".join(parts).decode(errors='replace')) - return open_r() + return open_r + + def open_w(self, parts: list[bytes]): + entry = self.get_fileentry(parts) - def open_w(self, parts) -> None: - _, open_w, _, _ = self.get_fileentry(parts) + open_w = entry.open_w() if open_w is None: raise UnsupportedOperation( "not writable: " + b"/".join(parts).decode(errors='replace')) + return open_w + def list(self, parts): fileentries, subdirs = self.get_direntries(parts) @@ -126,20 +133,14 @@ def list(self, parts): yield from fileentries def filesize(self, parts) -> int: - _, _, filesize, _ = self.get_fileentry(parts) + entry = self.get_fileentry(parts) - if filesize is None: - return None - - return filesize() + return entry.size() def mtime(self, parts) -> float: - _, _, _, mtime = self.get_fileentry(parts) - - if mtime is None: - return None + entry = self.get_fileentry(parts) - return mtime() + return entry.mtime() def mkdirs(self, parts) -> None: self.get_direntries(parts, create=True) @@ -248,3 +249,34 @@ def add_file_from_path(self, path: Path) -> None: open_w = None self.add_file(path.open_r, open_w, path.filesize, path.mtime) + + +class FileEntry: + """ + Entry in a file collection archive. + """ + # pylint: disable=no-self-use + + def open_r(self) -> StreamFragment: + """ + Returns a file-like object for reading. + """ + raise UnsupportedOperation("FileEntry.open_r") + + def open_w(self): + """ + Returns a file-like object for writing. + """ + raise UnsupportedOperation("FileEntry.open_w") + + def size(self) -> int: + """ + Returns the size of the entr<. + """ + raise UnsupportedOperation("FileEntry.size") + + def mtime(self) -> float: + """ + Returns the modification time of the entry. + """ + raise UnsupportedOperation("FileEntry.mtime") From fc95209b977c8909d009995c1c0d493d9603d115 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 4 Feb 2024 19:39:47 +0100 Subject: [PATCH 195/771] clang-format: Format multi-line comments. --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index b870e0ee95..0ed596d6b9 100644 --- a/.clang-format +++ b/.clang-format @@ -100,7 +100,7 @@ PenaltyBreakTemplateDeclaration: 10 PenaltyExcessCharacter: 1000000 PenaltyReturnTypeOnItsOwnLine: 200 PointerAlignment: Right -ReflowComments: false +ReflowComments: true SortIncludes: CaseInsensitive SortUsingDeclarations: true SpaceAfterCStyleCast: false From a41f9ebbb3c8fef3c8b243cc3b89d1a7d97446a0 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 4 Feb 2024 20:55:01 +0100 Subject: [PATCH 196/771] clang-format: Align trailing comments. --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index 0ed596d6b9..9264f3bc0f 100644 --- a/.clang-format +++ b/.clang-format @@ -12,7 +12,7 @@ AlignConsecutiveDeclarations: false AlignConsecutiveMacros: false AlignEscapedNewlines: DontAlign AlignOperands: Align -AlignTrailingComments: false +AlignTrailingComments: true AllowAllArgumentsOnNextLine: true AllowAllParametersOfDeclarationOnNextLine: true AllowShortBlocksOnASingleLine: Never From ab4f7a88ea33388b082b61e829ec330c252a9de4 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 4 Feb 2024 20:42:17 +0100 Subject: [PATCH 197/771] refactor: Reformat everything with clang-format. Converts spaces in multi-line comments to tabs. Also fixes a few unformatted files. --- libopenage/assets/mod_manager.h | 132 ++++----- libopenage/assets/modpack.h | 50 ++-- libopenage/audio/category.h | 10 +- libopenage/audio/dynamic_loader.h | 10 +- libopenage/audio/dynamic_resource.h | 26 +- libopenage/audio/error.h | 5 +- libopenage/audio/format.h | 7 +- libopenage/audio/hash_functions.h | 12 +- libopenage/audio/in_memory_loader.h | 7 +- libopenage/audio/in_memory_resource.h | 10 +- libopenage/audio/loader_policy.h | 7 +- libopenage/audio/opus_dynamic_loader.h | 11 +- libopenage/audio/opus_in_memory_loader.h | 7 +- libopenage/audio/opus_loading.h | 7 +- libopenage/audio/resource.h | 14 +- libopenage/audio/resource_def.h | 5 +- libopenage/audio/sound.h | 7 +- libopenage/audio/types.h | 5 +- libopenage/console/buf.h | 41 ++- libopenage/coord/chunk.h | 4 +- libopenage/curve/base_curve.h | 72 ++--- libopenage/curve/discrete_mod.h | 6 +- libopenage/curve/interpolated.h | 6 +- libopenage/curve/keyframe_container.h | 88 +++--- libopenage/curve/queue.h | 24 +- libopenage/cvar/cvar.h | 6 +- libopenage/datastructure/concurrent_queue.h | 20 +- libopenage/datastructure/pairing_heap.h | 45 ++- libopenage/engine/engine.h | 42 +-- libopenage/error/backtrace.h | 27 +- libopenage/error/handlers.h | 5 +- libopenage/event/demo/aicontroller.h | 4 +- libopenage/event/demo/main.h | 4 +- libopenage/event/event.h | 6 +- libopenage/event/event_loop.h | 96 +++--- libopenage/event/state.h | 6 +- libopenage/gamestate/activity/activity.h | 50 ++-- libopenage/gamestate/activity/end_node.h | 12 +- libopenage/gamestate/activity/task_node.h | 44 +-- .../gamestate/activity/task_system_node.h | 32 +- .../gamestate/activity/xor_event_gate.h | 52 ++-- libopenage/gamestate/activity/xor_gate.h | 80 ++--- libopenage/gamestate/api/ability.h | 42 +-- libopenage/gamestate/api/activity.h | 128 ++++---- libopenage/gamestate/api/animation.h | 46 +-- libopenage/gamestate/api/patch.h | 42 +-- libopenage/gamestate/api/player_setup.h | 50 ++-- libopenage/gamestate/api/property.h | 62 ++-- libopenage/gamestate/api/sound.h | 46 +-- libopenage/gamestate/api/terrain.h | 30 +- libopenage/gamestate/component/api/live.h | 26 +- .../gamestate/component/api_component.h | 10 +- .../gamestate/component/base_component.h | 10 +- .../gamestate/component/internal/activity.h | 86 +++--- .../internal/commands/base_command.h | 10 +- .../component/internal/commands/custom.h | 22 +- .../component/internal/commands/move.h | 22 +- .../gamestate/component/internal/ownership.h | 14 +- .../gamestate/component/internal/position.h | 84 +++--- libopenage/gamestate/entity_factory.h | 30 +- libopenage/gamestate/event/spawn_entity.h | 20 +- libopenage/gamestate/game.h | 56 ++-- libopenage/gamestate/game_entity.h | 60 ++-- libopenage/gamestate/game_state.h | 98 +++--- libopenage/gamestate/player.h | 6 +- libopenage/gamestate/simulation.h | 74 ++--- libopenage/gamestate/system/activity.h | 14 +- libopenage/gamestate/system/idle.h | 24 +- libopenage/gamestate/system/move.h | 38 +-- libopenage/gamestate/terrain.h | 30 +- libopenage/gamestate/terrain_chunk.h | 44 +-- libopenage/gamestate/terrain_factory.h | 36 +-- libopenage/gamestate/terrain_tile.h | 22 +- libopenage/gamestate/universe.h | 4 +- libopenage/gamestate/world.h | 8 +- .../input/controller/camera/binding_context.h | 34 +-- .../input/controller/game/binding_context.h | 34 +-- .../input/controller/hud/binding_context.h | 34 +-- libopenage/input/controller/hud/controller.h | 22 +- libopenage/input/event.h | 10 +- libopenage/input/input_context.h | 118 ++++---- libopenage/input/input_manager.h | 78 ++--- libopenage/job/abortable_job_state.h | 10 +- libopenage/job/job.h | 14 +- libopenage/job/job_aborted_exception.h | 5 +- libopenage/job/job_group.h | 14 +- libopenage/job/job_manager.h | 22 +- libopenage/job/job_state.h | 13 +- libopenage/job/job_state_base.h | 6 +- libopenage/job/typed_job_state_base.h | 21 +- libopenage/job/types.h | 16 +- libopenage/job/worker.h | 6 +- libopenage/log/logsink.h | 8 +- libopenage/main/demo/pong/aicontroller.h | 7 +- libopenage/presenter/presenter.h | 6 +- libopenage/pyinterface/defs.h | 4 +- libopenage/pyinterface/exctranslate.h | 5 +- libopenage/pyinterface/exctranslate_tests.h | 6 +- libopenage/pyinterface/functional.h | 144 ++++----- libopenage/pyinterface/pyexception.h | 10 +- libopenage/pyinterface/pyobject.h | 33 +-- libopenage/pyinterface/pyobject_tests.h | 6 +- libopenage/pyinterface/setup.h | 7 +- libopenage/renderer/camera/camera.h | 280 +++++++++--------- libopenage/renderer/color.h | 9 +- libopenage/renderer/font/font.h | 16 +- libopenage/renderer/font/font_manager.h | 11 +- libopenage/renderer/font/glyph_atlas.h | 7 +- libopenage/renderer/geometry.h | 13 +- libopenage/renderer/gui/gui.h | 10 +- .../renderer/gui/guisys/link/gui_item.h | 18 +- .../gui/guisys/link/gui_item_list_model.h | 7 +- .../gui/guisys/private/gui_engine_impl.h | 6 +- libopenage/renderer/opengl/context.h | 64 ++-- libopenage/renderer/opengl/debug.h | 4 +- libopenage/renderer/opengl/framebuffer.h | 60 ++-- libopenage/renderer/opengl/render_target.h | 80 ++--- libopenage/renderer/opengl/shader_data.h | 34 +-- libopenage/renderer/opengl/shader_program.h | 50 ++-- libopenage/renderer/opengl/uniform_buffer.h | 54 ++-- libopenage/renderer/opengl/uniform_input.h | 26 +- libopenage/renderer/render_factory.h | 48 +-- .../renderer/resources/animation/angle_info.h | 28 +- .../renderer/resources/animation/frame_info.h | 14 +- .../renderer/resources/animation/layer_info.h | 14 +- .../renderer/resources/assets/asset_manager.h | 108 +++---- libopenage/renderer/resources/assets/cache.h | 70 ++--- .../resources/assets/texture_manager.h | 92 +++--- libopenage/renderer/resources/buffer_info.h | 82 ++--- libopenage/renderer/resources/frame_timing.h | 106 +++---- libopenage/renderer/resources/palette_info.h | 18 +- .../resources/terrain/blendtable_info.h | 6 +- .../renderer/resources/terrain/frame_info.h | 30 +- libopenage/renderer/resources/texture_info.h | 6 +- .../renderer/resources/texture_subinfo.h | 82 ++--- libopenage/renderer/shader_program.h | 14 +- libopenage/renderer/stages/camera/manager.h | 100 +++---- libopenage/renderer/stages/hud/object.h | 116 ++++---- .../renderer/stages/hud/render_entity.h | 42 +-- libopenage/renderer/stages/hud/render_stage.h | 30 +- .../renderer/stages/screen/render_stage.h | 14 +- .../renderer/stages/skybox/render_stage.h | 44 +-- libopenage/renderer/stages/terrain/chunk.h | 78 ++--- libopenage/renderer/stages/terrain/mesh.h | 126 ++++---- libopenage/renderer/stages/terrain/model.h | 56 ++-- .../renderer/stages/terrain/render_entity.h | 50 ++-- .../renderer/stages/terrain/render_stage.h | 26 +- libopenage/renderer/stages/world/object.h | 100 +++---- .../renderer/stages/world/render_entity.h | 26 +- .../renderer/stages/world/render_stage.h | 34 +-- libopenage/renderer/texture.h | 11 +- libopenage/renderer/types.h | 4 +- libopenage/renderer/uniform_buffer.h | 10 +- libopenage/renderer/uniform_input.h | 4 +- libopenage/renderer/vulkan/graphics_device.h | 8 +- libopenage/renderer/vulkan/loader.h | 16 +- libopenage/renderer/vulkan/render_target.h | 8 +- libopenage/renderer/vulkan/renderer.h | 15 +- libopenage/renderer/vulkan/util.h | 12 +- libopenage/rng/rng.h | 12 +- libopenage/testing/testing.h | 38 ++- libopenage/testing/testlist.h | 5 +- libopenage/time/clock.h | 58 ++-- libopenage/time/time_loop.h | 26 +- libopenage/util/algorithm.h | 6 +- libopenage/util/compiler.h | 12 +- libopenage/util/compress/bitstream.h | 29 +- libopenage/util/compress/lzxd.h | 14 +- libopenage/util/constexpr.h | 13 +- libopenage/util/constinit_vector.h | 20 +- libopenage/util/enum.h | 49 +-- libopenage/util/enum_test.h | 6 +- libopenage/util/externalsstream.h | 8 +- libopenage/util/fds.h | 5 +- libopenage/util/file.h | 15 +- libopenage/util/filelike/filelike.h | 18 +- libopenage/util/filelike/native.h | 10 +- libopenage/util/filelike/python.h | 8 +- libopenage/util/fixed_point.h | 6 +- libopenage/util/fps.h | 5 +- libopenage/util/fslike/directory.h | 8 +- libopenage/util/fslike/fslike.h | 6 +- libopenage/util/fslike/native.h | 6 +- libopenage/util/fslike/python.h | 44 +-- libopenage/util/hash.h | 12 +- libopenage/util/init.h | 20 +- libopenage/util/language.h | 13 +- libopenage/util/macro/concat.h | 17 +- libopenage/util/macro/loop.h | 15 +- libopenage/util/math.h | 9 +- libopenage/util/math_constants.h | 10 +- libopenage/util/misc.h | 49 ++- libopenage/util/os.h | 4 +- libopenage/util/pty.h | 14 +- libopenage/util/quaternion.h | 110 ++++--- libopenage/util/signal.h | 6 +- libopenage/util/stringformatter.h | 51 ++-- libopenage/util/strings.h | 18 +- libopenage/util/subprocess.h | 6 +- libopenage/util/thread_id.h | 5 +- libopenage/util/timer.h | 9 +- libopenage/util/timing.h | 5 +- libopenage/util/unicode.h | 5 +- libopenage/util/variable.h | 23 +- libopenage/util/vector.h | 60 ++-- libopenage/versions/versions.h | 4 +- 206 files changed, 3217 insertions(+), 3163 deletions(-) diff --git a/libopenage/assets/mod_manager.h b/libopenage/assets/mod_manager.h index b049603f83..3866130b22 100644 --- a/libopenage/assets/mod_manager.h +++ b/libopenage/assets/mod_manager.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 @@ -22,67 +22,67 @@ class ModManager { ~ModManager() = default; /** - * Adds a modpack to the list of available modpacks. - * - * @param info_file Path to the modpack definition file. - */ + * Adds a modpack to the list of available modpacks. + * + * @param info_file Path to the modpack definition file. + */ void register_modpack(const util::Path &info_file); /** - * Adds a modpack to the list of available modpacks. - * - * @param info Modpack definition. - */ + * Adds a modpack to the list of available modpacks. + * + * @param info Modpack definition. + */ void register_modpack(const ModpackInfo &info); /** - * Ready a set of modpacks with the given IDs. - * - * This prepares the modpacks for loading by valiating the integrity of - * the contained data, checking if the load order and mounting the - * assets into the virtual filesystem. - * - * TODO: - * - Mount modpacks into virtual filesystem. - * - Validate manifest.toml - * - Verify signature of manifest.toml (if signed) - * - * Note that data inside the modpack is not loaded yet. Data - * loading is done inside the simulation before the game starts or - * in case of media files, when they are requested during the game. - * - * Modpacks must have been registered with \p register_modpack() before - * activating. - * - * @param load_order Load order of modpacks. - */ + * Ready a set of modpacks with the given IDs. + * + * This prepares the modpacks for loading by valiating the integrity of + * the contained data, checking if the load order and mounting the + * assets into the virtual filesystem. + * + * TODO: + * - Mount modpacks into virtual filesystem. + * - Validate manifest.toml + * - Verify signature of manifest.toml (if signed) + * + * Note that data inside the modpack is not loaded yet. Data + * loading is done inside the simulation before the game starts or + * in case of media files, when they are requested during the game. + * + * Modpacks must have been registered with \p register_modpack() before + * activating. + * + * @param load_order Load order of modpacks. + */ void activate_modpacks(const std::vector &load_order); /** - * Get a loaded modpack by its ID. - * - * @param modpack_id ID of the modpack to get. - * - * @return Modpack with the given ID. - */ + * Get a loaded modpack by its ID. + * + * @param modpack_id ID of the modpack to get. + * + * @return Modpack with the given ID. + */ std::shared_ptr get_modpack(const std::string &modpack_id) const; /** - * Get the load order of modpacks. - * - * @return Modpack IDs in the order that they should be loaded. - */ + * Get the load order of modpacks. + * + * @return Modpack IDs in the order that they should be loaded. + */ const std::vector &get_load_order() const; /** - * Enumerates all modpack ids in a given directory. - * - * This also loads available modpack definition files. - * - * @param directory Path to the directory to enumerate. - * - * @return Infos of the identified modpacks. - */ + * Enumerates all modpack ids in a given directory. + * + * This also loads available modpack definition files. + * + * @param directory Path to the directory to enumerate. + * + * @return Infos of the identified modpacks. + */ static std::vector enumerate_modpacks(const util::Path &directory) { std::vector result; @@ -106,37 +106,37 @@ class ModManager { private: /** - * Set the order in which modpack data should be loaded. - * - * This also checks whether the given load order is valid, i.e. - * by checking if all dependencies/conflicts are resolved. - * - * TODO: Dynamically resolve load order? - * - * @param load_order Load order of modpacks. - */ + * Set the order in which modpack data should be loaded. + * + * This also checks whether the given load order is valid, i.e. + * by checking if all dependencies/conflicts are resolved. + * + * TODO: Dynamically resolve load order? + * + * @param load_order Load order of modpacks. + */ void set_load_order(const std::vector &load_order); /** - * TODO: Mount point for modpacks. - */ + * TODO: Mount point for modpacks. + */ util::Path asset_base_dir; /** - * Active modpacks. Maps their ID ('name' in the modpack definition file) - * to the modpack. - */ + * Active modpacks. Maps their ID ('name' in the modpack definition file) + * to the modpack. + */ std::unordered_map> active; /** - * Available modpacks that can be activated. Maps their ID ('name' in the modpack - * definition file) to the modpack info. - */ + * Available modpacks that can be activated. Maps their ID ('name' in the modpack + * definition file) to the modpack info. + */ std::unordered_map available; /** - * Load order of modpacks. - */ + * Load order of modpacks. + */ std::vector load_order; }; diff --git a/libopenage/assets/modpack.h b/libopenage/assets/modpack.h index 7c858b5ed7..6d8b2f888d 100644 --- a/libopenage/assets/modpack.h +++ b/libopenage/assets/modpack.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 @@ -79,26 +79,26 @@ ModpackInfo parse_modepack_def(const util::Path &info_file); class Modpack { public: /** - * Create a new modpack. - * - * Loads the modpack using the information in the definition file. - * - * @param info_file Path to the modpack definition file. - */ + * Create a new modpack. + * + * Loads the modpack using the information in the definition file. + * + * @param info_file Path to the modpack definition file. + */ Modpack(const util::Path &info_file); /** - * Create a new modpack from an existing modpack info. - * - * @param info Modpack metadata information. - */ + * Create a new modpack from an existing modpack info. + * + * @param info Modpack metadata information. + */ Modpack(const ModpackInfo &info); /** - * Create a new modpack from an existing modpack info. - * - * @param info Modpack metadata information. - */ + * Create a new modpack from an existing modpack info. + * + * @param info Modpack metadata information. + */ Modpack(const ModpackInfo &&info); Modpack(const Modpack &) = delete; @@ -106,23 +106,23 @@ class Modpack { ~Modpack() = default; /** - * Get the metadata information of the modpack. - * - * @return Modpack metadata information. - */ + * Get the metadata information of the modpack. + * + * @return Modpack metadata information. + */ const ModpackInfo &get_info() const; /** - * Check if the modpack is valid. - * - * @return true if the modpack is valid, false otherwise. - */ + * Check if the modpack is valid. + * + * @return true if the modpack is valid, false otherwise. + */ bool check_integrity() const; private: /** - * Modpack metadata information. - */ + * Modpack metadata information. + */ ModpackInfo info; }; diff --git a/libopenage/audio/category.h b/libopenage/audio/category.h index 074ebe29b3..8d208fb0a8 100644 --- a/libopenage/audio/category.h +++ b/libopenage/audio/category.h @@ -1,12 +1,10 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once #include -namespace openage { -namespace audio { - +namespace openage::audio { enum class category_t { GAME, @@ -17,7 +15,7 @@ enum class category_t { const char *category_t_to_str(category_t val); -std::ostream &operator <<(std::ostream &os, category_t val); +std::ostream &operator<<(std::ostream &os, category_t val); -}} // openage::audio +} // namespace openage::audio diff --git a/libopenage/audio/dynamic_loader.h b/libopenage/audio/dynamic_loader.h index b8b0ade984..703982c121 100644 --- a/libopenage/audio/dynamic_loader.h +++ b/libopenage/audio/dynamic_loader.h @@ -1,13 +1,13 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once #include #include +#include "../util/path.h" #include "format.h" #include "types.h" -#include "../util/path.h" namespace openage { @@ -43,7 +43,8 @@ class DynamicLoader { * @param offset the offset from the resource's beginning * @param chunk_size the number of int16_t values that fit in one chunk */ - virtual size_t load_chunk(int16_t *chunk_buffer, size_t offset, + virtual size_t load_chunk(int16_t *chunk_buffer, + size_t offset, size_t chunk_size) = 0; /** @@ -55,4 +56,5 @@ class DynamicLoader { format_t format); }; -}} // openage::audio +} // namespace audio +} // namespace openage diff --git a/libopenage/audio/dynamic_resource.h b/libopenage/audio/dynamic_resource.h index 1f98116f51..e1605d5487 100644 --- a/libopenage/audio/dynamic_resource.h +++ b/libopenage/audio/dynamic_resource.h @@ -1,4 +1,4 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -8,15 +8,15 @@ #include #include +#include "../datastructure/concurrent_queue.h" +#include "../job/job.h" +#include "../job/job_group.h" +#include "../util/path.h" #include "category.h" #include "dynamic_loader.h" #include "format.h" #include "resource.h" #include "types.h" -#include "../datastructure/concurrent_queue.h" -#include "../job/job.h" -#include "../job/job_group.h" -#include "../util/path.h" namespace openage { namespace audio { @@ -59,10 +59,10 @@ class DynamicResource : public Resource { category_t category, int id, const util::Path &path, - format_t format=format_t::OPUS, - int preload_amount=DEFAULT_PRELOAD_AMOUNT, - size_t chunk_size=DEFAULT_CHUNK_SIZE, - size_t max_chunks=DEFAULT_MAX_CHUNKS); + format_t format = format_t::OPUS, + int preload_amount = DEFAULT_PRELOAD_AMOUNT, + size_t chunk_size = DEFAULT_CHUNK_SIZE, + size_t max_chunks = DEFAULT_MAX_CHUNKS); virtual ~DynamicResource() = default; @@ -95,7 +95,7 @@ class DynamicResource : public Resource { static constexpr int DEFAULT_PRELOAD_AMOUNT = 10; /** The default used chunk size in bytes (100ms for 48kHz stereo audio). */ - static constexpr size_t DEFAULT_CHUNK_SIZE = 9600*2; + static constexpr size_t DEFAULT_CHUNK_SIZE = 9600 * 2; /** The default number of chunks, that can be loaded at the same time. */ static constexpr size_t DEFAULT_MAX_CHUNKS = 100; @@ -138,11 +138,11 @@ class DynamicResource : public Resource { * Resource chunk index to chunk mapping. * Loading and usage state is reached through this. */ - std::unordered_map> chunks; + std::unordered_map> chunks; /** The background loading job group. */ job::JobGroup loading_job_group; }; -} -} +} // namespace audio +} // namespace openage diff --git a/libopenage/audio/error.h b/libopenage/audio/error.h index 147df0e520..75dade0dd8 100644 --- a/libopenage/audio/error.h +++ b/libopenage/audio/error.h @@ -1,4 +1,4 @@ -// Copyright 2017-2017 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -12,4 +12,5 @@ class Error : public error::Error { Error(const log::message &msg); }; -}} // openage::audio +} // namespace audio +} // namespace openage diff --git a/libopenage/audio/format.h b/libopenage/audio/format.h index 37060abcbc..6fe6e92c1b 100644 --- a/libopenage/audio/format.h +++ b/libopenage/audio/format.h @@ -1,4 +1,4 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -17,6 +17,7 @@ enum class format_t { const char *format_t_to_str(format_t val); -std::ostream &operator <<(std::ostream &os, format_t val); +std::ostream &operator<<(std::ostream &os, format_t val); -}} // openage::audio +} // namespace audio +} // namespace openage diff --git a/libopenage/audio/hash_functions.h b/libopenage/audio/hash_functions.h index 8e57a30fae..5fc18d3fcb 100644 --- a/libopenage/audio/hash_functions.h +++ b/libopenage/audio/hash_functions.h @@ -1,4 +1,4 @@ -// Copyright 2014-2016 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -8,18 +8,18 @@ namespace std { -template<> +template <> struct hash<::openage::audio::category_t> { size_t operator()(const ::openage::audio::category_t &c) const { return static_cast(c); } }; -template<> -struct hash> { - size_t operator()(const std::tuple<::openage::audio::category_t,int> &t) const { +template <> +struct hash> { + size_t operator()(const std::tuple<::openage::audio::category_t, int> &t) const { return static_cast(std::get<0>(t)) << (sizeof(size_t) * 8 - 2) | std::get<1>(t); } }; -} +} // namespace std diff --git a/libopenage/audio/in_memory_loader.h b/libopenage/audio/in_memory_loader.h index 056f661246..4c21ba237d 100644 --- a/libopenage/audio/in_memory_loader.h +++ b/libopenage/audio/in_memory_loader.h @@ -1,4 +1,4 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -6,9 +6,9 @@ #include #include +#include "../util/path.h" #include "format.h" #include "types.h" -#include "../util/path.h" namespace openage { @@ -47,4 +47,5 @@ class InMemoryLoader { format_t format); }; -}} // openage::audio +} // namespace audio +} // namespace openage diff --git a/libopenage/audio/in_memory_resource.h b/libopenage/audio/in_memory_resource.h index 0427f0ea1a..9573d7cb40 100644 --- a/libopenage/audio/in_memory_resource.h +++ b/libopenage/audio/in_memory_resource.h @@ -1,13 +1,13 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once #include +#include "../util/path.h" #include "format.h" #include "resource.h" #include "types.h" -#include "../util/path.h" namespace openage { @@ -26,7 +26,7 @@ class InMemoryResource : public Resource { category_t category, int id, const util::Path &path, - format_t format=format_t::OPUS); + format_t format = format_t::OPUS); virtual ~InMemoryResource() = default; void use() override; @@ -35,5 +35,5 @@ class InMemoryResource : public Resource { audio_chunk_t get_data(size_t position, size_t data_length) override; }; -} -} +} // namespace audio +} // namespace openage diff --git a/libopenage/audio/loader_policy.h b/libopenage/audio/loader_policy.h index 98bb64a814..5f2acca7ce 100644 --- a/libopenage/audio/loader_policy.h +++ b/libopenage/audio/loader_policy.h @@ -1,4 +1,4 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -17,6 +17,7 @@ enum class loader_policy_t { const char *loader_policy_t_to_str(loader_policy_t val); -std::ostream &operator <<(std::ostream &os, loader_policy_t val); +std::ostream &operator<<(std::ostream &os, loader_policy_t val); -}} // openage::audio +} // namespace audio +} // namespace openage diff --git a/libopenage/audio/opus_dynamic_loader.h b/libopenage/audio/opus_dynamic_loader.h index 5c6053cf11..a612a65237 100644 --- a/libopenage/audio/opus_dynamic_loader.h +++ b/libopenage/audio/opus_dynamic_loader.h @@ -1,14 +1,14 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once -#include #include +#include -#include "opus_loading.h" +#include "../util/path.h" #include "dynamic_loader.h" +#include "opus_loading.h" #include "types.h" -#include "../util/path.h" namespace openage { @@ -40,4 +40,5 @@ class OpusDynamicLoader : public DynamicLoader { size_t chunk_size) override; }; -}} // openage::audio +} // namespace audio +} // namespace openage diff --git a/libopenage/audio/opus_in_memory_loader.h b/libopenage/audio/opus_in_memory_loader.h index cf8b5b3ce1..021f23a1b2 100644 --- a/libopenage/audio/opus_in_memory_loader.h +++ b/libopenage/audio/opus_in_memory_loader.h @@ -1,12 +1,12 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once #include +#include "../util/path.h" #include "in_memory_loader.h" #include "types.h" -#include "../util/path.h" namespace openage { @@ -28,4 +28,5 @@ class OpusInMemoryLoader : public InMemoryLoader { pcm_data_t get_resource() override; }; -}} // openage::audio +} // namespace audio +} // namespace openage diff --git a/libopenage/audio/opus_loading.h b/libopenage/audio/opus_loading.h index 1156fd1199..c2251cd8c2 100644 --- a/libopenage/audio/opus_loading.h +++ b/libopenage/audio/opus_loading.h @@ -1,4 +1,4 @@ -// Copyright 2017-2017 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -21,7 +21,7 @@ struct opus_file_t { * The opusfile handle, with a custom deleter that frees * the opusfile memory. */ - std::unique_ptr> handle; + std::unique_ptr> handle; /** * File used to supply the data. @@ -37,4 +37,5 @@ struct opus_file_t { */ opus_file_t open_opus_file(const util::Path &path); -}} // openage::audio +} // namespace audio +} // namespace openage diff --git a/libopenage/audio/resource.h b/libopenage/audio/resource.h index 0dfe58d0b5..91d53133ed 100644 --- a/libopenage/audio/resource.h +++ b/libopenage/audio/resource.h @@ -1,4 +1,4 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -27,11 +27,11 @@ class Resource { Resource(AudioManager *manager, category_t category, int id); virtual ~Resource() = default; - Resource(const Resource&) = delete; - Resource &operator=(const Resource&) = delete; + Resource(const Resource &) = delete; + Resource &operator=(const Resource &) = delete; - Resource(Resource&&) = delete; - Resource &operator=(Resource&&) = delete; + Resource(Resource &&) = delete; + Resource &operator=(Resource &&) = delete; virtual category_t get_category() const; virtual int get_id() const; @@ -83,5 +83,5 @@ class Resource { int id; }; -} -} +} // namespace audio +} // namespace openage diff --git a/libopenage/audio/resource_def.h b/libopenage/audio/resource_def.h index b5e427e5aa..b18a79e9cf 100644 --- a/libopenage/audio/resource_def.h +++ b/libopenage/audio/resource_def.h @@ -1,4 +1,4 @@ -// Copyright 2017-2017 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -27,4 +27,5 @@ struct resource_def { }; -}} // openage::audio +} // namespace audio +} // namespace openage diff --git a/libopenage/audio/sound.h b/libopenage/audio/sound.h index 546e3bf136..f01a54cb53 100644 --- a/libopenage/audio/sound.h +++ b/libopenage/audio/sound.h @@ -1,4 +1,4 @@ -// Copyright 2014-2019 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -23,7 +23,7 @@ class Resource; */ class SoundImpl { public: - SoundImpl(std::shared_ptr resource, int32_t volume=128); + SoundImpl(std::shared_ptr resource, int32_t volume = 128); ~SoundImpl(); /** @@ -167,4 +167,5 @@ class Sound { }; -}} // namespace openage::audio +} // namespace audio +} // namespace openage diff --git a/libopenage/audio/types.h b/libopenage/audio/types.h index 348b3c777c..221063092c 100644 --- a/libopenage/audio/types.h +++ b/libopenage/audio/types.h @@ -1,4 +1,4 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -34,4 +34,5 @@ using pcm_data_t = std::vector; */ using pcm_chunk_t = std::vector; -}} // openage::audio +} // namespace audio +} // namespace openage diff --git a/libopenage/console/buf.h b/libopenage/console/buf.h index 71499f84cf..1260794624 100644 --- a/libopenage/console/buf.h +++ b/libopenage/console/buf.h @@ -1,4 +1,4 @@ -// Copyright 2014-2018 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -51,12 +51,11 @@ struct buf_char { /** * Default copy constructor */ - buf_char(int cp, chrcol_t fgcol, chrcol_t bgcol, chrflags_t flags) - : - cp{cp}, - fgcol{fgcol}, - bgcol{bgcol}, - flags{flags} { + buf_char(int cp, chrcol_t fgcol, chrcol_t bgcol, chrflags_t flags) : + cp{cp}, + fgcol{fgcol}, + bgcol{bgcol}, + flags{flags} { } buf_char() = default; @@ -78,16 +77,12 @@ struct buf_char { */ chrflags_t flags; - bool operator ==(const buf_char &other) const { - return - (this->cp == other.cp) && - (this->fgcol == other.fgcol) && - (this->bgcol == other.bgcol) && - (this->flags == other.flags); + bool operator==(const buf_char &other) const { + return (this->cp == other.cp) && (this->fgcol == other.fgcol) && (this->bgcol == other.bgcol) && (this->flags == other.flags); } - bool operator !=(const buf_char &other) const { - return not (*this == other); + bool operator!=(const buf_char &other) const { + return not(*this == other); } }; @@ -116,20 +111,19 @@ struct buf_line { linetype_t type; }; -constexpr buf_line BUF_LINE_DEFAULT {LINE_EMPTY}; +constexpr buf_line BUF_LINE_DEFAULT{LINE_EMPTY}; class Buf { friend class NewBuf; public: - Buf(coord::term dims, coord::term_t scrollback_lines, coord::term_t min_width, - buf_char default_char_fmt = {0x20, 254, 0, 0}); + Buf(coord::term dims, coord::term_t scrollback_lines, coord::term_t min_width, buf_char default_char_fmt = {0x20, 254, 0, 0}); ~Buf(); /* * we don't want this object to be copyable */ - Buf& operator=(const Buf &) = delete; + Buf &operator=(const Buf &) = delete; Buf(const Buf &) = delete; @@ -407,7 +401,7 @@ class Buf { */ buf_line *screen_linedata; - //following this line are all terminal size related variables + // following this line are all terminal size related variables /** * minimum screen buffer width @@ -425,7 +419,7 @@ class Buf { */ coord::term_t scrollback_lines; - //following this line are all cursor state related variables + // following this line are all cursor state related variables /** * cursor position @@ -450,7 +444,7 @@ class Buf { */ bool cursor_special_lastcol; - //following this line are misc variables + // following this line are misc variables /** * true if we are currently reading an escape sequence @@ -518,4 +512,5 @@ class Buf { coord::term_t scrollback_pos; }; -}} // openage::console +} // namespace console +} // namespace openage diff --git a/libopenage/coord/chunk.h b/libopenage/coord/chunk.h index 3790eb84cd..a894cc56c1 100644 --- a/libopenage/coord/chunk.h +++ b/libopenage/coord/chunk.h @@ -1,10 +1,10 @@ -// Copyright 2016-2018 the openage authors. See copying.md for legal info. +// Copyright 2016-2024 the openage authors. See copying.md for legal info. #pragma once -#include "declarations.h" #include "coord_nese.gen.h" #include "coord_neseup.gen.h" +#include "declarations.h" namespace openage { namespace coord { diff --git a/libopenage/curve/base_curve.h b/libopenage/curve/base_curve.h index bd5b6003ae..0e8d2d8100 100644 --- a/libopenage/curve/base_curve.h +++ b/libopenage/curve/base_curve.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 @@ -104,52 +104,52 @@ class BaseCurve : public event::EventEntity { void check_integrity() const; /** - * Copy keyframes from another curve to this curve. After syncing, the two curves - * are guaranteed to return the same values for t >= start. - * - * The operation may insert new keyframes at \p start on the curve. - * - * @param other Curve that keyframes are copied from. - * @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. - */ + * Copy keyframes from another curve to this curve. After syncing, the two curves + * are guaranteed to return the same values for t >= start. + * + * The operation may insert new keyframes at \p start on the curve. + * + * @param other Curve that keyframes are copied from. + * @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. + */ void sync(const BaseCurve &other, const time::time_t &start = time::TIME_MIN); /** - * Copy keyframes from another curve (with a different element type) to this curve. - * After syncing, the two curves are guaranteed to return the same values - * for t >= start. - * - * The operation may insert new keyframes at \p start on the curve. - * - * @param other Curve that keyframes are copied from. - * @param converter Function that converts the value type of \p other to the - * value type of \p this. - * @param start Start time at which keyframes are replaced (default = -INF). - * Using the default value replaces ALL keyframes of \p this with - * the keyframes of \p other. - */ + * Copy keyframes from another curve (with a different element type) to this curve. + * After syncing, the two curves are guaranteed to return the same values + * for t >= start. + * + * The operation may insert new keyframes at \p start on the curve. + * + * @param other Curve that keyframes are copied from. + * @param converter Function that converts the value type of \p other to the + * value type of \p this. + * @param start Start time at which keyframes are replaced (default = -INF). + * Using the default value replaces ALL keyframes of \p this with + * the keyframes of \p other. + */ template void sync(const BaseCurve &other, const std::function &converter, const time::time_t &start = time::TIME_MIN); /** - * Get the identifier of this curve. - * - * @return Identifier. - */ + * Get the identifier of this curve. + * + * @return Identifier. + */ size_t id() const override { return this->_id; } /** - * Get the human-readable identifier of this curve. - * - * @return Human-readable identifier. - */ + * Get the human-readable identifier of this curve. + * + * @return Human-readable identifier. + */ std::string idstr() const override { if (this->_idstr.size() == 0) { return std::to_string(this->id()); @@ -163,10 +163,10 @@ class BaseCurve : public event::EventEntity { std::string str() const; /** - * Get the container containing all keyframes of this curve. - * - * @return Keyframe container. - */ + * Get the container containing all keyframes of this curve. + * + * @return Keyframe container. + */ const KeyframeContainer &get_container() const { return this->container; } diff --git a/libopenage/curve/discrete_mod.h b/libopenage/curve/discrete_mod.h index a641541688..de036bbf10 100644 --- a/libopenage/curve/discrete_mod.h +++ b/libopenage/curve/discrete_mod.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 @@ -65,8 +65,8 @@ class DiscreteMod : public Discrete { private: /** - * Length of the time interval of this curve (time between first and last keyframe). - */ + * Length of the time interval of this curve (time between first and last keyframe). + */ time::time_t time_length; }; diff --git a/libopenage/curve/interpolated.h b/libopenage/curve/interpolated.h index 49a189a124..d5d4a1dedf 100644 --- a/libopenage/curve/interpolated.h +++ b/libopenage/curve/interpolated.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 @@ -59,8 +59,8 @@ T Interpolated::get(const time::time_t &time) const { // If the next element is at the same time, just return the value of this one. if (nxt == this->container.end() // 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 e->value; } diff --git a/libopenage/curve/keyframe_container.h b/libopenage/curve/keyframe_container.h index 13b50bd04f..b9e6850257 100644 --- a/libopenage/curve/keyframe_container.h +++ b/libopenage/curve/keyframe_container.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 @@ -47,25 +47,25 @@ class KeyframeContainer { using const_iterator = typename container_t::const_iterator; /** - * Create a new container. - * - * Inserts a default element with value \p T() at \p time = -INF to ensure - * that accessing the container always returns an element. - * - * TODO: need the datamanger for change management - */ + * Create a new container. + * + * Inserts a default element with value \p T() at \p time = -INF to ensure + * that accessing the container always returns an element. + * + * TODO: need the datamanger for change management + */ KeyframeContainer(); /** - * Create a new container. - * - * Inserts a default element at \p time = -INF to ensure - * that accessing the container always returns an element. - * - * @param defaultval Value of default element at -INF. - * - * TODO: need the datamanger for change management - */ + * Create a new container. + * + * Inserts a default element at \p time = -INF to ensure + * that accessing the container always returns an element. + * + * @param defaultval Value of default element at -INF. + * + * TODO: need the datamanger for change management + */ KeyframeContainer(const T &defaultval); /** @@ -259,42 +259,42 @@ class KeyframeContainer { } /** - * Remove all keyframes from the container, EXCEPT for the default value - * at -INF. - * - * Essentially, the container is reset to the state immediately after construction. - */ + * Remove all keyframes from the container, EXCEPT for the default value + * at -INF. + * + * Essentially, the container is reset to the state immediately after construction. + */ void clear() { this->container.erase(++this->begin(), this->end()); } /** - * Copy keyframes from another container to this container. - * - * Replaces all keyframes beginning at t >= start with keyframes from \p other. - * - * @param other Curve that keyframes are copied from. - * @param converter Function that converts the value type of \p other to the - * value type of \p this. - * @param start Start time at which keyframes are replaced (default = -INF). - * Using the default value replaces ALL keyframes of \p this with - * the keyframes of \p other. - */ + * Copy keyframes from another container to this container. + * + * Replaces all keyframes beginning at t >= start with keyframes from \p other. + * + * @param other Curve that keyframes are copied from. + * @param converter Function that converts the value type of \p other to the + * value type of \p this. + * @param start Start time at which keyframes are replaced (default = -INF). + * Using the default value replaces ALL keyframes of \p this with + * the keyframes of \p other. + */ iterator sync(const KeyframeContainer &other, const time::time_t &start = time::TIME_MIN); /** - * Copy keyframes from another container (with a different element type) to this container. - * - * Replaces all keyframes beginning at t >= start with keyframes from \p other. - * - * @param other Curve that keyframes are copied from. - * @param converter Function that converts the value type of \p other to the - * value type of \p this. - * @param start Start time at which keyframes are replaced (default = -INF). - * Using the default value replaces ALL keyframes of \p this with - * the keyframes of \p other. - */ + * Copy keyframes from another container (with a different element type) to this container. + * + * Replaces all keyframes beginning at t >= start with keyframes from \p other. + * + * @param other Curve that keyframes are copied from. + * @param converter Function that converts the value type of \p other to the + * value type of \p this. + * @param start Start time at which keyframes are replaced (default = -INF). + * Using the default value replaces ALL keyframes of \p this with + * the keyframes of \p other. + */ template iterator sync(const KeyframeContainer &other, const std::function &converter, diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index 32675a72d3..3e8b5db97b 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.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 @@ -70,7 +70,7 @@ class Queue : public event::EventEntity { /** * Get the first element in the queue at the given time and remove it from - * the queue. + * the queue. * * @param time The time to get the element at. * @param value Queue element. @@ -148,19 +148,19 @@ class Queue : public event::EventEntity { } /** - * Get the identifier of this curve. - * - * @return Identifier. - */ + * Get the identifier of this curve. + * + * @return Identifier. + */ size_t id() const override { return this->_id; } /** - * Get the human-readable identifier of this curve. - * - * @return Human-readable identifier. - */ + * Get the human-readable identifier of this curve. + * + * @return Human-readable identifier. + */ std::string idstr() const override { if (this->_idstr.size() == 0) { return std::to_string(this->id()); @@ -185,8 +185,8 @@ class Queue : public event::EventEntity { container_t container; /** - * The time of the last access to the queue. - */ + * The time of the last access to the queue. + */ time::time_t last_pop; }; diff --git a/libopenage/cvar/cvar.h b/libopenage/cvar/cvar.h index d391c7874f..dad8df775a 100644 --- a/libopenage/cvar/cvar.h +++ b/libopenage/cvar/cvar.h @@ -1,4 +1,4 @@ -// Copyright 2016-2017 the openage authors. See copying.md for legal info. +// Copyright 2016-2024 the openage authors. See copying.md for legal info. #pragma once @@ -40,7 +40,6 @@ using set_func = std::function; * void load_config(Path path) except + */ class OAAPI CVarManager { - public: CVarManager(const util::Path &path); @@ -102,4 +101,5 @@ class OAAPI CVarManager { */ extern OAAPI pyinterface::PyIfFunc pyx_load_config_file; -}} // openage::cvar +} // namespace cvar +} // namespace openage diff --git a/libopenage/datastructure/concurrent_queue.h b/libopenage/datastructure/concurrent_queue.h index 61c0fcfa24..84d5366e5a 100644 --- a/libopenage/datastructure/concurrent_queue.h +++ b/libopenage/datastructure/concurrent_queue.h @@ -1,4 +1,4 @@ -// Copyright 2015-2020 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -47,8 +47,8 @@ class ConcurrentQueue { } /** Copies the front item in the queue and removes it from the queue. */ - template - T pop([[maybe_unused]] typename std::enable_if_t and std::is_copy_constructible_v>* t = nullptr) { + template + T pop([[maybe_unused]] typename std::enable_if_t and std::is_copy_constructible_v> *t = nullptr) { static_assert(sizeof...(None) == 0, "User-specified template arguments are prohibited."); std::scoped_lock lock{this->mutex}; T ret = this->front(); @@ -60,8 +60,8 @@ class ConcurrentQueue { } /** Moves the front item in the queue and removes it from the queue. */ - template - T pop([[maybe_unused]] typename std::enable_if_t>* t = nullptr) { + template + T pop([[maybe_unused]] typename std::enable_if_t> *t = nullptr) { static_assert(sizeof...(None) == 0, "User-specified template arguments are prohibited."); std::scoped_lock lock{this->mutex}; T ret = std::move(this->front()); @@ -70,8 +70,8 @@ class ConcurrentQueue { } /** Appends the given item to the queue by copying it. */ - template - void push(typename std::enable_if_t, const T&> item) { + template + void push(typename std::enable_if_t, const T &> item) { static_assert(sizeof...(None) == 0, "User-specified template arguments are prohibited."); std::unique_lock lock{this->mutex}; this->queue.push(item); @@ -80,8 +80,8 @@ class ConcurrentQueue { } /** Appends the given item to the queue by moving it. */ - template - void push(typename std::enable_if_t, T&&> item) { + template + void push(typename std::enable_if_t, T &&> item) { static_assert(sizeof...(None) == 0, "User-specified template arguments are prohibited."); std::unique_lock lock{this->mutex}; this->queue.push(std::move(item)); @@ -111,4 +111,4 @@ class ConcurrentQueue { std::condition_variable_any elements_available; }; -} // openage::datastructure +} // namespace openage::datastructure diff --git a/libopenage/datastructure/pairing_heap.h b/libopenage/datastructure/pairing_heap.h index e2ab2109d2..dd5438091a 100644 --- a/libopenage/datastructure/pairing_heap.h +++ b/libopenage/datastructure/pairing_heap.h @@ -1,4 +1,4 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -16,13 +16,13 @@ * Algorithmica 1, no. 1-4 (1986): 111-129. */ -#include #include +#include #include #include -#include "../util/compiler.h" #include "../error/error.h" +#include "../util/compiler.h" #define OPENAGE_PAIRINGHEAP_DEBUG false @@ -31,14 +31,13 @@ namespace openage::datastructure { -template +template class PairingHeap; - -template> +template > class PairingHeapNode : public std::enable_shared_from_this> { public: using this_type = PairingHeapNode; @@ -49,12 +48,10 @@ class PairingHeapNode : public std::enable_shared_from_this new_child; if (this->cmp(this->data, node->data)) { - new_root = this->shared_from_this(); + new_root = this->shared_from_this(); new_child = node; } else { - new_root = node; + new_root = node; new_child = this->shared_from_this(); } @@ -182,16 +179,16 @@ class PairingHeapNode : public std::enable_shared_from_this first_child; std::shared_ptr prev_sibling; std::shared_ptr next_sibling; - std::shared_ptr parent; // for decrease-key and delete + std::shared_ptr parent; // for decrease-key and delete }; /** * (Quite) efficient heap implementation. */ -template, - typename heapnode_t=PairingHeapNode> +template , + typename heapnode_t = PairingHeapNode> class PairingHeap final { public: using node_t = heapnode_t; @@ -202,8 +199,7 @@ class PairingHeap final { /** * create a empty heap. */ - PairingHeap() - : + PairingHeap() : node_count(0), root_node(nullptr) { } @@ -291,7 +287,8 @@ class PairingHeap final { // link0 was the only node first_pair = link0; link0->prev_sibling = nullptr; - } else { + } + else { previous_pair->next_sibling = link0; link0->prev_sibling = previous_pair; } @@ -589,7 +586,6 @@ class PairingHeap final { protected: void walk_tree(const element_t &root, const std::function &func) const { - func(root); if (root) { @@ -629,7 +625,8 @@ class PairingHeap final { void root_insert(const element_t &node) { if (this->root_node == nullptr) [[unlikely]] { this->root_node = node; - } else { + } + else { this->root_node = this->root_node->link_with(node); } } @@ -644,4 +641,4 @@ class PairingHeap final { #endif }; -} // openage::datastructure +} // namespace openage::datastructure diff --git a/libopenage/engine/engine.h b/libopenage/engine/engine.h index 967174fe27..4f96ae74bf 100644 --- a/libopenage/engine/engine.h +++ b/libopenage/engine/engine.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 @@ -67,10 +67,10 @@ class Engine { /** * Create the engine instance for this run. - * - * @param mode The run mode to use. - * @param root_dir openage root directory. - * @param mods The mods to load. + * + * @param mode The run mode to use. + * @param root_dir openage root directory. + * @param mods The mods to load. * @param debug_graphics If true, enable OpenGL debug logging. */ Engine(mode mode, @@ -87,8 +87,8 @@ class Engine { /** - * Run the main loop. - */ + * Run the main loop. + */ void loop(); /** @@ -99,38 +99,38 @@ class Engine { private: /** - * The run mode to use. - */ + * The run mode to use. + */ mode run_mode; /** - * openage root directory. - */ + * openage root directory. + */ util::Path root_dir; /** - * The threads used by the engine. - */ + * The threads used by the engine. + */ std::vector threads; /** - * Environment variables. - */ + * Environment variables. + */ std::shared_ptr cvar_manager; /** - * Controls and update the clock for time-based measurements. - */ + * Controls and update the clock for time-based measurements. + */ std::shared_ptr time_loop; /** - * Gameplay simulation. - */ + * Gameplay simulation. + */ std::shared_ptr simulation; /** - * Video/audio/input management. Can be nullptr in headless mode. - */ + * Video/audio/input management. Can be nullptr in headless mode. + */ std::shared_ptr presenter; }; diff --git a/libopenage/error/backtrace.h b/libopenage/error/backtrace.h index ea736c75c6..b51805f994 100644 --- a/libopenage/error/backtrace.h +++ b/libopenage/error/backtrace.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -6,8 +6,8 @@ #include // pxd: from libcpp.string cimport string -#include #include +#include // pxd: from libopenage.pyinterface.functional cimport Func1 #include @@ -32,14 +32,14 @@ namespace error { * ctypedef const backtrace_symbol *backtrace_symbol_constptr */ struct backtrace_symbol { - std::string filename; // empty if unknown - unsigned int lineno; // 0 if unknown - std::string functionname; // empty if unknown - void *pc; // nullptr if unknown + std::string filename; // empty if unknown + unsigned int lineno; // 0 if unknown + std::string functionname; // empty if unknown + void *pc; // nullptr if unknown }; -std::ostream &operator <<(std::ostream &os, const backtrace_symbol &bt_sym); +std::ostream &operator<<(std::ostream &os, const backtrace_symbol &bt_sym); /** @@ -66,8 +66,8 @@ class Backtrace { * @param reversed * if true, the most recent call is given last. */ - virtual void get_symbols(std::function cb, - bool reversed=true) const = 0; + virtual void get_symbols(std::function cb, + bool reversed = true) const = 0; /** * Removes all the lower frames that are also present in the current stack. @@ -77,13 +77,16 @@ class Backtrace { * * Defaults to no-op. */ - virtual void trim_to_current_stack_frame() {}; + virtual void trim_to_current_stack_frame() { + return; + }; virtual ~Backtrace() = default; }; -std::ostream &operator <<(std::ostream &os, const Backtrace &bt); +std::ostream &operator<<(std::ostream &os, const Backtrace &bt); -}} // openage::error +} // namespace error +} // namespace openage diff --git a/libopenage/error/handlers.h b/libopenage/error/handlers.h index 4679c336c2..bb483ace89 100644 --- a/libopenage/error/handlers.h +++ b/libopenage/error/handlers.h @@ -1,4 +1,4 @@ -// Copyright 2016-2018 the openage authors. See copying.md for legal info. +// Copyright 2016-2024 the openage authors. See copying.md for legal info. #pragma once @@ -21,4 +21,5 @@ namespace error { OAAPI void set_exit_ok(bool value); -}} // openage::error +} // namespace error +} // namespace openage diff --git a/libopenage/event/demo/aicontroller.h b/libopenage/event/demo/aicontroller.h index 6200b1d99d..a54784887f 100644 --- a/libopenage/event/demo/aicontroller.h +++ b/libopenage/event/demo/aicontroller.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 @@ -12,4 +12,4 @@ std::vector get_ai_inputs(const std::shared_ptr &player, const std::shared_ptr &ball, const time::time_t &now); -} // openage::event::demo +} // namespace openage::event::demo diff --git a/libopenage/event/demo/main.h b/libopenage/event/demo/main.h index f873a11bda..097ef40f3d 100644 --- a/libopenage/event/demo/main.h +++ b/libopenage/event/demo/main.h @@ -1,4 +1,4 @@ -// Copyright 2018-2019 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #pragma once @@ -12,4 +12,4 @@ namespace openage::event::demo { OAAPI void curvepong(bool disable_gui, bool no_human); -} // openage::event::demo +} // namespace openage::event::demo diff --git a/libopenage/event/event.h b/libopenage/event/event.h index 34fcbc0cee..46d63df555 100644 --- a/libopenage/event/event.h +++ b/libopenage/event/event.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 @@ -63,8 +63,8 @@ class Event : public std::enable_shared_from_this { void depend_on(const std::shared_ptr &dependency); /** - * Cancel the event. - */ + * Cancel the event. + */ void cancel(const time::time_t reference_time); /** diff --git a/libopenage/event/event_loop.h b/libopenage/event/event_loop.h index 5a5a45bc64..01ee49fdaf 100644 --- a/libopenage/event/event_loop.h +++ b/libopenage/event/event_loop.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 @@ -33,31 +33,31 @@ class EventLoop { public: /** - * Create a new event loop. - */ + * Create a new event loop. + */ EventLoop() = default; ~EventLoop() = default; /** - * Register a new event handler. - * - * Created event can reference the event handler ID to invoke it on - * execution. - * - * @param eventhandler Event handler. - */ + * Register a new event handler. + * + * Created event can reference the event handler ID to invoke it on + * execution. + * + * @param eventhandler Event handler. + */ void add_event_handler(const std::shared_ptr eventhandler); /** * Add a new event to the queue using a registered event handler. - * - * @param eventhandler Event handler ID. The handler must already be registered on the loop. - * @param target Target entity. Can be \p nullptr. - * @param state Global state. - * @param reference_time Reference time to calculate the event execution time. The actual - * depends execution time on the type of event and may be changed - * by other events. - * @param params Event parameters map (default = {}). Passed to the event handler on event execution. + * + * @param eventhandler Event handler ID. The handler must already be registered on the loop. + * @param target Target entity. Can be \p nullptr. + * @param state Global state. + * @param reference_time Reference time to calculate the event execution time. The actual + * depends execution time on the type of event and may be changed + * by other events. + * @param params Event parameters map (default = {}). Passed to the event handler on event execution. */ std::shared_ptr create_event(const std::string eventhandler, const std::shared_ptr target, @@ -67,19 +67,19 @@ class EventLoop { /** * Add a new event to the queue using an arbritary event handler. If an event handler - * with the same ID is already registered, the registered event handler will be used - * instead. - * - * TODO: Why use this function when one can simply add the event handler and use the other - * create_event function? - * - * @param eventhandler Event handler. - * @param target Target entity. Can be \p nullptr. - * @param state Global state. - * @param reference_time Reference time to calculate the event execution time. The actual - * depends execution time on the type of event and may be changed - * by other events. - * @param params Event parameters map (default = {}). Passed to the event handler on event execution. + * with the same ID is already registered, the registered event handler will be used + * instead. + * + * TODO: Why use this function when one can simply add the event handler and use the other + * create_event function? + * + * @param eventhandler Event handler. + * @param target Target entity. Can be \p nullptr. + * @param state Global state. + * @param reference_time Reference time to calculate the event execution time. The actual + * depends execution time on the type of event and may be changed + * by other events. + * @param params Event parameters map (default = {}). Passed to the event handler on event execution. */ std::shared_ptr create_event(const std::shared_ptr eventhandler, const std::shared_ptr target, @@ -89,33 +89,33 @@ class EventLoop { /** * Execute events in the queue with execution time <= a given point in time. - * - * @param time_until Maximum time until which events are executed. - * @param state Global state. + * + * @param time_until Maximum time until which events are executed. + * @param state Global state. */ void reach_time(const time::time_t &time_until, const std::shared_ptr &state); /** * Initiate a reevaluation of a given event at a given time. - * + * * This usually happens because this event depended on an event entity * that got changed at this time. - * + * * This inserts the event into the changes queue * so it will be evaluated in the next loop iteration. - * - * @param event Event to reevaluate. - * @param changes_at Time at which the event should be reevaluated. + * + * @param event Event to reevaluate. + * @param changes_at Time at which the event should be reevaluated. */ void create_change(const std::shared_ptr event, const time::time_t changes_at); /** - * Get the event queue. - * - * @return Event queue. - */ + * Get the event queue. + * + * @return Event queue. + */ const EventQueue &get_queue() const { return this->queue; } @@ -124,9 +124,9 @@ class EventLoop { /** * Execute events in the queue with execution time <= a given point in time. * - * @param time_until Maximum time until which events are executed. - * @param state Global state. - * + * @param time_until Maximum time until which events are executed. + * @param state Global state. + * * @returns number of events processed */ int execute_events(const time::time_t &time_until, @@ -134,8 +134,8 @@ class EventLoop { /** * Call all the time change functions. This is constant on the state! - * - * @param state Global state. + * + * @param state Global state. */ void update_changes(const std::shared_ptr &state); diff --git a/libopenage/event/state.h b/libopenage/event/state.h index 3bad6f9a7d..6ab585cba2 100644 --- a/libopenage/event/state.h +++ b/libopenage/event/state.h @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #pragma once @@ -12,8 +12,8 @@ class EventLoop; class State { public: - State(const std::shared_ptr &/*mgr*/) {} + State(const std::shared_ptr & /*mgr*/) {} virtual ~State() = default; }; -} // openage::event +} // namespace openage::event diff --git a/libopenage/gamestate/activity/activity.h b/libopenage/gamestate/activity/activity.h index d0832068ad..af86f434df 100644 --- a/libopenage/gamestate/activity/activity.h +++ b/libopenage/gamestate/activity/activity.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 @@ -19,51 +19,51 @@ using activity_label = std::string; class Activity { public: /** - * Create a new activity. - * - * @param id Unique ID. - * @param start Start node in the graph. - * @param label Human-readable label (optional). - */ + * Create a new activity. + * + * @param id Unique ID. + * @param start Start node in the graph. + * @param label Human-readable label (optional). + */ Activity(activity_id id, const std::shared_ptr &start, activity_label label = ""); /** - * Get the unique ID of this activity. - * - * @return Unique ID. - */ + * Get the unique ID of this activity. + * + * @return Unique ID. + */ activity_id get_id() const; /** - * Get the human-readable label of this activity. - * - * @return Human-readable label. - */ + * Get the human-readable label of this activity. + * + * @return Human-readable label. + */ const activity_label get_label() const; /** - * Get the start node of this activity. - * - * @return Start node. - */ + * Get the start node of this activity. + * + * @return Start node. + */ const std::shared_ptr &get_start() const; private: /** - * Unique ID. - */ + * Unique ID. + */ const activity_id id; /** - * Human-readable label. - */ + * Human-readable label. + */ const activity_label label; /** - * Start node. - */ + * Start node. + */ std::shared_ptr start; }; diff --git a/libopenage/gamestate/activity/end_node.h b/libopenage/gamestate/activity/end_node.h index e20323d04a..b172325e10 100644 --- a/libopenage/gamestate/activity/end_node.h +++ b/libopenage/gamestate/activity/end_node.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 @@ -21,11 +21,11 @@ namespace openage::gamestate::activity { class EndNode : public Node { public: /** - * Create a new end node. - * - * @param id Unique identifier for this node. - * @param label Human-readable label (optional). - */ + * Create a new end node. + * + * @param id Unique identifier for this node. + * @param label Human-readable label (optional). + */ EndNode(node_id_t id, node_label_t label = "End"); virtual ~EndNode() = default; diff --git a/libopenage/gamestate/activity/task_node.h b/libopenage/gamestate/activity/task_node.h index a5eddf59c2..86dd7a48b9 100644 --- a/libopenage/gamestate/activity/task_node.h +++ b/libopenage/gamestate/activity/task_node.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 @@ -32,13 +32,13 @@ static const task_func_t no_task = [](const time::time_t &, class TaskCustom : public Node { public: /** - * Create a new task node. - * - * @param id Unique identifier for this node. - * @param label Human-readable label (optional). - * @param task_func Action to perform when visiting this node (can be set later). - * @param output Next node to visit (optional). - */ + * Create a new task node. + * + * @param id Unique identifier for this node. + * @param label Human-readable label (optional). + * @param task_func Action to perform when visiting this node (can be set later). + * @param output Next node to visit (optional). + */ TaskCustom(node_id_t id, node_label_t label = "TaskCustom", const std::shared_ptr &output = nullptr, @@ -50,24 +50,24 @@ class TaskCustom : public Node { } /** - * Set the current output node. - * - * @param output Output node. - */ + * Set the current output node. + * + * @param output Output node. + */ void add_output(const std::shared_ptr &output); /** - * Set the task function. - * - * @param task_func Action to perform when visiting this node. - */ + * Set the task function. + * + * @param task_func Action to perform when visiting this node. + */ void set_task_func(task_func_t task_func); /** - * Get the task function. - * - * @return Action to perform when visiting this node. - */ + * Get the task function. + * + * @return Action to perform when visiting this node. + */ task_func_t get_task_func() const; /** @@ -80,8 +80,8 @@ class TaskCustom : public Node { private: /** - * Action to perform when visiting this node. - */ + * Action to perform when visiting this node. + */ task_func_t task_func; }; diff --git a/libopenage/gamestate/activity/task_system_node.h b/libopenage/gamestate/activity/task_system_node.h index 74513cbd64..8724352b0e 100644 --- a/libopenage/gamestate/activity/task_system_node.h +++ b/libopenage/gamestate/activity/task_system_node.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 @@ -23,7 +23,7 @@ class TaskSystemNode : public Node { * @param id Unique identifier for this node. * @param label Human-readable label (optional). * @param output Next node to visit (optional). - * @param system_id System to run when visiting this node (can be set later). + * @param system_id System to run when visiting this node (can be set later). */ TaskSystemNode(node_id_t id, node_label_t label = "TaskSystem", @@ -36,24 +36,24 @@ class TaskSystemNode : public Node { } /** - * Set the current output node. - * - * @param output Output node. - */ + * Set the current output node. + * + * @param output Output node. + */ void add_output(const std::shared_ptr &output); /** - * Set the system id. - * - * @param system_id System to run when visiting this node. - */ + * Set the system id. + * + * @param system_id System to run when visiting this node. + */ void set_system_id(system::system_id_t system_id); /** - * Get the system id. - * - * @return System to run when visiting this node. - */ + * Get the system id. + * + * @return System to run when visiting this node. + */ system::system_id_t get_system_id() const; /** @@ -66,8 +66,8 @@ class TaskSystemNode : public Node { private: /** - * System to run when visiting this node. - */ + * System to run when visiting this node. + */ system::system_id_t system_id; }; diff --git a/libopenage/gamestate/activity/xor_event_gate.h b/libopenage/gamestate/activity/xor_event_gate.h index 6c4912d714..38c8ccacb1 100644 --- a/libopenage/gamestate/activity/xor_event_gate.h +++ b/libopenage/gamestate/activity/xor_event_gate.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 @@ -55,22 +55,22 @@ using event_primer_t = std::function(cons class XorEventGate : public Node { public: /** - * Create a new exclusive event gateway. - * - * @param id Unique identifier for this node. - * @param label Human-readable label (optional). - */ + * Create a new exclusive event gateway. + * + * @param id Unique identifier for this node. + * @param label Human-readable label (optional). + */ XorEventGate(node_id_t id, node_label_t label = "EventGateWay"); /** - * Create a new exclusive event gateway. - * - * @param id Unique identifier for this node. - * @param label Human-readable label. - * @param outputs Output nodes. - * @param primers Event primers for each output node. - */ + * Create a new exclusive event gateway. + * + * @param id Unique identifier for this node. + * @param label Human-readable label. + * @param outputs Output nodes. + * @param primers Event primers for each output node. + */ XorEventGate(node_id_t id, node_label_t label, const std::vector> &outputs, @@ -83,27 +83,27 @@ class XorEventGate : public Node { } /** - * Add an output node. - * - * @param output Output node. - * @param primer Creation function for the event associated with the output node. - */ + * Add an output node. + * + * @param output Output node. + * @param primer Creation function for the event associated with the output node. + */ void add_output(const std::shared_ptr &output, const event_primer_t &primer); /** - * Get the output->event primer mappings. - * - * @return Event primer functions for each output node. - */ + * Get the output->event primer mappings. + * + * @return Event primer functions for each output node. + */ const std::map &get_primers() const; private: /** - * Maps output node IDs to event primer functions. - * - * Events are created and registered on the event loop when the node is visited. - */ + * Maps output node IDs to event primer functions. + * + * Events are created and registered on the event loop when the node is visited. + */ std::map primers; }; diff --git a/libopenage/gamestate/activity/xor_gate.h b/libopenage/gamestate/activity/xor_gate.h index 0e85a4bf21..f73287ed86 100644 --- a/libopenage/gamestate/activity/xor_gate.h +++ b/libopenage/gamestate/activity/xor_gate.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 @@ -39,23 +39,23 @@ using condition_t = std::function> &outputs, @@ -69,49 +69,49 @@ class XorGate : public Node { } /** - * Add an output node. - * - * @param output Output node. - * @param condition_func Function that determines whether this output node is chosen. - * This must be a valid node ID of one of the output nodes. - */ + * Add an output node. + * + * @param output Output node. + * @param condition_func 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); /** - * Get the output->condition mappings. - * - * @return Conditions for each output node. - */ + * Get the output->condition mappings. + * + * @return Conditions for each output node. + */ const std::map &get_conditions() const; /** - * Get the default output node. - * - * @return Default output node. - */ + * 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 no condition is true. - * - * @param node Default output node. - */ + * Set the the default output node. + * + * This node is chosen if no condition is true. + * + * @param node Default output node. + */ void set_default(const std::shared_ptr &node); private: /** - * Maps output node IDs to condition functions. - * - * Conditions are checked in order they appear in the map. - */ + * Maps output node IDs to condition functions. + * + * Conditions are checked in order they appear in the map. + */ std::map conditions; /** - * Default output node. Chosen if no condition is true. - */ + * Default output node. Chosen if no condition is true. + */ std::shared_ptr default_node; }; diff --git a/libopenage/gamestate/api/ability.h b/libopenage/gamestate/api/ability.h index 34565d9f0e..d0ba6ea54e 100644 --- a/libopenage/gamestate/api/ability.h +++ b/libopenage/gamestate/api/ability.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,33 +14,33 @@ namespace openage::gamestate::api { class APIAbility { public: /** - * Check if a nyan object is an Ability (type == \p engine.ability.Ability). - * - * @param obj nyan object. - * - * @return true if the object is an ability, else false. - */ + * Check if a nyan object is an Ability (type == \p engine.ability.Ability). + * + * @param obj nyan object. + * + * @return true if the object is an ability, else false. + */ static bool is_ability(const nyan::Object &obj); /** - * Check if an ability has a given property. - * - * @param ability \p Ability nyan object (type == \p engine.ability.Ability). - * @param property Property type. - * - * @return true if the ability has the property, else false. - */ + * Check if an ability has a given property. + * + * @param ability \p Ability nyan object (type == \p engine.ability.Ability). + * @param property Property type. + * + * @return true if the ability has the property, else false. + */ static bool check_property(const nyan::Object &ability, const ability_property_t &property); /** - * Get the nyan object for a property from an ability. - * - * @param ability \p Ability nyan object (type == \p engine.ability.Ability). - * @param property Property type. - * - * @return \p Property nyan object (type == \p engine.ability.property.Property). - */ + * Get the nyan object for a property from an ability. + * + * @param ability \p Ability nyan object (type == \p engine.ability.Ability). + * @param property Property type. + * + * @return \p Property nyan object (type == \p engine.ability.property.Property). + */ static const nyan::Object get_property(const nyan::Object &ability, const ability_property_t &property); }; diff --git a/libopenage/gamestate/api/activity.h b/libopenage/gamestate/api/activity.h index f1736d8d1c..2001aa7548 100644 --- a/libopenage/gamestate/api/activity.h +++ b/libopenage/gamestate/api/activity.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 @@ -22,21 +22,21 @@ namespace api { class APIActivity { public: /** - * Check if a nyan object is an Activity (type == \p engine.util.activity.Activity). - * - * @param obj nyan object. - * - * @return true if the object is an activity, else false. - */ + * Check if a nyan object is an Activity (type == \p engine.util.activity.Activity). + * + * @param obj nyan object. + * + * @return true if the object is an activity, else false. + */ static bool is_activity(const nyan::Object &obj); /** - * Get the start node of an activity. - * - * @param activity nyan object. - * - * @return nyan object handle of the start node. - */ + * Get the start node of an activity. + * + * @param activity nyan object. + * + * @return nyan object handle of the start node. + */ static nyan::Object get_start(const nyan::Object &activity); }; @@ -46,42 +46,42 @@ class APIActivity { class APIActivityNode { public: /** - * Check if a nyan object is a node (type == \p engine.util.activity.node.Node). - * - * @param obj nyan object. - * - * @return true if the object is a node, else false. - */ + * Check if a nyan object is a node (type == \p engine.util.activity.node.Node). + * + * @param obj nyan object. + * + * @return true if the object is a node, else false. + */ static bool is_node(const nyan::Object &obj); /** - * Get the type of a node. - * - * @param node nyan object. - * - * @return Type of the node. - */ + * Get the type of a node. + * + * @param node nyan object. + * + * @return Type of the node. + */ static activity::node_t get_type(const nyan::Object &node); /** - * Get the next nodes of a node. - * - * The number of next nodes depends on the type of the node and can range - * from 0 (end nodes) to n (gateways). - * - * @param node nyan object. - * - * @return nyan object handles of the next nodes. - */ + * Get the next nodes of a node. + * + * The number of next nodes depends on the type of the node and can range + * from 0 (end nodes) to n (gateways). + * + * @param node nyan object. + * + * @return nyan object handles of the next nodes. + */ static std::vector get_next(const nyan::Object &node); /** - * Get the system id of an Ability node. - * - * @param node nyan object. - * - * @return System ID of the node. - */ + * Get the system id of an Ability node. + * + * @param node nyan object. + * + * @return System ID of the node. + */ static system::system_id_t get_system_id(const nyan::Object &ability_node); }; @@ -91,21 +91,21 @@ class APIActivityNode { class APIActivityCondition { public: /** - * Check if a nyan object is a condition (type == \p engine.util.activity.condition.Condition). - * - * @param obj nyan object. - * - * @return true if the object is a condition, else false. - */ + * Check if a nyan object is a condition (type == \p engine.util.activity.condition.Condition). + * + * @param obj nyan object. + * + * @return true if the object is a condition, else false. + */ static bool is_condition(const nyan::Object &obj); /** - * Get the condition function for a condition. - * - * @param condition nyan object. - * - * @return Condition function. - */ + * Get the condition function for a condition. + * + * @param condition nyan object. + * + * @return Condition function. + */ static activity::condition_t get_condition(const nyan::Object &condition); }; @@ -115,21 +115,21 @@ class APIActivityCondition { class APIActivityEvent { public: /** - * Check if a nyan object is an event (type == \p engine.util.activity.event.Event). - * - * @param obj nyan object. - * - * @return true if the object is an event, else false. - */ + * Check if a nyan object is an event (type == \p engine.util.activity.event.Event). + * + * @param obj nyan object. + * + * @return true if the object is an event, else false. + */ static bool is_event(const nyan::Object &obj); /** - * Get the primer function for an event type. - * - * @param event nyan object. - * - * @return Event primer function. - */ + * Get the primer function for an event type. + * + * @param event nyan object. + * + * @return Event primer function. + */ static activity::event_primer_t get_primer(const nyan::Object &event); }; diff --git a/libopenage/gamestate/api/animation.h b/libopenage/gamestate/api/animation.h index a002749786..c372f2769d 100644 --- a/libopenage/gamestate/api/animation.h +++ b/libopenage/gamestate/api/animation.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 @@ -16,34 +16,34 @@ namespace openage::gamestate::api { class APIAnimation { public: /** - * Check if a nyan object is an animation (type == \p engine.util.animation.Animation). - * - * @param obj nyan object handle. - * - * @return true if the object is an animation, else false. - */ + * Check if a nyan object is an animation (type == \p engine.util.animation.Animation). + * + * @param obj nyan object handle. + * + * @return true if the object is an animation, else false. + */ static bool is_animation(nyan::Object &obj); /** - * Get the sprite path of an animation. - * - * The path is relative to the directory the modpack is mounted in. - * - * @param animation \p Animation nyan object (type == \p engine.util.animation.Animation). - * - * @return Relative path to the animation sprite file. - */ + * Get the sprite path of an animation. + * + * The path is relative to the directory the modpack is mounted in. + * + * @param animation \p Animation nyan object (type == \p engine.util.animation.Animation). + * + * @return Relative path to the animation sprite file. + */ static const std::string get_animation_path(const nyan::Object &animation); /** - * Get the sprite paths for a collection of animations. - * - * Paths are relative to the directory the modpack is mounted in. - * - * @param animations \p Animation nyan objects (type == \p engine.util.animation.Animation). - * - * @return Relative paths to the animation sprite files. - */ + * Get the sprite paths for a collection of animations. + * + * Paths are relative to the directory the modpack is mounted in. + * + * @param animations \p Animation nyan objects (type == \p engine.util.animation.Animation). + * + * @return Relative paths to the animation sprite files. + */ static const std::vector get_animation_paths(const std::vector &animations); }; diff --git a/libopenage/gamestate/api/patch.h b/libopenage/gamestate/api/patch.h index d0547b6f61..576c5791d3 100644 --- a/libopenage/gamestate/api/patch.h +++ b/libopenage/gamestate/api/patch.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 @@ -15,33 +15,33 @@ namespace openage::gamestate::api { class APIPatch { public: /** - * Check if a nyan object is a patch (type == \p engine.util.patch.Patch). - * - * @param obj nyan object handle. - * - * @return true if the object is a patch, else false. - */ + * Check if a nyan object is a patch (type == \p engine.util.patch.Patch). + * + * @param obj nyan object handle. + * + * @return true if the object is a patch, else false. + */ static bool is_patch(const nyan::Object &obj); /** - * Check if a patch has a given property. - * - * @param patch \p Patch nyan object (type == \p engine.util.patch.Patch). - * @param property Property type. - * - * @return true if the patch has the property, else false. - */ + * Check if a patch has a given property. + * + * @param patch \p Patch nyan object (type == \p engine.util.patch.Patch). + * @param property Property type. + * + * @return true if the patch has the property, else false. + */ static bool check_property(const nyan::Object &patch, const patch_property_t &property); /** - * Get the nyan object for a property from a patch. - * - * @param patch \p Patch nyan object (type == \p engine.util.patch.Patch). - * @param property Property type. - * - * @return \p Property nyan object (type == \p engine.util.patch.property.PatchProperty). - */ + * Get the nyan object for a property from a patch. + * + * @param patch \p Patch nyan object (type == \p engine.util.patch.Patch). + * @param property Property type. + * + * @return \p Property nyan object (type == \p engine.util.patch.property.PatchProperty). + */ static const nyan::Object get_property(const nyan::Object &patch, const patch_property_t &property); }; diff --git a/libopenage/gamestate/api/player_setup.h b/libopenage/gamestate/api/player_setup.h index f3e23fbc81..fef91b0f54 100644 --- a/libopenage/gamestate/api/player_setup.h +++ b/libopenage/gamestate/api/player_setup.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 @@ -15,39 +15,39 @@ namespace openage::gamestate::api { class APIPlayerSetup { public: /** - * Check if a nyan object is a player setup (type == \p engine.util.setup.PlayerSetup). - * - * @param obj nyan object handle. - * - * @return true if the object is a player setup, else false. - */ + * Check if a nyan object is a player setup (type == \p engine.util.setup.PlayerSetup). + * + * @param obj nyan object handle. + * + * @return true if the object is a player setup, else false. + */ static bool is_player_setup(const nyan::Object &obj); /** - * Get the modifiers of a player setup. - * - * @param player_setup nyan object (type == \p engine.util.setup.PlayerSetup). - * - * @return \p Modifier nyan objects (type == \p engine.modifier.Modifier). - */ + * Get the modifiers of a player setup. + * + * @param player_setup nyan object (type == \p engine.util.setup.PlayerSetup). + * + * @return \p Modifier nyan objects (type == \p engine.modifier.Modifier). + */ static const std::vector get_modifiers(const nyan::Object &player_setup); /** - * Get the starting resources of a player setup. - * - * @param player_setup nyan object (type == \p engine.util.setup.PlayerSetup). - * - * @return \p ResourceAmount nyan objects (type == \p engine.util.resource.ResourceAmount). - */ + * Get the starting resources of a player setup. + * + * @param player_setup nyan object (type == \p engine.util.setup.PlayerSetup). + * + * @return \p ResourceAmount nyan objects (type == \p engine.util.resource.ResourceAmount). + */ static const std::vector get_start_resources(const nyan::Object &player_setup); /** - * Get the initial patches of a player setup. - * - * @param player_setup nyan object (type == \p engine.util.setup.PlayerSetup). - * - * @return \p Patch nyan objects (type == \p engine.util.patch.Patch). - */ + * Get the initial patches of a player setup. + * + * @param player_setup nyan object (type == \p engine.util.setup.PlayerSetup). + * + * @return \p Patch nyan objects (type == \p engine.util.patch.Patch). + */ static const std::vector get_patches(const nyan::Object &player_setup); }; diff --git a/libopenage/gamestate/api/property.h b/libopenage/gamestate/api/property.h index 702abd2088..0d42c16596 100644 --- a/libopenage/gamestate/api/property.h +++ b/libopenage/gamestate/api/property.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 @@ -15,48 +15,48 @@ namespace openage::gamestate::api { class APIAbilityProperty { public: /** - * Check if a nyan object is a property (type == \p engine.ability.property.Property). - * - * @param obj nyan object handle. - * - * @return true if the object is a property, else false. - */ + * Check if a nyan object is a property (type == \p engine.ability.property.Property). + * + * @param obj nyan object handle. + * + * @return true if the object is a property, else false. + */ static bool is_property(const nyan::Object &obj); /** - * Get the animations of an \p Animated property (type == \p engine.ability.property.type.Animated). - * - * @param property \p Property nyan object (type == \p engine.ability.property.Property). - * - * @return \p Animation nyan objects (type == \p engine.util.animation.Animation). - */ + * Get the animations of an \p Animated property (type == \p engine.ability.property.type.Animated). + * + * @param property \p Property nyan object (type == \p engine.ability.property.Property). + * + * @return \p Animation nyan objects (type == \p engine.util.animation.Animation). + */ static const std::vector get_animations(const nyan::Object &property); /** - * Get the sounds of a \p CommandSound property (type == \p engine.ability.property.type.CommandSound). - * - * @param property \p Property nyan object (type == \p engine.ability.property.Property). - * - * @return \p Sound nyan objects (type == \p engine.util.sound.Sound). - */ + * Get the sounds of a \p CommandSound property (type == \p engine.ability.property.type.CommandSound). + * + * @param property \p Property nyan object (type == \p engine.ability.property.Property). + * + * @return \p Sound nyan objects (type == \p engine.util.sound.Sound). + */ static const std::vector get_command_sounds(const nyan::Object &property); /** - * Get the sounds of an \p ExecutionSound property (type == \p engine.ability.property.type.ExecutionSound). - * - * @param property \p Property nyan object (type == \p engine.ability.property.Property). - * - * @return \p Sound nyan objects (type == \p engine.util.sound.Sound). - */ + * Get the sounds of an \p ExecutionSound property (type == \p engine.ability.property.type.ExecutionSound). + * + * @param property \p Property nyan object (type == \p engine.ability.property.Property). + * + * @return \p Sound nyan objects (type == \p engine.util.sound.Sound). + */ static const std::vector get_execution_sounds(const nyan::Object &property); /** - * Get the sounds of a \p Diplomatic property (type == \p engine.ability.property.type.Diplomatic). - * - * @param property \p Property nyan object (type == \p engine.ability.property.Property). - * - * @return \p DiplomaticStance nyan objects (type == \p engine.util.diplomatic_stance.DiplomaticStance). - */ + * Get the sounds of a \p Diplomatic property (type == \p engine.ability.property.type.Diplomatic). + * + * @param property \p Property nyan object (type == \p engine.ability.property.Property). + * + * @return \p DiplomaticStance nyan objects (type == \p engine.util.diplomatic_stance.DiplomaticStance). + */ static const std::vector get_diplo_stances(const nyan::Object &property); }; diff --git a/libopenage/gamestate/api/sound.h b/libopenage/gamestate/api/sound.h index c45fc7c397..44d2634f4d 100644 --- a/libopenage/gamestate/api/sound.h +++ b/libopenage/gamestate/api/sound.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 @@ -16,34 +16,34 @@ namespace openage::gamestate::api { class APISound { public: /** - * Check if a nyan object is a sound (type == \p engine.util.sound.Sound). - * - * @param obj nyan object handle. - * - * @return true if the object is a sound, else false. - */ + * Check if a nyan object is a sound (type == \p engine.util.sound.Sound). + * + * @param obj nyan object handle. + * + * @return true if the object is a sound, else false. + */ static bool is_sound(const nyan::Object &obj); /** - * Get the sound path of a sound. - * - * The path is relative to the directory the modpack is mounted in. - * - * @param sound \p Sound nyan object (type == \p engine.util.sound.Sound). - * - * @return Relative path to the sound file. - */ + * Get the sound path of a sound. + * + * The path is relative to the directory the modpack is mounted in. + * + * @param sound \p Sound nyan object (type == \p engine.util.sound.Sound). + * + * @return Relative path to the sound file. + */ static const std::string get_sound_path(const nyan::Object &sound); /** - * Get the sound paths for a collection of sounds. - * - * Paths are relative to the directory the modpack is mounted in. - * - * @param sounds \p Sound nyan objects (type == \p engine.util.sound.Sound). - * - * @return Relative paths to the sound files. - */ + * Get the sound paths for a collection of sounds. + * + * Paths are relative to the directory the modpack is mounted in. + * + * @param sounds \p Sound nyan objects (type == \p engine.util.sound.Sound). + * + * @return Relative paths to the sound files. + */ static const std::vector get_sound_paths(const std::vector &sounds); }; } // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/terrain.h b/libopenage/gamestate/api/terrain.h index ef4ca357a8..0046a47bdc 100644 --- a/libopenage/gamestate/api/terrain.h +++ b/libopenage/gamestate/api/terrain.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 @@ -12,23 +12,23 @@ namespace openage::gamestate::api { class APITerrain { public: /** - * Check if a nyan object is a terrain (type == \p engine.util.terrain.Terrain). - * - * @param obj nyan object handle. - * - * @return true if the object is a terrain, else false. - */ + * Check if a nyan object is a terrain (type == \p engine.util.terrain.Terrain). + * + * @param obj nyan object handle. + * + * @return true if the object is a terrain, else false. + */ static bool is_terrain(const nyan::Object &obj); /** - * Get the terrain path of a terrain. - * - * The path is relative to the directory the modpack is mounted in. - * - * @param terrain \p Terrain nyan object (type == \p engine.util.terrain.Terrain). - * - * @return Relative path to the terrain file. - */ + * Get the terrain path of a terrain. + * + * The path is relative to the directory the modpack is mounted in. + * + * @param terrain \p Terrain nyan object (type == \p engine.util.terrain.Terrain). + * + * @return Relative path to the terrain file. + */ static const std::string get_terrain_path(const nyan::Object &terrain); }; diff --git a/libopenage/gamestate/component/api/live.h b/libopenage/gamestate/component/api/live.h index dd3d8cb0a0..cf733d5199 100644 --- a/libopenage/gamestate/component/api/live.h +++ b/libopenage/gamestate/component/api/live.h @@ -1,4 +1,4 @@ -// 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 @@ -21,23 +21,23 @@ class Live : public APIComponent { component_t get_type() const override; /** - * Add a new attribute to the component attributes. - * - * @param time The time at which the attribute is added. - * @param attribute Attribute identifier (fqon of the nyan object). - * @param starting_values Attribute values at the time of addition. - */ + * Add a new attribute to the component attributes. + * + * @param time The time at which the attribute is added. + * @param attribute Attribute identifier (fqon of the nyan object). + * @param starting_values Attribute values at the time of addition. + */ void add_attribute(const time::time_t &time, const nyan::fqon_t &attribute, std::shared_ptr> starting_values); /** - * Set the value of an attribute at a given time. - * - * @param time The time at which the attribute is set. - * @param attribute Attribute identifier (fqon of the nyan object). - * @param value New attribute value. - */ + * Set the value of an attribute at a given time. + * + * @param time The time at which the attribute is set. + * @param attribute Attribute identifier (fqon of the nyan object). + * @param value New attribute value. + */ void set_attribute(const time::time_t &time, const nyan::fqon_t &attribute, int64_t value); diff --git a/libopenage/gamestate/component/api_component.h b/libopenage/gamestate/component/api_component.h index e994103c5e..6a18018500 100644 --- a/libopenage/gamestate/component/api_component.h +++ b/libopenage/gamestate/component/api_component.h @@ -1,4 +1,4 @@ -// 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 @@ -58,13 +58,13 @@ class APIComponent : public Component { private: /** - * nyan object holding the data for the component. - */ + * nyan object holding the data for the component. + */ nyan::Object ability; /** - * Determines if the component is available to its game entity. - */ + * Determines if the component is available to its game entity. + */ curve::Discrete enabled; }; diff --git a/libopenage/gamestate/component/base_component.h b/libopenage/gamestate/component/base_component.h index d444a57c5f..53a7e5aab9 100644 --- a/libopenage/gamestate/component/base_component.h +++ b/libopenage/gamestate/component/base_component.h @@ -1,4 +1,4 @@ -// 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 @@ -14,10 +14,10 @@ class Component { virtual ~Component() = default; /** - * Get the component type of the component. - * - * @return Component type of the component. - */ + * Get the component type of the component. + * + * @return Component type of the component. + */ virtual component_t get_type() const = 0; }; diff --git a/libopenage/gamestate/component/internal/activity.h b/libopenage/gamestate/component/internal/activity.h index a52a97bd57..16976b7e11 100644 --- a/libopenage/gamestate/component/internal/activity.h +++ b/libopenage/gamestate/component/internal/activity.h @@ -1,4 +1,4 @@ -// 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 @@ -30,80 +30,80 @@ namespace component { class Activity : 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. - */ + * 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. + */ Activity(const std::shared_ptr &loop, const std::shared_ptr &start_activity); component_t get_type() const override; /** - * Get the initial activity. - * - * @return Initial activity. - */ + * Get the initial activity. + * + * @return Initial activity. + */ const std::shared_ptr &get_start_activity() const; /** - * 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. - */ + * 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. + */ 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. - * - * @param time Time at which the node is set. - * @param node Current node in the flow graph. - */ + * Sets the current 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. + */ 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. - * - * @param time Time at which the node is set. - */ + * Set the current node to the start node of the start activity. + * + * @param time Time at which the node is set. + */ void init(const time::time_t &time); /** - * Add a scheduled event that is waited for to progress in the node graph. - * - * @param event Event to add. - */ + * Add a scheduled event that is waited for to progress in the node graph. + * + * @param event Event to add. + */ void add_event(const std::shared_ptr &event); /** - * Cancel all scheduled events. - * - * @param time Time at which the events are cancelled. - */ + * Cancel all scheduled events. + * + * @param time Time at which the events are cancelled. + */ void cancel_events(const time::time_t &time); private: /** - * Initial 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. - * - * TODO: Define as curve, so it's changeable? - */ + * Initial 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. + * + * TODO: Define as curve, so it's changeable? + */ std::shared_ptr start_activity; /** - * Current node in the activity flow graph. - */ + * Current node in the activity flow graph. + */ curve::Discrete> node; /** - * Scheduled events that are waited for to progress in the node graph. - */ + * Scheduled events that are waited for to progress in the node graph. + */ std::vector> scheduled_events; }; diff --git a/libopenage/gamestate/component/internal/commands/base_command.h b/libopenage/gamestate/component/internal/commands/base_command.h index 61b76c4bc5..2cdfd51169 100644 --- a/libopenage/gamestate/component/internal/commands/base_command.h +++ b/libopenage/gamestate/component/internal/commands/base_command.h @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #pragma once @@ -15,10 +15,10 @@ class Command { virtual ~Command() = default; /** - * Get the type of the command. - * - * @return Command type. - */ + * Get the type of the command. + * + * @return Command type. + */ virtual command_t get_type() const = 0; }; diff --git a/libopenage/gamestate/component/internal/commands/custom.h b/libopenage/gamestate/component/internal/commands/custom.h index 2cc8a6d910..14b67f80a5 100644 --- a/libopenage/gamestate/component/internal/commands/custom.h +++ b/libopenage/gamestate/component/internal/commands/custom.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 @@ -16,10 +16,10 @@ namespace openage::gamestate::component::command { class CustomCommand : public Command { public: /** - * Create a new custom command. - * - * @param id Command identifier. - */ + * Create a new custom command. + * + * @param id Command identifier. + */ CustomCommand(const std::string &id); virtual ~CustomCommand() = default; @@ -28,16 +28,16 @@ class CustomCommand : public Command { } /** - * Get the command identifier. - * - * @return Command identifier. - */ + * Get the command identifier. + * + * @return Command identifier. + */ const std::string &get_id() const; private: /** - * Command identifier. - */ + * Command identifier. + */ const std::string id; // TODO: Payload diff --git a/libopenage/gamestate/component/internal/commands/move.h b/libopenage/gamestate/component/internal/commands/move.h index 0516dddb46..f550b546d3 100644 --- a/libopenage/gamestate/component/internal/commands/move.h +++ b/libopenage/gamestate/component/internal/commands/move.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 @@ -15,10 +15,10 @@ namespace openage::gamestate::component::command { class MoveCommand : public Command { public: /** - * Creates a new move command. - * - * @param target Target position coordinates. - */ + * Creates a new move command. + * + * @param target Target position coordinates. + */ MoveCommand(const coord::phys3 &target); virtual ~MoveCommand() = default; @@ -27,16 +27,16 @@ class MoveCommand : public Command { } /** - * Get the target position. - * - * @return Target position coordinates. - */ + * Get the target position. + * + * @return Target position coordinates. + */ const coord::phys3 &get_target() const; private: /** - * Target position. - */ + * Target position. + */ const coord::phys3 target; }; diff --git a/libopenage/gamestate/component/internal/ownership.h b/libopenage/gamestate/component/internal/ownership.h index cc0f275249..8ac13a0ba7 100644 --- a/libopenage/gamestate/component/internal/ownership.h +++ b/libopenage/gamestate/component/internal/ownership.h @@ -1,4 +1,4 @@ -// 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 @@ -50,16 +50,16 @@ class Ownership : public InternalComponent { void set_owner(const time::time_t &time, const player_id_t owner_id); /** - * Get the owner IDs over time. - * - * @return Owner ID curve. - */ + * Get the owner IDs over time. + * + * @return Owner ID curve. + */ const curve::Discrete &get_owners() const; private: /** - * Owner ID storage over time. - */ + * Owner ID storage over time. + */ curve::Discrete owner; }; diff --git a/libopenage/gamestate/component/internal/position.h b/libopenage/gamestate/component/internal/position.h index af0502fbae..fc4808ed0f 100644 --- a/libopenage/gamestate/component/internal/position.h +++ b/libopenage/gamestate/component/internal/position.h @@ -1,4 +1,4 @@ -// 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 @@ -23,73 +23,73 @@ namespace gamestate::component { class Position : public InternalComponent { public: /** - * Create a Position component. - * - * @param loop Event loop that all events from the component are registered on. - * @param initial_pos Initial position at creation time. - * @param creation_time Ingame creation time of the component. - */ + * Create a Position component. + * + * @param loop Event loop that all events from the component are registered on. + * @param initial_pos Initial position at creation time. + * @param creation_time Ingame creation time of the component. + */ Position(const std::shared_ptr &loop, const coord::phys3 &initial_pos, const time::time_t &creation_time); /** - * Create a Position component. - * - * @param loop Event loop that all events from the component are registered on. - */ + * Create a Position component. + * + * @param loop Event loop that all events from the component are registered on. + */ Position(const std::shared_ptr &loop); component_t get_type() const override; /** - * Get the positions in the world coordinate system over time. - * - * @return Position curve. - */ + * Get the positions in the world coordinate system over time. + * + * @return Position curve. + */ const curve::Continuous &get_positions() const; /** - * Set the position at a given time. - * - * This adds a new keyframe to the position curve. - * - * @param time Time at which the position is set. - * @param pos New position. - */ + * Set the position at a given time. + * + * This adds a new keyframe to the position curve. + * + * @param time Time at which the position is set. + * @param pos New position. + */ void set_position(const time::time_t &time, const coord::phys3 &pos); /** - * Get the directions in degrees over time. - * - * @return Direction curve. - */ + * Get the directions in degrees over time. + * + * @return Direction curve. + */ const curve::Segmented &get_angles() const; /** - * Set the angle at a given time. - * - * This adds a new keyframe to the angle curve. - * - * @param time Time at which the angle is set. - * @param angle New angle. - */ + * Set the angle at a given time. + * + * This adds a new keyframe to the angle curve. + * + * @param time Time at which the angle is set. + * @param angle New angle. + */ void set_angle(const time::time_t &time, const coord::phys_angle_t &angle); private: /** - * Position storage over time. - */ + * Position storage over time. + */ curve::Continuous position; /** - * Angle the entity is facing over time. - * - * Represents degrees in the range [0, 360). At angle 0, the entity is facing - * towards the camera (direction vector {x, y} = {-1, 1}). - * - * Rotation is clockwise, so at 90 degrees the entity is facing left. - */ + * Angle the entity is facing over time. + * + * Represents degrees in the range [0, 360). At angle 0, the entity is facing + * towards the camera (direction vector {x, y} = {-1, 1}). + * + * Rotation is clockwise, so at 90 degrees the entity is facing left. + */ curve::Segmented angle; }; diff --git a/libopenage/gamestate/entity_factory.h b/libopenage/gamestate/entity_factory.h index 1a12ece80b..11efbe3f2e 100644 --- a/libopenage/gamestate/entity_factory.h +++ b/libopenage/gamestate/entity_factory.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 @@ -36,17 +36,17 @@ class Player; class EntityFactory { public: /** - * Create a new entity factory for game entities. - */ + * Create a new entity factory for game entities. + */ EntityFactory(); ~EntityFactory() = default; /** - * Create a new game entity. + * Create a new game entity. * * This just creates the entity. The caller is responsible for initializing * its components and placing it into the game. - * + * * @param loop Event loop for the gamestate. * @param state State of the game. * @param owner_id ID of the player owning the entity. @@ -103,10 +103,10 @@ class EntityFactory { const nyan::Object &ability); /** - * Get a unique ID for creating a game entity. - * - * @return Unique ID for a game entity. - */ + * Get a unique ID for creating a game entity. + * + * @return Unique ID for a game entity. + */ entity_id_t get_next_entity_id(); /** @@ -117,8 +117,8 @@ class EntityFactory { player_id_t get_next_player_id(); /** - * ID of the next game entity to be created. - */ + * ID of the next game entity to be created. + */ entity_id_t next_entity_id; /** @@ -134,13 +134,13 @@ class EntityFactory { // TODO: Cache created game entities. /** - * Cache for activities. - */ + * Cache for activities. + */ std::unordered_map> activity_cache; /** - * Mutex for thread safety. - */ + * Mutex for thread safety. + */ std::shared_mutex mutex; }; } // namespace gamestate diff --git a/libopenage/gamestate/event/spawn_entity.h b/libopenage/gamestate/event/spawn_entity.h index 12f99378b3..948df0a855 100644 --- a/libopenage/gamestate/event/spawn_entity.h +++ b/libopenage/gamestate/event/spawn_entity.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 @@ -41,11 +41,11 @@ class Spawner : public openage::event::EventEntity { class SpawnEntityHandler : public openage::event::OnceEventHandler { public: /** - * Creates a new SpawnEntityHandler. - * - * @param loop: Event loop that the components register on. - * @param factory: Factory that is used to create the entity. - */ + * Creates a new SpawnEntityHandler. + * + * @param loop: Event loop that the components register on. + * @param factory: Factory that is used to create the entity. + */ SpawnEntityHandler(const std::shared_ptr &loop, const std::shared_ptr &factory); ~SpawnEntityHandler() = default; @@ -92,13 +92,13 @@ class SpawnEntityHandler : public openage::event::OnceEventHandler { private: /** - * Event loop that the entity components are registered on. - */ + * Event loop that the entity components are registered on. + */ std::shared_ptr loop; /** - * The factory that is used to create the entity. - */ + * The factory that is used to create the entity. + */ std::shared_ptr factory; }; diff --git a/libopenage/gamestate/game.h b/libopenage/gamestate/game.h index 139c4e87fa..e48c837b19 100644 --- a/libopenage/gamestate/game.h +++ b/libopenage/gamestate/game.h @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #pragma once @@ -48,8 +48,8 @@ class Game { /** * Create a new game. * - * @param event_loop Event simulation loop for the gamestate. - * @param mod_manager Mod manager. + * @param event_loop Event simulation loop for the gamestate. + * @param mod_manager Mod manager. * @param entity_factory Factory for creating entities. Used for creating the players. */ Game(const std::shared_ptr &event_loop, @@ -59,8 +59,8 @@ class Game { ~Game() = default; /** - * Get the current game state. - */ + * Get the current game state. + */ const std::shared_ptr &get_state() const; /** @@ -74,44 +74,44 @@ class Game { private: /** - * Load game data from the filesystem. - * - * @param mod_manager Mod manager. - */ + * Load game data from the filesystem. + * + * @param mod_manager Mod manager. + */ void load_data(const std::shared_ptr &mod_manager); /** - * Load game data from the filesystem recursively. - * - * TODO: Move this into nyan. - * - * @param base_dir Base directory where mods are stored. - * @param mod_dir Name of the mod directory. - * @param search Search path relative to the mod directory. - * @param recursive if true, recursively search subfolders if the the search path is a directory. - */ + * Load game data from the filesystem recursively. + * + * TODO: Move this into nyan. + * + * @param base_dir Base directory where mods are stored. + * @param mod_dir Name of the mod directory. + * @param search Search path relative to the mod directory. + * @param recursive if true, recursively search subfolders if the the search path is a directory. + */ void load_path(const util::Path &base_dir, const std::string &mod_dir, const std::string &search, bool recursive = false); /** - * Generate the terrain for the current game. - * - * TODO: Use a real map generator. - * - * @param terrain_factory Factory for creating terrain objects. - */ + * Generate the terrain for the current game. + * + * TODO: Use a real map generator. + * + * @param terrain_factory Factory for creating terrain objects. + */ void generate_terrain(const std::shared_ptr &terrain_factory); /** - * Nyan game data database. - */ + * Nyan game data database. + */ std::shared_ptr db; /** - * State of the current game. - */ + * State of the current game. + */ std::shared_ptr state; /** diff --git a/libopenage/gamestate/game_entity.h b/libopenage/gamestate/game_entity.h index f72544427d..9b30e8ec76 100644 --- a/libopenage/gamestate/game_entity.h +++ b/libopenage/gamestate/game_entity.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -30,10 +30,10 @@ class Component; class GameEntity { public: /** - * Create a new game entity. - * - * @param id Unique identifier. - */ + * Create a new game entity. + * + * @param id Unique identifier. + */ GameEntity(entity_id_t id); ~GameEntity() = default; @@ -51,10 +51,10 @@ class GameEntity { std::shared_ptr copy(entity_id_t id); /** - * Get the unique identifier of this entity. - * - * @return Unique identifier. - */ + * Get the unique identifier of this entity. + * + * @return Unique identifier. + */ entity_id_t get_id() const; /** @@ -79,32 +79,32 @@ class GameEntity { const std::shared_ptr &get_manager() const; /** - * Get a component of this entity. - * - * @param type Component type. - */ + * Get a component of this entity. + * + * @param type Component type. + */ const std::shared_ptr &get_component(component::component_t type); /** - * Add a component to this entity. - * - * @param component Component to add. - */ + * Add a component to this entity. + * + * @param component Component to add. + */ void add_component(const std::shared_ptr &component); /** - * Check if this entity has a component of the given type. - * - * @param type Component type. - */ + * Check if this entity has a component of the given type. + * + * @param type Component type. + */ bool has_component(component::component_t type); /** - * Update the render entity. - * - * @param time Simulation time of the update. - * @param animation_path Path to the animation definition used at \p time. - */ + * Update the render entity. + * + * @param time Simulation time of the update. + * @param animation_path Path to the animation definition used at \p time. + */ void render_update(const time::time_t &time, const std::string &animation_path); @@ -133,10 +133,10 @@ class GameEntity { entity_id_t id; /** - * Data components. - * - * TODO: Multiple components of the same type. - */ + * Data components. + * + * TODO: Multiple components of the same type. + */ std::unordered_map> components; /** diff --git a/libopenage/gamestate/game_state.h b/libopenage/gamestate/game_state.h index 401070780c..bf1531bd55 100644 --- a/libopenage/gamestate/game_state.h +++ b/libopenage/gamestate/game_state.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 @@ -38,58 +38,58 @@ class Terrain; class GameState : public openage::event::State { public: /** - * Create a new game state. - * - * @param db Nyan game data database. - * @param event_loop Event loop for the game state. - */ + * Create a new game state. + * + * @param db Nyan game data database. + * @param event_loop Event loop for the game state. + */ explicit GameState(const std::shared_ptr &db, const std::shared_ptr &event_loop); /** - * Get the nyan database view for the whole game. - * - * Players have individual views for their own data. - * - * @return nyan database view. - */ + * Get the nyan database view for the whole game. + * + * Players have individual views for their own data. + * + * @return nyan database view. + */ const std::shared_ptr &get_db_view(); /** - * Add a new game entity to the index. - * - * @param entity New game entity. - */ + * Add a new game entity to the index. + * + * @param entity New game entity. + */ void add_game_entity(const std::shared_ptr &entity); /** - * Add a new player to the index. - * - * @param player New player. - */ + * Add a new player to the index. + * + * @param player New player. + */ void add_player(const std::shared_ptr &player); /** - * Set the terrain of the current game. - * - * @param terrain Terrain object. - */ + * Set the terrain of the current game. + * + * @param terrain Terrain object. + */ void set_terrain(const std::shared_ptr &terrain); /** - * Get a game entity by its ID. - * - * @param id ID of the game entity. + * Get a game entity by its ID. + * + * @param id ID of the game entity. * - * @return Game entity with the given ID. - */ + * @return Game entity with the given ID. + */ const std::shared_ptr &get_game_entity(entity_id_t id) const; /** - * Get all game entities in the current game. - * - * @return Map of all game entities in the current game by their ID. - */ + * Get all game entities in the current game. + * + * @return Map of all game entities in the current game by their ID. + */ const std::unordered_map> &get_game_entities() const; /** @@ -102,42 +102,42 @@ class GameState : public openage::event::State { const std::shared_ptr &get_player(player_id_t id) const; /** - * Get the terrain of the current game. - * - * @return Terrain object. - */ + * Get the terrain of the current game. + * + * @return Terrain object. + */ const std::shared_ptr &get_terrain() const; /** - * TODO: Only for testing. - */ + * TODO: Only for testing. + */ const std::shared_ptr &get_mod_manager() const; void set_mod_manager(const std::shared_ptr &mod_manager); private: /** - * View for the nyan game data database. - */ + * View for the nyan game data database. + */ std::shared_ptr db_view; /** - * Map of all game entities in the current game by their ID. - */ + * Map of all game entities in the current game by their ID. + */ std::unordered_map> game_entities; /** - * Map of all players in the current game by their ID. - */ + * Map of all players in the current game by their ID. + */ std::unordered_map> players; /** - * Terrain of the current game. - */ + * Terrain of the current game. + */ std::shared_ptr terrain; /** - * TODO: Only for testing - */ + * TODO: Only for testing + */ std::shared_ptr mod_manager; }; } // namespace gamestate diff --git a/libopenage/gamestate/player.h b/libopenage/gamestate/player.h index a7eb5bac4d..f7ef4987ac 100644 --- a/libopenage/gamestate/player.h +++ b/libopenage/gamestate/player.h @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #pragma once @@ -80,8 +80,8 @@ class Player { player_id_t id; /** - * Player view of the nyan game data database. - */ + * Player view of the nyan game data database. + */ std::shared_ptr db_view; }; diff --git a/libopenage/gamestate/simulation.h b/libopenage/gamestate/simulation.h index fe2de8257c..b7e4331775 100644 --- a/libopenage/gamestate/simulation.h +++ b/libopenage/gamestate/simulation.h @@ -1,4 +1,4 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #pragma once @@ -47,10 +47,10 @@ class GameSimulation final { public: /** * Create the game simulation subsystems depending on the requested run mode. - * - * @param root_dir openage root directory. - * @param cvar_manager Environment variable manager. - * @param time_loop Time management loop. + * + * @param root_dir openage root directory. + * @param cvar_manager Environment variable manager. + * @param time_loop Time management loop. */ GameSimulation(const util::Path &root_dir, const std::shared_ptr &cvar_manager, @@ -85,32 +85,32 @@ class GameSimulation final { /** * Get this simulation's cvar manager. - * - * @return CVarManager instance. + * + * @return CVarManager instance. */ const std::shared_ptr get_cvar_manager(); /** - * Get the game running in the simulation. - * - * @return Game instance. - */ + * Get the game running in the simulation. + * + * @return Game instance. + */ const std::shared_ptr get_game(); /** - * Get the event loop for the gamestate. - * - * @return Event loop. - */ + * Get the event loop for the gamestate. + * + * @return Event loop. + */ const std::shared_ptr get_event_loop(); /** - * Get the event entity for spawing game entities. - * - * TODO: Move somewhere else or remove. - * - * @return Spawner for entity creation. - */ + * Get the event entity for spawing game entities. + * + * TODO: Move somewhere else or remove. + * + * @return Spawner for entity creation. + */ const std::shared_ptr get_spawner(); /** @@ -130,10 +130,10 @@ class GameSimulation final { void attach_renderer(const std::shared_ptr &render_factory); /** - * Set the modpacks to load for a game. - * - * @param modpacks IDs of the modpacks to load. - */ + * Set the modpacks to load for a game. + * + * @param modpacks IDs of the modpacks to load. + */ void set_modpacks(const std::vector &modpacks); /** @@ -144,8 +144,8 @@ class GameSimulation final { private: /** - * Initialize event handlers. - */ + * Initialize event handlers. + */ void init_event_handlers(); /** @@ -170,23 +170,23 @@ class GameSimulation final { std::shared_ptr time_loop; /** - * Event loop for processing events in the game. - */ + * Event loop for processing events in the game. + */ std::shared_ptr event_loop; /** - * Factory for creating game entities. - */ + * Factory for creating game entities. + */ std::shared_ptr entity_factory; /** - * Factory for creating terrain. - */ + * Factory for creating terrain. + */ std::shared_ptr terrain_factory; /** - * Mod manager. - */ + * Mod manager. + */ std::shared_ptr mod_manager; // TODO: move somewhere sensible or remove @@ -197,8 +197,8 @@ class GameSimulation final { std::shared_ptr game; /** - * Mutex for thread-safe access to the simulation. - */ + * Mutex for thread-safe access to the simulation. + */ std::shared_mutex mutex; }; diff --git a/libopenage/gamestate/system/activity.h b/libopenage/gamestate/system/activity.h index d6c10215fc..a5c38e6f49 100644 --- a/libopenage/gamestate/system/activity.h +++ b/libopenage/gamestate/system/activity.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 @@ -25,11 +25,11 @@ namespace system { class Activity { public: /** - * Advance in the activity flow graph of the game entity. - * - * @param start_time Start time of change. - * @param entity Game entity. - */ + * Advance in the activity flow graph of the game entity. + * + * @param start_time Start time of change. + * @param entity Game entity. + */ static void advance(const time::time_t &start_time, const std::shared_ptr &entity, const std::shared_ptr &loop, @@ -44,7 +44,7 @@ class Activity { * @param start_time Start time of change. * @param system_id ID of the subsystem to run. * - * @return Runtime of the change in simulation time. + * @return Runtime of the change in simulation time. */ static const time::time_t handle_subsystem(const std::shared_ptr &entity, const time::time_t &start_time, diff --git a/libopenage/gamestate/system/idle.h b/libopenage/gamestate/system/idle.h index 8a9a6e6a9a..eb4434fdb7 100644 --- a/libopenage/gamestate/system/idle.h +++ b/libopenage/gamestate/system/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 @@ -15,18 +15,18 @@ namespace system { class Idle { public: /** - * Let a game entity idle. - * - * This does not change the state of a unit. It only changes its animation and - * sounds. - * - * @param entity Game entity. - * @param start_time Start time of change. - * - * @return Runtime of the change in simulation time. - */ + * Let a game entity idle. + * + * This does not change the state of a unit. It only changes its animation and + * sounds. + * + * @param entity Game entity. + * @param start_time Start time of change. + * + * @return Runtime of the change in simulation time. + */ static const time::time_t idle(const std::shared_ptr &entity, - const time::time_t &start_time); + const time::time_t &start_time); }; } // namespace system diff --git a/libopenage/gamestate/system/move.h b/libopenage/gamestate/system/move.h index 11d6a6fa5f..7ca2dbae75 100644 --- a/libopenage/gamestate/system/move.h +++ b/libopenage/gamestate/system/move.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 @@ -16,28 +16,28 @@ namespace system { class Move { public: /** - * Move a game entity to a destination from a move command. - * - * @param entity Game entity. - * @param start_time Start time of change. - * - * @return Runtime of the change in simulation time. - */ + * Move a game entity to a destination from a move command. + * + * @param entity Game entity. + * @param start_time Start time of change. + * + * @return Runtime of the change in simulation time. + */ static const time::time_t move_command(const std::shared_ptr &entity, - const time::time_t &start_time); + const time::time_t &start_time); /** - * Move a game entity to a destination. - * - * @param entity Game entity. - * @param destination Destination coordinates. - * @param start_time Start time of change. - * - * @return Runtime of the change in simulation time. - */ + * Move a game entity to a destination. + * + * @param entity Game entity. + * @param destination Destination coordinates. + * @param start_time Start time of change. + * + * @return Runtime of the change in simulation time. + */ static const time::time_t move_default(const std::shared_ptr &entity, - const coord::phys3 &destination, - const time::time_t &start_time); + const coord::phys3 &destination, + const time::time_t &start_time); }; } // namespace system diff --git a/libopenage/gamestate/terrain.h b/libopenage/gamestate/terrain.h index c84b926d9a..97ffcbdffd 100644 --- a/libopenage/gamestate/terrain.h +++ b/libopenage/gamestate/terrain.h @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #pragma once @@ -22,23 +22,23 @@ class TerrainChunk; class Terrain { public: /** - * Create a new terrain. - */ + * Create a new terrain. + */ Terrain(); ~Terrain() = default; /** - * Add a chunk to the terrain. - * - * @param chunk New chunk. - */ + * Add a chunk to the terrain. + * + * @param chunk New chunk. + */ void add_chunk(const std::shared_ptr &chunk); /** - * Get the chunks of the terrain. - * - * @return Terrain chunks. - */ + * Get the chunks of the terrain. + * + * @return Terrain chunks. + */ const std::vector> &get_chunks() const; /** @@ -56,15 +56,15 @@ class Terrain { private: /** - * Total size of the map + * Total size of the map * origin is the left corner * x = top left edge; y = top right edge - */ + */ util::Vector2s size; /** - * Subdivision of the main terrain entity. - */ + * Subdivision of the main terrain entity. + */ std::vector> chunks; }; diff --git a/libopenage/gamestate/terrain_chunk.h b/libopenage/gamestate/terrain_chunk.h index e7abce08f5..0cd1840028 100644 --- a/libopenage/gamestate/terrain_chunk.h +++ b/libopenage/gamestate/terrain_chunk.h @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #pragma once @@ -35,26 +35,26 @@ class TerrainChunk { void set_render_entity(const std::shared_ptr &entity); /** - * Update the render entity. - * - * @param time Simulation time of the update. - * @param terrain_path Path to the terrain definition used at \p time. - */ + * Update the render entity. + * + * @param time Simulation time of the update. + * @param terrain_path Path to the terrain definition used at \p time. + */ void render_update(const time::time_t &time, const std::string &terrain_path); /** - * Get the size of this terrain chunk. - * - * @return Size of the terrain chunk (in tiles). - */ + * Get the size of this terrain chunk. + * + * @return Size of the terrain chunk (in tiles). + */ const util::Vector2s &get_size() const; /** - * Get the offset of this terrain chunk to the terrain origin. - * - * @return Offset of the terrain chunk (in tiles). - */ + * Get the offset of this terrain chunk to the terrain origin. + * + * @return Offset of the terrain chunk (in tiles). + */ const coord::tile_delta &get_offset() const; // TODO: Remove test texture references @@ -69,20 +69,20 @@ class TerrainChunk { private: /** - * Size of the terrain chunk. - * Origin is the left corner. - * x = top left edge; y = top right edge. - */ + * Size of the terrain chunk. + * Origin is the left corner. + * x = top left edge; y = top right edge. + */ util::Vector2s size; /** - * Offset of the terrain chunk to the origin. - */ + * Offset of the terrain chunk to the origin. + */ coord::tile_delta offset; /** - * Height map of the terrain chunk. - */ + * Height map of the terrain chunk. + */ std::vector tiles; /** diff --git a/libopenage/gamestate/terrain_factory.h b/libopenage/gamestate/terrain_factory.h index 32f804257f..ab419acc30 100644 --- a/libopenage/gamestate/terrain_factory.h +++ b/libopenage/gamestate/terrain_factory.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 @@ -26,26 +26,26 @@ class TerrainChunk; class TerrainFactory { public: /** - * Create a new terrain factory. - */ + * Create a new terrain factory. + */ TerrainFactory() = default; ~TerrainFactory() = default; /** - * Create a new empty terrain object. - * - * @return New terrain object. - */ + * Create a new empty terrain object. + * + * @return New terrain object. + */ std::shared_ptr add_terrain(); /** - * Create a new empty terrain chunk. - * - * @param size Size of the chunk. - * @param offset Offset of the chunk. - * - * @return New terrain chunk. - */ + * Create a new empty terrain chunk. + * + * @param size Size of the chunk. + * @param offset Offset of the chunk. + * + * @return New terrain chunk. + */ std::shared_ptr add_chunk(const std::shared_ptr &gstate, const util::Vector2s size, const coord::tile_delta offset); @@ -57,8 +57,8 @@ class TerrainFactory { /** * Attach a render factory for graphical display. - * - * This enables rendering for all created terrain chunks. + * + * This enables rendering for all created terrain chunks. * * @param render_factory Factory for creating connector objects for gamestate->renderer * communication. @@ -72,8 +72,8 @@ class TerrainFactory { std::shared_ptr render_factory; /** - * Mutex for thread safety. - */ + * Mutex for thread safety. + */ std::shared_mutex mutex; }; diff --git a/libopenage/gamestate/terrain_tile.h b/libopenage/gamestate/terrain_tile.h index e2a046bb07..36ae11ddf3 100644 --- a/libopenage/gamestate/terrain_tile.h +++ b/libopenage/gamestate/terrain_tile.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 @@ -19,22 +19,22 @@ using terrain_elevation_t = util::FixedPoint; */ struct TerrainTile { /** - * Terrain definition used by this tile. - * - * TODO: Make this non-optional once all modpacks support terrain graphics. - */ + * Terrain definition used by this tile. + * + * TODO: Make this non-optional once all modpacks support terrain graphics. + */ std::optional terrain; /** - * Path to the terrain asset used by this tile. - * - * TODO: Remove this and fetch the asset path from the terrain definition. - */ + * Path to the terrain asset used by this tile. + * + * TODO: Remove this and fetch the asset path from the terrain definition. + */ std::string terrain_asset_path; /** - * Height of this tile on the terrain. - */ + * Height of this tile on the terrain. + */ terrain_elevation_t elevation; }; diff --git a/libopenage/gamestate/universe.h b/libopenage/gamestate/universe.h index 7e33a83765..60df5a18b2 100644 --- a/libopenage/gamestate/universe.h +++ b/libopenage/gamestate/universe.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -26,7 +26,7 @@ class Universe { /** * Create a new universe. * - * @param state State of the game. + * @param state State of the game. */ Universe(const std::shared_ptr &state); ~Universe() = default; diff --git a/libopenage/gamestate/world.h b/libopenage/gamestate/world.h index 2fd825143b..34f1470a43 100644 --- a/libopenage/gamestate/world.h +++ b/libopenage/gamestate/world.h @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #pragma once @@ -23,7 +23,7 @@ class World { /** * Create a new world. * - * @param state State of the game. + * @param state State of the game. */ World(const std::shared_ptr &state); ~World() = default; @@ -38,8 +38,8 @@ class World { private: /** - * State of the current game. - */ + * State of the current game. + */ std::shared_ptr state; /** diff --git a/libopenage/input/controller/camera/binding_context.h b/libopenage/input/controller/camera/binding_context.h index a01341e548..0bc1325078 100644 --- a/libopenage/input/controller/camera/binding_context.h +++ b/libopenage/input/controller/camera/binding_context.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 @@ -25,9 +25,9 @@ class BindingContext { * Bind a specific key combination to a binding. * * This is the first matching priority. - * - * @param ev Input event triggering the action. - * @param bind Binding for the event. + * + * @param ev Input event triggering the action. + * @param bind Binding for the event. */ void bind(const Event &ev, const binding_action bind); @@ -35,26 +35,26 @@ class BindingContext { * Bind an event class to an action. * * This is the second matching priority. - * - * @param ev Input event triggering the action. - * @param bind Binding for the event. + * + * @param ev Input event triggering the action. + * @param bind Binding for the event. */ void bind(const event_class &cl, const binding_action bind); /** - * Check whether a specific key event is bound in this context. - * - * @param ev Input event. - * - * @return true if event is bound, else false. - */ + * Check whether a specific key event is bound in this context. + * + * @param ev Input event. + * + * @return true if event is bound, else false. + */ bool is_bound(const Event &ev) const; /** - * Get the bindings for a specific event. - * - * @param ev Input event mapped to the binding. - */ + * Get the bindings for a specific event. + * + * @param ev Input event mapped to the binding. + */ const binding_action &lookup(const Event &ev) const; private: diff --git a/libopenage/input/controller/game/binding_context.h b/libopenage/input/controller/game/binding_context.h index 84b331c690..318e37661d 100644 --- a/libopenage/input/controller/game/binding_context.h +++ b/libopenage/input/controller/game/binding_context.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 @@ -25,9 +25,9 @@ class BindingContext { * Bind a specific key combination to a binding. * * This is the first matching priority. - * - * @param ev Input event triggering the action. - * @param bind Binding for the event. + * + * @param ev Input event triggering the action. + * @param bind Binding for the event. */ void bind(const Event &ev, const binding_action bind); @@ -35,26 +35,26 @@ class BindingContext { * Bind an event class to an action. * * This is the second matching priority. - * - * @param ev Input event triggering the action. - * @param bind Binding for the event. + * + * @param ev Input event triggering the action. + * @param bind Binding for the event. */ void bind(const event_class &cl, const binding_action bind); /** - * Check whether a specific key event is bound in this context. - * - * @param ev Input event. - * - * @return true if event is bound, else false. - */ + * Check whether a specific key event is bound in this context. + * + * @param ev Input event. + * + * @return true if event is bound, else false. + */ bool is_bound(const Event &ev) const; /** - * Get the bindings for a specific event. - * - * @param ev Input event mapped to the binding. - */ + * Get the bindings for a specific event. + * + * @param ev Input event mapped to the binding. + */ const binding_action &lookup(const Event &ev) const; private: diff --git a/libopenage/input/controller/hud/binding_context.h b/libopenage/input/controller/hud/binding_context.h index fac8283df6..c2a269b512 100644 --- a/libopenage/input/controller/hud/binding_context.h +++ b/libopenage/input/controller/hud/binding_context.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 @@ -25,9 +25,9 @@ class BindingContext { * Bind a specific key combination to a binding. * * This is the first matching priority. - * - * @param ev Input event triggering the action. - * @param bind Binding for the event. + * + * @param ev Input event triggering the action. + * @param bind Binding for the event. */ void bind(const Event &ev, const binding_action bind); @@ -35,26 +35,26 @@ class BindingContext { * Bind an event class to an action. * * This is the second matching priority. - * - * @param ev Input event triggering the action. - * @param bind Binding for the event. + * + * @param ev Input event triggering the action. + * @param bind Binding for the event. */ void bind(const event_class &cl, const binding_action bind); /** - * Check whether a specific key event is bound in this context. - * - * @param ev Input event. - * - * @return true if event is bound, else false. - */ + * Check whether a specific key event is bound in this context. + * + * @param ev Input event. + * + * @return true if event is bound, else false. + */ bool is_bound(const Event &ev) const; /** - * Get the bindings for a specific event. - * - * @param ev Input event mapped to the binding. - */ + * Get the bindings for a specific event. + * + * @param ev Input event mapped to the binding. + */ const binding_action &lookup(const Event &ev) const; private: diff --git a/libopenage/input/controller/hud/controller.h b/libopenage/input/controller/hud/controller.h index e7c391846e..60e9ff2bb8 100644 --- a/libopenage/input/controller/hud/controller.h +++ b/libopenage/input/controller/hud/controller.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 @@ -38,23 +38,23 @@ class Controller : public std::enable_shared_from_this { const std::shared_ptr &ctx); /** - * Set the render entity for the selection box. - * - * @param entity New render entity. - */ + * Set the render entity for the selection box. + * + * @param entity New render entity. + */ void set_drag_entity(const std::shared_ptr &entity); /** - * Get the render entity for the selection box. - * - * @return Render entity for the selection box. - */ + * Get the render entity for the selection box. + * + * @return Render entity for the selection box. + */ const std::shared_ptr &get_drag_entity() const; private: /** - * Render entity for the selection box. - */ + * Render entity for the selection box. + */ std::shared_ptr drag_entity; }; diff --git a/libopenage/input/event.h b/libopenage/input/event.h index 31e9986b32..0749eebfdd 100644 --- a/libopenage/input/event.h +++ b/libopenage/input/event.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -30,11 +30,11 @@ enum class event_class { GUI, // keyboard subclasses - ALPHA, // abc - DIGIT, // 123 - PRINT, // remaining printable chars + ALPHA, // abc + DIGIT, // 123 + PRINT, // remaining printable chars NONPRINT, // tab, return, backspace, delete - OTHER, // arrows, home, end + OTHER, // arrows, home, end // mouse subclasses MOUSE_BUTTON, diff --git a/libopenage/input/input_context.h b/libopenage/input/input_context.h index 55e3cd2974..5e287d40d1 100644 --- a/libopenage/input/input_context.h +++ b/libopenage/input/input_context.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -41,61 +41,61 @@ class InputContext { virtual ~InputContext() = default; /** - * Get the unique ID of the context. - * - * @return Context ID. - */ + * Get the unique ID of the context. + * + * @return Context ID. + */ const std::string &get_id(); /** - * Set the associated context for binding input events to game events. - * - * @param bindings Binding context for gamestate events. - */ + * Set the associated context for binding input events to game events. + * + * @param bindings Binding context for gamestate events. + */ void set_game_bindings(const std::shared_ptr &bindings); /** - * Set the associated context for binding input events to camera actions. - * - * @param bindings Binding context for camera actions. - */ + * Set the associated context for binding input events to camera actions. + * + * @param bindings Binding context for camera actions. + */ void set_camera_bindings(const std::shared_ptr &bindings); /** - * Set the associated context for binding input events to HUD actions. - * - * @param bindings Binding context for HUD actions. - */ + * Set the associated context for binding input events to HUD actions. + * + * @param bindings Binding context for HUD actions. + */ void set_hud_bindings(const std::shared_ptr &bindings); /** - * Get the associated context for binding input events to game events. - * - * @return Binding context of the input context. - */ + * Get the associated context for binding input events to game events. + * + * @return Binding context of the input context. + */ const std::shared_ptr &get_game_bindings(); /** - * Get the associated context for binding input events to camera actions. - * - * @return Binding context of the input context. - */ + * Get the associated context for binding input events to camera actions. + * + * @return Binding context of the input context. + */ const std::shared_ptr &get_camera_bindings(); /** - * Get the associated context for binding input events to HUD actions. - * - * @return Binding context of the input context. - */ + * Get the associated context for binding input events to HUD actions. + * + * @return Binding context of the input context. + */ const std::shared_ptr &get_hud_bindings(); /** * Bind a specific key combination to a single action. * * This is the first matching priority. - * - * @param ev Input event triggering the action. - * @param act Action executed by the event. + * + * @param ev Input event triggering the action. + * @param act Action executed by the event. */ void bind(const Event &ev, const input_action act); @@ -103,9 +103,9 @@ class InputContext { * Bind an event class to a single action. * * This is the second matching priority. - * - * @param ev Input event triggering the action. - * @param act Action executed by the event. + * + * @param ev Input event triggering the action. + * @param act Action executed by the event. */ void bind(const event_class &cl, const input_action act); @@ -113,9 +113,9 @@ class InputContext { * Bind a specific key combination to a list of actions. * * This is the first matching priority. - * - * @param ev Input event triggering the action. - * @param act Actions executed by the event. + * + * @param ev Input event triggering the action. + * @param act Actions executed by the event. */ void bind(const Event &ev, const std::vector &&acts); @@ -123,26 +123,26 @@ class InputContext { * Bind an event class to a list of actions. * * This is the second matching priority. - * - * @param ev Input event triggering the action. - * @param act Actions executed by the event. + * + * @param ev Input event triggering the action. + * @param act Actions executed by the event. */ void bind(const event_class &cl, const std::vector &&acts); /** - * Check whether a specific key event is bound in this context. - * - * @param ev Input event. - * - * @return true if event is bound, else false. - */ + * Check whether a specific key event is bound in this context. + * + * @param ev Input event. + * + * @return true if event is bound, else false. + */ bool is_bound(const Event &ev) const; /** - * Get the action(s) bound to the given event. - * - * @param ev Input event triggering the action. - */ + * Get the action(s) bound to the given event. + * + * @param ev Input event triggering the action. + */ const std::vector &lookup(const Event &ev) const; /** @@ -162,8 +162,8 @@ class InputContext { private: /** - * Unique ID of the context. - */ + * Unique ID of the context. + */ std::string id; /** @@ -177,18 +177,18 @@ class InputContext { std::unordered_map, event_class_hash> by_class; /** - * Additional context for game simulation events. - */ + * Additional context for game simulation events. + */ std::shared_ptr game_bindings; /** - * Additional context for camera actions. - */ + * Additional context for camera actions. + */ std::shared_ptr camera_bindings; /** - * Additional context for HUD actions. - */ + * Additional context for HUD actions. + */ std::shared_ptr hud_bindings; }; diff --git a/libopenage/input/input_manager.h b/libopenage/input/input_manager.h index 36bd158d59..25b6315341 100644 --- a/libopenage/input/input_manager.h +++ b/libopenage/input/input_manager.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -44,30 +44,30 @@ class InputManager { /** * Set the GUI input handler. - * - * @param gui_input GUI input handler. + * + * @param gui_input GUI input handler. */ void set_gui(const std::shared_ptr &gui_input); /** - * Set the controller for the camera. - * - * @param controller Camera controller. - */ + * Set the controller for the camera. + * + * @param controller Camera controller. + */ void set_camera_controller(const std::shared_ptr &controller); /** - * Set the controller for the game simulation. - * - * @param controller Game controller. - */ + * Set the controller for the game simulation. + * + * @param controller Game controller. + */ void set_game_controller(const std::shared_ptr &controller); /** - * Set the controller for the HUD. - * - * @param controller HUD controller. - */ + * Set the controller for the HUD. + * + * @param controller HUD controller. + */ void set_hud_controller(const std::shared_ptr controller); /** @@ -91,7 +91,7 @@ class InputManager { /** * Push a context on top of the stack, making it the - * current top context. + * current top context. * * if other contexts are registered afterwards, * it wanders down the stack, i.e. loses priority. @@ -100,7 +100,7 @@ class InputManager { /** * Push the context with the specified ID on top of the stack, - * making it the current top context. + * making it the current top context. * * if other contexts are registered afterwards, * it wanders down the stack, i.e. loses priority. @@ -141,23 +141,23 @@ class InputManager { void set_motion(int x, int y); /** - * Process an input event from the Qt window management. - * - * @param ev Qt input event. - * - * @return true if the event is accepted, else false. - */ + * Process an input event from the Qt window management. + * + * @param ev Qt input event. + * + * @return true if the event is accepted, else false. + */ bool process(const QEvent &ev); private: /** - * Process the (default) action for an input event. - * - * @param ev Input event. - * @param action Action bound to the event. - * @param bind_ctx Context the action is bound in. - */ + * Process the (default) action for an input event. + * + * @param ev Input event. + * @param action Action bound to the event. + * @param bind_ctx Context the action is bound in. + */ void process_action(const input::Event &ev, const input_action &action, const std::shared_ptr &ctx); @@ -175,29 +175,29 @@ class InputManager { /** * Map of all available contexts, referencable by an ID. - * - * TODO: Move this to cvar manager? + * + * TODO: Move this to cvar manager? */ std::unordered_map> available_contexts; /** - * Interface to the game simulation. - */ + * Interface to the game simulation. + */ std::shared_ptr game_controller; /** - * Interface to the camera. - */ + * Interface to the camera. + */ std::shared_ptr camera_controller; /** - * Interface to the HUD. - */ + * Interface to the HUD. + */ std::shared_ptr hud_controller; /** - * Interface to the GUI. - */ + * Interface to the GUI. + */ std::shared_ptr gui_input; /** diff --git a/libopenage/job/abortable_job_state.h b/libopenage/job/abortable_job_state.h index 5f0036ddce..ff7b01af27 100644 --- a/libopenage/job/abortable_job_state.h +++ b/libopenage/job/abortable_job_state.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -17,7 +17,7 @@ namespace job { * providing two function objects to the job's function. One is used to check * whether the job should be aborted, while the other one aborts the job. */ -template +template class AbortableJobState : public TypedJobStateBase { public: /** The job's function. */ @@ -25,8 +25,7 @@ class AbortableJobState : public TypedJobStateBase { /** Creates a new abortable job with the given function and callback. */ AbortableJobState(abortable_function_t function, - callback_function_t callback) - : + callback_function_t callback) : TypedJobStateBase{callback}, function{function} { } @@ -48,4 +47,5 @@ class AbortableJobState : public TypedJobStateBase { }; -}} // namespace openage::job +} // namespace job +} // namespace openage diff --git a/libopenage/job/job.h b/libopenage/job/job.h index 78181dc7a6..46841de674 100644 --- a/libopenage/job/job.h +++ b/libopenage/job/job.h @@ -1,4 +1,4 @@ -// Copyright 2014-2016 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -24,7 +24,7 @@ class JobManager; * * @param T the job's result type */ -template +template class Job { private: /** A shared pointer to the job's shared state. */ @@ -58,7 +58,8 @@ class Job { ENSURE(this->state->finished.load(), "trying to report a result of an unfinished job"); if (this->state->exception != nullptr) { std::rethrow_exception(this->state->exception); - } else { + } + else { return std::move(this->state->result); } } @@ -68,8 +69,7 @@ class Job { * Creates a job with the given shared state. This method may only be called * by the job manager. */ - Job(std::shared_ptr> state) - : + Job(std::shared_ptr> state) : state{state} { } @@ -81,5 +81,5 @@ class Job { friend class JobManager; }; -} -} +} // namespace job +} // namespace openage diff --git a/libopenage/job/job_aborted_exception.h b/libopenage/job/job_aborted_exception.h index 77d1379a81..34916b33d9 100644 --- a/libopenage/job/job_aborted_exception.h +++ b/libopenage/job/job_aborted_exception.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -15,4 +15,5 @@ class JobAbortedException : public std::exception { } }; -}} // openage::job +} // namespace job +} // namespace openage diff --git a/libopenage/job/job_group.h b/libopenage/job/job_group.h index 70812217c4..3b911ee4e6 100644 --- a/libopenage/job/job_group.h +++ b/libopenage/job/job_group.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -42,9 +42,9 @@ class JobGroup { * @param callback the callback function that is executed, when the background * job has finished */ - template + template Job enqueue(job_function_t function, - callback_function_t callback={}) { + callback_function_t callback = {}) { ENSURE(this->parent_worker, "job group has no worker thread associated"); auto state = std::make_shared>(function, callback); this->parent_worker->enqueue(state); @@ -63,9 +63,9 @@ class JobGroup { * @param callback the callback function that is executed, when the background * job has finished */ - template + template Job enqueue(abortable_function_t function, - callback_function_t callback={}) { + callback_function_t callback = {}) { ENSURE(this->parent_worker, "job group has no worker thread associated"); auto state = std::make_shared>(function, callback); this->parent_worker->enqueue(state); @@ -83,5 +83,5 @@ class JobGroup { friend class JobManager; }; -} -} +} // namespace job +} // namespace openage diff --git a/libopenage/job/job_manager.h b/libopenage/job/job_manager.h index 65e323f320..b06a56bcad 100644 --- a/libopenage/job/job_manager.h +++ b/libopenage/job/job_manager.h @@ -1,4 +1,4 @@ -// Copyright 2014-2019 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -65,11 +65,11 @@ class JobManager { /** Destructor that stops the job manager if it is still running. */ ~JobManager(); - JobManager(const JobManager&) = delete; - JobManager(JobManager&&) = delete; + JobManager(const JobManager &) = delete; + JobManager(JobManager &&) = delete; - JobManager &operator=(const JobManager&) = delete; - JobManager &operator=(JobManager&&) = delete; + JobManager &operator=(const JobManager &) = delete; + JobManager &operator=(JobManager &&) = delete; /** Start the job manager's worker threads. */ void start(); @@ -89,9 +89,9 @@ class JobManager { * @param callback the callback function that is executed, when the background * job has finished */ - template + template Job enqueue(job_function_t function, - callback_function_t callback={}) { + callback_function_t callback = {}) { auto state = std::make_shared>(function, callback); this->enqueue_state(state); return Job{state}; @@ -109,9 +109,9 @@ class JobManager { * @param callback the callback function that is executed, when the background * job has finished */ - template + template Job enqueue(abortable_function_t function, - callback_function_t callback={}) { + callback_function_t callback = {}) { auto state = std::make_shared>(function, callback); this->enqueue_state(state); return Job{state}; @@ -152,5 +152,5 @@ class JobManager { friend class Worker; }; -} -} +} // namespace job +} // namespace openage diff --git a/libopenage/job/job_state.h b/libopenage/job/job_state.h index 882bcb044d..0d32bf7d42 100644 --- a/libopenage/job/job_state.h +++ b/libopenage/job/job_state.h @@ -1,4 +1,4 @@ -// Copyright 2014-2016 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -14,15 +14,14 @@ namespace job { * A job state supports simple job's with functions that return a single * result. While executing the job, it cannot be aborted safely. */ -template +template class JobState : public TypedJobStateBase { public: /** A function object which is executed by the JobManager. */ job_function_t function; /** Creates a new JobState with the given function, that is to be executed. */ - JobState(job_function_t function, callback_function_t callback) - : + JobState(job_function_t function, callback_function_t callback) : TypedJobStateBase{callback}, function{function} { } @@ -31,10 +30,10 @@ class JobState : public TypedJobStateBase { virtual ~JobState() = default; protected: - T execute_and_get(should_abort_t /*should_abort*/) override{ + T execute_and_get(should_abort_t /*should_abort*/) override { return this->function(); } }; -} -} +} // namespace job +} // namespace openage diff --git a/libopenage/job/job_state_base.h b/libopenage/job/job_state_base.h index ccd1015e21..ef3f7e236f 100644 --- a/libopenage/job/job_state_base.h +++ b/libopenage/job/job_state_base.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -38,5 +38,5 @@ class JobStateBase { virtual size_t get_thread_id() = 0; }; -} -} +} // namespace job +} // namespace openage diff --git a/libopenage/job/typed_job_state_base.h b/libopenage/job/typed_job_state_base.h index 4acbb5a8eb..e0f2f360ed 100644 --- a/libopenage/job/typed_job_state_base.h +++ b/libopenage/job/typed_job_state_base.h @@ -1,4 +1,4 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -6,8 +6,8 @@ #include #include -#include "../util/thread_id.h" #include "../error/error.h" +#include "../util/thread_id.h" #include "job_aborted_exception.h" #include "job_state_base.h" #include "types.h" @@ -22,7 +22,7 @@ namespace job { * @param T the result type of this job state. This type must have a default * constructor and support move semantics. */ -template +template class TypedJobStateBase : public JobStateBase { public: /** Id of the thread, that created this job state. */ @@ -48,8 +48,7 @@ class TypedJobStateBase : public JobStateBase { std::exception_ptr exception; /** Creates a new typed job with the given callback. */ - TypedJobStateBase(callback_function_t callback) - : + TypedJobStateBase(callback_function_t callback) : thread_id{openage::util::get_current_thread_id()}, callback{callback}, finished{false} { @@ -66,9 +65,11 @@ class TypedJobStateBase : public JobStateBase { bool execute(should_abort_t should_abort) override { try { this->result = this->execute_and_get(should_abort); - } catch (JobAbortedException &e) { + } + catch (JobAbortedException &e) { return true; - } catch (...) { + } + catch (...) { this->exception = std::current_exception(); } this->finished.store(true); @@ -85,7 +86,8 @@ class TypedJobStateBase : public JobStateBase { auto get_result = [this]() { if (this->exception != nullptr) { std::rethrow_exception(this->exception); - } else { + } + else { return std::move(this->result); } }; @@ -105,4 +107,5 @@ class TypedJobStateBase : public JobStateBase { virtual T execute_and_get(should_abort_t should_abort) = 0; }; -}} // openage::job +} // namespace job +} // namespace openage diff --git a/libopenage/job/types.h b/libopenage/job/types.h index bc2dbd077e..a197663da6 100644 --- a/libopenage/job/types.h +++ b/libopenage/job/types.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -12,7 +12,7 @@ namespace job { * * @param T the job's result type */ -template +template using job_function_t = std::function; /** @@ -21,8 +21,8 @@ using job_function_t = std::function; * * @param T the job's result type */ -template -using abortable_function_t = std::function,std::function)>; +template +using abortable_function_t = std::function, std::function)>; /** * Type of a function to retrieve the result of a job. If the job threw an @@ -30,7 +30,7 @@ using abortable_function_t = std::function,std::function * * @param T the job's result type */ -template +template using result_function_t = std::function; /** @@ -39,7 +39,7 @@ using result_function_t = std::function; * * @param T the job's result type */ -template +template using callback_function_t = std::function)>; /** Type of a function that returns whether a job should be aborted. */ @@ -48,5 +48,5 @@ using should_abort_t = std::function; /** Type of a function that aborts a job. */ using abort_t = std::function; -} -} +} // namespace job +} // namespace openage diff --git a/libopenage/job/worker.h b/libopenage/job/worker.h index 6332ac8b30..b5873feedc 100644 --- a/libopenage/job/worker.h +++ b/libopenage/job/worker.h @@ -1,4 +1,4 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -79,5 +79,5 @@ class Worker { void process(); }; -} -} +} // namespace job +} // namespace openage diff --git a/libopenage/log/logsink.h b/libopenage/log/logsink.h index 6edfa7890c..a4583ee48e 100644 --- a/libopenage/log/logsink.h +++ b/libopenage/log/logsink.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -46,9 +46,9 @@ class LogSink { /** -* Holds a list of all registered log sinks; -* Maintained from the LogSink constructors/destructors. -*/ + * Holds a list of all registered log sinks; + * Maintained from the LogSink constructors/destructors. + */ class OAAPI LogSinkList { public: static LogSinkList &instance(); diff --git a/libopenage/main/demo/pong/aicontroller.h b/libopenage/main/demo/pong/aicontroller.h index 9bdfd2502e..f642a22460 100644 --- a/libopenage/main/demo/pong/aicontroller.h +++ b/libopenage/main/demo/pong/aicontroller.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 @@ -12,7 +12,6 @@ std::vector get_ai_inputs( const std::shared_ptr &ball, const std::shared_ptr> &area_size, const time::time_t &now, - bool right_player -); + bool right_player); -} // openage::main::tests::pong +} // namespace openage::main::tests::pong diff --git a/libopenage/presenter/presenter.h b/libopenage/presenter/presenter.h index db28522a7c..b43d4fc60f 100644 --- a/libopenage/presenter/presenter.h +++ b/libopenage/presenter/presenter.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 @@ -195,8 +195,8 @@ class Presenter { std::shared_ptr world_renderer; /** - * Graphics output for the HUD. - */ + * Graphics output for the HUD. + */ std::shared_ptr hud_renderer; /** diff --git a/libopenage/pyinterface/defs.h b/libopenage/pyinterface/defs.h index 3e00a07b8a..2af26198eb 100644 --- a/libopenage/pyinterface/defs.h +++ b/libopenage/pyinterface/defs.h @@ -1,4 +1,4 @@ -// Copyright 2017-2017 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,6 @@ #ifndef Py_OBJECT_H // pxd: from cpython.ref cimport PyObject extern "C" { - typedef struct _object PyObject; +typedef struct _object PyObject; } #endif diff --git a/libopenage/pyinterface/exctranslate.h b/libopenage/pyinterface/exctranslate.h index 92124d3968..760d87c159 100644 --- a/libopenage/pyinterface/exctranslate.h +++ b/libopenage/pyinterface/exctranslate.h @@ -1,4 +1,4 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -146,4 +146,5 @@ OAAPI void set_exc_translation_funcs( void (*describe_py_exception)(PyException *)); -}} // openage::pyinterface +} // namespace pyinterface +} // namespace openage diff --git a/libopenage/pyinterface/exctranslate_tests.h b/libopenage/pyinterface/exctranslate_tests.h index 9446fe147b..622ec0165f 100644 --- a/libopenage/pyinterface/exctranslate_tests.h +++ b/libopenage/pyinterface/exctranslate_tests.h @@ -1,4 +1,4 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -44,4 +44,6 @@ OAAPI void bounce_call(const Func &func, int times); extern OAAPI PyIfFunc, int> bounce_call_py; -}}} // openage::pyinterface::tests +} // namespace tests +} // namespace pyinterface +} // namespace openage diff --git a/libopenage/pyinterface/functional.h b/libopenage/pyinterface/functional.h index b6e7e51e8c..1f5801ea74 100644 --- a/libopenage/pyinterface/functional.h +++ b/libopenage/pyinterface/functional.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -54,53 +54,52 @@ namespace pyinterface { * initialization time, use PyIfFunc instead of Func; that class has some * additional code to verify successful initialization. */ -template +template class Func { public: - Func() - : + Func() : fptr{nullptr} {} // for construction from lambdas and other callables (from C++). - template + template Func(F &&f) { this->fptr = f; } - template + template Func(std::reference_wrapper f) { this->fptr = f; } // for construction from std::function objects (from C++). - Func(const std::function &f) { + Func(const std::function &f) { this->fptr = f; } - Func(std::function &&f) { + Func(std::function &&f) { this->fptr = f; } // for assignment of lambdas and other callables (from C++). - template - Func &operator =(F &&f) { + template + Func &operator=(F &&f) { this->fptr = f; return *this; } - template - Func &operator =(std::reference_wrapper f) { + template + Func &operator=(std::reference_wrapper f) { this->fptr = f; return *this; } // for assignment of std::function objects (from C++). - Func &operator =(const std::function &f) { + Func &operator=(const std::function &f) { this->fptr = f; return *this; } - Func &operator =(std::function &&f) { + Func &operator=(std::function &&f) { this->fptr = f; return *this; } @@ -111,9 +110,10 @@ class Func { inline void check_fptr() const { if (not this->fptr) [[unlikely]] { throw Error( - MSG(err) << "Uninitialized Func object at " << - util::symbol_name(static_cast(this)) << ": " - "Can not call or convert to std::function.", + MSG(err) << "Uninitialized Func object at " + << util::symbol_name(static_cast(this)) + << ": " + "Can not call or convert to std::function.", true // collect backtrace info ); @@ -123,7 +123,7 @@ class Func { /** * for direct usage (mostly from Cython) */ - ReturnType call(ArgTypes ...args) const { + ReturnType call(ArgTypes... args) const { this->check_fptr(); return this->fptr(args...); } @@ -132,7 +132,7 @@ class Func { * for implicit conversion to std::function, * for usage in a context where std::function would be expected. */ - operator const std::function &() const { + operator const std::function &() const { this->check_fptr(); return this->fptr; } @@ -140,7 +140,7 @@ class Func { /** * for explicit conversion to std::function. */ - const std::function &get() const { + const std::function &get() const { this->check_fptr(); return this->fptr; } @@ -154,9 +154,9 @@ class Func { * Note that with clang, it's possible to directly pass function pointers, while with * gcc they need to be explicitly converted. Meh. */ - template - inline void bind(util::FunctionPtr f, BoundArgTypes ...bound_args) { - this->bind_catchexcept_impl::value, BoundArgTypes ...>(f, bound_args...); + template + inline void bind(util::FunctionPtr f, BoundArgTypes... bound_args) { + this->bind_catchexcept_impl::value, BoundArgTypes...>(f, bound_args...); } @@ -164,9 +164,9 @@ class Func { /** * Specialization for bind() with void return types. */ - template - inline typename std::enable_if::type bind_catchexcept_impl(util::FunctionPtr f, BoundArgTypes ...bound_args) { - this->fptr = [=](ArgTypes ...args) -> ReturnType { + template + inline typename std::enable_if::type bind_catchexcept_impl(util::FunctionPtr f, BoundArgTypes... bound_args) { + this->fptr = [=](ArgTypes... args) -> ReturnType { f.ptr(bound_args..., args...); translate_exc_py_to_cpp(); }; @@ -176,9 +176,9 @@ class Func { /** * Specialization for bind() with non-void return types. */ - template - inline typename std::enable_if::type bind_catchexcept_impl(util::FunctionPtr f, BoundArgTypes ...bound_args) { - this->fptr = [=](ArgTypes ...args) -> ReturnType { + template + inline typename std::enable_if::type bind_catchexcept_impl(util::FunctionPtr f, BoundArgTypes... bound_args) { + this->fptr = [=](ArgTypes... args) -> ReturnType { ReturnType &&result = f.ptr(bound_args..., args...); translate_exc_py_to_cpp(); return result; @@ -190,62 +190,62 @@ class Func { /** * Like bind, but does _not_ add an exception checker. */ - template - void bind_noexcept(util::FunctionPtr f, BoundArgTypes ...bound_args) { - this->fptr = [=](ArgTypes ...args) -> ReturnType { + template + void bind_noexcept(util::FunctionPtr f, BoundArgTypes... bound_args) { + this->fptr = [=](ArgTypes... args) -> ReturnType { return f.ptr(bound_args..., args...); }; } // non-variadic aliases for bind, for use by Cython - inline void bind0(ReturnType (*f)(ArgTypes ...)) { + inline void bind0(ReturnType (*f)(ArgTypes...)) { this->bind<>( - util::FunctionPtr(f)); + util::FunctionPtr(f)); } - inline void bind_noexcept0(ReturnType (*f)(ArgTypes ...)) { + inline void bind_noexcept0(ReturnType (*f)(ArgTypes...)) { this->bind_noexcept<>( - util::FunctionPtr(f)); + util::FunctionPtr(f)); } - template - inline void bind1(ReturnType (*f)(BoundArgType0, ArgTypes ...), BoundArgType0 bound_arg0) { + template + inline void bind1(ReturnType (*f)(BoundArgType0, ArgTypes...), BoundArgType0 bound_arg0) { this->bind( - util::FunctionPtr(f), bound_arg0); + util::FunctionPtr(f), bound_arg0); } - template - inline void bind_noexcept1(ReturnType (*f)(BoundArgType0, ArgTypes ...), BoundArgType0 bound_arg0) { + template + inline void bind_noexcept1(ReturnType (*f)(BoundArgType0, ArgTypes...), BoundArgType0 bound_arg0) { this->bind_noexcept( - util::FunctionPtr(f), bound_arg0); + util::FunctionPtr(f), bound_arg0); } - template - inline void bind2(ReturnType (*f)(BoundArgType0, BoundArgType1, ArgTypes ...), BoundArgType0 bound_arg0, BoundArgType1 bound_arg1) { + template + inline void bind2(ReturnType (*f)(BoundArgType0, BoundArgType1, ArgTypes...), BoundArgType0 bound_arg0, BoundArgType1 bound_arg1) { this->bind( - util::FunctionPtr(f), bound_arg0, bound_arg1); + util::FunctionPtr(f), bound_arg0, bound_arg1); } - template - inline void bind_noexcept2(ReturnType (*f)(BoundArgType0, BoundArgType1, ArgTypes ...), BoundArgType0 bound_arg0, BoundArgType1 bound_arg1) { + template + inline void bind_noexcept2(ReturnType (*f)(BoundArgType0, BoundArgType1, ArgTypes...), BoundArgType0 bound_arg0, BoundArgType1 bound_arg1) { this->bind_noexcept( - util::FunctionPtr(f), bound_arg0, bound_arg1); + util::FunctionPtr(f), bound_arg0, bound_arg1); } - template - inline void bind3(ReturnType (*f)(BoundArgType0, BoundArgType1, BoundArgType2, ArgTypes ...), BoundArgType0 bound_arg0, BoundArgType1 bound_arg1, BoundArgType2 bound_arg2) { + template + inline void bind3(ReturnType (*f)(BoundArgType0, BoundArgType1, BoundArgType2, ArgTypes...), BoundArgType0 bound_arg0, BoundArgType1 bound_arg1, BoundArgType2 bound_arg2) { this->bind( - util::FunctionPtr(f), bound_arg0, bound_arg1, bound_arg2); + util::FunctionPtr(f), bound_arg0, bound_arg1, bound_arg2); } - template - inline void bind_noexcept3(ReturnType (*f)(BoundArgType0, BoundArgType1, BoundArgType2, ArgTypes ...), BoundArgType0 bound_arg0, BoundArgType1 bound_arg1, BoundArgType2 bound_arg2) { + template + inline void bind_noexcept3(ReturnType (*f)(BoundArgType0, BoundArgType1, BoundArgType2, ArgTypes...), BoundArgType0 bound_arg0, BoundArgType1 bound_arg1, BoundArgType2 bound_arg2) { this->bind_noexcept( - util::FunctionPtr(f), bound_arg0, bound_arg1, bound_arg2); + util::FunctionPtr(f), bound_arg0, bound_arg1, bound_arg2); } private: - std::function fptr; + std::function fptr; }; @@ -271,7 +271,7 @@ class Func { * void bind_noexcept2 [BT0, BT1] (RT (*f)(BT0, BT1) with gil, BT0, BT1 ) except + * void bind_noexcept3 [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2) with gil, BT0, BT1, BT2) except + */ -template +template using Func0 = Func; /* @@ -292,7 +292,7 @@ using Func0 = Func; * void bind_noexcept2 [BT0, BT1] (RT (*f)(BT0, BT1, AT0) with gil, BT0, BT1 ) except + * void bind_noexcept3 [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0) with gil, BT0, BT1, BT2) except + */ -template +template using Func1 = Func; /* @@ -314,7 +314,7 @@ using Func1 = Func; * void bind_noexcept3 [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0, AT1) with gil, BT0, BT1, BT2) except + */ -template +template using Func2 = Func; /* @@ -336,7 +336,7 @@ using Func2 = Func; * void bind_noexcept3 [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0, AT1, AT2) with gil, BT0, BT1, BT2) except + */ -template +template using Func3 = Func; /* @@ -358,7 +358,7 @@ using Func3 = Func; * void bind_noexcept3 [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0, AT1, AT2, AT3) with gil, BT0, BT1, BT2) except + */ -template +template using Func4 = Func; @@ -381,7 +381,7 @@ using Func4 = Func; * void bind_noexcept3 [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0, AT1, AT2, AT3, AT4) with gil, BT0, BT1, BT2) except + */ -template +template using Func5 = Func; @@ -403,15 +403,16 @@ using Func5 = Func; * ctypedef Func4 PyIfFunc4 * ctypedef Func5 PyIfFunc5 */ -template -class PyIfFunc : public Func { +template +class PyIfFunc : public Func { public: PyIfFunc() { add_py_if_component(this, [=, this]() -> bool { try { this->check_fptr(); return true; - } catch (Error &) { + } + catch (Error &) { return false; } }); @@ -422,16 +423,17 @@ class PyIfFunc : public Func { } // no copy construction! - PyIfFunc(const PyIfFunc &other) = delete; - PyIfFunc(PyIfFunc &&other) = delete; - PyIfFunc &operator =(const PyIfFunc &other) = delete; - PyIfFunc &operator =(PyIfFunc &&other) = delete; + PyIfFunc(const PyIfFunc &other) = delete; + PyIfFunc(PyIfFunc &&other) = delete; + PyIfFunc &operator=(const PyIfFunc &other) = delete; + PyIfFunc &operator=(PyIfFunc &&other) = delete; // but you may convert this to a regular Func object. - operator const Func &() const { - return static_cast>(this->fptr); + operator const Func &() const { + return static_cast>(this->fptr); } }; -}} // openage::pyinterface +} // namespace pyinterface +} // namespace openage diff --git a/libopenage/pyinterface/pyexception.h b/libopenage/pyinterface/pyexception.h index 2cdb7260c2..92cd676553 100644 --- a/libopenage/pyinterface/pyexception.h +++ b/libopenage/pyinterface/pyexception.h @@ -1,4 +1,4 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -33,12 +33,13 @@ class PyExceptionBacktrace : public error::Backtrace { /** * ref is a raw reference to the associated PyObject. */ - PyExceptionBacktrace(PyObject *ref) : ref{ref} {} + PyExceptionBacktrace(PyObject *ref) : + ref{ref} {} /** * Accesses the associated Python exception object to translate the traceback as needed. */ - void get_symbols(std::function cb, bool reversed) const override; + void get_symbols(std::function cb, bool reversed) const override; private: PyObject *ref; @@ -83,4 +84,5 @@ class OAAPI PyException : public error::Error { extern OAAPI PyIfFunc> pyexception_bt_get_symbols; -}} // openage::pyinterface +} // namespace pyinterface +} // namespace openage diff --git a/libopenage/pyinterface/pyobject.h b/libopenage/pyinterface/pyobject.h index 0e90717e88..7ce39505e6 100644 --- a/libopenage/pyinterface/pyobject.h +++ b/libopenage/pyinterface/pyobject.h @@ -1,4 +1,4 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -72,13 +72,13 @@ class OAAPI PyObjectRef { * Assigns from an other PyObjectRef * (calls Py_XDECREF on the old value, and Py_XINCREF on the new one). */ - PyObjectRef &operator =(const PyObjectRef &other); + PyObjectRef &operator=(const PyObjectRef &other); /** * Move-assigns from an other PyObject * (calls Py_XDECREF on the old value). */ - PyObjectRef &operator =(PyObjectRef &&other); + PyObjectRef &operator=(PyObjectRef &&other); /** * Destroys the object, calls Py_XDECREF. @@ -116,12 +116,11 @@ class OAAPI PyObjectRef { /** * obj(args...) */ - template + template PyObjectRef call(Args... args) const { // this vector collects the function call arguments - std::vector arg_objs { - PyObjectRef(args)... - }; + std::vector arg_objs{ + PyObjectRef(args)...}; return this->call_impl(arg_objs); } @@ -251,7 +250,7 @@ class OAAPI PyObjectRef { * Implicit conversion to PyObject *. * Mainly for convenience to avoid all the get_ref() calls. */ - PyObject *operator ()() const noexcept { + PyObject *operator()() const noexcept { return this->ref; } @@ -285,7 +284,7 @@ using PyObj = PyObjectRef; /** * Stream operator for printing PyObjects */ -std::ostream &operator <<(std::ostream &os, const PyObjectRef &ref); +std::ostream &operator<<(std::ostream &os, const PyObjectRef &ref); // now follow the various Python callbacks that implement all of the above, @@ -313,7 +312,7 @@ extern OAAPI PyIfFunc py_callable; // pxd: PyIfFunc2[void, PyObjectRefPtr, PyObjectPtr] py_call0 extern OAAPI PyIfFunc py_call0; // pxd: PyIfFunc3[void, PyObjectRefPtr, PyObjectPtr, vector[PyObjectPtr]] py_calln -extern OAAPI PyIfFunc&> py_calln; +extern OAAPI PyIfFunc &> py_calln; // pxd: PyIfFunc2[cppbool, PyObjectPtr, string] py_hasattr extern OAAPI PyIfFunc py_hasattr; // pxd: PyIfFunc3[void, PyObjectRefPtr, PyObjectPtr, string] py_getattr @@ -346,13 +345,13 @@ extern OAAPI PyIfFunc py_modulename; extern OAAPI PyIfFunc py_classname; // pxd: PyIfFunc2[void, PyObjectRefPtr, const string] py_builtin -extern OAAPI PyIfFunc py_builtin; +extern OAAPI PyIfFunc py_builtin; // pxd: PyIfFunc2[void, PyObjectRefPtr, const string] py_import -extern OAAPI PyIfFunc py_import; +extern OAAPI PyIfFunc py_import; // pxd: PyIfFunc2[void, PyObjectRefPtr, const string] py_createstr -extern OAAPI PyIfFunc py_createstr; +extern OAAPI PyIfFunc py_createstr; // pxd: PyIfFunc2[void, PyObjectRefPtr, const string] py_createbytes -extern OAAPI PyIfFunc py_createbytes; +extern OAAPI PyIfFunc py_createbytes; // pxd: PyIfFunc2[void, PyObjectRefPtr, int] py_createint extern OAAPI PyIfFunc py_createint; // pxd: PyIfFunc1[void, PyObjectRefPtr] py_createdict @@ -367,7 +366,7 @@ extern OAAPI PyObjectRef True; // pxd: PyObjectRef False extern OAAPI PyObjectRef False; -} // pyinterface +} // namespace pyinterface /** @@ -440,5 +439,5 @@ using pyinterface::True; */ using pyinterface::False; -} // py -} // openage +} // namespace py +} // namespace openage diff --git a/libopenage/pyinterface/pyobject_tests.h b/libopenage/pyinterface/pyobject_tests.h index fa8492c0ab..3c8ab49d9d 100644 --- a/libopenage/pyinterface/pyobject_tests.h +++ b/libopenage/pyinterface/pyobject_tests.h @@ -1,4 +1,4 @@ -// Copyright 2017-2017 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -9,4 +9,6 @@ namespace tests { void pyobject(); void pyobject_demo(); -}}} // openage::pyinterface +} // namespace tests +} // namespace pyinterface +} // namespace openage diff --git a/libopenage/pyinterface/setup.h b/libopenage/pyinterface/setup.h index 2aa387c8d9..34a4fbf36e 100644 --- a/libopenage/pyinterface/setup.h +++ b/libopenage/pyinterface/setup.h @@ -1,4 +1,4 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include @@ -30,7 +30,7 @@ namespace pyinterface { * It shall return true if the object has been properly initialized, * and shall not throw any exceptions. */ -void add_py_if_component(void *thisptr, std::function checker); +void add_py_if_component(void *thisptr, std::function checker); /** @@ -56,4 +56,5 @@ void destroy_py_if_component(void *thisptr); OAAPI void check(); -}} // openage::pyinterface +} // namespace pyinterface +} // namespace openage diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index cb7b1868fb..9cdd559e6f 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -38,25 +38,25 @@ static const Eigen::Vector3f cam_direction{ class Camera { public: /** - * Create a new camera for the renderer. - * - * The camera uses default values. Its centered on the origin of the scene (0.0f, 0.0f, 0.0f) - * and has a zoom level of 1.0f. - * - * @param viewport_size Initial viewport size of the camera (width x height). - */ + * Create a new camera for the renderer. + * + * The camera uses default values. Its centered on the origin of the scene (0.0f, 0.0f, 0.0f) + * and has a zoom level of 1.0f. + * + * @param viewport_size Initial viewport size of the camera (width x height). + */ Camera(const std::shared_ptr &renderer, util::Vector2s viewport_size); /** - * Create a new camera for the renderer. - * - * @param viewport_size Viewport size of the camera (width x height). - * @param scene_pos Position of the camera in the scene. - * @param zoom Zoom level of the camera (defaults to 1.0f). - * @param max_zoom_out Maximum zoom out level (defaults to 64.0f). - * @param default_zoom_ratio Default zoom level calibration (defaults to (1.0f / 49)). - */ + * Create a new camera for the renderer. + * + * @param viewport_size Viewport size of the camera (width x height). + * @param scene_pos Position of the camera in the scene. + * @param zoom Zoom level of the camera (defaults to 1.0f). + * @param max_zoom_out Maximum zoom out level (defaults to 64.0f). + * @param default_zoom_ratio Default zoom level calibration (defaults to (1.0f / 49)). + */ Camera(const std::shared_ptr &renderer, util::Vector2s viewport_size, Eigen::Vector3f scene_pos, @@ -66,68 +66,68 @@ class Camera { ~Camera() = default; /** - * Move the camera so that the center of its viewpoint points - * to the given position in the 3D scene. - * - * @param scene_pos Position in the 3D scene that the camera should center on. - */ + * Move the camera so that the center of its viewpoint points + * to the given position in the 3D scene. + * + * @param scene_pos Position in the 3D scene that the camera should center on. + */ void look_at_scene(Eigen::Vector3f scene_pos); /** - * Move the camera so that the center of its viewpoint points - * to the given ingame coordinates. - * - * @param scene_pos Position of the ingame coordinates that the camera should center on. - */ + * Move the camera so that the center of its viewpoint points + * to the given ingame coordinates. + * + * @param scene_pos Position of the ingame coordinates that the camera should center on. + */ void look_at_coord(coord::scene3 coord_pos); /** - * Move the camera position in the direction of a given vector. - * - * @param scene_pos New 3D position of the camera in the scene. - */ + * Move the camera position in the direction of a given vector. + * + * @param scene_pos New 3D position of the camera in the scene. + */ void move_to(Eigen::Vector3f scene_pos); /** - * Move the camera position in the direction of a given vector. - * - * @param direction Direction vector. Added to the current position. - * @param delta Delta for controlling the amount by which the camera is moved. The - * value is multiplied with the directional vector before its applied to - * the positional vector. - */ + * Move the camera position in the direction of a given vector. + * + * @param direction Direction vector. Added to the current position. + * @param delta Delta for controlling the amount by which the camera is moved. The + * value is multiplied with the directional vector before its applied to + * the positional vector. + */ void move_rel(Eigen::Vector3f direction, float delta = 1.0f); /** - * Set the zoom level of the camera. Values smaller than 1.0f let the - * camera zoom in, values greater than 1.0f let the camera zoom out. - * - * The max zoom in value is 0.05f. Passing values lower than that - * will just set the zoom level to max zoom. - * - * For incremental zooming, use the \p zoom_in() and \p zoom_out() - * methods. - * - * @param zoom New zoom level. - */ + * Set the zoom level of the camera. Values smaller than 1.0f let the + * camera zoom in, values greater than 1.0f let the camera zoom out. + * + * The max zoom in value is 0.05f. Passing values lower than that + * will just set the zoom level to max zoom. + * + * For incremental zooming, use the \p zoom_in() and \p zoom_out() + * methods. + * + * @param zoom New zoom level. + */ void set_zoom(float zoom); /** - * Zoom into the scene. - * - * Decreases the current zoom level. - * - * @param zoom_delta How far the camera should zoom in. - */ + * Zoom into the scene. + * + * Decreases the current zoom level. + * + * @param zoom_delta How far the camera should zoom in. + */ void zoom_in(float zoom_delta); /** - * Zoom out of the scene. - * - * Increases the current zoom level. - * - * @param zoom_delta How far the camera should zoom out. - */ + * Zoom out of the scene. + * + * Increases the current zoom level. + * + * @param zoom_delta How far the camera should zoom out. + */ void zoom_out(float zoom_delta); /** @@ -139,138 +139,138 @@ class Camera { void resize(size_t width, size_t height); /** - * Get the current zoom level of the camera. - * - * @return Zoom level. - */ + * Get the current zoom level of the camera. + * + * @return Zoom level. + */ float get_zoom() const; /** - * Get the view matrix for this camera. - * - * @return Camera view matrix. - */ + * Get the view matrix for this camera. + * + * @return Camera view matrix. + */ const Eigen::Matrix4f &get_view_matrix(); /** - * Get the projection matrix for this camera. - * - * @return Camera projection matrix. - */ + * Get the projection matrix for this camera. + * + * @return Camera projection matrix. + */ const Eigen::Matrix4f &get_projection_matrix(); /** - * Get the size of the camera viewport. - * - * @return Viewport size as a 2D vector (x, y). - */ + * Get the size of the camera viewport. + * + * @return Viewport size as a 2D vector (x, y). + */ const util::Vector2s &get_viewport_size() const; /** - * Get the corresponding 3D position of a 2D input coordinate in the viewport. - * The position is on the plane created by the camera's orthographic projection. - * - * This may be used to get the 3D position of a mouse click and subsequent - * ray casting calculations. - * - * @param coord 2D input coordinate in the viewport. - * - * @return Position of the input in the 3D scene. - */ + * Get the corresponding 3D position of a 2D input coordinate in the viewport. + * The position is on the plane created by the camera's orthographic projection. + * + * This may be used to get the 3D position of a mouse click and subsequent + * ray casting calculations. + * + * @param coord 2D input coordinate in the viewport. + * + * @return Position of the input in the 3D scene. + */ Eigen::Vector3f get_input_pos(const coord::input &coord) const; /** - * Get the uniform buffer for this camera. - * - * @return Uniform buffer. - */ + * Get the uniform buffer for this camera. + * + * @return Uniform buffer. + */ const std::shared_ptr &get_uniform_buffer() const; private: /** - * Position in the 3D scene. - */ + * Position in the 3D scene. + */ Eigen::Vector3f scene_pos; /** - * Size of the camera viewport. - */ + * Size of the camera viewport. + */ util::Vector2s viewport_size; /** - * Zoom level. - * - * 0.0f < z < 1.0f => zoom in - * z = 1.0f => default view - * z > 1.0f => zoom out - */ + * Zoom level. + * + * 0.0f < z < 1.0f => zoom in + * z = 1.0f => default view + * z > 1.0f => zoom out + */ float zoom; /** - * Maximum possible zoom in level. - * - * Has to be above 0.0f, otherwise we get zero division errors. - */ + * Maximum possible zoom in level. + * + * Has to be above 0.0f, otherwise we get zero division errors. + */ static constexpr float MAX_ZOOM_IN = 0.005f; /** - * Maximum possible zoom out level. - * - * This can be set per camera. - */ + * Maximum possible zoom out level. + * + * This can be set per camera. + */ float max_zoom_out; /** - * Modifier that controls what the default zoom level (zoom = 1.0f) - * looks like. Essentially, this value is also a zoom that is pre-applied - * before other calculations. - * - * This value is important for calibrating the default zoom to match - * the pixel size of assets. For example, terrain in AoE2 has a height - * of 49 pixels, while in the renderer scene the height is always 1.0. - * Setting \p default_zoom_ratio to 1 / 49 ensure that the default zoom - * level matches the pixel ration of the original game. - */ + * Modifier that controls what the default zoom level (zoom = 1.0f) + * looks like. Essentially, this value is also a zoom that is pre-applied + * before other calculations. + * + * This value is important for calibrating the default zoom to match + * the pixel size of assets. For example, terrain in AoE2 has a height + * of 49 pixels, while in the renderer scene the height is always 1.0. + * Setting \p default_zoom_ratio to 1 / 49 ensure that the default zoom + * level matches the pixel ration of the original game. + */ float default_zoom_ratio; /** - * Flag set when the camera is moved. - * - * If true, the view matrix needs to be recalculated. - */ + * Flag set when the camera is moved. + * + * If true, the view matrix needs to be recalculated. + */ bool moved; /** - * Flag set when the camera zoom is changed. - * - * If true, the projection matrix needs to be recalculated. - */ + * Flag set when the camera zoom is changed. + * + * If true, the projection matrix needs to be recalculated. + */ bool zoom_changed; /** - * Flag set when the camera viewport is resized. - * - * If true, the projection matrix needs to be recalculated. - */ + * Flag set when the camera viewport is resized. + * + * If true, the projection matrix needs to be recalculated. + */ bool viewport_changed; /** - * Current view matrix for the camera. - * - * Cached because it may be requested many times. - */ + * Current view matrix for the camera. + * + * Cached because it may be requested many times. + */ Eigen::Matrix4f view; /** - * Current projection matrix for the camera. - * - * Cached because it may be requested many times. - */ + * Current projection matrix for the camera. + * + * Cached because it may be requested many times. + */ Eigen::Matrix4f proj; /** - * Uniform buffer for the camera matrices. - */ + * Uniform buffer for the camera matrices. + */ std::shared_ptr uniform_buffer; }; diff --git a/libopenage/renderer/color.h b/libopenage/renderer/color.h index 96b13f81f7..a7d445a56a 100644 --- a/libopenage/renderer/color.h +++ b/libopenage/renderer/color.h @@ -1,4 +1,4 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -9,7 +9,6 @@ namespace renderer { class Color { public: - Color(); Color(uint8_t r, uint8_t g, uint8_t b, uint8_t a); @@ -22,15 +21,13 @@ class Color { uint8_t g; uint8_t b; uint8_t a; - }; class Colors { public: - static Color WHITE; static Color BLACK; - }; -}} // openage::renderer +} // namespace renderer +} // namespace openage diff --git a/libopenage/renderer/font/font.h b/libopenage/renderer/font/font.h index 24d44df619..65496b3f62 100644 --- a/libopenage/renderer/font/font.h +++ b/libopenage/renderer/font/font.h @@ -1,4 +1,4 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -68,8 +68,8 @@ struct font_description { font_description(std::string font_file, unsigned int size, font_direction direction = font_direction::left_to_right, - std::string language="en", - std::string script="Latin"); + std::string language = "en", + std::string script = "Latin"); /** * Constructs a font_description instance. @@ -89,11 +89,9 @@ struct font_description { bool operator==(const font_description &other) const; bool operator!=(const font_description &other) const; - }; class Font { - public: /** * Create a font instance from the description. @@ -189,14 +187,14 @@ class Font { // The HarfBuzz font instance that drives the operations of this font hb_font_t *hb_font; - }; -}} // openage::renderer +} // namespace renderer +} // namespace openage namespace std { -template<> +template <> struct hash { size_t operator()(const openage::renderer::font_description &fd) const { size_t hash = std::hash()(std::type_index(typeid(openage::renderer::font_description))); @@ -206,4 +204,4 @@ struct hash { } }; -} +} // namespace std diff --git a/libopenage/renderer/font/font_manager.h b/libopenage/renderer/font/font_manager.h index 43d4f4e729..540ee1649d 100644 --- a/libopenage/renderer/font/font_manager.h +++ b/libopenage/renderer/font/font_manager.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -14,7 +14,6 @@ namespace openage { namespace renderer { class FreeTypeLibrary { - public: FT_Library ft_library; @@ -36,13 +35,11 @@ class FreeTypeLibrary { FreeTypeLibrary(FreeTypeLibrary &&other) = delete; FreeTypeLibrary &operator=(FreeTypeLibrary &&other) = delete; - }; class Font; class FontManager { - public: /** * Gets the filepath of a particular font family and style. @@ -80,7 +77,7 @@ class FontManager { * @param size: The size of the font in points. * @returns The pointer to font instance. */ - Font *get_font(const char* font_file, unsigned int size); + Font *get_font(const char *font_file, unsigned int size); private: // The freetype library instance @@ -88,7 +85,7 @@ class FontManager { // Font cache. the hash of font's description is used as the key std::unordered_map> fonts; - }; -}} // openage::renderer +} // namespace renderer +} // namespace openage diff --git a/libopenage/renderer/font/glyph_atlas.h b/libopenage/renderer/font/glyph_atlas.h index 8e77ce2eaa..2f8be76763 100644 --- a/libopenage/renderer/font/glyph_atlas.h +++ b/libopenage/renderer/font/glyph_atlas.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -24,7 +24,6 @@ namespace renderer { * glyph atlas can be easily modified. */ class GlyphAtlas { - public: /** * Datastructure for a single atlas entry @@ -129,7 +128,7 @@ class GlyphAtlas { // List of shelves currently used in the atlas std::vector shelves; - }; -}} // openage::renderer +} // namespace renderer +} // namespace openage diff --git a/libopenage/renderer/geometry.h b/libopenage/renderer/geometry.h index 90ce58b8cd..f6f6853925 100644 --- a/libopenage/renderer/geometry.h +++ b/libopenage/renderer/geometry.h @@ -1,10 +1,10 @@ -// Copyright 2015-2018 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once -#include -#include #include +#include +#include namespace openage { @@ -30,13 +30,13 @@ class Geometry { /// In a meshed geometry, updates the vertex data. The size and type of the vertex data has to be the same as before. /// If the mesh is indexed, indices will stay the same. /// @throws if there is a size mismatch between the new and old vertex data - void update_verts(std::vector const& verts); + void update_verts(std::vector const &verts); /// In a meshed geometry, updates the vertex data starting from the offset-th vertex. The type of the vertex /// data has to be the same as it was on initializing the geometry. The size plus the offset cannot exceed the /// previous size of the vertex data. If the mesh is indexed, indices will stay the same. /// @throws if there is a size mismatch between the new and old vertex data - virtual void update_verts_offset(std::vector const& verts, size_t offset) = 0; + virtual void update_verts_offset(std::vector const &verts, size_t offset) = 0; protected: /// Initialize the geometry to a given type. @@ -46,4 +46,5 @@ class Geometry { geometry_t type; }; -}} // openage::renderer +} // namespace renderer +} // namespace openage diff --git a/libopenage/renderer/gui/gui.h b/libopenage/renderer/gui/gui.h index 1cecfb7bd6..7baa7306d4 100644 --- a/libopenage/renderer/gui/gui.h +++ b/libopenage/renderer/gui/gui.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -47,10 +47,10 @@ class GUI { virtual ~GUI() = default; /** - * Get the input handler of the GUI. - * - * @return Input handler of the GUI. - */ + * Get the input handler of the GUI. + * + * @return Input handler of the GUI. + */ std::shared_ptr get_input_handler() const; /** diff --git a/libopenage/renderer/gui/guisys/link/gui_item.h b/libopenage/renderer/gui/guisys/link/gui_item.h index 9f8a5bd59f..a0a5001404 100644 --- a/libopenage/renderer/gui/guisys/link/gui_item.h +++ b/libopenage/renderer/gui/guisys/link/gui_item.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -222,8 +222,7 @@ class GuiItemCoreInstantiator : public GuiItemMethods { * Sets up a factory for the type T. */ explicit GuiItemCoreInstantiator(GuiItemBase *item_base) : - GuiItemMethods {} - { + GuiItemMethods{} { using namespace std::placeholders; item_base->instantiate_core_func = std::bind(&GuiItemCoreInstantiator::instantiate_core, this); @@ -298,10 +297,7 @@ class GuiItem : public GuiItemOrigin */ explicit GuiItem(GuiItemBase *item_base) : GuiItemOrigin{}, - GuiItemCoreInstantiator { - item_base - } - { + GuiItemCoreInstantiator{item_base} { } }; @@ -311,8 +307,7 @@ class GuiItemInterface : public GuiItemOrigin public: explicit GuiItemInterface() : GuiItemOrigin{}, - GuiItemMethods {} - { + GuiItemMethods{} { } }; @@ -356,10 +351,7 @@ class Inherits : public Shadow Inherits(QObject *parent = nullptr) : Shadow{parent}, - GuiItemCoreInstantiator

{ - this - } - { + GuiItemCoreInstantiator

{this} { } virtual ~Inherits() { diff --git a/libopenage/renderer/gui/guisys/link/gui_item_list_model.h b/libopenage/renderer/gui/guisys/link/gui_item_list_model.h index 7158247ccd..ccb3d34ea7 100644 --- a/libopenage/renderer/gui/guisys/link/gui_item_list_model.h +++ b/libopenage/renderer/gui/guisys/link/gui_item_list_model.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -20,10 +20,7 @@ template class GuiItemListModel : public GuiItem { public: explicit GuiItemListModel(GuiItemBase *item_base) : - GuiItem { - item_base - } - { + GuiItem{item_base} { item_base->on_core_adopted_func = std::bind(&GuiItemListModel::on_core_adopted, this); } diff --git a/libopenage/renderer/gui/guisys/private/gui_engine_impl.h b/libopenage/renderer/gui/guisys/private/gui_engine_impl.h index bee83ef4ea..019a9c7a1f 100644 --- a/libopenage/renderer/gui/guisys/private/gui_engine_impl.h +++ b/libopenage/renderer/gui/guisys/private/gui_engine_impl.h @@ -1,4 +1,4 @@ -// Copyright 2015-2022 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -37,8 +37,8 @@ class GuiQmlEngineImpl : public QObject { static GuiQmlEngineImpl *impl(GuiQmlEngine *engine); /** - * Get the underlying QQmlEngine object. - */ + * Get the underlying QQmlEngine object. + */ std::shared_ptr get_qml_engine(); /** diff --git a/libopenage/renderer/opengl/context.h b/libopenage/renderer/opengl/context.h index ba5459ed5f..42c7564118 100644 --- a/libopenage/renderer/opengl/context.h +++ b/libopenage/renderer/opengl/context.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -16,7 +16,7 @@ class GlShaderProgram; /** * Stores information about context capabilities and limitations. -*/ + */ struct gl_context_spec { /// The maximum number of vertex attributes in a shader. size_t max_vertex_attributes; @@ -67,14 +67,14 @@ class GlContext { std::shared_ptr get_raw_context() const; /** - * Get the ID of the default framebuffer used for displaying to - * the window. - * - * This value may change on every frame, so it should be called every - * time the default framebuffer is bound. - * - * @return ID of the default (display) framebuffer. - */ + * Get the ID of the default framebuffer used for displaying to + * the window. + * + * This value may change on every frame, so it should be called every + * time the default framebuffer is bound. + * + * @return ID of the default (display) framebuffer. + */ unsigned int get_default_framebuffer_id(); /** @@ -122,29 +122,29 @@ class GlContext { void set_current_program(const std::shared_ptr &prog); /** - * Get a free uniform buffer binding point that is not bound to any buffer. - * - * The number of available binding points is limited by the OpenGL implementation. - * When the context is created, there are \p capabilities.max_uniform_buffer_bindings - * free binding points available. - * - * @return Binding point ID. - * - * @throw Error if no binding point is available. - */ + * Get a free uniform buffer binding point that is not bound to any buffer. + * + * The number of available binding points is limited by the OpenGL implementation. + * When the context is created, there are \p capabilities.max_uniform_buffer_bindings + * free binding points available. + * + * @return Binding point ID. + * + * @throw Error if no binding point is available. + */ size_t get_uniform_buffer_binding(); /** - * Free a buffer binding point, indicating that newly created buffers can use it. - * - * When calling this function, it must be ensured that the binding point is not - * assigned to any buffer or shader. Otherwise, reassigning the binding point - * can corrupt the uniform data. - * - * @param binding_point Binding point ID. - * - * @throw Error if the binding point is not valid. - */ + * Free a buffer binding point, indicating that newly created buffers can use it. + * + * When calling this function, it must be ensured that the binding point is not + * assigned to any buffer or shader. Otherwise, reassigning the binding point + * can corrupt the uniform data. + * + * @param binding_point Binding point ID. + * + * @throw Error if the binding point is not valid. + */ void free_uniform_buffer_binding(size_t binding_point); /** @@ -181,8 +181,8 @@ class GlContext { std::weak_ptr last_program; /** - * Store the currently active binding points for uniform buffers. - */ + * Store the currently active binding points for uniform buffers. + */ std::vector uniform_buffer_bindings; }; diff --git a/libopenage/renderer/opengl/debug.h b/libopenage/renderer/opengl/debug.h index 3c945eb76f..69deff452d 100644 --- a/libopenage/renderer/opengl/debug.h +++ b/libopenage/renderer/opengl/debug.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -32,7 +32,7 @@ class GlDebugLogHandler : public QObject { /** * Stop logging OpenGL debug messages. - */ + */ void stop(); public slots: diff --git a/libopenage/renderer/opengl/framebuffer.h b/libopenage/renderer/opengl/framebuffer.h index cd37ccb76a..398f7e81d0 100644 --- a/libopenage/renderer/opengl/framebuffer.h +++ b/libopenage/renderer/opengl/framebuffer.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 @@ -16,12 +16,12 @@ class GlTexture2d; */ enum class gl_framebuffer_t { /** - * The actual window. This is visible to the user after swapping front and back buffers. - */ + * The actual window. This is visible to the user after swapping front and back buffers. + */ display, /** - * A bunch of textures. These can be color texture, depth textures, etc. - */ + * A bunch of textures. These can be color texture, depth textures, etc. + */ textures, }; @@ -33,48 +33,48 @@ enum class gl_framebuffer_t { class GlFramebuffer final : public GlSimpleObject { public: /** - * Construct a framebuffer pointing at the default framebuffer - the window. - * - * Drawing into this framebuffer draws onto the screen. - * - * @param context OpenGL context used for drawing. - */ + * Construct a framebuffer pointing at the default framebuffer - the window. + * + * Drawing into this framebuffer draws onto the screen. + * + * @param context OpenGL context used for drawing. + */ GlFramebuffer(const std::shared_ptr &context); /** - * Construct a framebuffer pointing at the given textures. - * - * Texture are attached to points specific to their pixel format, - * e.g. a depth texture will be set as the depth target. - * - * @param context OpenGL context used for drawing. - * @param textures Textures targeted by the framebuffer. They are automatically - * attached to the correct attachement points depending on their type. - */ + * Construct a framebuffer pointing at the given textures. + * + * Texture are attached to points specific to their pixel format, + * e.g. a depth texture will be set as the depth target. + * + * @param context OpenGL context used for drawing. + * @param textures Textures targeted by the framebuffer. They are automatically + * attached to the correct attachement points depending on their type. + */ GlFramebuffer(const std::shared_ptr &context, std::vector> const &textures); /** - * Get the type of this framebuffer. - * - * @return Framebuffer type. - */ + * Get the type of this framebuffer. + * + * @return Framebuffer type. + */ gl_framebuffer_t get_type() const; /** - * Bind this framebuffer to \p GL_READ_FRAMEBUFFER. - */ + * Bind this framebuffer to \p GL_READ_FRAMEBUFFER. + */ void bind_read() const; /** - * Bind this framebuffer to \p GL_DRAW_FRAMEBUFFER. - */ + * Bind this framebuffer to \p GL_DRAW_FRAMEBUFFER. + */ void bind_write() const; private: /** - * Type of this framebuffer. - */ + * Type of this framebuffer. + */ gl_framebuffer_t type; }; diff --git a/libopenage/renderer/opengl/render_target.h b/libopenage/renderer/opengl/render_target.h index acf0c0af00..ccf67bfd87 100644 --- a/libopenage/renderer/opengl/render_target.h +++ b/libopenage/renderer/opengl/render_target.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 @@ -22,8 +22,8 @@ class GlTexture2d; */ enum class gl_render_target_t { /** - * Render into a framebuffer. - */ + * Render into a framebuffer. + */ framebuffer, // TODO renderbuffers mixed with textures }; @@ -35,24 +35,24 @@ enum class gl_render_target_t { class GlRenderTarget final : public RenderTarget { public: /** - * Construct a render target pointed at the default framebuffer - the window. - * - * @param context OpenGL context used for drawing. - * @param width Current width of the window. - * @param height Current height of the window. - */ + * Construct a render target pointed at the default framebuffer - the window. + * + * @param context OpenGL context used for drawing. + * @param width Current width of the window. + * @param height Current height of the window. + */ GlRenderTarget(const std::shared_ptr &context, size_t width, size_t height); /** - * Construct a render target pointing at the given textures. - * Texture are attached to points specific to their pixel format, - * e.g. a depth texture will be set as the depth target. - * - * @param context OpenGL context used for drawing. - * @param textures Texture attachements. - */ + * Construct a render target pointing at the given textures. + * Texture are attached to points specific to their pixel format, + * e.g. a depth texture will be set as the depth target. + * + * @param context OpenGL context used for drawing. + * @param textures Texture attachements. + */ GlRenderTarget(const std::shared_ptr &context, std::vector> const &textures); @@ -64,52 +64,52 @@ class GlRenderTarget final : public RenderTarget { resources::Texture2dData into_data() override; /** - * Get the targeted textures. - * - * @return Textures drawn into by the render target. - */ + * Get the targeted textures. + * + * @return Textures drawn into by the render target. + */ std::vector> get_texture_targets() override; /** - * Resize the render target to the specified dimensions. - * - * This is used to scale the viewport to the correct size when - * binding the render target with write access. - * - * @param width New width. - * @param height New height. - */ + * Resize the render target to the specified dimensions. + * + * This is used to scale the viewport to the correct size when + * binding the render target with write access. + * + * @param width New width. + * @param height New height. + */ void resize(size_t width, size_t height); /** - * Bind this render target to be drawn into. - */ + * Bind this render target to be drawn into. + */ void bind_write() const; /** - * Bind this render target to be read from. - */ + * Bind this render target to be read from. + */ void bind_read() const; private: /** - * Type of this render target. - */ + * Type of this render target. + */ gl_render_target_t type; /** - * Size of the window or the texture targets. - */ + * Size of the window or the texture targets. + */ std::pair size; /** - * For framebuffer target type, the framebuffer. - */ + * For framebuffer target type, the framebuffer. + */ std::optional framebuffer; /** - * Target textures if the render target is a textured fbo. - */ + * Target textures if the render target is a textured fbo. + */ std::optional>> textures; }; diff --git a/libopenage/renderer/opengl/shader_data.h b/libopenage/renderer/opengl/shader_data.h index 83d140e591..456c60d6cf 100644 --- a/libopenage/renderer/opengl/shader_data.h +++ b/libopenage/renderer/opengl/shader_data.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 @@ -18,9 +18,9 @@ struct GlUniform { GLenum type; /** - * Location of the uniform for use with glUniform and glGetUniform. + * Location of the uniform for use with glUniform and glGetUniform. * NOT the same as the uniform index. - */ + */ GLuint location; }; @@ -31,27 +31,27 @@ struct GlInBlockUniform { GLenum type; /** - * Offset relative to the beginning of the block at which this uniform is placed. - */ + * Offset relative to the beginning of the block at which this uniform is placed. + */ size_t offset; /** - * The size in bytes of the whole uniform. If the uniform is an array, + * The size in bytes of the whole uniform. If the uniform is an array, * the size of the whole array. - */ + */ size_t size; /** - * Only relevant for arrays and matrices. + * Only relevant for arrays and matrices. * In arrays, specifies the distance between the start of each element. * In row-major matrices, specifies the distance between the start of each row. * In column-major matrices, specifies the distance between the start of each column. - */ + */ size_t stride; /** - * Only relevant for arrays. The number of elements in the array. - */ + * Only relevant for arrays. The number of elements in the array. + */ size_t count; }; @@ -62,19 +62,19 @@ struct GlUniformBlock { GLuint index; /** - * Size of the entire block. How uniforms are packed within depends + * Size of the entire block. How uniforms are packed within depends * on the block layout and is described in corresponding GlUniforms. - */ + */ size_t data_size; /** - * Maps uniform names within this block to their descriptions. - */ + * Maps uniform names within this block to their descriptions. + */ std::unordered_map uniforms; /** - * The binding point assigned to this block. - */ + * The binding point assigned to this block. + */ GLuint binding_point; }; diff --git a/libopenage/renderer/opengl/shader_program.h b/libopenage/renderer/opengl/shader_program.h index 7fa2d96c7a..a49c8214a9 100644 --- a/libopenage/renderer/opengl/shader_program.h +++ b/libopenage/renderer/opengl/shader_program.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -30,38 +30,38 @@ class GlShaderProgram final : public ShaderProgram , public GlSimpleObject { public: /** - * Tries to create a shader program from the given sources. + * Tries to create a shader program from the given sources. * Throws an exception on compile/link errors. - */ + */ explicit GlShaderProgram(const std::shared_ptr &, const std::vector &); /** - * Bind this program as the currently used one in the OpenGL context. - */ + * Bind this program as the currently used one in the OpenGL context. + */ void use(); /** - * Check if this program is currently in use in the OpenGL context. - * - * @return true if the program is in use, false otherwise. - */ + * Check if this program is currently in use in the OpenGL context. + * + * @return true if the program is in use, false otherwise. + */ bool in_use() const; /** - * Updates the uniform values with the given input specification. - * - * @param input The uniform input specification. - */ + * Updates the uniform values with the given input specification. + * + * @param input The uniform input specification. + */ void update_uniforms(std::shared_ptr const &unif_in); /** - * Get the uniform block with the given name. - * - * @param block_name Name of the uniform block. - * - * @return Uniform block. - */ + * Get the uniform block with the given name. + * + * @param block_name Name of the uniform block. + * + * @return Uniform block. + */ const GlUniformBlock &get_uniform_block(const char *block_name) const; uniform_id_t get_uniform_id(const char *name) override; @@ -69,12 +69,12 @@ class GlShaderProgram final : public ShaderProgram bool has_uniform(const char *name) override; /** - * Binds a uniform block in the shader program to the same binding point as - * the given uniform buffer. - * - * @param buffer Uniform buffer to bind. - * @param block_name Name of the uniform block in the shader program. - */ + * Binds a uniform block in the shader program to the same binding point as + * the given uniform buffer. + * + * @param buffer Uniform buffer to bind. + * @param block_name Name of the uniform block in the shader program. + */ void bind_uniform_buffer(const char *block_name, std::shared_ptr const &) override; diff --git a/libopenage/renderer/opengl/uniform_buffer.h b/libopenage/renderer/opengl/uniform_buffer.h index e26f101efc..6c7ca3d869 100644 --- a/libopenage/renderer/opengl/uniform_buffer.h +++ b/libopenage/renderer/opengl/uniform_buffer.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 @@ -27,17 +27,17 @@ class GlUniformBuffer final : public UniformBuffer GLenum usage = GL_DYNAMIC_DRAW); /** - * Get the binding point of the buffer. - * - * @return Binding point ID. - */ + * Get the binding point of the buffer. + * + * @return Binding point ID. + */ GLuint get_binding_point() const; /** - * Set the binding point of the buffer. - * - * @param binding_point Binding point ID. - */ + * Set the binding point of the buffer. + * + * @param binding_point Binding point ID. + */ void set_binding_point(GLuint binding_point); void update_uniforms(std::shared_ptr const &unif_in) override; @@ -45,8 +45,8 @@ class GlUniformBuffer final : public UniformBuffer bool has_uniform(const char *) override; /** - * Bind the buffer. - */ + * Bind the buffer. + */ void bind() const; protected: @@ -70,34 +70,34 @@ class GlUniformBuffer final : public UniformBuffer private: /** - * Update a uniform value in a uniform buffer input object. - * - * Note that the uniform buffer itself is not updated by this. Data is only uploaded - * to the buffer when \p update_uniforms is eventually called. - * - * @param in Uniform buffer input object. - * @param name Name of the uniform. - * @param val Pointer to the value to update the uniform with. - * @param type Type of the uniform. - */ + * Update a uniform value in a uniform buffer input object. + * + * Note that the uniform buffer itself is not updated by this. Data is only uploaded + * to the buffer when \p update_uniforms is eventually called. + * + * @param in Uniform buffer input object. + * @param name Name of the uniform. + * @param val Pointer to the value to update the uniform with. + * @param type Type of the uniform. + */ void set_unif(std::shared_ptr const &in, const char *name, void const *val, GLenum type); /** - * Uniform definitions inside the buffer. - */ + * Uniform definitions inside the buffer. + */ std::unordered_map uniforms; /** - * Size of the buffer (in bytes). - */ + * Size of the buffer (in bytes). + */ size_t data_size; /** - * Binding point of the buffer. - */ + * Binding point of the buffer. + */ GLuint binding_point; }; diff --git a/libopenage/renderer/opengl/uniform_input.h b/libopenage/renderer/opengl/uniform_input.h index 4fdb79e72e..1bb54f3cd4 100644 --- a/libopenage/renderer/opengl/uniform_input.h +++ b/libopenage/renderer/opengl/uniform_input.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -27,18 +27,18 @@ class GlUniformInput final : public UniformInput { GlUniformInput(std::shared_ptr const &); /** - * We store uniform updates lazily. They are only actually uploaded to GPU + * We store uniform updates lazily. They are only actually uploaded to GPU * when a draw call is made. - * - * \p update_offs maps the uniform IDs to where their + * + * \p update_offs maps the uniform IDs to where their * value is in \p update_data in terms of a byte-wise offset. This is only a partial * valuation, so not all uniforms have to be present here. - */ + */ std::unordered_map update_offs; /** - * Buffer containing untyped uniform update data. - */ + * Buffer containing untyped uniform update data. + */ std::vector update_data; }; @@ -50,18 +50,18 @@ class GlUniformBufferInput final : public UniformBufferInput { GlUniformBufferInput(std::shared_ptr const &); /** - * We store uniform updates lazily. They are only actually uploaded to GPU + * We store uniform updates lazily. They are only actually uploaded to GPU * when a draw call is made. - * - * \p update_offs maps the uniform names to where their + * + * \p update_offs maps the uniform names to where their * value is in \p update_data in terms of a byte-wise offset. This is only a partial * valuation, so not all uniforms have to be present here. - */ + */ std::unordered_map update_offs; /** - * Buffer containing untyped uniform update data. - */ + * Buffer containing untyped uniform update data. + */ std::vector update_data; }; diff --git a/libopenage/renderer/render_factory.h b/libopenage/renderer/render_factory.h index 113c9ac966..26601bcf7f 100644 --- a/libopenage/renderer/render_factory.h +++ b/libopenage/renderer/render_factory.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -27,45 +27,45 @@ class WorldRenderEntity; class RenderFactory { public: /** - * Create a new factory for render entities. - * - * @param terrain_renderer Terrain renderer. - * @param world_renderer World renderer. - */ + * Create a new factory for render entities. + * + * @param terrain_renderer Terrain renderer. + * @param world_renderer World renderer. + */ RenderFactory(const std::shared_ptr terrain_renderer, const std::shared_ptr world_renderer); ~RenderFactory() = default; /** - * Create a new terrain render entity and register it at the terrain renderer. - * - * Render entities for terrain are associated with chunks, so a new render entity - * will result in the creation of a new chunk in the renderer. - * - * Size/offset of the chunk in the game simulation should match size/offset - * in the renderer. - * - * @return Render entity for pushing terrain updates. - */ + * Create a new terrain render entity and register it at the terrain renderer. + * + * Render entities for terrain are associated with chunks, so a new render entity + * will result in the creation of a new chunk in the renderer. + * + * Size/offset of the chunk in the game simulation should match size/offset + * in the renderer. + * + * @return Render entity for pushing terrain updates. + */ std::shared_ptr add_terrain_render_entity(const util::Vector2s chunk_size, const coord::tile_delta chunk_offset); /** - * Create a new world render entity and register it at the world renderer. - * - * @return Render entity for pushing terrain updates. - */ + * Create a new world render entity and register it at the world renderer. + * + * @return Render entity for pushing terrain updates. + */ std::shared_ptr add_world_render_entity(); private: /** - * Render stage for terrain drawing. - */ + * Render stage for terrain drawing. + */ std::shared_ptr terrain_renderer; /** - * Render stage for game entity drawing. - */ + * Render stage for game entity drawing. + */ std::shared_ptr world_renderer; }; diff --git a/libopenage/renderer/resources/animation/angle_info.h b/libopenage/renderer/resources/animation/angle_info.h index 14e4a8b8da..3b84e0423d 100644 --- a/libopenage/renderer/resources/animation/angle_info.h +++ b/libopenage/renderer/resources/animation/angle_info.h @@ -1,4 +1,4 @@ -// 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 @@ -14,9 +14,9 @@ class FrameInfo; * Describe whether/how the frame is mirrored */ enum class flip_type { - NONE, // do not flip + NONE, // do not flip FLIP_X, // flip across x axis - FLIP_Y // flip across y axis + FLIP_Y // flip across y axis }; /** @@ -32,7 +32,7 @@ class AngleInfo { * @param angle_start Rotation in degress at which the frames are displayed. * @param frames Frame information. * @param mirror_from Mirror frames from another angle instead of using uniquely - * defined frames. + * defined frames. */ AngleInfo(const float angle_start, std::vector> &frames, @@ -50,10 +50,10 @@ class AngleInfo { float get_angle_start() const; /** - * Check if the angle is mirrored from another angle. - * - * @return true if a mirrored angle has been defined, else false. - */ + * Check if the angle is mirrored from another angle. + * + * @return true if a mirrored angle has been defined, else false. + */ bool is_mirrored() const; /** @@ -88,18 +88,18 @@ class AngleInfo { private: /** - * Starting rotation in degress at which the frames are displayed. - */ + * Starting rotation in degress at which the frames are displayed. + */ float angle_start; /** - * Frame information. - */ + * Frame information. + */ std::vector> frames; /** - * Mirrored angle information. - */ + * Mirrored angle information. + */ std::shared_ptr mirror_from = nullptr; /** diff --git a/libopenage/renderer/resources/animation/frame_info.h b/libopenage/renderer/resources/animation/frame_info.h index 3742c678d4..5d43e2edfb 100644 --- a/libopenage/renderer/resources/animation/frame_info.h +++ b/libopenage/renderer/resources/animation/frame_info.h @@ -1,4 +1,4 @@ -// 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 @@ -16,9 +16,9 @@ class FrameInfo { * Create a 2D Frame Info. * * @param texture_idx Index of the texture containing the frame in the - * Animation2dInfo object. + * Animation2dInfo object. * @param subtexture_idx Index of the subtexture containing the frame - * in the Texture2dInfo object. + * in the Texture2dInfo object. */ FrameInfo(const size_t texture_idx, const size_t subtexture_idx); @@ -42,13 +42,13 @@ class FrameInfo { private: /** - * Index of the texture containing the frame in the Animation2dInfo object. - */ + * Index of the texture containing the frame in the Animation2dInfo object. + */ size_t texture_idx; /** - * Index of the subtexture containing the frame in the Texture2dInfo object. - */ + * Index of the subtexture containing the frame in the Texture2dInfo object. + */ size_t subtexture_idx; }; diff --git a/libopenage/renderer/resources/animation/layer_info.h b/libopenage/renderer/resources/animation/layer_info.h index eafb92dfec..163b1c0aa6 100644 --- a/libopenage/renderer/resources/animation/layer_info.h +++ b/libopenage/renderer/resources/animation/layer_info.h @@ -1,4 +1,4 @@ -// 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 @@ -12,9 +12,9 @@ class AngleInfo; class FrameTiming; enum class display_mode { - OFF, // Only show first frame + OFF, // Only show first frame ONCE, // Play loop once - LOOP // Loop indefinitely + LOOP // Loop indefinitely }; /** @@ -95,10 +95,10 @@ class LayerInfo { const std::shared_ptr &get_direction_angle(float direction) const; /** - * Get the frame timing information of this layer. - * - * @return Frame timing information. - */ + * Get the frame timing information of this layer. + * + * @return Frame timing information. + */ const std::shared_ptr &get_frame_timing() const; private: diff --git a/libopenage/renderer/resources/assets/asset_manager.h b/libopenage/renderer/resources/assets/asset_manager.h index 0d45e473dd..b77c29afe2 100644 --- a/libopenage/renderer/resources/assets/asset_manager.h +++ b/libopenage/renderer/resources/assets/asset_manager.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 @@ -36,32 +36,32 @@ class Texture2dInfo; class AssetManager { public: /** - * Create a new asset manager. - * - * @param renderer The openage renderer instance. - * @param asset_base_dir Base path for all assets. - */ + * Create a new asset manager. + * + * @param renderer The openage renderer instance. + * @param asset_base_dir Base path for all assets. + */ AssetManager(const std::shared_ptr &renderer, const util::Path &asset_base_dir); ~AssetManager() = default; /** - * Prevent accidental copy or assignment because it would defeat the - * point of a persistent cache. - */ + * Prevent accidental copy or assignment because it would defeat the + * point of a persistent cache. + */ AssetManager(const AssetManager &) = delete; AssetManager &operator=(const AssetManager &) = delete; /** - * Get the corresponding asset for the specified path. - * - * If the asset does not exist in the cache yet, it will be loaded - * using the given path. - * - * @param path Path to the asset resource. - * - * @return Texture resource at the given path. - */ + * Get the corresponding asset for the specified path. + * + * If the asset does not exist in the cache yet, it will be loaded + * using the given path. + * + * @param path Path to the asset resource. + * + * @return Texture resource at the given path. + */ const std::shared_ptr &request_animation(const util::Path &path); const std::shared_ptr &request_blpattern(const util::Path &path); const std::shared_ptr &request_bltable(const util::Path &path); @@ -70,16 +70,16 @@ class AssetManager { const std::shared_ptr &request_texture(const util::Path &path); /** - * Get the corresponding asset for a path string relative to the - * asset base directory. - * - * If the asset does not exist in the cache yet, it will be loaded - * using the given path. - * - * @param path Relative path to the asset resource (from the asset base dir). - * - * @return Texture resource at the given path. - */ + * Get the corresponding asset for a path string relative to the + * asset base directory. + * + * If the asset does not exist in the cache yet, it will be loaded + * using the given path. + * + * @param path Relative path to the asset resource (from the asset base dir). + * + * @return Texture resource at the given path. + */ const std::shared_ptr &request_animation(const std::string &rel_path); const std::shared_ptr &request_blpattern(const std::string &rel_path); const std::shared_ptr &request_bltable(const std::string &rel_path); @@ -95,11 +95,11 @@ class AssetManager { using placeholder_texture_t = std::optional>>; /** - * Set a placeholder asset that is returned in case a regular request cannot - * find the requested asset. - * - * @param path Path to the placeholder asset resource. - */ + * Set a placeholder asset that is returned in case a regular request cannot + * find the requested asset. + * + * @param path Path to the placeholder asset resource. + */ void set_placeholder_animation(const util::Path &path); void set_placeholder_blpattern(const util::Path &path); void set_placeholder_bltable(const util::Path &path); @@ -108,10 +108,10 @@ class AssetManager { void set_placeholder_texture(const util::Path &path); /** - * Get the placeholder asset for a specific asset type. - * - * @return Placeholder asset resource. - */ + * Get the placeholder asset for a specific asset type. + * + * @return Placeholder asset resource. + */ const placeholder_anim_t &get_placeholder_animation(); const placeholder_blpattern_t &get_placeholder_blpattern(); const placeholder_bltable_t &get_placeholder_bltable(); @@ -120,39 +120,39 @@ class AssetManager { const placeholder_texture_t &get_placeholder_texture(); /** - * Get the texture manager for accessing cached image resources. - * - * @return Texture manager. - */ + * Get the texture manager for accessing cached image resources. + * + * @return Texture manager. + */ const std::shared_ptr &get_texture_manager(); private: /** - * openage renderer. - */ + * openage renderer. + */ std::shared_ptr renderer; /** - * Cache of already loaded assets. - */ + * Cache of already loaded assets. + */ std::shared_ptr cache; /** - * Manages individual textures/image resources used by the - * high level asset formats cached by the asset manager. - */ + * Manages individual textures/image resources used by the + * high level asset formats cached by the asset manager. + */ std::shared_ptr texture_manager; /** - * Base path for all assets. - * - * TODO: This should be the mount point of modpacks? - */ + * Base path for all assets. + * + * TODO: This should be the mount point of modpacks? + */ util::Path asset_base_dir; /** - * Placeholder assets that can be used if a resource is not found. - */ + * Placeholder assets that can be used if a resource is not found. + */ placeholder_anim_t placeholder_animation; placeholder_blpattern_t placeholder_blpattern; placeholder_bltable_t placeholder_bltable; diff --git a/libopenage/renderer/resources/assets/cache.h b/libopenage/renderer/resources/assets/cache.h index 34ec62567e..803fc0b6fd 100644 --- a/libopenage/renderer/resources/assets/cache.h +++ b/libopenage/renderer/resources/assets/cache.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 @@ -27,19 +27,19 @@ class Texture2dInfo; class AssetCache { public: /** - * Create a new asset cache. - */ + * Create a new asset cache. + */ AssetCache() = default; ~AssetCache() = default; /** - * Get the corresponding asset for the specified path. - * - * @param path Path to the asset resource. - * - * @return Asset resource at the given path. + * Get the corresponding asset for the specified path. + * + * @param path Path to the asset resource. + * + * @return Asset resource at the given path. * @throw Error if the resource is not in the cache. - */ + */ const std::shared_ptr &get_animation(const util::Path &path); const std::shared_ptr &get_blpattern(const util::Path &path); const std::shared_ptr &get_bltable(const util::Path &path); @@ -49,12 +49,12 @@ class AssetCache { // TODO: use std::optional> /** - * Map a specific asset info to the given path and add it to the cache. - * Overwrites existing references if the path already exists in the cache. - * - * @param path Path to the asset resource. - * @param info Existing asset object. - */ + * Map a specific asset info to the given path and add it to the cache. + * Overwrites existing references if the path already exists in the cache. + * + * @param path Path to the asset resource. + * @param info Existing asset object. + */ void add_animation(const util::Path &path, const std::shared_ptr info); void add_blpattern(const util::Path &path, const std::shared_ptr info); void add_bltable(const util::Path &path, const std::shared_ptr info); @@ -63,10 +63,10 @@ class AssetCache { void add_texture(const util::Path &path, const std::shared_ptr info); /** - * Remove an asset reference from the cache. - * - * @param path Path to the asset resource. - */ + * Remove an asset reference from the cache. + * + * @param path Path to the asset resource. + */ void remove_animation(const util::Path &path); void remove_blpattern(const util::Path &path); void remove_bltable(const util::Path &path); @@ -75,12 +75,12 @@ class AssetCache { void remove_texture(const util::Path &path); /** - * Check if an asset reference is in the cache. - * - * @param path Path to the asset resource. + * Check if an asset reference is in the cache. + * + * @param path Path to the asset resource. * * @return true if the resource is cached, else false. - */ + */ bool check_animation_cache(const util::Path &path); bool check_blpattern_cache(const util::Path &path); bool check_bltable_cache(const util::Path &path); @@ -97,33 +97,33 @@ class AssetCache { using texture_cache_t = std::unordered_map>; /** - * Cache of already loaded animations. - */ + * Cache of already loaded animations. + */ anim_cache_t loaded_animations; /** - * Cache of already loaded blending patterns. - */ + * Cache of already loaded blending patterns. + */ blpattern_cache_t loaded_blpatterns; /** - * Cache of already loaded blending tables. - */ + * Cache of already loaded blending tables. + */ bltable_cache_t loaded_bltables; /** - * Cache of already loaded colour palettes. - */ + * Cache of already loaded colour palettes. + */ palette_cache_t loaded_palettes; /** - * Cache of already loaded terrains. - */ + * Cache of already loaded terrains. + */ terrain_cache_t loaded_terrains; /** - * Cache of already loaded textures. - */ + * Cache of already loaded textures. + */ texture_cache_t loaded_textures; }; diff --git a/libopenage/renderer/resources/assets/texture_manager.h b/libopenage/renderer/resources/assets/texture_manager.h index cf7dbdab60..55c365081a 100644 --- a/libopenage/renderer/resources/assets/texture_manager.h +++ b/libopenage/renderer/resources/assets/texture_manager.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -26,89 +26,89 @@ namespace resources { class TextureManager { public: /** - * Create a new texture manager. - * - * @param renderer The openage renderer instance. - */ + * Create a new texture manager. + * + * @param renderer The openage renderer instance. + */ TextureManager(const std::shared_ptr &renderer); ~TextureManager() = default; /** - * Prevent accidental copy or assignment because it would defeat the - * point of a persistent cache. - */ + * Prevent accidental copy or assignment because it would defeat the + * point of a persistent cache. + */ TextureManager(const TextureManager &) = delete; TextureManager &operator=(const TextureManager &) = delete; /** - * Get the corresponding texture for the specified path. - * - * If the texture does not exist in the cache yet, it will be loaded - * using the given path. - * - * @param path Path to the texture resource. - * - * @return Texture resource at the given path. - */ + * Get the corresponding texture for the specified path. + * + * If the texture does not exist in the cache yet, it will be loaded + * using the given path. + * + * @param path Path to the texture resource. + * + * @return Texture resource at the given path. + */ const std::shared_ptr &request(const util::Path &path); /** - * Load the texture at the given path. Does nothing if the path - * already exists in the cache. - * - * @param path Path to the texture resource. - */ + * Load the texture at the given path. Does nothing if the path + * already exists in the cache. + * + * @param path Path to the texture resource. + */ void add(const util::Path &path); /** - * Assign a specific texture to the given path. Overwrites existing - * textures references if the path already exists in the cache. - * - * @param path Path to the texture resource. - * @param texture Existing texture object. - */ + * Assign a specific texture to the given path. Overwrites existing + * textures references if the path already exists in the cache. + * + * @param path Path to the texture resource. + * @param texture Existing texture object. + */ void add(const util::Path &path, const std::shared_ptr &texture); /** - * Remove a texture reference from the cache. - * - * @param path Path to the texture resource. - */ + * Remove a texture reference from the cache. + * + * @param path Path to the texture resource. + */ void remove(const util::Path &path); /** - * Set the placeholder texture. - * - * @param path Path to the texture resource. - */ + * Set the placeholder texture. + * + * @param path Path to the texture resource. + */ void set_placeholder(const util::Path &path); using placeholder_t = std::optional>>; /** - * Get the placeholder texture. - * - * @return Placeholder texture. Can be \p nullptr. - */ + * Get the placeholder texture. + * + * @return Placeholder texture. Can be \p nullptr. + */ const placeholder_t &get_placeholder() const; private: /** - * openage renderer. - */ + * openage renderer. + */ std::shared_ptr renderer; using texture_cache_t = std::unordered_map>; /** - * Cache of already created textures. - */ + * Cache of already created textures. + */ texture_cache_t loaded; /** - * Placeholder texture to use if a texture could not be loaded. - */ + * Placeholder texture to use if a texture could not be loaded. + */ placeholder_t placeholder; }; diff --git a/libopenage/renderer/resources/buffer_info.h b/libopenage/renderer/resources/buffer_info.h index b654c3090f..1059169f94 100644 --- a/libopenage/renderer/resources/buffer_info.h +++ b/libopenage/renderer/resources/buffer_info.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 @@ -81,71 +81,71 @@ static constexpr auto STD140_INPUT_SIZE = datastructure::create_const_map &inputs); /** - * Create a new uniform buffer definition. - * - * @param layout Layout of the uniform block targeted by the buffer. - * @param inputs Uniform definitions inside the uniform block. - */ + * Create a new uniform buffer definition. + * + * @param layout Layout of the uniform block targeted by the buffer. + * @param inputs Uniform definitions inside the uniform block. + */ UniformBufferInfo(ubo_layout_t layout, std::vector &&inputs); /** - * Get the layout of the uniform block targeted by the buffer. - * - * @return Uniform block layout. - */ + * Get the layout of the uniform block targeted by the buffer. + * + * @return Uniform block layout. + */ ubo_layout_t get_layout() const; /** - * Get the uniform definitions inside the uniform block. - * - * @return Uniform definitions. - */ + * Get the uniform definitions inside the uniform block. + * + * @return Uniform definitions. + */ const std::vector &get_inputs() const; /** - * Get the total size of the uniform block (in bytes). - * - * @return Size of the uniform block (in bytes). - */ + * Get the total size of the uniform block (in bytes). + * + * @return Size of the uniform block (in bytes). + */ size_t get_size() const; /** - * Get the size of a single uniform input (in bytes). - * - * @param input Uniform input type. - * @param layout Layout of the uniform block targeted by the buffer. - * - * @return Size of the uniform input (in bytes). - */ + * Get the size of a single uniform input (in bytes). + * + * @param input Uniform input type. + * @param layout Layout of the uniform block targeted by the buffer. + * + * @return Size of the uniform input (in bytes). + */ static size_t get_size(const UBOInput &input, ubo_layout_t layout = ubo_layout_t::STD140); /** - * Get the stride size of a uniform input in an array (in bytes). - * - * @param input Uniform input type. - * @param layout Layout of the uniform block targeted by the buffer. - * - * @return Size of the uniform input (in bytes). - */ + * Get the stride size of a uniform input in an array (in bytes). + * + * @param input Uniform input type. + * @param layout Layout of the uniform block targeted by the buffer. + * + * @return Size of the uniform input (in bytes). + */ static size_t get_stride_size(ubo_input_t input, ubo_layout_t layout = ubo_layout_t::STD140); private: /** - * Uniform block layout. - */ + * Uniform block layout. + */ ubo_layout_t layout; /** - * Uniform definitions. - */ + * Uniform definitions. + */ std::vector inputs; }; diff --git a/libopenage/renderer/resources/frame_timing.h b/libopenage/renderer/resources/frame_timing.h index c253501900..adb36b23a6 100644 --- a/libopenage/renderer/resources/frame_timing.h +++ b/libopenage/renderer/resources/frame_timing.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 @@ -20,86 +20,86 @@ class FrameTiming { using keyframe_t = time::time_t; /** - * Create a new frame timing sequence. - * - * @param total_length Total length of the sequence (in seconds). - * @param keyframes Time of each frame (in seconds). Expected to be sorted. - */ + * Create a new frame timing sequence. + * + * @param total_length Total length of the sequence (in seconds). + * @param keyframes Time of each frame (in seconds). Expected to be sorted. + */ FrameTiming(const time::time_t &total_length, const std::vector &&keyframes); ~FrameTiming() = default; /** - * Set the total length of the frame timing sequence. - * - * @param total_length Total length of the sequence (in seconds). - */ + * Set the total length of the frame timing sequence. + * + * @param total_length Total length of the sequence (in seconds). + */ void set_total_length(const time::time_t &total_length); /** - * Insert a new keyframe into the sequence. - * - * Insertion complexity is linear (O(n)), so it should be avoided during rendering - * when possible. Prefer to create several alternative sequence instead if the - * timing needs to be changed. - * - * @param time Time of the new keyframe (in seconds). - */ + * Insert a new keyframe into the sequence. + * + * Insertion complexity is linear (O(n)), so it should be avoided during rendering + * when possible. Prefer to create several alternative sequence instead if the + * timing needs to be changed. + * + * @param time Time of the new keyframe (in seconds). + */ void insert(const time::time_t &time); /** - * Get the index of the frame in the sequence that should be displayed at the - * given time (closest frame with t <= time). - * - * @param time Time in the sequence (in seconds). - * @return Frame index in the sequence. - */ + * Get the index of the frame in the sequence that should be displayed at the + * given time (closest frame with t <= time). + * + * @param time Time in the sequence (in seconds). + * @return Frame index in the sequence. + */ size_t get_frame(const time::time_t &time) const; /** - * Get the index of the frame in the sequence that should be displayed at a specified - * time. This method is intended for looping frame sequences. - * - * Sequence time is determined from the modulus of the time difference between - * \p current (current simulation time) and \p start (time when the sequence - * was first started): - * - * time = (current - start) % total_length - * - * @param current Current simulation time (in seconds). - * @param start Start time of the sequence (in seconds). - * @return Frame index in the sequence. - */ + * Get the index of the frame in the sequence that should be displayed at a specified + * time. This method is intended for looping frame sequences. + * + * Sequence time is determined from the modulus of the time difference between + * \p current (current simulation time) and \p start (time when the sequence + * was first started): + * + * time = (current - start) % total_length + * + * @param current Current simulation time (in seconds). + * @param start Start time of the sequence (in seconds). + * @return Frame index in the sequence. + */ size_t get_frame(const time::time_t ¤t, const time::time_t &start) const; /** - * Get the number of frames in the sequence. - * - * @return Number of frames. - */ + * Get the number of frames in the sequence. + * + * @return Number of frames. + */ size_t size() const; private: /** - * Search for the closest frame index with t <= time. - * - * Uses binary search to find the frame index. Complexity is therefore - * O(log_2(this->keyframes.size())). - * - * @param time Time in the sequence (in seconds). - * @return Frame index in the sequence. - */ + * Search for the closest frame index with t <= time. + * + * Uses binary search to find the frame index. Complexity is therefore + * O(log_2(this->keyframes.size())). + * + * @param time Time in the sequence (in seconds). + * @return Frame index in the sequence. + */ size_t search_frame(const time::time_t &time) const; /** - * Time of each frame in the sequence relative to the sequence start (in seconds). - */ + * Time of each frame in the sequence relative to the sequence start (in seconds). + */ std::vector keyframes; /** - * Total length of the sequence (in seconds). - */ + * Total length of the sequence (in seconds). + */ time::time_t total_length; }; diff --git a/libopenage/renderer/resources/palette_info.h b/libopenage/renderer/resources/palette_info.h index d147522feb..61de54be1e 100644 --- a/libopenage/renderer/resources/palette_info.h +++ b/libopenage/renderer/resources/palette_info.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 @@ -34,17 +34,17 @@ class PaletteInfo { ~PaletteInfo() = default; /** - * Get a color in the palette with a specified index. - * - * @return Normalized RGBA color vector. - */ + * Get a color in the palette with a specified index. + * + * @return Normalized RGBA color vector. + */ Eigen::Vector4f get_color(size_t idx); /** - * Get the colors of the palette. - * - * @return List of normalized RGBA colors. - */ + * Get the colors of the palette. + * + * @return List of normalized RGBA colors. + */ const std::vector get_colors(); private: diff --git a/libopenage/renderer/resources/terrain/blendtable_info.h b/libopenage/renderer/resources/terrain/blendtable_info.h index 933a31201c..dfdefcbd97 100644 --- a/libopenage/renderer/resources/terrain/blendtable_info.h +++ b/libopenage/renderer/resources/terrain/blendtable_info.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 @@ -29,8 +29,8 @@ class BlendTableInfo { /** * Get the blending lookup table. - * - * @return Lookup table for blending patterns. + * + * @return Lookup table for blending patterns. */ const std::vector &get_table() const; diff --git a/libopenage/renderer/resources/terrain/frame_info.h b/libopenage/renderer/resources/terrain/frame_info.h index b4673dc377..046fc9514f 100644 --- a/libopenage/renderer/resources/terrain/frame_info.h +++ b/libopenage/renderer/resources/terrain/frame_info.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 @@ -17,9 +17,9 @@ class TerrainFrameInfo { * Create a 2D Frame Info. * * @param texture_idx Index of the texture containing the frame in the - * TerrainInfo object. + * TerrainInfo object. * @param subtexture_idx Index of the subtexture containing the frame - * in the Texture2dInfo object. + * in the Texture2dInfo object. * @param priority Blending priority. * @param priority Blend mode index. */ @@ -61,27 +61,27 @@ class TerrainFrameInfo { private: /** - * Index of the texture containing the frame in the TerrainInfo object. - */ + * Index of the texture containing the frame in the TerrainInfo object. + */ size_t texture_idx; /** - * Index of the subtexture containing the frame in the Texture2dInfo object. - */ + * Index of the subtexture containing the frame in the Texture2dInfo object. + */ size_t subtexture_idx; /** - * Decides which blending table of the two adjacent terrain textures is selected. - * - * If two adjacent terrains have equal priority, the blending table of the terrain - * with a lower x coordinate value is selected. If the x coordinate value is also - * equal, the blending table of the terrain with the lowest y coordinate is selected. - */ + * Decides which blending table of the two adjacent terrain textures is selected. + * + * If two adjacent terrains have equal priority, the blending table of the terrain + * with a lower x coordinate value is selected. If the x coordinate value is also + * equal, the blending table of the terrain with the lowest y coordinate is selected. + */ size_t priority; /** - * Used for looking up the blending pattern index in a blending table. - */ + * Used for looking up the blending pattern index in a blending table. + */ std::optional blend_mode; }; diff --git a/libopenage/renderer/resources/texture_info.h b/libopenage/renderer/resources/texture_info.h index f51f34ed73..7884fe0f79 100644 --- a/libopenage/renderer/resources/texture_info.h +++ b/libopenage/renderer/resources/texture_info.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 @@ -162,8 +162,8 @@ class Texture2dInfo { /** * Get the coordinates of a specific subtexture inside the main texture. * Coordinates are returned as normalized values (floats in range 0.0 to 1.0). - * - * @deprecated Use \p get_subtex_tile_params() instead. + * + * @deprecated Use \p get_subtex_tile_params() instead. * * @param subidx Index of the subtexture. * diff --git a/libopenage/renderer/resources/texture_subinfo.h b/libopenage/renderer/resources/texture_subinfo.h index 139d63e0d6..c4114f0fd3 100644 --- a/libopenage/renderer/resources/texture_subinfo.h +++ b/libopenage/renderer/resources/texture_subinfo.h @@ -1,4 +1,4 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #pragma once @@ -24,8 +24,8 @@ class Texture2dSubInfo { * @param h Height of subtexture. * @param cx Vertical anchor of subtexture. * @param cy Horizontal anchor of subtexture. - * @param atlas_width Width of the texture atlas containing the subtexture. - * @param atlas_height Height of the texture atlas containing the subtexture. + * @param atlas_width Width of the texture atlas containing the subtexture. + * @param atlas_height Height of the texture atlas containing the subtexture. */ Texture2dSubInfo(int32_t x, int32_t y, @@ -39,52 +39,52 @@ class Texture2dSubInfo { Texture2dSubInfo() = default; /** - * Get the position of the subtexture within the atlas. - * - * @return Pixel coordinates as 2-dimensional Eigen vector: (x, y) - */ + * Get the position of the subtexture within the atlas. + * + * @return Pixel coordinates as 2-dimensional Eigen vector: (x, y) + */ const Eigen::Vector2i &get_pos() const; /** - * Get the size of the subtexture. - * - * @return Pixel coordinates as 2-dimensional Eigen vector: (width, height) - */ + * Get the size of the subtexture. + * + * @return Pixel coordinates as 2-dimensional Eigen vector: (width, height) + */ const Eigen::Vector2 &get_size() const; /** - * Get the position of the subtexture anchor. - * - * @return Anchor coordinates as 2-dimensional Eigen vector: (x, y) - */ + * Get the position of the subtexture anchor. + * + * @return Anchor coordinates as 2-dimensional Eigen vector: (x, y) + */ const Eigen::Vector2i &get_anchor_pos() const; /** - * Get the normalized shader parameters of the subtexture. Use in the shader - * to sample the subtexture from the atlas. - * - * Values are in range (0.0, 1.0) and can be passed directly to a shader uniform. - * These parameters pre-computed and should be used whenever possible. - * - * @return Tile parameters as 4-dimensional Eigen vector: (x, y, width, height) - */ + * Get the normalized shader parameters of the subtexture. Use in the shader + * to sample the subtexture from the atlas. + * + * Values are in range (0.0, 1.0) and can be passed directly to a shader uniform. + * These parameters pre-computed and should be used whenever possible. + * + * @return Tile parameters as 4-dimensional Eigen vector: (x, y, width, height) + */ const Eigen::Vector4f &get_tile_params() const; /** - * Get the anchor parameters of the subtexture center. Used in the model matrix - * to calculate the offset position for displaying the subtexture inside - * the OpenGL viewport. - * - * The parameters represent the pixel distance of the anchor point to the subtexture - * center, multiplied by 2 to account for the normalized viewport size (which is 2.0 - * because it spans from -1.0 to 1.0). - * - * To get the normalized offset distance, the parameters have to be divided by the - * viewport size and then multiplied by additional scaling factors (e.g. from the - * animation). - * - * @return Parameters as 2-dimensional Eigen vector: (x, y) - */ + * Get the anchor parameters of the subtexture center. Used in the model matrix + * to calculate the offset position for displaying the subtexture inside + * the OpenGL viewport. + * + * The parameters represent the pixel distance of the anchor point to the subtexture + * center, multiplied by 2 to account for the normalized viewport size (which is 2.0 + * because it spans from -1.0 to 1.0). + * + * To get the normalized offset distance, the parameters have to be divided by the + * viewport size and then multiplied by additional scaling factors (e.g. from the + * animation). + * + * @return Parameters as 2-dimensional Eigen vector: (x, y) + */ const Eigen::Vector2i &get_anchor_params() const; private: @@ -104,13 +104,13 @@ class Texture2dSubInfo { Eigen::Vector2i anchor_pos; /** - * Pre-computed normalized coordinates of the subtexture. - */ + * Pre-computed normalized coordinates of the subtexture. + */ Eigen::Vector4f tile_params; /** - * Pre-computed normalized coordinates of the subtexture anchor. - */ + * Pre-computed normalized coordinates of the subtexture anchor. + */ Eigen::Vector2i anchor_params; }; diff --git a/libopenage/renderer/shader_program.h b/libopenage/renderer/shader_program.h index de515f213a..beb365b2a4 100644 --- a/libopenage/renderer/shader_program.h +++ b/libopenage/renderer/shader_program.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -45,12 +45,12 @@ class ShaderProgram : public std::enable_shared_from_this { virtual bool has_uniform(const char *name) = 0; /** - * Binds a uniform block in the shader program to the same binding point as - * the given uniform buffer. - * - * @param buffer Uniform buffer to bind. - * @param block_name Name of the uniform block in the shader program. - */ + * Binds a uniform block in the shader program to the same binding point as + * the given uniform buffer. + * + * @param buffer Uniform buffer to bind. + * @param block_name Name of the uniform block in the shader program. + */ virtual void bind_uniform_buffer(const char *block_name, std::shared_ptr const &) = 0; diff --git a/libopenage/renderer/stages/camera/manager.h b/libopenage/renderer/stages/camera/manager.h index 342b4bd8ef..13d876b25f 100644 --- a/libopenage/renderer/stages/camera/manager.h +++ b/libopenage/renderer/stages/camera/manager.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 @@ -44,63 +44,63 @@ enum class ZoomDirection { class CameraManager { public: /** - * Create a new camera manager. - * - * @param camera Camera to manage. - */ + * Create a new camera manager. + * + * @param camera Camera to manage. + */ CameraManager(const std::shared_ptr &camera); ~CameraManager() = default; /** - * Update the camera position and zoom level. - * - * Additionally updates the camera uniform buffer with the view and projection - * matrices. - */ + * Update the camera position and zoom level. + * + * Additionally updates the camera uniform buffer with the view and projection + * matrices. + */ void update(); /** - * Move into the given direction for the current frame. - * - * @param direction Move direction. - * @param delta Move speed, i.e. a distance multiplier. - */ + * Move into the given direction for the current frame. + * + * @param direction Move direction. + * @param delta Move speed, i.e. a distance multiplier. + */ void move_frame(MoveDirection direction, float speed = 1.0f); /** - * Zoom into the given direction for the current frame. - * - * @param direction Zoom direction. - * @param delta Zoom speed, i.e. a distance multiplier. - */ + * Zoom into the given direction for the current frame. + * + * @param direction Zoom direction. + * @param delta Zoom speed, i.e. a distance multiplier. + */ void zoom_frame(ZoomDirection direction, float speed = 1.0f); /** - * Set the move directions of the camera. - * - * @param directions Bitfield of the move directions. - */ + * Set the move directions of the camera. + * + * @param directions Bitfield of the move directions. + */ void set_move_motion_dirs(int directions); /** - * Set the zoom direction of the camera. - * - * @param direction Zoom direction. - */ + * Set the zoom direction of the camera. + * + * @param direction Zoom direction. + */ void set_zoom_motion_dir(int direction); /** - * Set the move speed of the camera. - * - * @param speed Speed of the camera. - */ + * Set the move speed of the camera. + * + * @param speed Speed of the camera. + */ void set_move_motion_speed(float speed); /** - * Set the zoom speed of the camera. - * - * @param speed Speed of the camera. - */ + * Set the zoom speed of the camera. + * + * @param speed Speed of the camera. + */ void set_zoom_motion_speed(float speed); private: @@ -110,38 +110,38 @@ class CameraManager { void update_motion(); /** - * Update the camera uniform buffer. - */ + * Update the camera uniform buffer. + */ void update_uniforms(); /** - * Camera. - */ + * Camera. + */ std::shared_ptr camera; /** - * Bitfield of the current move motion directions. - */ + * Bitfield of the current move motion directions. + */ int move_motion_directions; /** - * Bitfield of the current zoom motion direction. - */ + * Bitfield of the current zoom motion direction. + */ int zoom_motion_direction; /** - * Move motion speed of the camera. - */ + * Move motion speed of the camera. + */ float move_motion_speed; /** - * Zoom motion speed of the camera. - */ + * Zoom motion speed of the camera. + */ float zoom_motion_speed; /** - * Uniform buffer input for the camera. - */ + * Uniform buffer input for the camera. + */ std::shared_ptr uniforms; }; diff --git a/libopenage/renderer/stages/hud/object.h b/libopenage/renderer/stages/hud/object.h index b116821067..fac062f18d 100644 --- a/libopenage/renderer/stages/hud/object.h +++ b/libopenage/renderer/stages/hud/object.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 @@ -34,62 +34,62 @@ class HudDragRenderEntity; class HudDragObject { public: /** - * Create a new object for the HUD render stage. - * - * @param asset_manager Asset manager for loading resources. - */ + * Create a new object for the HUD render stage. + * + * @param asset_manager Asset manager for loading resources. + */ HudDragObject(const std::shared_ptr &asset_manager); ~HudDragObject() = default; /** - * Set the world render entity. - * - * @param entity New world render entity. - */ + * Set the world render entity. + * + * @param entity New world render entity. + */ void set_render_entity(const std::shared_ptr &entity); /** - * Set the current camera of the scene. - * - * @param camera Camera object viewing the scene. - */ + * Set the current camera of the scene. + * + * @param camera Camera object viewing the scene. + */ void set_camera(const std::shared_ptr &camera); /** - * Fetch updates from the render entity. - * - * @param time Current simulation time. - */ + * Fetch updates from the render entity. + * + * @param time Current simulation time. + */ void fetch_updates(const time::time_t &time = 0.0); /** - * Update the uniforms of the renderable associated with this object. - * - * @param time Current simulation time. - */ + * Update the uniforms of the renderable associated with this object. + * + * @param time Current simulation time. + */ void update_uniforms(const time::time_t &time = 0.0); /** - * Update the geometry of the renderable associated with this object. - * - * @param time Current simulation time. - */ + * Update the geometry of the renderable associated with this object. + * + * @param time Current simulation time. + */ void update_geometry(const time::time_t &time = 0.0); /** - * Check whether a new renderable needs to be created for this mesh. - * - * If true, the old renderable should be removed from the render pass. - * The updated uniforms and geometry should be passed to this mesh. - * Afterwards, clear the requirement flag with \p clear_requires_renderable(). - * - * @return true if a new renderable is required, else false. - */ + * Check whether a new renderable needs to be created for this mesh. + * + * If true, the old renderable should be removed from the render pass. + * The updated uniforms and geometry should be passed to this mesh. + * Afterwards, clear the requirement flag with \p clear_requires_renderable(). + * + * @return true if a new renderable is required, else false. + */ bool requires_renderable(); /** - * Indicate to this mesh that a new renderable has been created. - */ + * Indicate to this mesh that a new renderable has been created. + */ void clear_requires_renderable(); /** @@ -105,28 +105,28 @@ class HudDragObject { void clear_changed_flag(); /** - * Set the reference to the uniform inputs of the renderable - * associated with this object. Relevant uniforms are updated - * when calling \p update(). - * - * @param uniforms Uniform inputs of this object's renderable. - */ + * Set the reference to the uniform inputs of the renderable + * associated with this object. Relevant uniforms are updated + * when calling \p update(). + * + * @param uniforms Uniform inputs of this object's renderable. + */ void set_uniforms(const std::shared_ptr &uniforms); /** - * Set the geometry of the renderable associated with this object. - * - * The geometry is updated when calling \p update(). - * - * @param geometry Geometry of this object's renderable. - */ + * Set the geometry of the renderable associated with this object. + * + * The geometry is updated when calling \p update(). + * + * @param geometry Geometry of this object's renderable. + */ void set_geometry(const std::shared_ptr &geometry); private: /** - * Stores whether a new renderable for this object needs to be created - * for the render pass. - */ + * Stores whether a new renderable for this object needs to be created + * for the render pass. + */ bool require_renderable; /** @@ -150,23 +150,23 @@ class HudDragObject { std::shared_ptr render_entity; /** - * Position of the dragged corner. - */ + * Position of the dragged corner. + */ curve::Continuous drag_pos; /** - * Position of the start corner. - */ + * Position of the start corner. + */ coord::input drag_start; /** - * Shader uniforms for the renderable in the HUD render pass. - */ + * Shader uniforms for the renderable in the HUD render pass. + */ std::shared_ptr uniforms; /** - * Geometry of the renderable in the HUD render pass. - */ + * Geometry of the renderable in the HUD render pass. + */ std::shared_ptr geometry; /** diff --git a/libopenage/renderer/stages/hud/render_entity.h b/libopenage/renderer/stages/hud/render_entity.h index 7bbdbd7366..e2e2da65fa 100644 --- a/libopenage/renderer/stages/hud/render_entity.h +++ b/libopenage/renderer/stages/hud/render_entity.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 @@ -19,19 +19,19 @@ namespace openage::renderer::hud { class HudDragRenderEntity { public: /** - * Create a new render entity for drag selection in the HUD. - * - * @param drag_start Position of the start corner. - */ + * Create a new render entity for drag selection in the HUD. + * + * @param drag_start Position of the start corner. + */ HudDragRenderEntity(const coord::input drag_start); ~HudDragRenderEntity() = default; /** * Update the render entity with information from the gamestate - * or input system. - * - * @param drag_pos Position of the dragged corner. - * @param time Current simulation time. + * or input system. + * + * @param drag_pos Position of the dragged corner. + * @param time Current simulation time. */ void update(const coord::input drag_pos, const time::time_t time = 0.0); @@ -44,17 +44,17 @@ class HudDragRenderEntity { time::time_t get_update_time(); /** - * Get the position of the dragged corner. - * - * @return Coordinates of the dragged corner. - */ + * Get the position of the dragged corner. + * + * @return Coordinates of the dragged corner. + */ const curve::Continuous &get_drag_pos(); /** - * Get the position of the start corner. - * - * @return Coordinates of the start corner. - */ + * Get the position of the start corner. + * + * @return Coordinates of the start corner. + */ const coord::input &get_drag_start(); /** @@ -83,13 +83,13 @@ class HudDragRenderEntity { time::time_t last_update; /** - * Position of the dragged corner. - */ + * Position of the dragged corner. + */ curve::Continuous drag_pos; /** - * Position of the start corner. - */ + * Position of the start corner. + */ coord::input drag_start; /** diff --git a/libopenage/renderer/stages/hud/render_stage.h b/libopenage/renderer/stages/hud/render_stage.h index 6d3b68e9f6..f63662aa40 100644 --- a/libopenage/renderer/stages/hud/render_stage.h +++ b/libopenage/renderer/stages/hud/render_stage.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 @@ -42,15 +42,15 @@ class HudDragRenderEntity; class HudRenderStage { public: /** - * Create a new render stage for the HUD. - * - * @param window openage window targeted for rendering. - * @param renderer openage low-level renderer. - * @param camera Camera used for the rendered scene. - * @param shaderdir Directory containing the shader source files. - * @param asset_manager Asset manager for loading resources. - * @param clock Simulation clock for timing animations. - */ + * Create a new render stage for the HUD. + * + * @param window openage window targeted for rendering. + * @param renderer openage low-level renderer. + * @param camera Camera used for the rendered scene. + * @param shaderdir Directory containing the shader source files. + * @param asset_manager Asset manager for loading resources. + * @param clock Simulation clock for timing animations. + */ HudRenderStage(const std::shared_ptr &window, const std::shared_ptr &renderer, const std::shared_ptr &camera, @@ -74,10 +74,10 @@ class HudRenderStage { void add_drag_entity(const std::shared_ptr entity); /** - * Remove the render object for drag selection. - * - * @param render_entity Render entity to remove. - */ + * Remove the render object for drag selection. + * + * @param render_entity Render entity to remove. + */ void remove_drag_entity(); /** @@ -87,7 +87,7 @@ class HudRenderStage { /** * Resize the FBO for the HUD rendering. This basically updates the output - * texture size. + * texture size. * * @param width New width of the FBO. * @param height New height of the FBO. diff --git a/libopenage/renderer/stages/screen/render_stage.h b/libopenage/renderer/stages/screen/render_stage.h index f528850784..4dc40c0d03 100644 --- a/libopenage/renderer/stages/screen/render_stage.h +++ b/libopenage/renderer/stages/screen/render_stage.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -25,12 +25,12 @@ namespace screen { class ScreenRenderStage { public: /** - * Create a new render stage for drawing to the screen. - * - * @param window openage window targeted for rendering. - * @param renderer openage low-level renderer. - * @param shaderdir Directory containing the shader source files. - */ + * Create a new render stage for drawing to the screen. + * + * @param window openage window targeted for rendering. + * @param renderer openage low-level renderer. + * @param shaderdir Directory containing the shader source files. + */ ScreenRenderStage(const std::shared_ptr &window, const std::shared_ptr &renderer, const util::Path &shaderdir); diff --git a/libopenage/renderer/stages/skybox/render_stage.h b/libopenage/renderer/stages/skybox/render_stage.h index fc2deb0a65..e6967be901 100644 --- a/libopenage/renderer/stages/skybox/render_stage.h +++ b/libopenage/renderer/stages/skybox/render_stage.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -27,12 +27,12 @@ namespace skybox { class SkyboxRenderStage { public: /** - * Create a new render stage for the skybox. - * - * @param window openage window targeted for rendering. - * @param renderer openage low-level renderer. - * @param shaderdir Directory containing the shader source files. - */ + * Create a new render stage for the skybox. + * + * @param window openage window targeted for rendering. + * @param renderer openage low-level renderer. + * @param shaderdir Directory containing the shader source files. + */ SkyboxRenderStage(const std::shared_ptr &window, const std::shared_ptr &renderer, const util::Path &shaderdir); @@ -46,26 +46,26 @@ class SkyboxRenderStage { std::shared_ptr get_render_pass(); /** - * Set the color used for the skybox using integer values - * for each channel (0 to 255). - * - * @param col RGBA color value. - */ + * Set the color used for the skybox using integer values + * for each channel (0 to 255). + * + * @param col RGBA color value. + */ void set_color(const Eigen::Vector4i col); void set_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a); /** - * Set the color used for the skybox using float values - * for each channel (0.0 to 1.0). - * - * @param col RGBA color value. - */ + * Set the color used for the skybox using float values + * for each channel (0.0 to 1.0). + * + * @param col RGBA color value. + */ void set_color(const Eigen::Vector4f col); void set_color(float r, float g, float b, float a); /** * Resize the FBO for the skybox rendering. This basically updates the output - * texture size. + * texture size. * * @param width New width of the FBO. * @param height New height of the FBO. @@ -103,13 +103,13 @@ class SkyboxRenderStage { std::shared_ptr output_texture; /** - * Background color. - */ + * Background color. + */ Eigen::Vector4f bg_color; /** - * Stores background color uniform for the shader. - */ + * Stores background color uniform for the shader. + */ std::shared_ptr color_unif; }; diff --git a/libopenage/renderer/stages/terrain/chunk.h b/libopenage/renderer/stages/terrain/chunk.h index 4506877230..9dc7f028e0 100644 --- a/libopenage/renderer/stages/terrain/chunk.h +++ b/libopenage/renderer/stages/terrain/chunk.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 @@ -27,10 +27,10 @@ class TerrainRenderEntity; class TerrainChunk { public: /** - * Create a new terrain chunk. - * - * @param asset_manager Asset manager for loading textures. - */ + * Create a new terrain chunk. + * + * @param asset_manager Asset manager for loading textures. + */ TerrainChunk(const std::shared_ptr &asset_manager, const util::Vector2s size, const coord::scene2_delta offset); @@ -38,47 +38,47 @@ class TerrainChunk { ~TerrainChunk() = default; /** - * Set the terrain render entity for vertex updates of this mesh. - * - * @param entity New terrain render entity. - * @param size Size of the chunk in tiles. - * @param offset Offset of the chunk from origin in tiles. - */ + * Set the terrain render entity for vertex updates of this mesh. + * + * @param entity New terrain render entity. + * @param size Size of the chunk in tiles. + * @param offset Offset of the chunk from origin in tiles. + */ void set_render_entity(const std::shared_ptr &entity); /** - * Fetch updates from the render entity. - * - * @param time Current simulation time. - */ + * Fetch updates from the render entity. + * + * @param time Current simulation time. + */ void fetch_updates(const time::time_t &time = 0.0); /** - * Update the uniforms of the meshes. - * - * @param time Current simulation time. - */ + * Update the uniforms of the meshes. + * + * @param time Current simulation time. + */ void update_uniforms(const time::time_t &time = 0.0); /** - * Get the meshes composing the terrain. - * - * @return Vector of terrain meshes. - */ + * Get the meshes composing the terrain. + * + * @return Vector of terrain meshes. + */ const std::vector> &get_meshes() const; /** - * Get the size of the chunk in tiles. - * - * @return Size of the chunk (in tiles). - */ + * Get the size of the chunk in tiles. + * + * @return Size of the chunk (in tiles). + */ util::Vector2s &get_size(); /** - * Get the offset of the chunk from origin in tiles. - * - * @return Offset of the chunk (in tiles). - */ + * Get the offset of the chunk from origin in tiles. + * + * @return Offset of the chunk (in tiles). + */ coord::scene2_delta &get_offset(); private: @@ -90,13 +90,13 @@ class TerrainChunk { std::shared_ptr create_mesh(); /** - * Size of the chunk in tiles (width x height). - */ + * Size of the chunk in tiles (width x height). + */ util::Vector2s size; /** - * Offset of the chunk from origin in tiles (x, y). - */ + * Offset of the chunk from origin in tiles (x, y). + */ coord::scene2_delta offset; /** @@ -107,13 +107,13 @@ class TerrainChunk { /** * Asset manager for central accessing and loading textures. - */ + */ std::shared_ptr asset_manager; /** - * Source for ingame terrain coordinates. These coordinates are translated into - * our render vertex mesh when \p update() is called. - */ + * Source for ingame terrain coordinates. These coordinates are translated into + * our render vertex mesh when \p update() is called. + */ std::shared_ptr render_entity; }; diff --git a/libopenage/renderer/stages/terrain/mesh.h b/libopenage/renderer/stages/terrain/mesh.h index e2c7545ff8..118af0ca00 100644 --- a/libopenage/renderer/stages/terrain/mesh.h +++ b/libopenage/renderer/stages/terrain/mesh.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -30,21 +30,21 @@ namespace terrain { class TerrainRenderMesh { public: /** - * Create a new terrain render mesh with empty values. - * - * Mesh and texture need to be set before the terrain mesh becomes renderable. - * - * @param asset_manager Asset manager for central accessing and loading textures. - */ + * Create a new terrain render mesh with empty values. + * + * Mesh and texture need to be set before the terrain mesh becomes renderable. + * + * @param asset_manager Asset manager for central accessing and loading textures. + */ TerrainRenderMesh(const std::shared_ptr &asset_manager); /** - * Create a new terrain render mesh. - * - * @param asset_manager Asset manager for central accessing and loading textures. - * @param info Terrain info for the renderable. - * @param mesh Vertex data of the mesh. - */ + * Create a new terrain render mesh. + * + * @param asset_manager Asset manager for central accessing and loading textures. + * @param info Terrain info for the renderable. + * @param mesh Vertex data of the mesh. + */ TerrainRenderMesh(const std::shared_ptr &asset_manager, const curve::Discrete &terrain_path, renderer::resources::MeshData &&mesh); @@ -52,69 +52,69 @@ class TerrainRenderMesh { ~TerrainRenderMesh() = default; /** - * Set the reference to the uniform inputs of the renderable - * associated with this mesh. Relevant uniforms are updated - * when calling \p update(). - * - * @param uniforms Uniform inputs of this mesh's renderable. - */ + * Set the reference to the uniform inputs of the renderable + * associated with this mesh. Relevant uniforms are updated + * when calling \p update(). + * + * @param uniforms Uniform inputs of this mesh's renderable. + */ void set_uniforms(const std::shared_ptr &uniforms); /** - * Get the reference to the uniform inputs of the mesh's renderable. - * - * @return Uniform inputs of the renderable. - */ + * Get the reference to the uniform inputs of the mesh's renderable. + * + * @return Uniform inputs of the renderable. + */ const std::shared_ptr &get_uniforms(); /** - * Set the vertex mesh for the terrain. - * - * @param mesh Mesh for creating a renderer geometry object. - */ + * Set the vertex mesh for the terrain. + * + * @param mesh Mesh for creating a renderer geometry object. + */ void set_mesh(renderer::resources::MeshData &&mesh); /** - * Get the vertex mesh for the terrain. - * - * @return Mesh for creating a renderer geometry object. - */ + * Get the vertex mesh for the terrain. + * + * @return Mesh for creating a renderer geometry object. + */ const renderer::resources::MeshData &get_mesh(); /** - * Set the terrain info that is drawn onto the mesh. - * - * @param texture Terrain info. - */ + * Set the terrain info that is drawn onto the mesh. + * + * @param texture Terrain info. + */ void set_terrain_path(const curve::Discrete &info); /** - * Update the uniforms of the renderable associated with this object. - * - * @param time Current simulation time. - */ + * Update the uniforms of the renderable associated with this object. + * + * @param time Current simulation time. + */ void update_uniforms(const time::time_t &time = 0.0); /** - * Check whether a new renderable needs to be created for this mesh. - * - * If true, the old renderable should be removed from the render pass. - * The updated uniforms and geometry should be passed to this mesh. - * Afterwards, clear the requirement flag with \p clear_requires_renderable(). - * - * @return true if a new renderable is required, else false. - */ + * Check whether a new renderable needs to be created for this mesh. + * + * If true, the old renderable should be removed from the render pass. + * The updated uniforms and geometry should be passed to this mesh. + * Afterwards, clear the requirement flag with \p clear_requires_renderable(). + * + * @return true if a new renderable is required, else false. + */ bool requires_renderable(); /** - * Indicate to this mesh that a new renderable has been created. - */ + * Indicate to this mesh that a new renderable has been created. + */ void clear_requires_renderable(); /** * Create the model transformation matrix for rendering. - * - * @param offset Offset of the terrain mesh to the scene origin. + * + * @param offset Offset of the terrain mesh to the scene origin. */ void create_model_matrix(const coord::scene2_delta &offset); @@ -132,9 +132,9 @@ class TerrainRenderMesh { private: /** - * Stores whether a new renderable for this mesh needs to be created - * for the render pass. - */ + * Stores whether a new renderable for this mesh needs to be created + * for the render pass. + */ bool require_renderable; /** @@ -144,27 +144,27 @@ class TerrainRenderMesh { /** * Asset manager for central accessing and loading textures. - */ + */ std::shared_ptr asset_manager; /** - * Terrain information for the renderables. - */ + * Terrain information for the renderables. + */ curve::Discrete> terrain_info; /** - * Shader uniforms for the renderable in the terrain render pass. - */ + * Shader uniforms for the renderable in the terrain render pass. + */ std::shared_ptr uniforms; /** - * Pre-transformation vertices for the terrain model. - */ + * Pre-transformation vertices for the terrain model. + */ renderer::resources::MeshData mesh; /** - * Transformation matrix for the terrain model. - */ + * Transformation matrix for the terrain model. + */ Eigen::Matrix4f model_matrix; }; } // namespace terrain diff --git a/libopenage/renderer/stages/terrain/model.h b/libopenage/renderer/stages/terrain/model.h index a24441f191..c06ea351c0 100644 --- a/libopenage/renderer/stages/terrain/model.h +++ b/libopenage/renderer/stages/terrain/model.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -32,50 +32,50 @@ class TerrainChunk; class TerrainRenderModel { public: /** - * Create a new model for the terrain. - * - * @param asset_manager Asset manager for loading resources. - */ + * Create a new model for the terrain. + * + * @param asset_manager Asset manager for loading resources. + */ TerrainRenderModel(const std::shared_ptr &asset_manager); ~TerrainRenderModel() = default; /** - * Add a new chunk to the terrain model. - * - * @param entity Render entity of the chunk. - * @param chunk_size Size of the chunk in tiles. - * @param chunk_offset Offset of the chunk from origin in tiles. - */ + * Add a new chunk to the terrain model. + * + * @param entity Render entity of the chunk. + * @param chunk_size Size of the chunk in tiles. + * @param chunk_offset Offset of the chunk from origin in tiles. + */ void add_chunk(const std::shared_ptr &entity, const util::Vector2s chunk_size, const coord::scene2_delta chunk_offset); /** - * Set the current camera of the scene. - * - * @param camera Camera object viewing the scene. - */ + * Set the current camera of the scene. + * + * @param camera Camera object viewing the scene. + */ void set_camera(const std::shared_ptr &camera); /** - * Fetch updates from the render entity. - * - * @param time Current simulation time. - */ + * Fetch updates from the render entity. + * + * @param time Current simulation time. + */ void fetch_updates(const time::time_t &time = 0.0); /** - * Update the uniforms of the renderable associated with this object. - * - * @param time Current simulation time. - */ + * Update the uniforms of the renderable associated with this object. + * + * @param time Current simulation time. + */ void update_uniforms(const time::time_t &time = 0.0); /** - * Get the chunks composing the terrain. - * - * @return Chunks of the terrain. - */ + * Get the chunks composing the terrain. + * + * @return Chunks of the terrain. + */ const std::vector> &get_chunks() const; private: @@ -91,7 +91,7 @@ class TerrainRenderModel { /** * Asset manager for central accessing and loading textures. - */ + */ std::shared_ptr asset_manager; }; diff --git a/libopenage/renderer/stages/terrain/render_entity.h b/libopenage/renderer/stages/terrain/render_entity.h index dd95edda73..054ee73931 100644 --- a/libopenage/renderer/stages/terrain/render_entity.h +++ b/libopenage/renderer/stages/terrain/render_entity.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -28,15 +28,15 @@ class TerrainRenderEntity { using tiles_t = std::vector>; /** - * Update a single tile of the displayed terrain (chunk) with information from the - * gamestate. + * Update a single tile of the displayed terrain (chunk) with information from the + * gamestate. * * @param size Size of the terrain in tiles (width x length) * @param pos Position of the tile in the chunk. * @param elevation Height of terrain tile. * @param terrain_path Path to the terrain definition. - * @param time Simulation time of the update. - */ + * @param time Simulation time of the update. + */ void update_tile(const util::Vector2s size, const coord::tile &pos, const terrain_elevation_t elevation, @@ -44,38 +44,38 @@ class TerrainRenderEntity { const time::time_t time = 0.0); /** - * Update the full grid of the displayed terrain (chunk) with information from the - * gamestate. + * Update the full grid of the displayed terrain (chunk) with information from the + * gamestate. * * @param size Size of the terrain in tiles (width x length) * @param tiles Animation data for each tile (elevation, terrain path). - * @param time Simulation time of the update. - */ + * @param time Simulation time of the update. + */ void update(const util::Vector2s size, const tiles_t tiles, const time::time_t time = 0.0); /** - * Get the vertices of the terrain. - * - * @return Vector of vertex coordinates. - */ + * Get the vertices of the terrain. + * + * @return Vector of vertex coordinates. + */ const std::vector &get_vertices(); /** - * Get the texture mapping for the terrain. - * - * TODO: Return the actual mapping. - * - * @return Texture mapping of textures to vertex area. - */ + * Get the texture mapping for the terrain. + * + * TODO: Return the actual mapping. + * + * @return Texture mapping of textures to vertex area. + */ const curve::Discrete &get_terrain_path(); /** - * Get the number of vertices on each side of the terrain. - * - * @return Vector with width as first element and height as second element. - */ + * Get the number of vertices on each side of the terrain. + * + * @return Vector with width as first element and height as second element. + */ const util::Vector2s &get_size(); /** @@ -100,8 +100,8 @@ class TerrainRenderEntity { bool changed; /** - * Chunk dimensions (width x height). - */ + * Chunk dimensions (width x height). + */ util::Vector2s size; /** diff --git a/libopenage/renderer/stages/terrain/render_stage.h b/libopenage/renderer/stages/terrain/render_stage.h index d8d1659329..40caf2b309 100644 --- a/libopenage/renderer/stages/terrain/render_stage.h +++ b/libopenage/renderer/stages/terrain/render_stage.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -42,15 +42,15 @@ class TerrainRenderModel; class TerrainRenderStage { public: /** - * Create a new render stage for the terrain. - * - * @param window openage window targeted for rendering. - * @param renderer openage low-level renderer. - * @param camera Camera used for the rendered scene. - * @param shaderdir Directory containing the shader source files. - * @param asset_manager Asset manager for loading resources. - * @param clock Simulation clock for timing animations. - */ + * Create a new render stage for the terrain. + * + * @param window openage window targeted for rendering. + * @param renderer openage low-level renderer. + * @param camera Camera used for the rendered scene. + * @param shaderdir Directory containing the shader source files. + * @param asset_manager Asset manager for loading resources. + * @param clock Simulation clock for timing animations. + */ TerrainRenderStage(const std::shared_ptr &window, const std::shared_ptr &renderer, const std::shared_ptr &camera, @@ -68,8 +68,8 @@ class TerrainRenderStage { /** * Add a new render entity to the terrain renderer. - * - * This creates a new terrain chunk and add it to the model. + * + * This creates a new terrain chunk and add it to the model. * * @param render_entity New render entity. */ @@ -84,7 +84,7 @@ class TerrainRenderStage { /** * Resize the FBO for the terrain rendering. This basically updates the output - * texture size. + * texture size. * * @param width New width of the FBO. * @param height New height of the FBO. diff --git a/libopenage/renderer/stages/world/object.h b/libopenage/renderer/stages/world/object.h index c558e4f93f..e2fc865dd6 100644 --- a/libopenage/renderer/stages/world/object.h +++ b/libopenage/renderer/stages/world/object.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -37,39 +37,39 @@ class WorldRenderEntity; class WorldObject { public: /** - * Create a new object for the World render stage. - * - * @param asset_manager Asset manager for loading resources. - */ + * Create a new object for the World render stage. + * + * @param asset_manager Asset manager for loading resources. + */ WorldObject(const std::shared_ptr &asset_manager); ~WorldObject() = default; /** - * Set the world render entity. - * - * @param entity New world render entity. - */ + * Set the world render entity. + * + * @param entity New world render entity. + */ void set_render_entity(const std::shared_ptr &entity); /** - * Set the current camera of the scene. - * - * @param camera Camera object viewing the scene. - */ + * Set the current camera of the scene. + * + * @param camera Camera object viewing the scene. + */ void set_camera(const std::shared_ptr &camera); /** - * Fetch updates from the render entity. - * - * @param time Current simulation time. - */ + * Fetch updates from the render entity. + * + * @param time Current simulation time. + */ void fetch_updates(const time::time_t &time = 0.0); /** - * Update the uniforms of the renderable associated with this object. - * - * @param time Current simulation time. - */ + * Update the uniforms of the renderable associated with this object. + * + * @param time Current simulation time. + */ void update_uniforms(const time::time_t &time = 0.0); /** @@ -80,26 +80,26 @@ class WorldObject { uint32_t get_id(); /** - * Get the quad for creating the geometry. - * - * @return Mesh for creating a renderer geometry object. - */ + * Get the quad for creating the geometry. + * + * @return Mesh for creating a renderer geometry object. + */ static const renderer::resources::MeshData get_mesh(); /** - * Check whether a new renderable needs to be created for this mesh. - * - * If true, the old renderable should be removed from the render pass. - * The updated uniforms and geometry should be passed to this mesh. - * Afterwards, clear the requirement flag with \p clear_requires_renderable(). - * - * @return true if a new renderable is required, else false. - */ + * Check whether a new renderable needs to be created for this mesh. + * + * If true, the old renderable should be removed from the render pass. + * The updated uniforms and geometry should be passed to this mesh. + * Afterwards, clear the requirement flag with \p clear_requires_renderable(). + * + * @return true if a new renderable is required, else false. + */ bool requires_renderable(); /** - * Indicate to this mesh that a new renderable has been created. - */ + * Indicate to this mesh that a new renderable has been created. + */ void clear_requires_renderable(); /** @@ -115,12 +115,12 @@ class WorldObject { void clear_changed_flag(); /** - * Set the reference to the uniform inputs of the renderable - * associated with this object. Relevant uniforms are updated - * when calling \p update(). - * - * @param uniforms Uniform inputs of this object's renderable. - */ + * Set the reference to the uniform inputs of the renderable + * associated with this object. Relevant uniforms are updated + * when calling \p update(). + * + * @param uniforms Uniform inputs of this object's renderable. + */ void set_uniforms(const std::shared_ptr &uniforms); /** @@ -136,9 +136,9 @@ class WorldObject { private: /** - * Stores whether a new renderable for this object needs to be created - * for the render pass. - */ + * Stores whether a new renderable for this object needs to be created + * for the render pass. + */ bool require_renderable; /** @@ -173,18 +173,18 @@ class WorldObject { curve::Continuous position; /** - * Angle of the object. - */ + * Angle of the object. + */ curve::Segmented angle; /** - * Animation information for the renderables. - */ + * Animation information for the renderables. + */ curve::Discrete> animation_info; /** - * Shader uniforms for the renderable in the terrain render pass. - */ + * Shader uniforms for the renderable in the terrain render pass. + */ std::shared_ptr uniforms; /** diff --git a/libopenage/renderer/stages/world/render_entity.h b/libopenage/renderer/stages/world/render_entity.h index b1ca52c0d3..7c56635f89 100644 --- a/libopenage/renderer/stages/world/render_entity.h +++ b/libopenage/renderer/stages/world/render_entity.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -32,7 +32,7 @@ class WorldRenderEntity { * * @param ref_id Game entity ID. * @param position Position of the game entity inside the game world. - * @param angle Angle of the game entity inside the game world. + * @param angle Angle of the game entity inside the game world. * @param animation_path Path to the animation definition. * @param time Simulation time of the update. */ @@ -44,7 +44,7 @@ class WorldRenderEntity { /** * This function is for DEBUGGING and should not be used. - * + * * Update the render entity with information from the gamestate. * * @param ref_id Game entity ID. @@ -72,17 +72,17 @@ class WorldRenderEntity { const curve::Continuous &get_position(); /** - * Get the angle of the entity inside the game world. - * - * @return Angle curve of the entity. - */ + * Get the angle of the entity inside the game world. + * + * @return Angle curve of the entity. + */ const curve::Segmented &get_angle(); /** - * Get the animation definition path. - * - * @return Path to the animation definition file. - */ + * Get the animation definition path. + * + * @return Path to the animation definition file. + */ const curve::Discrete &get_animation_path(); /** @@ -124,8 +124,8 @@ class WorldRenderEntity { curve::Continuous position; /** - * Angle of the entity inside the game world. - */ + * Angle of the entity inside the game world. + */ curve::Segmented angle; /** diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index fe3cf1970b..e22399180b 100644 --- a/libopenage/renderer/stages/world/render_stage.h +++ b/libopenage/renderer/stages/world/render_stage.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -40,15 +40,15 @@ class WorldObject; class WorldRenderStage { public: /** - * Create a new render stage for the game world. - * - * @param window openage window targeted for rendering. - * @param renderer openage low-level renderer. - * @param camera Camera used for the rendered scene. - * @param shaderdir Directory containing the shader source files. - * @param asset_manager Asset manager for loading resources. - * @param clock Simulation clock for timing animations. - */ + * Create a new render stage for the game world. + * + * @param window openage window targeted for rendering. + * @param renderer openage low-level renderer. + * @param camera Camera used for the rendered scene. + * @param shaderdir Directory containing the shader source files. + * @param asset_manager Asset manager for loading resources. + * @param clock Simulation clock for timing animations. + */ WorldRenderStage(const std::shared_ptr &window, const std::shared_ptr &renderer, const std::shared_ptr &camera, @@ -78,7 +78,7 @@ class WorldRenderStage { /** * Resize the FBO for the world rendering. This basically updates the output - * texture size. + * texture size. * * @param width New width of the FBO. * @param height New height of the FBO. @@ -144,12 +144,12 @@ class WorldRenderStage { std::shared_ptr clock; /** - * Default geometry for every world object. - * - * Since all world objects are sprites, their mesh is always quad - * with the same vertex info. Reusing the geometry allows us to - * use the same vetrex buffer for every object. - */ + * Default geometry for every world object. + * + * Since all world objects are sprites, their mesh is always quad + * with the same vertex info. Reusing the geometry allows us to + * use the same vetrex buffer for every object. + */ const std::shared_ptr default_geometry; /** diff --git a/libopenage/renderer/texture.h b/libopenage/renderer/texture.h index 7e14cba664..c2abdab2d2 100644 --- a/libopenage/renderer/texture.h +++ b/libopenage/renderer/texture.h @@ -1,4 +1,4 @@ -// Copyright 2015-2018 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -15,7 +15,7 @@ class Texture2d { virtual ~Texture2d(); /// Returns the texture information. - const resources::Texture2dInfo& get_info() const; + const resources::Texture2dInfo &get_info() const; /// Copies this texture's data from graphics hardware into a CPU-accessible /// Texture2dData buffer. @@ -23,14 +23,15 @@ class Texture2d { /// Uploads the provided data into the GPU texture storage. The format has /// to match the format this Texture was originally created with. - virtual void upload(resources::Texture2dData const&) = 0; + virtual void upload(resources::Texture2dData const &) = 0; protected: /// Constructs the base with the given information. - Texture2d(const resources::Texture2dInfo&); + Texture2d(const resources::Texture2dInfo &); /// Information about the size, format, etc. of this texture. resources::Texture2dInfo info; }; -}} +} // namespace renderer +} // namespace openage diff --git a/libopenage/renderer/types.h b/libopenage/renderer/types.h index 56fdee7b49..cade7159f0 100644 --- a/libopenage/renderer/types.h +++ b/libopenage/renderer/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 @@ -12,4 +12,4 @@ namespace openage::renderer { */ using uniform_id_t = uint32_t; -} +} // namespace openage::renderer diff --git a/libopenage/renderer/uniform_buffer.h b/libopenage/renderer/uniform_buffer.h index 2ba5790127..85040a4976 100644 --- a/libopenage/renderer/uniform_buffer.h +++ b/libopenage/renderer/uniform_buffer.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 @@ -21,10 +21,10 @@ class UniformBuffer : public std::enable_shared_from_this { virtual ~UniformBuffer() = default; /** - * Update the uniforms in the buffer. - * - * @param unif_in Uniform input to update the buffer with. - */ + * Update the uniforms in the buffer. + * + * @param unif_in Uniform input to update the buffer with. + */ virtual void update_uniforms(std::shared_ptr const &unif_in) = 0; /** diff --git a/libopenage/renderer/uniform_input.h b/libopenage/renderer/uniform_input.h index 515a0a0e4e..9e66b64712 100644 --- a/libopenage/renderer/uniform_input.h +++ b/libopenage/renderer/uniform_input.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 @@ -53,7 +53,7 @@ class DataInput { /** * Abstract base for uniform input. Besides the uniform values, it stores information about * which shader program the input was created for. -*/ + */ class UniformInput : public DataInput , public std::enable_shared_from_this { protected: diff --git a/libopenage/renderer/vulkan/graphics_device.h b/libopenage/renderer/vulkan/graphics_device.h index e29297461c..9cf8ffe23c 100644 --- a/libopenage/renderer/vulkan/graphics_device.h +++ b/libopenage/renderer/vulkan/graphics_device.h @@ -1,4 +1,4 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -57,7 +57,7 @@ class VlkGraphicsDevice { /// Given a physical device and a list of queue family indices in that device, instantiates /// a logical device with a queue per each of the families. The device has to support the /// swapchain extension. - VlkGraphicsDevice(VkPhysicalDevice dev, std::vector const& q_fams); + VlkGraphicsDevice(VkPhysicalDevice dev, std::vector const &q_fams); VkPhysicalDevice get_physical_device() const; VkDevice get_device() const; @@ -68,4 +68,6 @@ class VlkGraphicsDevice { ~VlkGraphicsDevice(); }; -}}} // openage::renderer::vulkan +} // namespace vulkan +} // namespace renderer +} // namespace openage diff --git a/libopenage/renderer/vulkan/loader.h b/libopenage/renderer/vulkan/loader.h index 66375790c0..9436eb806c 100644 --- a/libopenage/renderer/vulkan/loader.h +++ b/libopenage/renderer/vulkan/loader.h @@ -1,4 +1,4 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -28,19 +28,19 @@ class VlkLoader { /// Part of VK_EXT_debug_report, allows setting a callback for debug events. VkResult vkCreateDebugReportCallbackEXT( VkInstance instance, - const VkDebugReportCallbackCreateInfoEXT* pCreateInfo, - const VkAllocationCallbacks* pAllocator, - VkDebugReportCallbackEXT* pCallback - ); + const VkDebugReportCallbackCreateInfoEXT *pCreateInfo, + const VkAllocationCallbacks *pAllocator, + VkDebugReportCallbackEXT *pCallback); /// Part of VK_EXT_debug_report, destroys the debug callback object. void vkDestroyDebugReportCallbackEXT( VkInstance instance, VkDebugReportCallbackEXT callback, - const VkAllocationCallbacks* pAllocator - ); + const VkAllocationCallbacks *pAllocator); #endif }; -}}} // openage::renderer::vulkan +} // namespace vulkan +} // namespace renderer +} // namespace openage diff --git a/libopenage/renderer/vulkan/render_target.h b/libopenage/renderer/vulkan/render_target.h index c7c3ab2bed..f25c684fd4 100644 --- a/libopenage/renderer/vulkan/render_target.h +++ b/libopenage/renderer/vulkan/render_target.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 @@ -158,9 +158,9 @@ class VlkDrawableDisplay { }; class VlkFramebuffer final : public RenderTarget { - //std::vector attachments; - //VkFramebuffer framebuffer; - //VkViewport viewport; + // std::vector attachments; + // VkFramebuffer framebuffer; + // VkViewport viewport; public: VlkFramebuffer(VkRenderPass /*pass*/, std::vector const & /*attachments*/) { diff --git a/libopenage/renderer/vulkan/renderer.h b/libopenage/renderer/vulkan/renderer.h index 7bb13cd6ce..f1afc1c99d 100644 --- a/libopenage/renderer/vulkan/renderer.h +++ b/libopenage/renderer/vulkan/renderer.h @@ -1,11 +1,11 @@ -// Copyright 2017-2018 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 "../renderer.h" #include "../../util/path.h" +#include "../renderer.h" #include "graphics_device.h" @@ -21,12 +21,13 @@ class VlkRenderer { VkSurfaceKHR surface; public: - VlkRenderer(VkInstance instance, VkSurfaceKHR surface) - : instance(instance) - , surface(surface) {} + VlkRenderer(VkInstance instance, VkSurfaceKHR surface) : + instance(instance), surface(surface) {} /// Testing function that draws a triangle. Not part of the final renderer implementation. - void do_the_thing(util::Path& dir); + void do_the_thing(util::Path &dir); }; -}}} // openage::renderer::vulkan +} // namespace vulkan +} // namespace renderer +} // namespace openage diff --git a/libopenage/renderer/vulkan/util.h b/libopenage/renderer/vulkan/util.h index 04ab3c872c..6e97551098 100644 --- a/libopenage/renderer/vulkan/util.h +++ b/libopenage/renderer/vulkan/util.h @@ -1,4 +1,4 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -8,7 +8,7 @@ template -std::vector vk_do_ritual(R2 (*func)(uint32_t*, R*)) { +std::vector vk_do_ritual(R2 (*func)(uint32_t *, R *)) { uint32_t count = 0; func(&count, nullptr); std::vector ret(count); @@ -18,7 +18,7 @@ std::vector vk_do_ritual(R2 (*func)(uint32_t*, R*)) { } template -std::vector vk_do_ritual(R2 (*func)(T, uint32_t*, R*), T2&& a) { +std::vector vk_do_ritual(R2 (*func)(T, uint32_t *, R *), T2 &&a) { uint32_t count = 0; func(std::forward(a), &count, nullptr); std::vector ret(count); @@ -28,7 +28,7 @@ std::vector vk_do_ritual(R2 (*func)(T, uint32_t*, R*), T2&& a) { } template -std::vector vk_do_ritual(R2 (*func)(T, U, uint32_t*, R*), T2&& a, U2&& b) { +std::vector vk_do_ritual(R2 (*func)(T, U, uint32_t *, R *), T2 &&a, U2 &&b) { uint32_t count = 0; func(std::forward(a), std::forward(b), &count, nullptr); std::vector ret(count); @@ -38,7 +38,7 @@ std::vector vk_do_ritual(R2 (*func)(T, U, uint32_t*, R*), T2&& a, U2&& b) { } template -std::vector vk_do_ritual(R2 (*func)(T, U, V, uint32_t*, R*), T2&& a, U2&& b, V2&& c) { +std::vector vk_do_ritual(R2 (*func)(T, U, V, uint32_t *, R *), T2 &&a, U2 &&b, V2 &&c) { uint32_t count = 0; func(std::forward(a), std::forward(b), std::forward(c), &count, nullptr); std::vector ret(count); @@ -47,7 +47,7 @@ std::vector vk_do_ritual(R2 (*func)(T, U, V, uint32_t*, R*), T2&& a, U2&& b, return ret; } -#define VK_CALL_CHECKED(fun, ...) \ +#define VK_CALL_CHECKED(fun, ...) \ { \ VkResult res = fun(__VA_ARGS__); \ if (res != VK_SUCCESS) { \ diff --git a/libopenage/rng/rng.h b/libopenage/rng/rng.h index 4f96ba31ab..83faddee68 100644 --- a/libopenage/rng/rng.h +++ b/libopenage/rng/rng.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -21,13 +21,13 @@ class RNG { */ explicit RNG(uint64_t seed); - [[maybe_unused]] /** + /** * Initializes the rng using data from the buffer pointed to by data * @param data The buffer that contains data for seeding the rng * @param count The number of bytes in the buffer * @throws Error if 0 bytes are passed */ - RNG(const void *data, size_t count); + [[maybe_unused]] RNG(const void *data, size_t count); /** @@ -73,7 +73,7 @@ class RNG { /** * Retrieves a random value from the generator */ - inline uint64_t operator ()() { + inline uint64_t operator()() { return this->random(); } @@ -189,13 +189,13 @@ class RNG { * Reads the rng from a stream * @throws Error if reading from the stream fails */ -std::istream &operator >>(std::istream &instream, RNG &inrng); +std::istream &operator>>(std::istream &instream, RNG &inrng); /** * Writes the rng state to a stream * @throws Error if writing data fails */ -std::ostream &operator <<(std::ostream &ostream, const RNG &inrng); +std::ostream &operator<<(std::ostream &ostream, const RNG &inrng); /** diff --git a/libopenage/testing/testing.h b/libopenage/testing/testing.h index b8ae4f2946..50e03338e0 100644 --- a/libopenage/testing/testing.h +++ b/libopenage/testing/testing.h @@ -1,4 +1,4 @@ -// Copyright 2014-2016 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -53,12 +53,15 @@ bool fail(const log::message &msg); if (test_result_left != (right)) { \ TESTFAILMSG("unexpected value: " << (test_result_left)); \ } \ - } catch (::openage::testing::TestError &e) { \ + } \ + catch (::openage::testing::TestError & e) { \ throw; \ - } catch (::openage::error::Error &e) { \ + } \ + catch (::openage::error::Error & e) { \ TESTFAILMSG("unexpected exception: " << e); \ } \ - } while (0) + } \ + while (0) /** @@ -69,16 +72,18 @@ bool fail(const log::message &msg); do { \ try { \ auto &&test_result_left = (left); \ - if ((test_result_left < (right - epsilon)) or \ - (test_result_left > (right + epsilon))) { \ + if ((test_result_left < (right - epsilon)) or (test_result_left > (right + epsilon))) { \ TESTFAILMSG("unexpected value: " << (test_result_left)); \ } \ - } catch (::openage::testing::TestError &e) { \ + } \ + catch (::openage::testing::TestError & e) { \ throw; \ - } catch (::openage::error::Error &e) { \ + } \ + catch (::openage::error::Error & e) { \ TESTFAILMSG("unexpected exception: " << e); \ } \ - } while (0) + } \ + while (0) /** @@ -89,13 +94,15 @@ bool fail(const log::message &msg); bool expr_has_thrown = false; \ try { \ expression; \ - } catch (::openage::error::Error &e) { \ + } \ + catch (::openage::error::Error & e) { \ expr_has_thrown = true; \ } \ if (not expr_has_thrown) { \ TESTFAILMSG("no exception"); \ } \ - } while (0) + } \ + while (0) /** @@ -105,10 +112,13 @@ bool fail(const log::message &msg); do { \ try { \ expression; \ - } catch (::openage::error::Error &e) { \ + } \ + catch (::openage::error::Error & e) { \ TESTFAILMSG("unexpected exception"); \ } \ - } while (0) + } \ + while (0) -}} // openage::testing +} // namespace testing +} // namespace openage diff --git a/libopenage/testing/testlist.h b/libopenage/testing/testlist.h index d3da00e128..21ffcbe0eb 100644 --- a/libopenage/testing/testlist.h +++ b/libopenage/testing/testlist.h @@ -1,4 +1,4 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -21,4 +21,5 @@ namespace testing { OAAPI void run_method(const std::string &name); -}} // openage::testing +} // namespace testing +} // namespace openage diff --git a/libopenage/time/clock.h b/libopenage/time/clock.h index 1021f599cd..05fbb9590a 100644 --- a/libopenage/time/clock.h +++ b/libopenage/time/clock.h @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -38,20 +38,20 @@ class Clock { ~Clock() = default; /** - * Get the current state of the clock. - */ + * Get the current state of the clock. + */ ClockState get_state(); /** - * Update the simulation time. - */ + * Update the simulation time. + */ void update_time(); /** * Get the current simulation time (in seconds). - * - * The returned value has a precision of milliseconds, so it is - * accurate to three decimal places. + * + * The returned value has a precision of milliseconds, so it is + * accurate to three decimal places. * * @return Time passed (in seconds). */ @@ -59,28 +59,28 @@ class Clock { /** * Get the current simulation time without speed adjustments. - * - * The returned value has a precision of milliseconds, so it is - * accurate to three decimal places. + * + * The returned value has a precision of milliseconds, so it is + * accurate to three decimal places. * * @return Time passed (in seconds). */ time::time_t get_real_time(); /** - * Get the current speed of the clock. + * Get the current speed of the clock. * * @return Speed of the clock. - */ + */ speed_t get_speed(); /** - * Set the speed of the clock. + * Set the speed of the clock. * * Simulation time is updated before changing speed. - * - * @param speed New speed of the clock. - */ + * + * @param speed New speed of the clock. + */ void set_speed(speed_t speed); /** @@ -109,8 +109,8 @@ class Clock { private: /** - * Status of the clock (init, running, stopped, ...). - */ + * Status of the clock (init, running, stopped, ...). + */ ClockState state; /** @@ -119,29 +119,29 @@ class Clock { uint16_t max_tick_time; /** - * How fast time passes relative to real time. - */ + * How fast time passes relative to real time. + */ speed_t speed; /** - * Last point in time where the clock was updated. - */ + * Last point in time where the clock was updated. + */ timepoint_t last_check; /** - * Start of the simulation in real time. - */ + * Start of the simulation in real time. + */ timepoint_t start_time; /** - * Stores how much time has passed inside the simulation (in milliseconds). - */ + * Stores how much time has passed inside the simulation (in milliseconds). + */ time::time_t sim_time; /** - * Stores how much time has passed inside the simulation (in milliseconds) + * Stores how much time has passed inside the simulation (in milliseconds) * _without_ speed adjustments (i.e. it acts as if speed = 1.0). - */ + */ time::time_t sim_real_time; /** diff --git a/libopenage/time/time_loop.h b/libopenage/time/time_loop.h index b48916d4e4..1a9e4dccdd 100644 --- a/libopenage/time/time_loop.h +++ b/libopenage/time/time_loop.h @@ -1,4 +1,4 @@ -// 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 @@ -15,20 +15,20 @@ class Clock; class TimeLoop { public: /** - * Create a new time loop with default values. - */ + * Create a new time loop with default values. + */ TimeLoop(); /** - * Create a new time loop from an existing and clock. - */ + * Create a new time loop from an existing and clock. + */ TimeLoop(const std::shared_ptr clock); ~TimeLoop() = default; /** * Run the time loop. - * - * Updates the clock and dispatches events that happened. + * + * Updates the clock and dispatches events that happened. */ void run(); @@ -43,10 +43,10 @@ class TimeLoop { void stop(); /** - * Get the clock used by this time loop. - * - * @return Simulation clock. - */ + * Get the clock used by this time loop. + * + * @return Simulation clock. + */ const std::shared_ptr get_clock(); private: @@ -56,8 +56,8 @@ class TimeLoop { bool running; /** - * Manage time and speed inside the simulation. - */ + * Manage time and speed inside the simulation. + */ std::shared_ptr clock; /** diff --git a/libopenage/util/algorithm.h b/libopenage/util/algorithm.h index a4d22a9d9b..cd68d4481d 100644 --- a/libopenage/util/algorithm.h +++ b/libopenage/util/algorithm.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -13,7 +13,7 @@ namespace util { /** * std::for_each except just on containers. */ -template +template inline Function for_each(Container &&container, Function &&func) { // why cpp why... return std::for_each(std::begin(std::forward(container)), @@ -24,7 +24,7 @@ inline Function for_each(Container &&container, Function &&func) { /** * Filters items from a container which satisfy a certain predicate. */ -template +template inline void remove_from(Container &container, Function &&func) { container.erase(std::remove_if(std::begin(container), std::end(container), diff --git a/libopenage/util/compiler.h b/libopenage/util/compiler.h index 9068284abf..9419bc1fa0 100644 --- a/libopenage/util/compiler.h +++ b/libopenage/util/compiler.h @@ -1,4 +1,4 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -18,6 +18,8 @@ /** * DLL entry-point decorations. */ +// clang-format off +// otherwise clang-format removes indentation #if defined(_WIN32) #if defined(libopenage_EXPORTS) #define OAAPI __declspec(dllexport) @@ -40,18 +42,21 @@ #define HAVE_SSIZE_T 1 #endif // HAVE_SSIZE_T #endif // _MSC_VER +// clang-format on /** * Software breakpoint if you're too lazy * to add it in gdb but instead wanna add it into the code directly. */ +// clang-format off #ifdef _WIN32 #define BREAKPOINT __debugbreak() #else #include #define BREAKPOINT raise(SIGTRAP) #endif +// clang-format on /** @@ -89,7 +94,7 @@ std::string demangle(const char *symbol); * * pxd: string symbol_name(const void *addr) except + */ -OAAPI std::string symbol_name(const void *addr, bool require_exact_addr=true, bool no_pure_addrs=false); +OAAPI std::string symbol_name(const void *addr, bool require_exact_addr = true, bool no_pure_addrs = false); /** @@ -129,4 +134,5 @@ std::string typestring(const T &ref) { } -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/compress/bitstream.h b/libopenage/util/compress/bitstream.h index 9548689cd3..43d6a45c01 100644 --- a/libopenage/util/compress/bitstream.h +++ b/libopenage/util/compress/bitstream.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -20,7 +20,7 @@ namespace compress { * * See lzxd.h for documentation. */ -using read_callback_t = std::function; +using read_callback_t = std::function; /** @@ -65,7 +65,7 @@ using read_callback_t = std::function; * Calling the modeswitch methods while already in the respective mode does * nothing. */ -template +template class BitStream { public: /** @@ -148,7 +148,8 @@ class BitStream { if (read_bytes == 0) [[unlikely]] { if (this->eof) { throw Error(MSG(err) << "Unexpected EOF in the middle of a block"); - } else { + } + else { read_bytes = 2; this->inbuf[0] = 0; this->inbuf[1] = 0; @@ -156,7 +157,7 @@ class BitStream { } } - if (read_bytes > (int) inbuf_size) [[unlikely]] { + if (read_bytes > (int)inbuf_size) [[unlikely]] { throw Error(MSG(err) << "read() returned more data than requested"); } @@ -174,7 +175,7 @@ class BitStream { /* * allow our friend HuffmanTable to directly use ensure_bits, peek_bits and remove_bits. */ - template + template friend class HuffmanTable; /** @@ -266,7 +267,7 @@ class BitStream { * * If min_discard is given, at least that amount of bits is discarded. */ - void align_bitstream(unsigned int min_discard=0) { + void align_bitstream(unsigned int min_discard = 0) { unsigned int nbits = this->stream_position % 16; if (nbits != 0) { nbits = 16 - nbits; @@ -292,8 +293,7 @@ class BitStream { } public: - BitStream(read_callback_t read_callback) - : + BitStream(read_callback_t read_callback) : eof{false}, read_callback{read_callback}, i_ptr{inbuf}, @@ -302,7 +302,6 @@ class BitStream { bits_left{0}, stream_position{0}, bitstream_mode{true} { - static_assert(inbuf_size >= 2, "inbuf size must be at least 2"); static_assert(inbuf_size % 2 == 0, "inbuf size must be even"); } @@ -368,8 +367,8 @@ class BitStream { unsigned int read_4bytes_le() { unsigned int result; - result = this->read_single_byte() << 0; - result |= this->read_single_byte() << 8; + result = this->read_single_byte() << 0; + result |= this->read_single_byte() << 8; result |= this->read_single_byte() << 16; result |= this->read_single_byte() << 24; @@ -404,7 +403,7 @@ class BitStream { * Discards 1 to 16 bits to align the bitstream first. */ void switch_to_bytestream_mode() { - if (! this->bitstream_mode) { + if (!this->bitstream_mode) { return; } @@ -433,4 +432,6 @@ class BitStream { }; -}}} // openage::util::compress +} // namespace compress +} // namespace util +} // namespace openage diff --git a/libopenage/util/compress/lzxd.h b/libopenage/util/compress/lzxd.h index b3607a5228..4c9dbea30e 100644 --- a/libopenage/util/compress/lzxd.h +++ b/libopenage/util/compress/lzxd.h @@ -1,4 +1,4 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -54,8 +54,8 @@ class OAAPI LZXDecompressor { * resets, such as in CAB LZX streams. */ LZXDecompressor(read_callback_t read_callback, - unsigned int window_bits=21, - unsigned int reset_interval=0); + unsigned int window_bits = 21, + unsigned int reset_interval = 0); /** * Frees the internally-allocated LZXDStream object. @@ -83,8 +83,10 @@ class OAAPI LZXDecompressor { LZXDecompressor(const LZXDecompressor &other) = delete; LZXDecompressor(LZXDecompressor &&other) = delete; - LZXDecompressor &operator =(const LZXDecompressor &other) = delete; - LZXDecompressor &operator =(LZXDecompressor &&other) = delete; + LZXDecompressor &operator=(const LZXDecompressor &other) = delete; + LZXDecompressor &operator=(LZXDecompressor &&other) = delete; }; -}}} // openage::util::compress +} // namespace compress +} // namespace util +} // namespace openage diff --git a/libopenage/util/constexpr.h b/libopenage/util/constexpr.h index 451ba9701a..20b8f15c4a 100644 --- a/libopenage/util/constexpr.h +++ b/libopenage/util/constexpr.h @@ -1,4 +1,4 @@ -// Copyright 2014-2018 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -15,7 +15,7 @@ namespace openage::util::constexpr_ { * Returns true IFF the string literals have equal content. */ constexpr bool streq(const char *a, const char *b) { - for (;*a == *b; ++a, ++b) { + for (; *a == *b; ++a, ++b) { if (*a == '\0') { return true; } @@ -60,10 +60,12 @@ constexpr truncated_string_literal get_prefix(const char *str, const char *suffi if (strlen(str) < strlen(suffix)) { // suffix is longer than str throw false; - } else if (streq(str + (strlen(str) - strlen(suffix)), suffix)) { + } + else if (streq(str + (strlen(str) - strlen(suffix)), suffix)) { // str ends with suffix return truncated_string_literal{str, strlen(str) - strlen(suffix)}; - } else { + } + else { throw false; } } @@ -97,7 +99,8 @@ constexpr bool has_prefix(const char *str, const truncated_string_literal prefix constexpr const char *strip_prefix(const char *str, const truncated_string_literal prefix) { if (has_prefix(str, prefix)) { return str + prefix.length; - } else { + } + else { return str; } } diff --git a/libopenage/util/constinit_vector.h b/libopenage/util/constinit_vector.h index e0f2be48a6..0b49506296 100644 --- a/libopenage/util/constinit_vector.h +++ b/libopenage/util/constinit_vector.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -20,10 +20,11 @@ namespace util { * be pretty uncommon; there generally are better ways of guaranteeing dynamic * initialization order, such as static function variables). */ -template +template class ConstInitVector { public: - constexpr ConstInitVector() noexcept : data{nullptr}, capacity{16}, count{0} {} + constexpr ConstInitVector() noexcept : + data{nullptr}, capacity{16}, count{0} {} ~ConstInitVector() { @@ -43,8 +44,8 @@ class ConstInitVector { */ ConstInitVector(const ConstInitVector &other) = delete; ConstInitVector(ConstInitVector &&other) = delete; - ConstInitVector &operator =(const ConstInitVector &other) = delete; - ConstInitVector &operator =(ConstInitVector &&other) = delete; + ConstInitVector &operator=(const ConstInitVector &other) = delete; + ConstInitVector &operator=(ConstInitVector &&other) = delete; void push_back(const T &val) { @@ -60,7 +61,7 @@ class ConstInitVector { size_t newcapacity = capacity * 2; T *newdata = alloc.allocate(newcapacity); for (size_t i = 0; i < this->capacity; i++) { - new(static_cast(&newdata[i])) T(std::move_if_noexcept(this->data[i])); + new (static_cast(&newdata[i])) T(std::move_if_noexcept(this->data[i])); (this->data[i]).~T(); } alloc.deallocate(this->data, this->capacity); @@ -69,7 +70,7 @@ class ConstInitVector { } // add val at the end. - new(static_cast(&this->data[this->count])) T(val); + new (static_cast(&this->data[this->count])) T(val); this->count += 1; } @@ -78,7 +79,7 @@ class ConstInitVector { * The returned reference is invalid if n >= this->size(). * It may be invalidated by a call to push_back(). */ - const T &operator[] (size_t idx) const { + const T &operator[](size_t idx) const { return this->data[idx]; } @@ -97,4 +98,5 @@ class ConstInitVector { }; -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/enum.h b/libopenage/util/enum.h index 07e07256a3..394db95b93 100644 --- a/libopenage/util/enum.h +++ b/libopenage/util/enum.h @@ -1,4 +1,4 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -43,45 +43,46 @@ namespace util { * const char *name * NumericType numeric */ -template +template class OAAPI EnumValue { public: - constexpr EnumValue(const char *value_name, NumericType numeric_value): name(value_name), numeric(numeric_value) {} + constexpr EnumValue(const char *value_name, NumericType numeric_value) : + name(value_name), numeric(numeric_value) {} // enum values cannot be copied EnumValue(const EnumValue &other) = delete; - EnumValue &operator =(const EnumValue &other) = delete; + EnumValue &operator=(const EnumValue &other) = delete; // an explicit deletion of the implicitly defined copy constructor and assignment operator // will implicitly delete the implicitly defined move constructor and assignment operator. // yay for C++ // enum values are equal if the pointers are equal. - constexpr bool operator ==(const DerivedType &other) const { + constexpr bool operator==(const DerivedType &other) const { return (this == &other); } - constexpr bool operator !=(const DerivedType &other) const { + constexpr bool operator!=(const DerivedType &other) const { return !(*this == other); } - constexpr bool operator <=(const DerivedType &other) const { + constexpr bool operator<=(const DerivedType &other) const { return this->numeric <= other.numeric; } - constexpr bool operator <(const DerivedType &other) const { + constexpr bool operator<(const DerivedType &other) const { return this->numeric < other.numeric; } - constexpr bool operator >=(const DerivedType &other) const { + constexpr bool operator>=(const DerivedType &other) const { return this->numeric >= other.numeric; } - constexpr bool operator >(const DerivedType &other) const { + constexpr bool operator>(const DerivedType &other) const { return this->numeric > other.numeric; } - friend std::ostream &operator <<(std::ostream &os, const DerivedType &arg) { + friend std::ostream &operator<<(std::ostream &os, const DerivedType &arg) { os << util::typestring() << "::" << arg.name; return os; } @@ -122,49 +123,50 @@ class OAAPI EnumValue { * bool operator <=(Enum[DerivedType] other) except + * bool operator >=(Enum[DerivedType] other) except + */ -template +template class OAAPI Enum { using this_type = Enum; public: // disallow the empty constructor to ensure that value is always a valid pointer. constexpr Enum() = delete; - constexpr Enum(const DerivedType &value) : value{&value} {} + constexpr Enum(const DerivedType &value) : + value{&value} {} constexpr explicit operator const DerivedType &() const { return *this->value; } - constexpr Enum &operator =(const DerivedType &value) { + constexpr Enum &operator=(const DerivedType &value) { this->value = &value; return *this; } - constexpr const DerivedType *operator ->() const { + constexpr const DerivedType *operator->() const { return this->value; } - constexpr bool operator ==(const this_type &other) const { + constexpr bool operator==(const this_type &other) const { return *this->value == *other.value; } - constexpr bool operator !=(const this_type &other) const { + constexpr bool operator!=(const this_type &other) const { return *this->value != *other.value; } - constexpr bool operator <=(const this_type &other) const { + constexpr bool operator<=(const this_type &other) const { return *this->value <= *other.value; } - constexpr bool operator <(const this_type &other) const { + constexpr bool operator<(const this_type &other) const { return *this->value < *other.value; } - constexpr bool operator >=(const this_type &other) const { + constexpr bool operator>=(const this_type &other) const { return *this->value >= *other.value; } - constexpr bool operator >(const this_type &other) const { + constexpr bool operator>(const this_type &other) const { return *this->value > *other.value; } @@ -172,7 +174,7 @@ class OAAPI Enum { return *this->value; } - friend std::ostream &operator <<(std::ostream &os, const this_type &arg) { + friend std::ostream &operator<<(std::ostream &os, const this_type &arg) { os << *arg.value; return os; } @@ -181,4 +183,5 @@ class OAAPI Enum { const DerivedType *value; }; -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/enum_test.h b/libopenage/util/enum_test.h index 6f6409d5a6..0cc9ce0cab 100644 --- a/libopenage/util/enum_test.h +++ b/libopenage/util/enum_test.h @@ -1,4 +1,4 @@ -// Copyright 2015-2018 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -48,4 +48,6 @@ struct OAAPI testenum : Enum { static constexpr testenum_value bar{{"bar", 2}, "barrrrrrrrrrrrrrrrr"}; }; -}}} // openage::util::tests +} // namespace tests +} // namespace util +} // namespace openage diff --git a/libopenage/util/externalsstream.h b/libopenage/util/externalsstream.h index 8f30c3d3dd..3211b0c30a 100644 --- a/libopenage/util/externalsstream.h +++ b/libopenage/util/externalsstream.h @@ -1,4 +1,4 @@ -// Copyright 2015-2017 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include #include @@ -43,8 +43,7 @@ class ExternalOStringStream : public std::ostream { /** * Creates a stream without a valid accumulator. */ - explicit ExternalOStringStream() - : + explicit ExternalOStringStream() : std::ostream{&this->buf} {} /** @@ -60,4 +59,5 @@ class ExternalOStringStream : public std::ostream { }; -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/fds.h b/libopenage/util/fds.h index fe5bb08abf..251ad0aec7 100644 --- a/libopenage/util/fds.h +++ b/libopenage/util/fds.h @@ -1,4 +1,4 @@ -// Copyright 2014-2019 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -89,4 +89,5 @@ class FD { #endif }; -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/file.h b/libopenage/util/file.h index 217bd0f943..6724b5f795 100644 --- a/libopenage/util/file.h +++ b/libopenage/util/file.h @@ -1,4 +1,4 @@ -// Copyright 2013-2019 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #pragma once @@ -56,7 +56,7 @@ class OAAPI File { /** * Open a filesystem path. */ - File(const std::string &path, mode_t mode=mode_t::R); + File(const std::string &path, mode_t mode = mode_t::R); /** * Create a file from an already created filelike. @@ -77,7 +77,7 @@ class OAAPI File { * Read data from the file and return a string. * If max is negative, return the full remaining file. */ - std::string read(ssize_t max=-1); + std::string read(ssize_t max = -1); /** * Read data from the file into a buffer, @@ -87,11 +87,11 @@ class OAAPI File { * * Returns the number of bytes that were read. */ - size_t read_to(void *buf, ssize_t max=-1); + size_t read_to(void *buf, ssize_t max = -1); bool readable(); void write(const std::string &data); bool writable(); - void seek(ssize_t offset, seek_t how=seek_t::SET); + void seek(ssize_t offset, seek_t how = seek_t::SET); bool seekable(); size_t tell(); void close(); @@ -104,7 +104,8 @@ class OAAPI File { protected: std::shared_ptr filelike; - friend std::ostream &operator <<(std::ostream &stream, const File &file); + friend std::ostream &operator<<(std::ostream &stream, const File &file); }; -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/filelike/filelike.h b/libopenage/util/filelike/filelike.h index 370c1eb99c..ed1bc8b2f4 100644 --- a/libopenage/util/filelike/filelike.h +++ b/libopenage/util/filelike/filelike.h @@ -1,4 +1,4 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -46,9 +46,9 @@ class OAAPI FileLike { * Don't change the numbers, they're used from Cython. */ enum class seek_t : int { - SET = 0, //!< offset from file beginning - CUR = 1, //!< offset from current position - END = 2 //!< offset from file end + SET = 0, //!< offset from file beginning + CUR = 1, //!< offset from current position + END = 2 //!< offset from file end }; /** @@ -70,8 +70,8 @@ class OAAPI FileLike { virtual ~FileLike() = default; - virtual std::string read(ssize_t max=-1) = 0; - virtual size_t read_to(void *buf, ssize_t max=-1) = 0; + virtual std::string read(ssize_t max = -1) = 0; + virtual size_t read_to(void *buf, ssize_t max = -1) = 0; virtual bool readable() = 0; @@ -79,7 +79,7 @@ class OAAPI FileLike { virtual bool writable() = 0; - virtual void seek(ssize_t offset, seek_t how=seek_t::SET) = 0; + virtual void seek(ssize_t offset, seek_t how = seek_t::SET) = 0; virtual bool seekable() = 0; virtual size_t tell() = 0; virtual void close() = 0; @@ -93,4 +93,6 @@ class OAAPI FileLike { }; -}}} // openage::util::filelike +} // namespace filelike +} // namespace util +} // namespace openage diff --git a/libopenage/util/filelike/native.h b/libopenage/util/filelike/native.h index d245c92f2c..60844db7ff 100644 --- a/libopenage/util/filelike/native.h +++ b/libopenage/util/filelike/native.h @@ -1,4 +1,4 @@ -// Copyright 2017-2017 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -19,7 +19,7 @@ namespace filelike { */ class Native : public FileLike { public: - Native(const std::string &path, mode_t mode=mode_t::R); + Native(const std::string &path, mode_t mode = mode_t::R); virtual ~Native(); std::string read(ssize_t max) override; @@ -31,7 +31,7 @@ class Native : public FileLike { bool writable() override; - void seek(ssize_t offset, seek_t how=seek_t::SET) override; + void seek(ssize_t offset, seek_t how = seek_t::SET) override; bool seekable() override; size_t tell() override; void close() override; @@ -48,4 +48,6 @@ class Native : public FileLike { std::fstream file; }; -}}} // openage::util::filelike +} // namespace filelike +} // namespace util +} // namespace openage diff --git a/libopenage/util/filelike/python.h b/libopenage/util/filelike/python.h index 91ba161c92..02e397092e 100644 --- a/libopenage/util/filelike/python.h +++ b/libopenage/util/filelike/python.h @@ -1,4 +1,4 @@ -// Copyright 2017-2019 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -41,7 +41,7 @@ class Python : public FileLike { bool writable() override; - void seek(ssize_t offset, seek_t how=seek_t::SET) override; + void seek(ssize_t offset, seek_t how = seek_t::SET) override; bool seekable() override; size_t tell() override; void close() override; @@ -95,4 +95,6 @@ extern OAAPI pyinterface::PyIfFunc pyx_file_flush; extern OAAPI pyinterface::PyIfFunc pyx_file_size; -}}} // openage::util::filelike +} // namespace filelike +} // namespace util +} // namespace openage diff --git a/libopenage/util/fixed_point.h b/libopenage/util/fixed_point.h index 39717540bf..7558ac9cb8 100644 --- a/libopenage/util/fixed_point.h +++ b/libopenage/util/fixed_point.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -261,13 +261,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))); diff --git a/libopenage/util/fps.h b/libopenage/util/fps.h index 45959126c3..d853047c42 100644 --- a/libopenage/util/fps.h +++ b/libopenage/util/fps.h @@ -1,4 +1,4 @@ -// Copyright 2013-2019 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #pragma once @@ -34,4 +34,5 @@ class FrameCounter { Timer frame_timer; }; -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/fslike/directory.h b/libopenage/util/fslike/directory.h index 838caf6288..2872b9554b 100644 --- a/libopenage/util/fslike/directory.h +++ b/libopenage/util/fslike/directory.h @@ -1,4 +1,4 @@ -// Copyright 2017-2019 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -23,7 +23,7 @@ namespace fslike { */ class Directory : public FSLike { public: - Directory(std::string basepath, bool create_if_missing=false); + Directory(std::string basepath, bool create_if_missing = false); bool is_file(const Path::parts_t &parts) override; bool is_dir(const Path::parts_t &parts) override; @@ -60,4 +60,6 @@ class Directory : public FSLike { std::string basepath; }; -}}} // openage::util::fslike +} // namespace fslike +} // namespace util +} // namespace openage diff --git a/libopenage/util/fslike/fslike.h b/libopenage/util/fslike/fslike.h index 5973782f8c..f9b36631e0 100644 --- a/libopenage/util/fslike/fslike.h +++ b/libopenage/util/fslike/fslike.h @@ -1,4 +1,4 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -104,4 +104,6 @@ class OAAPI FSLike : public std::enable_shared_from_this { virtual std::ostream &repr(std::ostream &) = 0; }; -}}} // openage::util::fslike +} // namespace fslike +} // namespace util +} // namespace openage diff --git a/libopenage/util/fslike/native.h b/libopenage/util/fslike/native.h index 75f4936681..5a343b54f5 100644 --- a/libopenage/util/fslike/native.h +++ b/libopenage/util/fslike/native.h @@ -1,4 +1,4 @@ -// Copyright 2017-2017 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -13,4 +13,6 @@ namespace fslike { constexpr char PATHSEP = '/'; -}}} // openage::util::fslike +} // namespace fslike +} // namespace util +} // namespace openage diff --git a/libopenage/util/fslike/python.h b/libopenage/util/fslike/python.h index afc8d34f90..483c2e40ce 100644 --- a/libopenage/util/fslike/python.h +++ b/libopenage/util/fslike/python.h @@ -1,4 +1,4 @@ -// Copyright 2017-2019 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -76,65 +76,67 @@ class Python : public FSLike { // pxd: PyIfFunc2[bool, PyObjectPtr, const vector[string]] pyx_fs_is_file -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_is_file; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_is_file; // pxd: PyIfFunc2[bool, PyObjectPtr, const vector[string]] pyx_fs_is_dir -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_is_dir; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_is_dir; // pxd: PyIfFunc2[bool, PyObjectPtr, const vector[string]] pyx_fs_writable -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_writable; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_writable; // pxd: PyIfFunc2[vector[string], PyObjectPtr, const vector[string]] pyx_fs_list -extern OAAPI pyinterface::PyIfFunc, PyObject *, const std::vector&> pyx_fs_list; +extern OAAPI pyinterface::PyIfFunc, PyObject *, const std::vector &> pyx_fs_list; // pxd: PyIfFunc2[bool, PyObjectPtr, const vector[string]] pyx_fs_mkdirs -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_mkdirs; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_mkdirs; // pxd: PyIfFunc2[File, PyObjectPtr, const vector[string]] pyx_fs_open_r -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_open_r; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_open_r; // pxd: PyIfFunc2[File, PyObjectPtr, const vector[string]] pyx_fs_open_w -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_open_w; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_open_w; // pxd: PyIfFunc2[File, PyObjectPtr, const vector[string]] pyx_fs_open_rw -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_open_rw; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_open_rw; // pxd: PyIfFunc2[File, PyObjectPtr, const vector[string]] pyx_fs_open_a -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_open_a; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_open_a; // pxd: PyIfFunc2[File, PyObjectPtr, const vector[string]] pyx_fs_open_ar -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_open_ar; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_open_ar; // pxd: PyIfFunc2[Path, PyObjectPtr, const vector[string]] pyx_fs_resolve_r -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_resolve_r; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_resolve_r; // pxd: PyIfFunc2[Path, PyObjectPtr, const vector[string]] pyx_fs_resolve_w -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_resolve_w; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_resolve_w; // pxd: PyIfFunc2[PyObjectRef, PyObjectPtr, const vector[string]] pyx_fs_get_native_path -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_get_native_path; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_get_native_path; // pxd: PyIfFunc3[bool, PyObjectPtr, const vector[string], const vector[string]] pyx_fs_rename -extern OAAPI pyinterface::PyIfFunc&, const std::vector&> pyx_fs_rename; +extern OAAPI pyinterface::PyIfFunc &, const std::vector &> pyx_fs_rename; // pxd: PyIfFunc2[bool, PyObjectPtr, const vector[string]] pyx_fs_rmdir -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_rmdir; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_rmdir; // pxd: PyIfFunc2[bool, PyObjectPtr, const vector[string]] pyx_fs_touch -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_touch; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_touch; // pxd: PyIfFunc2[bool, PyObjectPtr, const vector[string]] pyx_fs_unlink -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_unlink; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_unlink; // pxd: PyIfFunc2[int, PyObjectPtr, const vector[string]] pyx_fs_get_mtime -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_get_mtime; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_get_mtime; // pxd: PyIfFunc2[uint64_t, PyObjectPtr, const vector[string]] pyx_fs_get_filesize -extern OAAPI pyinterface::PyIfFunc&> pyx_fs_get_filesize; +extern OAAPI pyinterface::PyIfFunc &> pyx_fs_get_filesize; // pxd: PyIfFunc1[bool, PyObjectPtr] pyx_fs_is_fslike_directory extern OAAPI pyinterface::PyIfFunc pyx_fs_is_fslike_directory; -}}} // openage::util::fslike +} // namespace fslike +} // namespace util +} // namespace openage diff --git a/libopenage/util/hash.h b/libopenage/util/hash.h index 2e13b6e2a7..114f7a9b82 100644 --- a/libopenage/util/hash.h +++ b/libopenage/util/hash.h @@ -1,10 +1,10 @@ -// Copyright 2015-2021 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once +#include #include #include -#include #include #include @@ -17,7 +17,7 @@ namespace util { template size_t type_hash() { - return std::hash()(std::type_index(typeid(T))); + return std::hash()(std::type_index(typeid(T))); } /** @@ -29,7 +29,6 @@ size_t type_hash() { size_t hash_combine(size_t hash1, size_t hash2); - /** \class Siphash * Contains a Siphash implementration. * @@ -49,7 +48,7 @@ class Siphash { * @param k Key to use with this hasher. * @return Reference to itself, for method chaining. */ - Siphash& set_key(std::array key); + Siphash &set_key(std::array key); /** @@ -81,4 +80,5 @@ class Siphash { }; -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/init.h b/libopenage/util/init.h index 3a3a80a7ae..1251e9ab85 100644 --- a/libopenage/util/init.h +++ b/libopenage/util/init.h @@ -1,4 +1,4 @@ -// Copyright 2015-2018 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -23,7 +23,7 @@ namespace util { */ class OnInit { public: - explicit OnInit(std::function code) { + explicit OnInit(std::function code) { code(); } @@ -34,8 +34,8 @@ class OnInit { OnInit(const OnInit &) = delete; OnInit(OnInit &&) = delete; - OnInit &operator =(const OnInit &) = delete; - OnInit &operator =(OnInit &&) = delete; + OnInit &operator=(const OnInit &) = delete; + OnInit &operator=(OnInit &&) = delete; }; @@ -47,8 +47,7 @@ class OnInit { */ class OnDeInit { public: - explicit OnDeInit(std::function code) - : + explicit OnDeInit(std::function code) : code{code} {} @@ -57,15 +56,16 @@ class OnDeInit { } private: - std::function code; + std::function code; // nope. OnDeInit(const OnDeInit &) = delete; OnDeInit(OnDeInit &&) = delete; - OnDeInit &operator =(const OnDeInit &) = delete; - OnDeInit &operator =(OnDeInit &&) = delete; + OnDeInit &operator=(const OnDeInit &) = delete; + OnDeInit &operator=(OnDeInit &&) = delete; }; -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/language.h b/libopenage/util/language.h index 9d0e5a887e..889cdbd001 100644 --- a/libopenage/util/language.h +++ b/libopenage/util/language.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -16,16 +16,16 @@ namespace util { * Simple wrapper type that contains a function pointer. * Needed as workaround for http://stackoverflow.com/questions/31040075 */ -template +template class FunctionPtr { public: /** * Implicit conversion from raw type. */ - FunctionPtr(ReturnType (*ptr)(ArgTypes ...)) : + FunctionPtr(ReturnType (*ptr)(ArgTypes...)) : ptr{ptr} {} - ReturnType (*ptr)(ArgTypes ...); + ReturnType (*ptr)(ArgTypes...); }; @@ -34,8 +34,9 @@ class FunctionPtr { * readers, compilers and linters that you are, in fact, ignoring the * function's return value on purpose. */ -template +template inline void ignore_result(T /* unused result */) {} -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/macro/concat.h b/libopenage/util/macro/concat.h index 90c069b3d2..b8f7b746ca 100644 --- a/libopenage/util/macro/concat.h +++ b/libopenage/util/macro/concat.h @@ -1,23 +1,24 @@ -// Copyright 2013-2017 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #pragma once -#define CONCAT_1(OP, X) (X) -#define CONCAT_2(OP, X, Y) (X) OP (Y) -#define CONCAT_3(OP, X, Y, Z) (X) OP (Y) OP (Z) +#define CONCAT_1(OP, X) (X) +#define CONCAT_2(OP, X, Y) (X) OP(Y) +#define CONCAT_3(OP, X, Y, Z) (X) OP(Y) OP(Z) #define CONCAT_N(_1, _2, _3, NAME, ...) NAME #ifdef _MSC_VER #define CONCAT_COUNT_ARGS_IMPL(args) CONCAT_N args -#define CONCAT_COUNT_ARGS(...) CONCAT_COUNT_ARGS_IMPL((__VA_ARGS__, 3, 2, 1)) +#define CONCAT_COUNT_ARGS(...) CONCAT_COUNT_ARGS_IMPL((__VA_ARGS__, 3, 2, 1)) #define CONCAT_HELPER2(count) CONCAT_##count #define CONCAT_HELPER1(count) CONCAT_HELPER2(count) -#define CONCAT_HELPER(count) CONCAT_HELPER1(count) +#define CONCAT_HELPER(count) CONCAT_HELPER1(count) #define CONCAT_GLUE(x, y) x y #define CONCAT(OP, ...) CONCAT_GLUE(CONCAT_HELPER(CONCAT_COUNT_ARGS(__VA_ARGS__)), (OP, __VA_ARGS__)) #else #define CONCAT(OP, ...) CONCAT_N(__VA_ARGS__, \ - CONCAT_3, CONCAT_2, CONCAT_1 \ - ) (OP, __VA_ARGS__) + CONCAT_3, \ + CONCAT_2, \ + CONCAT_1)(OP, __VA_ARGS__) #endif diff --git a/libopenage/util/macro/loop.h b/libopenage/util/macro/loop.h index 0678afb960..5b8dc59e8e 100644 --- a/libopenage/util/macro/loop.h +++ b/libopenage/util/macro/loop.h @@ -1,23 +1,24 @@ -// Copyright 2013-2017 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #pragma once -#define LOOP_1(MACRO, X) MACRO(X) -#define LOOP_2(MACRO, X, Y) MACRO(X), MACRO(Y) +#define LOOP_1(MACRO, X) MACRO(X) +#define LOOP_2(MACRO, X, Y) MACRO(X), MACRO(Y) #define LOOP_3(MACRO, X, Y, Z) MACRO(X), MACRO(Y), MACRO(Z) #define LOOP_N(_1, _2, _3, NAME, ...) NAME #ifdef _MSC_VER #define LOOP_COUNT_ARGS_IMPL(args) LOOP_N args -#define LOOP_COUNT_ARGS(...) LOOP_COUNT_ARGS_IMPL((__VA_ARGS__, 3, 2, 1)) +#define LOOP_COUNT_ARGS(...) LOOP_COUNT_ARGS_IMPL((__VA_ARGS__, 3, 2, 1)) #define LOOP_HELPER2(count) LOOP_##count #define LOOP_HELPER1(count) LOOP_HELPER2(count) -#define LOOP_HELPER(count) LOOP_HELPER1(count) +#define LOOP_HELPER(count) LOOP_HELPER1(count) #define LOOP_GLUE(x, y) x y #define LOOP(MACRO, ...) LOOP_GLUE(LOOP_HELPER(LOOP_COUNT_ARGS(__VA_ARGS__)), (MACRO, __VA_ARGS__)) #else #define LOOP(MACRO, ...) LOOP_N(__VA_ARGS__, \ - LOOP_3, LOOP_2, LOOP_1 \ - ) (MACRO, __VA_ARGS__) + LOOP_3, \ + LOOP_2, \ + LOOP_1)(MACRO, __VA_ARGS__) #endif diff --git a/libopenage/util/math.h b/libopenage/util/math.h index f31b650a1c..afbdc81b8a 100644 --- a/libopenage/util/math.h +++ b/libopenage/util/math.h @@ -1,4 +1,4 @@ -// Copyright 2015-2018 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -7,15 +7,16 @@ namespace openage { namespace math { -template +template T square(T arg) { return arg * arg; } -template +template T hypot3(T x, T y, T z) { return sqrt(x * x + y * y + z * z); } -}} // openage::util +} // namespace math +} // namespace openage diff --git a/libopenage/util/math_constants.h b/libopenage/util/math_constants.h index fe5172549e..97a402b49c 100644 --- a/libopenage/util/math_constants.h +++ b/libopenage/util/math_constants.h @@ -1,14 +1,16 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once -#include #include +#include namespace openage { namespace math { // TODO: use std::numbers instead of these where appropriate +// clang-format off +// keep equal signs aligned for readability constexpr double E = std::numbers::e; //!< e constexpr double LOG2E = std::numbers::log2e; //!< log_2 e constexpr double LOG10E = std::numbers::log10e; //!< log_10 e @@ -25,9 +27,11 @@ constexpr double DEGSPERRAD = 0.017453292519943295769; //!< tau/360 constexpr double RADSPERDEG = 57.29577951308232087679; //!< 360/tau constexpr double SQRT_2 = std::numbers::sqrt2; //!< sqrt(2) constexpr double INV_SQRT_2 = 0.707106781186547524401; //!< 1/sqrt(2) +// clang-format on constexpr unsigned int UINT_INF = std::numeric_limits::max(); constexpr int INT_INF = std::numeric_limits::max(); constexpr double DOUBLE_INF = std::numeric_limits::max(); -}} // openage::math +} // namespace math +} // namespace openage diff --git a/libopenage/util/misc.h b/libopenage/util/misc.h index 9ea6591567..9fdfcc9ef9 100644 --- a/libopenage/util/misc.h +++ b/libopenage/util/misc.h @@ -1,14 +1,14 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #pragma once #include -#include #include #include +#include +#include #include #include -#include #include "../error/error.h" #include "compiler.h" @@ -28,13 +28,13 @@ extern std::string empty_string; * modulo operation that guarantees to return positive values. */ template -constexpr -T mod(T x, T m) { +constexpr T mod(T x, T m) { T r = x % m; if (r < 0) { return r + m; - } else { + } + else { return r; } } @@ -43,13 +43,13 @@ T mod(T x, T m) { * compiletime defined modulo function. */ template -constexpr -T mod(T x) { +constexpr T mod(T x) { T r = x % modulo; if (r < 0) { return r + modulo; - } else { + } + else { return r; } } @@ -59,10 +59,9 @@ T mod(T x) { * compiletime defined rotate left function */ template -constexpr -T rol(T x) { - static_assert(sizeof(T)*CHAR_BIT > amount && amount > 0, "invalid rotation amount"); - return (x << amount) | (x >> (sizeof(T)*CHAR_BIT - amount)); +constexpr T rol(T x) { + static_assert(sizeof(T) * CHAR_BIT > amount && amount > 0, "invalid rotation amount"); + return (x << amount) | (x >> (sizeof(T) * CHAR_BIT - amount)); } @@ -71,8 +70,7 @@ T rol(T x) { * which always rounds to -inf */ template -constexpr -inline T div(T x, T m) { +constexpr inline T div(T x, T m) { return (x - mod(x, m)) / m; } @@ -84,7 +82,7 @@ inline T div(T x, T m) { */ template struct less { - bool operator ()(const T x, const T y) const { + bool operator()(const T x, const T y) const { return *x < *y; } }; @@ -108,7 +106,7 @@ static constexpr size_t uint64_s = 8; * @return Input data as a 64 bit number. */ inline uint64_t -array8_to_uint64(const uint8_t *start, size_t count, bool big_endian=false) { +array8_to_uint64(const uint8_t *start, size_t count, bool big_endian = false) { if (count > uint64_s) { throw Error(MSG(err) << "Tried to copy more than " << uint64_s << " bytes"); } @@ -142,7 +140,7 @@ array8_to_uint64(const uint8_t *start, size_t count, bool big_endian=false) { * @return Input data as a 8 bit number array. */ inline std::vector -uint64_to_array8(const uint64_t value, bool big_endian=false) { +uint64_to_array8(const uint64_t value, bool big_endian = false) { std::vector result(uint64_s, 0); if (big_endian) { @@ -187,7 +185,7 @@ inline constexpr size_t array64_size(size_t count) { * @return Input data as a 64 bit number vector. */ inline std::vector -array8_to_array64(const uint8_t *start, size_t count, bool big_endian=false) { +array8_to_array64(const uint8_t *start, size_t count, bool big_endian = false) { size_t size{array64_size(count)}; std::vector result(size, 0); @@ -197,8 +195,7 @@ array8_to_array64(const uint8_t *start, size_t count, bool big_endian=false) { result[i] = array8_to_uint64( start + (i * uint64_s), std::min(rem_bytes, uint64_s), - big_endian - ); + big_endian); } return result; } @@ -219,7 +216,7 @@ array8_to_array64(const uint8_t *start, size_t count, bool big_endian=false) { * @return Input data as a 8 bit number vector. */ inline std::vector -array64_to_array8(const uint64_t *start, size_t count, bool big_endian=false) { +array64_to_array8(const uint64_t *start, size_t count, bool big_endian = false) { std::vector result; result.reserve(count * uint64_s); @@ -274,7 +271,8 @@ void vector_remove_swap_end(std::vector &vec, size_t idx) { else if (idx < vec.size()) { std::swap(vec[idx], vec.back()); vec.pop_back(); - } else { + } + else { return; } } @@ -287,9 +285,8 @@ void vector_remove_swap_end(std::vector &vec, size_t idx) { */ template struct SharedPtrLess { - bool operator ()(const std::shared_ptr &left, - const std::shared_ptr &right) { - + bool operator()(const std::shared_ptr &left, + const std::shared_ptr &right) { if (not left or not right) [[unlikely]] { return false; } diff --git a/libopenage/util/os.h b/libopenage/util/os.h index 305fdd1de4..4573c5e10d 100644 --- a/libopenage/util/os.h +++ b/libopenage/util/os.h @@ -1,4 +1,4 @@ -// Copyright 2014-2016 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -24,7 +24,7 @@ std::string self_exec_filename(); /** * tries to xdg-open the file */ -int execute_file(const char *path, bool background=true); +int execute_file(const char *path, bool background = true); } // namespace os } // namespace openage diff --git a/libopenage/util/pty.h b/libopenage/util/pty.h index cb071af71a..0ae9e72d23 100644 --- a/libopenage/util/pty.h +++ b/libopenage/util/pty.h @@ -1,16 +1,16 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once #ifdef __APPLE__ -# include +#include #elif defined(__FreeBSD__) -# include -# include -# include -# include +#include +#include +#include +#include #elif _WIN32 // TODO not yet implemented #else -# include +#include #endif diff --git a/libopenage/util/quaternion.h b/libopenage/util/quaternion.h index 671d2a2410..b0761028ad 100644 --- a/libopenage/util/quaternion.h +++ b/libopenage/util/quaternion.h @@ -1,11 +1,11 @@ -// Copyright 2017-2018 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 "matrix.h" #include "math_constants.h" +#include "matrix.h" #include "vector.h" #include "../error/error.h" @@ -35,15 +35,13 @@ class Quaternion { static constexpr T default_eps = 1e-4; - Quaternion(T w, T x, T y, T z) - : + Quaternion(T w, T x, T y, T z) : w{w}, x{x}, y{y}, z{z} {} /** * Create a identity quaternion. */ - Quaternion() - : + Quaternion() : w{1}, x{0}, y{0}, z{0} {} /** @@ -62,9 +60,8 @@ class Quaternion { * max diagonal entry <=> max (|x|, |y|, |z|) * which is larger than |w| and >= 1/2 */ - template + template Quaternion(const Matrix &mat) { - static_assert(N == 3 or N == 4, "only 3 and 4 dimensional matrices can be converted to a quaternion!"); T trace = mat.trace(); @@ -72,7 +69,8 @@ class Quaternion { if (N == 4) { trace_cmp -= mat[3][3]; - } else { + } + else { trace += 1.0; } @@ -111,14 +109,15 @@ class Quaternion { if (N == 4) { trace_ordered += mat[3][3]; - } else { + } + else { trace_ordered += 1.0; } T trace_root = std::sqrt(trace_ordered); - *ptrs[n0] = trace_root * 0.5; // = w - trace_root = 0.5 / trace_root; // = 1/4w + *ptrs[n0] = trace_root * 0.5; // = w + trace_root = 0.5 / trace_root; // = 1/4w *ptrs[n1] = (mat[n0][n1] + mat[n1][n0]) * trace_root; *ptrs[n2] = (mat[n2][n0] + mat[n0][n2]) * trace_root; @@ -134,8 +133,8 @@ class Quaternion { Quaternion(const this_type &other) = default; Quaternion(this_type &&other) = default; - Quaternion &operator =(const this_type &other) = default; - Quaternion &operator =(this_type &&other) = default; + Quaternion &operator=(const this_type &other) = default; + Quaternion &operator=(this_type &&other) = default; virtual ~Quaternion() = default; @@ -148,8 +147,7 @@ class Quaternion { std::cos(rot), axis[0] * std::sin(rot), axis[1] * std::sin(rot), - axis[2] * std::sin(rot) - }; + axis[2] * std::sin(rot)}; return q; } @@ -240,15 +238,15 @@ class Quaternion { /** * Test if the rotation of both quaternions is the same. */ - bool equals(const this_type &other, T eps=default_eps) const { + bool equals(const this_type &other, T eps = default_eps) const { T ori = this->dot(other); - return (1 - (ori*ori)) < eps; + return (1 - (ori * ori)) < eps; } /** * Test if both quaternion store the same numbers. */ - bool equals_number(const this_type &other, T eps=default_eps) const { + bool equals_number(const this_type &other, T eps = default_eps) const { bool result = true; (this->w - other.w) < eps or (result = false); (this->x - other.x) < eps or (result = false); @@ -261,7 +259,7 @@ class Quaternion { * Test rotation equality with another quaternion * with given precision in radians. */ - bool equals_rad(const this_type &other, T rad_eps=default_eps) const { + bool equals_rad(const this_type &other, T rad_eps = default_eps) const { T ori = this->dot(other); T angle = std::acos((2.0 * (ori * ori)) - 1.0); @@ -272,7 +270,7 @@ class Quaternion { * Test rotation equality with another quaternion * with given precision in degree. */ - bool equals_deg(const this_type &other, T deg_eps=default_eps) const { + bool equals_deg(const this_type &other, T deg_eps = default_eps) const { return this->equals_rad(other, deg_eps * math::RADSPERDEG); } @@ -280,27 +278,29 @@ class Quaternion { * Generate the corresponding rotation matrix. */ Matrix3t to_matrix() const { - T x2 = this->x * 2; - T y2 = this->y * 2; - T z2 = this->z * 2; + T x2 = this->x * 2; + T y2 = this->y * 2; + T z2 = this->z * 2; T x2w = x2 * this->w; T y2w = y2 * this->w; T z2w = z2 * this->w; - T x3 = x2 * this->x; + T x3 = x2 * this->x; T y2x = y2 * this->x; T z2x = z2 * this->x; - T y3 = y2 * this->y; + T y3 = y2 * this->y; T z2y = z2 * this->y; - T z3 = z2 * this->z; + T z3 = z2 * this->z; + // clang-format off Matrix3t m{ 1.0 - (y3 + z3), y2x - z2w, z2x + y2w, y2x + z2w, 1.0 - (x3 + z3), z2y - x2w, z2x - y2w, z2y + x2w, 1.0 - (x3 + y3) }; + // clang-format on return m; } @@ -308,7 +308,7 @@ class Quaternion { /** * Transforms a vector by this quaternion. */ - Vector3t operator *(const Vector3t &vec) const { + Vector3t operator*(const Vector3t &vec) const { Vector3t axis{this->x, this->y, this->z}; Vector3t axis_vec_normal = axis.cross_product(vec); @@ -320,7 +320,7 @@ class Quaternion { return vec + axis_vec_normal + axis_vec_inplane; } - const this_type &operator +=(const this_type &other) { + const this_type &operator+=(const this_type &other) { this->w += other.w; this->x += other.x; this->y += other.y; @@ -329,13 +329,13 @@ class Quaternion { return *this; } - this_type operator +(const this_type &other) const { + this_type operator+(const this_type &other) const { this_type q{*this}; q += other; return q; } - const this_type &operator -=(const this_type &other) { + const this_type &operator-=(const this_type &other) { this->w -= other.w; this->x -= other.x; this->y -= other.y; @@ -344,13 +344,13 @@ class Quaternion { return *this; } - this_type operator -(const this_type &other) const { + this_type operator-(const this_type &other) const { this_type q{*this}; q -= other; return q; } - const this_type &operator *=(const T &fac) { + const this_type &operator*=(const T &fac) { this->w *= fac; this->x *= fac; this->y *= fac; @@ -359,21 +359,21 @@ class Quaternion { return *this; } - this_type operator *(const T &fac) const { + this_type operator*(const T &fac) const { this_type q{*this}; q *= fac; return q; } - const this_type &operator *=(const this_type &other) { - T w_new = (this->w * other.w - this->x * other.x - - this->y * other.y - this->z * other.z); - T x_new = (this->w * other.x + this->x * other.w + - this->y * other.z - this->z * other.y); - T y_new = (this->w * other.y - this->x * other.z + - this->y * other.w + this->z * other.x); - T z_new = (this->w * other.z + this->x * other.y - - this->y * other.x + this->z * other.w); + const this_type &operator*=(const this_type &other) { + T w_new = (this->w * other.w - this->x * other.x + - this->y * other.y - this->z * other.z); + T x_new = (this->w * other.x + this->x * other.w + + this->y * other.z - this->z * other.y); + T y_new = (this->w * other.y - this->x * other.z + + this->y * other.w + this->z * other.x); + T z_new = (this->w * other.z + this->x * other.y + - this->y * other.x + this->z * other.w); this->w = w_new; this->x = x_new; @@ -383,13 +383,13 @@ class Quaternion { return *this; } - this_type operator *(const this_type &other) const { + this_type operator*(const this_type &other) const { this_type q{*this}; q *= other; return q; } - const this_type &operator /=(const T &fac) { + const this_type &operator/=(const T &fac) { this->w /= fac; this->x /= fac; this->y /= fac; @@ -398,28 +398,25 @@ class Quaternion { return *this; } - this_type operator /(const T &fac) const { + this_type operator/(const T &fac) const { this_type q{*this}; q /= fac; return q; } - const this_type operator -() const { + const this_type operator-() const { return this_type{-this->w, -this->x, -this->y, -this->z}; } - bool operator ==(const this_type &other) const { - return ((this->w == other.w) and - (this->x == other.x) and - (this->y == other.y) and - (this->z == other.z)); + bool operator==(const this_type &other) const { + return ((this->w == other.w) and (this->x == other.x) and (this->y == other.y) and (this->z == other.z)); } - bool operator !=(const this_type &other) const { - return not (*this == other); + bool operator!=(const this_type &other) const { + return not(*this == other); } - friend std::ostream &operator <<(std::ostream &o, const this_type &q) { + friend std::ostream &operator<<(std::ostream &o, const this_type &q) { o << "Quaternion(" << q.w << ", " << q.x; o << ", " << q.y << ", " << q.z << ")"; return o; @@ -435,4 +432,5 @@ class Quaternion { using Quaternionf = Quaternion; using Quaterniond = Quaternion; -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/signal.h b/libopenage/util/signal.h index 7e589462ac..b84b2deec8 100644 --- a/libopenage/util/signal.h +++ b/libopenage/util/signal.h @@ -1,4 +1,4 @@ -// Copyright 2014-2016 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -6,8 +6,8 @@ // TODO: change these to ifndef __linux ? #ifdef __APPLE__ - typedef void (*sighandler_t)(int); +typedef void (*sighandler_t)(int); #endif #ifdef _WIN32 - typedef void (*sighandler_t)(int); +typedef void (*sighandler_t)(int); #endif diff --git a/libopenage/util/stringformatter.h b/libopenage/util/stringformatter.h index d87142328f..6426efa35b 100644 --- a/libopenage/util/stringformatter.h +++ b/libopenage/util/stringformatter.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -88,15 +88,14 @@ class CachableOSStream { * As an optimization, instead of creating a new ExternalOStringStream object, * CachableOSStream.acquire() is used internally. */ -template +template class StringFormatter { public: /** * @param buffer * All input data is appended to this object. */ - StringFormatter(std::string &output) - : + StringFormatter(std::string &output) : output{&output}, stream_ptr{nullptr} {} @@ -115,11 +114,10 @@ class StringFormatter { : output{other.output}, stream_ptr{other.stream_ptr} { - other.stream_ptr = nullptr; } - StringFormatter &operator =(StringFormatter &&other) noexcept { + StringFormatter &operator=(StringFormatter &&other) noexcept { this->output = other.output; this->stream_ptr = other.stream_ptr; @@ -129,11 +127,11 @@ class StringFormatter { // no copy construction! StringFormatter(const StringFormatter &) = delete; - StringFormatter &operator =(const StringFormatter &) = delete; + StringFormatter &operator=(const StringFormatter &) = delete; // These methods allow usage like an ostream object. - template - ChildType &operator <<(const T &t) { + template + ChildType &operator<<(const T &t) { if (this->should_format()) { this->ensure_stream_obj(); this->stream_ptr->stream << t; @@ -142,7 +140,7 @@ class StringFormatter { } - ChildType &operator <<(std::ios &(*x)(std::ios &)) { + ChildType &operator<<(std::ios &(*x)(std::ios &)) { if (this->should_format()) { this->ensure_stream_obj(); this->stream_ptr->stream << x; @@ -151,7 +149,7 @@ class StringFormatter { } - ChildType &operator <<(std::ostream &(*x)(std::ostream &)) { + ChildType &operator<<(std::ostream &(*x)(std::ostream &)) { if (this->should_format()) { this->ensure_stream_obj(); this->stream_ptr->stream << x; @@ -162,7 +160,7 @@ class StringFormatter { // Optimizations to prevent needless stream-acquiring if just a simple // string is printed. - ChildType &operator <<(const char *s) { + ChildType &operator<<(const char *s) { if (this->should_format()) { this->output->append(s); } @@ -170,7 +168,7 @@ class StringFormatter { } - ChildType &operator <<(const std::string &s) { + ChildType &operator<<(const std::string &s) { if (this->should_format()) { this->output->append(s); } @@ -192,8 +190,8 @@ class StringFormatter { // Allow direct inputting of stuff that's wrapped in the C++11 pointer types. - template - ChildType &operator <<(const std::unique_ptr &ptr) { + template + ChildType &operator<<(const std::unique_ptr &ptr) { if (this->should_format()) { *this << ptr.get(); } @@ -201,8 +199,8 @@ class StringFormatter { } - template - ChildType &operator <<(const std::shared_ptr &ptr) { + template + ChildType &operator<<(const std::shared_ptr &ptr) { if (this->should_format()) { *this << ptr.get(); } @@ -221,8 +219,8 @@ class StringFormatter { } /** - * Returns if formatting should actually occur. - */ + * Returns if formatting should actually occur. + */ virtual bool should_format() const { return true; } @@ -269,14 +267,12 @@ class Formatter : public StringFormatter {}; */ class FString : public StringFormatter { public: - FString() - : + FString() : StringFormatter{this->buffer} {} // allow assignment and construction from std::string. - FString(const std::string &other) - : + FString(const std::string &other) : StringFormatter{this->buffer}, buffer{other} {} @@ -285,12 +281,12 @@ class FString : public StringFormatter { StringFormatter{this->buffer}, buffer{std::move(other)} {} - FString &operator =(const std::string &other) { + FString &operator=(const std::string &other) { this->buffer = other; return *this; } - FString &operator =(std::string &&other) noexcept { + FString &operator=(std::string &&other) noexcept { this->buffer = std::move(other); return *this; } @@ -305,7 +301,7 @@ class FString : public StringFormatter { return this->buffer; } - operator std::string () && { + operator std::string() && { return std::move(this->buffer); } @@ -319,4 +315,5 @@ class FString : public StringFormatter { }; -}} // namespace openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/strings.h b/libopenage/util/strings.h index 029cf57773..4ab90de510 100644 --- a/libopenage/util/strings.h +++ b/libopenage/util/strings.h @@ -1,4 +1,4 @@ -// Copyright 2013-2018 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #pragma once @@ -11,7 +11,7 @@ #include #if defined(__GNUC__) -#define ATTRIBUTE_FORMAT(i, j) __attribute__ ((format (printf, i, j))) +#define ATTRIBUTE_FORMAT(i, j) __attribute__((format(printf, i, j))) #else #define ATTRIBUTE_FORMAT(i, j) #endif @@ -24,14 +24,14 @@ namespace util { * Quick-formatter for floats when working with string streams. * Usage: cout << FormatFloat{1.0, 10}; */ -template +template struct FloatFixed { float value; }; -template -std::ostream &operator <<(std::ostream &os, FloatFixed f) { +template +std::ostream &operator<<(std::ostream &os, FloatFixed f) { static_assert(decimals < 50, "Refusing to print float with >= 50 decimals"); static_assert(w < 70, "Refusing to print float with a width >= 70"); @@ -84,7 +84,7 @@ bool string_matches_pattern(const char *str, const char *pattern); * Split a string at a delimiter, push the result back in an iterator. * Why doesn't the fucking standard library have std::string::split(delimiter)? */ -template +template void split(const std::string &txt, char delimiter, ret_t result) { std::stringstream splitter; splitter.str(txt); @@ -113,6 +113,8 @@ std::vector split(const std::string &txt, char delim); * to literal X, including the deliminiter. */ std::vector split_escape(const std::string &txt, - char delim, size_t size_hint=0); + char delim, + size_t size_hint = 0); -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/subprocess.h b/libopenage/util/subprocess.h index 1588fd7d11..ef2fb881ae 100644 --- a/libopenage/util/subprocess.h +++ b/libopenage/util/subprocess.h @@ -1,4 +1,4 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -40,8 +40,8 @@ std::string which(const char *name); * the existing file is overwritten. */ int call(const std::vector &argv, - bool wait=false, - const char *redirect_stdout_to=nullptr); + bool wait = false, + const char *redirect_stdout_to = nullptr); } // namespace subprocess } // namespace openage diff --git a/libopenage/util/thread_id.h b/libopenage/util/thread_id.h index 20fa0e07ac..d84c966890 100644 --- a/libopenage/util/thread_id.h +++ b/libopenage/util/thread_id.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -15,4 +15,5 @@ namespace util { */ size_t get_current_thread_id(); -}} // namespace openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/timer.h b/libopenage/util/timer.h index 4ce7610741..64d7ff58e3 100644 --- a/libopenage/util/timer.h +++ b/libopenage/util/timer.h @@ -1,4 +1,4 @@ -// Copyright 2013-2016 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #pragma once @@ -31,12 +31,12 @@ class Timer { /** * creates the timer, in either stopped or running state. */ - Timer(bool stopped=true); + Timer(bool stopped = true); /** * resets the timer, in either stopped or running state. */ - void reset(bool stopped=true); + void reset(bool stopped = true); /** * stops/pauses the timer. @@ -66,4 +66,5 @@ class Timer { }; -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/timing.h b/libopenage/util/timing.h index 9bfaf30e4f..d67c3debe9 100644 --- a/libopenage/util/timing.h +++ b/libopenage/util/timing.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -27,4 +27,5 @@ time_nsec_t get_monotonic_time(); */ time_nsec_t get_real_time(); -}} // namespace openage::timing +} // namespace timing +} // namespace openage diff --git a/libopenage/util/unicode.h b/libopenage/util/unicode.h index 2576c9bcc9..094029f2d0 100644 --- a/libopenage/util/unicode.h +++ b/libopenage/util/unicode.h @@ -1,4 +1,4 @@ -// Copyright 2013-2016 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #pragma once @@ -107,4 +107,5 @@ size_t utf8_last_char_size(char *str); */ void utf8_pop_back(std::string &str); -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/variable.h b/libopenage/util/variable.h index c7908cc606..0692dc727e 100644 --- a/libopenage/util/variable.h +++ b/libopenage/util/variable.h @@ -1,4 +1,4 @@ -// Copyright 2015-2016 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -15,23 +15,23 @@ class VariableBase { /** * sets the type and value */ - template void set(const V &value); + template + void set(const V &value); /** * returns the stored value * throws an exception if the template * does not match the set type */ - template const T &get() const; - + template + const T &get() const; }; -template +template class Variable : public VariableBase { public: - Variable(const T &initial_value) - : + Variable(const T &initial_value) : value(initial_value) {} @@ -50,15 +50,16 @@ class Variable : public VariableBase { }; -template +template const T &VariableBase::get() const { return dynamic_cast &>(*this).get(); } -template -void VariableBase::set(const V& value) { +template +void VariableBase::set(const V &value) { return dynamic_cast &>(*this).set(value); } -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/vector.h b/libopenage/util/vector.h index 062ab8f693..8e7c7bceb8 100644 --- a/libopenage/util/vector.h +++ b/libopenage/util/vector.h @@ -1,4 +1,4 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -21,7 +21,7 @@ namespace openage::util { * N = dimensions * T = underlying single value type (double, float, ...) */ -template +template class Vector : public std::array { public: static_assert(N > 0, "0-dimensional vector not allowed"); @@ -45,18 +45,16 @@ class Vector : public std::array { /** * Constructor for initialisation with N T values */ - template - Vector(Ts ... args) - : + template + Vector(Ts... args) : std::array{static_cast(args)...} { - static_assert(sizeof...(args) == N, "not all values supplied."); } /** * Cast every value to NT and return the new Vector. */ - template + template Vector casted() const { Vector ret; std::copy(std::begin(*this), std::end(*this), std::begin(ret)); @@ -66,7 +64,7 @@ class Vector : public std::array { /** * Equality test with given precision. */ - bool equals(const this_type &other, T eps=default_eps) { + bool equals(const this_type &other, T eps = default_eps) { for (size_t i = 0; i < N; i++) { T diff = std::abs((*this)[i] - other[i]); if (diff >= eps) { @@ -79,7 +77,7 @@ class Vector : public std::array { /** * Vector addition with assignment */ - this_type &operator +=(const this_type &other) { + this_type &operator+=(const this_type &other) { for (size_t i = 0; i < N; i++) { (*this)[i] += other[i]; } @@ -89,7 +87,7 @@ class Vector : public std::array { /** * Vector addition */ - this_type operator +(const this_type &other) const { + this_type operator+(const this_type &other) const { this_type res(*this); res += other; return res; @@ -98,7 +96,7 @@ class Vector : public std::array { /** * Vector subtraction with assignment */ - this_type &operator -=(const this_type &other) { + this_type &operator-=(const this_type &other) { for (size_t i = 0; i < N; i++) { (*this)[i] -= other[i]; } @@ -108,7 +106,7 @@ class Vector : public std::array { /** * Vector subtraction */ - this_type operator -(const this_type &other) const { + this_type operator-(const this_type &other) const { this_type res(*this); res -= other; return res; @@ -117,7 +115,7 @@ class Vector : public std::array { /** * Scalar multiplication with assignment */ - this_type &operator *=(T a) { + this_type &operator*=(T a) { for (size_t i = 0; i < N; i++) { (*this)[i] *= a; } @@ -127,7 +125,7 @@ class Vector : public std::array { /** * Scalar multiplication */ - this_type operator *(T a) const { + this_type operator*(T a) const { this_type res(*this); res *= a; return res; @@ -136,7 +134,7 @@ class Vector : public std::array { /** * Scalar division with assignment */ - this_type &operator /=(T a) { + this_type &operator/=(T a) { for (size_t i = 0; i < N; i++) { (*this)[i] /= a; } @@ -146,7 +144,7 @@ class Vector : public std::array { /** * Scalar division */ - this_type operator /(T a) const { + this_type operator/(T a) const { this_type res(*this); res /= a; return res; @@ -181,50 +179,50 @@ class Vector : public std::array { /** * Cross-product of two 3-dimensional vectors */ - template - typename std::enable_if::type - /*Vector*/ cross_product(const this_type &other) const { + template + typename std::enable_if::type + /*Vector*/ + cross_product(const this_type &other) const { return this_type( ((*this)[1] * other[2] - (*this)[2] * other[1]), ((*this)[2] * other[0] - (*this)[0] * other[2]), - ((*this)[0] * other[1] - (*this)[1] * other[0]) - ); + ((*this)[0] * other[1] - (*this)[1] * other[0])); } /** * Scalar multiplication with swapped arguments */ - friend this_type operator *(T a, const this_type &v) { + friend this_type operator*(T a, const this_type &v) { return v * a; } /** * Print to output stream using '<<' */ - friend std::ostream &operator <<(std::ostream &o, const this_type &v) { + friend std::ostream &operator<<(std::ostream &o, const this_type &v) { o << "("; - for (size_t i = 0; i < N-1; i++) { + for (size_t i = 0; i < N - 1; i++) { o << v[i] << ", "; } - o << v[N-1] << ")"; + o << v[N - 1] << ")"; return o; } }; -template +template using Vector2t = Vector<2, T>; -template +template using Vector3t = Vector<3, T>; -template +template using Vector4t = Vector<4, T>; -template +template using Vectorf = Vector; -template +template using Vectord = Vector; using Vector2f = Vector<2, float>; @@ -247,4 +245,4 @@ using Vector2ss = Vector<2, ssize_t>; using Vector3ss = Vector<3, ssize_t>; using Vector4ss = Vector<4, ssize_t>; -} // openage::util +} // namespace openage::util diff --git a/libopenage/versions/versions.h b/libopenage/versions/versions.h index cdbb25bbe9..310e98ad0d 100644 --- a/libopenage/versions/versions.h +++ b/libopenage/versions/versions.h @@ -1,4 +1,4 @@ -// Copyright 2020-2020 the openage authors. See copying.md for legal info. +// Copyright 2020-2024 the openage authors. See copying.md for legal info. #pragma once @@ -19,6 +19,6 @@ namespace openage::versions { * pxd: * map[string,string] get_version_numbers() except + */ -OAAPI std::map get_version_numbers(); +OAAPI std::map get_version_numbers(); } // namespace openage::versions From 2402b6d3b4fcd4c1787b0f5216e59b4b112f021f Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 4 Feb 2024 20:54:05 +0100 Subject: [PATCH 198/771] refactor: Turn clang-format off for better visibility in matrices. --- libopenage/renderer/resources/mesh_data.cpp | 82 ++++++++++++++++++--- 1 file changed, 71 insertions(+), 11 deletions(-) diff --git a/libopenage/renderer/resources/mesh_data.cpp b/libopenage/renderer/resources/mesh_data.cpp index 98c5d7dbdb..d228d15326 100644 --- a/libopenage/renderer/resources/mesh_data.cpp +++ b/libopenage/renderer/resources/mesh_data.cpp @@ -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. #include "mesh_data.h" @@ -35,11 +35,18 @@ size_t vertex_input_count(vertex_input_t in) { return vin_count.get(in); } -VertexInputInfo::VertexInputInfo(std::vector inputs, vertex_layout_t layout, vertex_primitive_t primitive) : - inputs(std::move(inputs)), layout(layout), primitive(primitive) {} +VertexInputInfo::VertexInputInfo(std::vector inputs, + vertex_layout_t layout, + vertex_primitive_t primitive) : + inputs(std::move(inputs)), + layout(layout), primitive(primitive) {} -VertexInputInfo::VertexInputInfo(std::vector inputs, vertex_layout_t layout, vertex_primitive_t primitive, index_t index_type) : - inputs(std::move(inputs)), layout(layout), primitive(primitive), index_type(index_type) {} +VertexInputInfo::VertexInputInfo(std::vector inputs, + vertex_layout_t layout, + vertex_primitive_t primitive, + index_t index_type) : + inputs(std::move(inputs)), + layout(layout), primitive(primitive), index_type(index_type) {} void VertexInputInfo::add_shader_input_map(std::unordered_map &&in_map) { for (auto mapping : in_map) { @@ -105,12 +112,30 @@ VertexInputInfo MeshData::get_info() const { /// Vertices of a quadrilateral filling the whole screen. /// Format: (pos, tex_coords) = (x, y, u, v) static constexpr const std::array QUAD_DATA_CENTERED = { - {-1.0f, 1.0f, 0.0f, 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f}}; + // clang-format off + // prevent clang-format from putting this matrix on single line + { + -1.0f, 1.0f, 0.0f, 1.0f, + -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, -1.0f, 1.0f, 0.0f + } + // clang-format on +}; /// Vertices of a quad from (0, 0) to (1, 1) /// Format: (pos, tex_coords) = (x, y, u, v) static constexpr const std::array QUAD_DATA_UNIT = { - {0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f}}; + // clang-format off + // prevent clang-format from putting this matrix on single line + { + 0.0f, 1.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 0.0f, 1.0f, 0.0f + } + // clang-format on +}; namespace { @@ -154,11 +179,29 @@ MeshData MeshData::make_quad(float sidelength, bool centered) { if (centered) { float halfsidelength = sidelength / 2; positions = { - {-halfsidelength, halfsidelength, 0.0f, 1.0f, -halfsidelength, -halfsidelength, 0.0f, 0.0f, halfsidelength, halfsidelength, 1.0f, 1.0f, halfsidelength, -halfsidelength, 1.0f, 0.0f}}; + // clang-format off + // prevent clang-format from putting this matrix on single line + { + -halfsidelength, halfsidelength, 0.0f, 1.0f, + -halfsidelength, -halfsidelength, 0.0f, 0.0f, + halfsidelength, halfsidelength, 1.0f, 1.0f, + halfsidelength, -halfsidelength, 1.0f, 0.0f + } + // clang-format on + }; } else { positions = { - {0.0f, sidelength, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, sidelength, sidelength, 1.0f, 1.0f, sidelength, 0.0f, 1.0f, 0.0f}}; + // clang-format off + // prevent clang-format from putting this matrix on single line + { + 0.0f, sidelength, 0.0f, 1.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + sidelength, sidelength, 1.0f, 1.0f, + sidelength, 0.0f, 1.0f, 0.0f + } + // clang-format on + }; } return create_float_mesh(positions); @@ -174,11 +217,28 @@ MeshData MeshData::make_quad(float width, float height, bool centered) { float halfwidth = width / 2; float halfheight = height / 2; positions = { - {-halfwidth, halfheight, 0.0f, 1.0f, -halfwidth, -halfheight, 0.0f, 0.0f, halfwidth, halfheight, 1.0f, 1.0f, halfwidth, -halfheight, 1.0f, 0.0f}}; + { + // clang-format off + // prevent clang-format from putting this matrix on single line + -halfwidth, halfheight, 0.0f, 1.0f, + -halfwidth, -halfheight, 0.0f, 0.0f, + halfwidth, halfheight, 1.0f, 1.0f, + halfwidth, -halfheight, 1.0f, 0.0f + // clang-format on + }}; } else { positions = { - {0.0f, height, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, width, height, 1.0f, 1.0f, width, 0.0f, 1.0f, 0.0f}}; + // clang-format off + // prevent clang-format from putting this matrix on single line + { + 0.0f, height, 0.0f, 1.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + width, height, 1.0f, 1.0f, + width, 0.0f, 1.0f, 0.0f + } + // clang-format on + }; } return create_float_mesh(positions); From 8b2a8362f98ed1990e92671ab8678b3dbcc591c2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Jan 2024 15:56:56 +0100 Subject: [PATCH 199/771] curve: Store erasure time in queue element. --- libopenage/curve/queue.h | 55 ++++++++++++++++++------ libopenage/curve/queue_filter_iterator.h | 2 +- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index 3e8b5db97b..0c2a0db098 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -30,15 +30,28 @@ namespace curve { template class Queue : public event::EventEntity { struct queue_wrapper { - time::time_t _time; + // Insertion time of the element + time::time_t _alive; + // Erase time of the element + time::time_t _dead; + // Element value T value; queue_wrapper(const time::time_t &time, const T &value) : - _time{time}, + _alive{time}, + _dead{time::TIME_MAX}, value{value} {} - time::time_t time() const { - return _time; + time::time_t alive() const { + return _alive; + } + + time::time_t dead() const { + return _dead; + } + + void set_dead(const time::time_t &time) { + _dead = time; } }; @@ -122,6 +135,14 @@ class Queue : public event::EventEntity { */ void erase(const CurveIterator> &it); + /** + * Erase an element from the queue at a specific time. + * + * @param time The time to erase at. + * @param it The iterator to the element to erase. + */ + void erase(const time::time_t &time, const CurveIterator> &it); + /** * Insert a new element into the queue. * @@ -129,7 +150,7 @@ class Queue : public event::EventEntity { * @param e The element to insert. * @return Iterator to the inserted element. */ - QueueFilterIterator> insert(const time::time_t &, const T &e); + QueueFilterIterator> insert(const time::time_t &time, const T &e); /** * Erase all elements that are at or after the given time. @@ -143,7 +164,7 @@ class Queue : public event::EventEntity { */ void dump() { for (auto i : container) { - std::cout << i.value << " at " << i.time() << std::endl; + std::cout << i.value << " at " << i.alive() << std::endl; } } @@ -201,7 +222,7 @@ const T &Queue::front(const time::time_t &time) const { // search for the last element before the given time auto it = this->container.end(); --it; - while (it->time() > time and it != this->container.begin()) { + while (it->alive() > time and it != this->container.begin()) { --it; } @@ -218,7 +239,7 @@ const T Queue::pop_front(const time::time_t &time) { // search for the last element before the given time auto it = this->container.end(); --it; - while (it->time() > time and it != this->container.begin()) { + while (it->alive() > time and it != this->container.begin()) { --it; } @@ -226,7 +247,7 @@ const T Queue::pop_front(const time::time_t &time) { auto val = std::move(it->value); // get the time span between current time and the next element - auto to = (++it)->time(); + auto to = (++it)->alive(); --it; auto from = time; @@ -249,13 +270,13 @@ bool Queue::empty(const time::time_t &time) const { // search for the first element that is after the given time auto begin = this->begin(time).get_base(); - return begin == this->container.begin() and begin->time() > time; + return begin == this->container.begin() and begin->alive() > time; } template QueueFilterIterator> Queue::begin(const time::time_t &t) const { for (auto it = this->container.begin(); it != this->container.end(); ++it) { - if (it->time() >= t) { + if (it->alive() >= t) { return QueueFilterIterator>( it, this, @@ -296,7 +317,13 @@ QueueFilterIterator> Queue::between(const time::time_t &begin, template void Queue::erase(const CurveIterator> &it) { container.erase(it.get_base()); - return; +} + + +template +inline void Queue::erase(const time::time_t &time, + const CurveIterator> &it) { + it.get_base()->set_dead(time); } @@ -305,7 +332,7 @@ QueueFilterIterator> Queue::insert(const time::time_t &time, const T &e) { const_iterator insertion_point = this->container.end(); for (auto it = this->container.begin(); it != this->container.end(); ++it) { - if (time < it->time()) { + if (time < it->alive()) { insertion_point = this->container.insert(it, queue_wrapper(time, e)); break; } @@ -334,7 +361,7 @@ QueueFilterIterator> Queue::insert(const time::time_t &time, template void Queue::clear(const time::time_t &time) { for (auto it = this->container.begin(); - it != this->container.end() and it->time() < time; + it != this->container.end() and it->alive() < time; it = this->container.erase(it)) { } diff --git a/libopenage/curve/queue_filter_iterator.h b/libopenage/curve/queue_filter_iterator.h index b8ecf77036..7ecea74d36 100644 --- a/libopenage/curve/queue_filter_iterator.h +++ b/libopenage/curve/queue_filter_iterator.h @@ -33,7 +33,7 @@ class QueueFilterIterator : public CurveIteratorcontainer->end().get_base() != this->get_base()) { - return (this->get_base()->time() >= this->from and this->get_base()->time() < this->to); + return (this->get_base()->alive() >= this->from and this->get_base()->alive() < this->to); } return false; } From fa69365a413f8ba182558908d95204ecc94b730b Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Jan 2024 19:08:26 +0100 Subject: [PATCH 200/771] curve: Keep dead elements in the queue. --- libopenage/curve/iterator.h | 4 +- libopenage/curve/queue.h | 136 ++++++++++++++++----------- libopenage/curve/tests/container.cpp | 26 +++-- 3 files changed, 95 insertions(+), 71 deletions(-) diff --git a/libopenage/curve/iterator.h b/libopenage/curve/iterator.h index 1062ddcc1e..d0346e1b5d 100644 --- a/libopenage/curve/iterator.h +++ b/libopenage/curve/iterator.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 @@ -98,7 +98,7 @@ class CurveIterator { } /** - * Access the underlying + * Access the underlying iterator. */ const iterator_t &get_base() const { return base; diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index 0c2a0db098..23235caff7 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -42,11 +42,11 @@ class Queue : public event::EventEntity { _dead{time::TIME_MAX}, value{value} {} - time::time_t alive() const { + const time::time_t &alive() const { return _alive; } - time::time_t dead() const { + const time::time_t &dead() const { return _dead; } @@ -58,7 +58,7 @@ class Queue : public event::EventEntity { public: using container_t = typename std::deque; using const_iterator = typename container_t::const_iterator; - using iterator = typename container_t::const_iterator; + using iterator = typename container_t::iterator; Queue(const std::shared_ptr &loop, size_t id, @@ -74,24 +74,23 @@ class Queue : public event::EventEntity { // Reading Access /** - * Get the first element in the queue at the given time. + * Get the first element inserted at t <= time. + * + * Ignores dead elements. * * @param time The time to get the element at. + * * @return Queue element. */ const T &front(const time::time_t &time) const; /** - * Get the first element in the queue at the given time and remove it from - * the queue. - * - * @param time The time to get the element at. - * @param value Queue element. - */ - const T pop_front(const time::time_t &time); - - /** - * Check if the queue is empty at a given time. + * Check if the queue is empty at the given time (no elements alive + * before t <= time). + * + * Ignores dead elements. + * + * @param time The time to check at. * * @return true if the queue is empty, false otherwise. */ @@ -100,22 +99,36 @@ class Queue : public event::EventEntity { // Modifying access /** - * Get an iterator to the first/front element in the queue at the given time. + * Get the first element inserted at t <= time and erase it from the + * queue. + * + * Ignores dead elements. * - * @param t The time to get the element at. + * @param time The time to get the element at. + * @param value Queue element. + */ + const T &pop_front(const time::time_t &time); + + /** + * Get an iterator to the first element inserted at t >= time. + * + * Does not ignore dead elements. + * + * @param time The time to get the element at (default: \p time::TIME_MIN ). + * * @return Iterator to the first element. */ - QueueFilterIterator> begin( - const time::time_t &t = -time::TIME_MAX) const; + QueueFilterIterator> begin(const time::time_t &time = time::TIME_MIN) const; /** * Get an iterator to the last element in the queue at the given time. + * + * Does not ignore dead elements. * * @param t The time to get the element at. * @return Iterator to the last element. */ - QueueFilterIterator> end( - const time::time_t &t = time::TIME_MAX) const; + QueueFilterIterator> end(const time::time_t &time = time::TIME_MAX) const; /** * Get an iterator to elements that are in the queue between two time frames. @@ -135,29 +148,22 @@ class Queue : public event::EventEntity { */ void erase(const CurveIterator> &it); - /** - * Erase an element from the queue at a specific time. - * - * @param time The time to erase at. - * @param it The iterator to the element to erase. - */ - void erase(const time::time_t &time, const CurveIterator> &it); - /** * Insert a new element into the queue. * * @param time The time to insert at. * @param e The element to insert. + * * @return Iterator to the inserted element. */ QueueFilterIterator> insert(const time::time_t &time, const T &e); /** - * Erase all elements that are at or after the given time. + * Erase all elements at t <= time. * * @param time The time to clear at. */ - void clear(const time::time_t &); + void clear(const time::time_t &time); /** * Print the queue to stdout. @@ -190,6 +196,14 @@ class Queue : public event::EventEntity { } private: + /** + * Erase an element from the queue at the given time. + * + * @param time The time to erase at. + * @param it The iterator to the element to erase. + */ + void erase(const time::time_t &time, iterator &it); + /** * Identifier for the container */ @@ -220,59 +234,68 @@ const T &Queue::front(const time::time_t &time) const { } // search for the last element before the given time - auto it = this->container.end(); - --it; - while (it->alive() > time and it != this->container.begin()) { - --it; + auto it = this->container.begin(); + while (it->alive() <= time and it != this->container.end()) { + if (it->dead() > time) { + break; + } + ++it; } return it->value; } + template -const T Queue::pop_front(const time::time_t &time) { +const T &Queue::pop_front(const time::time_t &time) { if (this->empty(time)) [[unlikely]] { throw Error{MSG(err) << "Tried accessing front at " << time << " but queue is empty."}; } // search for the last element before the given time - auto it = this->container.end(); - --it; - while (it->alive() > time and it != this->container.begin()) { - --it; + auto it = this->container.begin(); + while (it->alive() <= time and it != this->container.end()) { + if (it->dead() > time) { + break; + } + ++it; } - // get the last element inserted before the given time - auto val = std::move(it->value); - // get the time span between current time and the next element auto to = (++it)->alive(); --it; auto from = time; // erase the element - // TODO: We should be able to reinsert elements - auto filter_iterator = QueueFilterIterator>(it, this, to, from); - this->erase(filter_iterator); + this->erase(time, it); this->last_pop = time; - return val; + this->changes(time); + + return it->value; } + template bool Queue::empty(const time::time_t &time) const { if (this->container.empty()) { return true; } - // search for the first element that is after the given time - auto begin = this->begin(time).get_base(); + auto it = this->container.begin(); + while (it->alive() <= time and it != this->container.end()) { + if (it->dead() > time) { + return false; + } + ++it; + } - return begin == this->container.begin() and begin->alive() > time; + return true; } + template QueueFilterIterator> Queue::begin(const time::time_t &t) const { for (auto it = this->container.begin(); it != this->container.end(); ++it) { @@ -321,9 +344,9 @@ void Queue::erase(const CurveIterator> &it) { template -inline void Queue::erase(const time::time_t &time, - const CurveIterator> &it) { - it.get_base()->set_dead(time); +void Queue::erase(const time::time_t &time, + iterator &it) { + it->set_dead(time); } @@ -360,9 +383,12 @@ QueueFilterIterator> Queue::insert(const time::time_t &time, template void Queue::clear(const time::time_t &time) { - for (auto it = this->container.begin(); - it != this->container.end() and it->alive() < time; - it = this->container.erase(it)) { + auto it = this->container.begin(); + while (it->alive() <= time and it != this->container.end()) { + if (it->dead() > time) { + it->set_dead(time); + } + ++it; } this->changes(time); diff --git a/libopenage/curve/tests/container.cpp b/libopenage/curve/tests/container.cpp index 9787243db3..020e2aad99 100644 --- a/libopenage/curve/tests/container.cpp +++ b/libopenage/curve/tests/container.cpp @@ -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. #include #include @@ -178,17 +178,6 @@ void test_queue() { TESTEQUALS(*q.begin(12), 5); TESTEQUALS(*q.begin(100000), 5); - TESTEQUALS(q.front(0), 1); - TESTEQUALS(q.front(1), 1); - TESTEQUALS(q.front(2), 2); - TESTEQUALS(q.front(3), 2); - TESTEQUALS(q.front(4), 3); - TESTEQUALS(q.front(5), 3); - TESTEQUALS(q.front(10), 4); - TESTEQUALS(q.front(12), 4); - TESTEQUALS(q.front(100000), 4); - TESTEQUALS(q.front(100001), 6); - { std::unordered_set reference = {1, 2, 3}; for (auto it = q.between(0, 6); it != q.end(); ++it) { @@ -230,18 +219,27 @@ void test_queue() { TESTEQUALS(reference.empty(), true); } + TESTEQUALS(q.front(0), 1); + TESTEQUALS(q.front(2), 1); + TESTEQUALS(q.front(5), 1); + TESTEQUALS(q.front(10), 1); + TESTEQUALS(q.front(100001), 1); TESTEQUALS(q.pop_front(0), 1); TESTEQUALS(q.empty(0), true); - TESTEQUALS(q.pop_front(12), 4); + TESTEQUALS(q.pop_front(12), 2); TESTEQUALS(q.empty(12), false); TESTEQUALS(q.pop_front(12), 3); TESTEQUALS(q.empty(12), false); - TESTEQUALS(q.pop_front(12), 2); + TESTEQUALS(q.pop_front(12), 4); TESTEQUALS(q.empty(12), true); + + q.clear(0); + TESTEQUALS(q.empty(0), true); + TESTEQUALS(q.empty(100001), false); } From 87f3b1b2ad5f907a25f0288588c1b2238e5f5634 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Jan 2024 22:59:27 +0100 Subject: [PATCH 201/771] curve: Cache the latest front() position in the queue. --- libopenage/curve/queue.h | 119 +++++++++++++++-------- libopenage/curve/queue_filter_iterator.h | 2 +- 2 files changed, 80 insertions(+), 41 deletions(-) diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index 23235caff7..c803b4e356 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -33,7 +33,8 @@ class Queue : public event::EventEntity { // Insertion time of the element time::time_t _alive; // Erase time of the element - time::time_t _dead; + // TODO: this has to be mutable because erase() will complain otherwise + mutable time::time_t _dead; // Element value T value; @@ -50,7 +51,8 @@ class Queue : public event::EventEntity { return _dead; } - void set_dead(const time::time_t &time) { + // TODO: this has to be const because erase() will complain otherwise + void set_dead(const time::time_t &time) const { _dead = time; } }; @@ -66,7 +68,8 @@ class Queue : public event::EventEntity { EventEntity{loop}, _id{id}, _idstr{idstr}, - last_pop{time::TIME_ZERO} {} + last_change{time::TIME_ZERO}, + front_start{this->container.begin()} {} // prevent accidental copy of queue Queue(const Queue &) = delete; @@ -202,7 +205,16 @@ class Queue : public event::EventEntity { * @param time The time to erase at. * @param it The iterator to the element to erase. */ - void erase(const time::time_t &time, iterator &it); + void erase(const time::time_t &time, const_iterator &it); + + /** + * Get the first alive element inserted at t <= time. + * + * @param time The time to get the element at. + * + * @return Iterator to the first alive element or end() if no such element exists. + */ + const_iterator first_alive(const time::time_t &time) const; /** * Identifier for the container @@ -220,12 +232,44 @@ class Queue : public event::EventEntity { container_t container; /** - * The time of the last access to the queue. - */ - time::time_t last_pop; + * Simulation time of the last modifying change to the queue. + */ + time::time_t last_change; + + /** + * Caches the search start position for the next front() call. + * + * All iterators before are guaranteed to be dead at t >= last_change. + */ + mutable typename Queue::const_iterator front_start; }; +template +Queue::const_iterator Queue::first_alive(const time::time_t &time) const { + auto hint = this->container.begin(); + + // check if the access is later than the last change + if (this->last_change <= time) [[likely]] { + // start searching from the last front position + hint = this->front_start; + } + + // Iterate until we find an alive element + bool al = hint->alive() <= time; + bool en = hint != this->container.end(); + while (hint->alive() <= time + and hint != this->container.end()) { + if (hint->dead() > time) { + return hint; + } + ++hint; + } + + return this->container.end(); +} + + template const T &Queue::front(const time::time_t &time) const { if (this->empty(time)) [[unlikely]] { @@ -233,14 +277,7 @@ const T &Queue::front(const time::time_t &time) const { << time << " but queue is empty."}; } - // search for the last element before the given time - auto it = this->container.begin(); - while (it->alive() <= time and it != this->container.end()) { - if (it->dead() > time) { - break; - } - ++it; - } + auto it = this->first_alive(time); return it->value; } @@ -253,25 +290,15 @@ const T &Queue::pop_front(const time::time_t &time) { << time << " but queue is empty."}; } - // search for the last element before the given time - auto it = this->container.begin(); - while (it->alive() <= time and it != this->container.end()) { - if (it->dead() > time) { - break; - } - ++it; - } + Queue::const_iterator it = this->first_alive(time); - // get the time span between current time and the next element - auto to = (++it)->alive(); - --it; - auto from = time; + // cache the search start position for the next front() call + this->front_start = it; + this->last_change = time; // erase the element this->erase(time, it); - this->last_pop = time; - this->changes(time); return it->value; @@ -284,15 +311,7 @@ bool Queue::empty(const time::time_t &time) const { return true; } - auto it = this->container.begin(); - while (it->alive() <= time and it != this->container.end()) { - if (it->dead() > time) { - return false; - } - ++it; - } - - return true; + return this->first_alive(time) == this->container.end(); } @@ -345,7 +364,7 @@ void Queue::erase(const CurveIterator> &it) { template void Queue::erase(const time::time_t &time, - iterator &it) { + const_iterator &it) { it->set_dead(time); } @@ -360,11 +379,19 @@ QueueFilterIterator> Queue::insert(const time::time_t &time, break; } } + if (insertion_point == this->container.end()) { insertion_point = this->container.insert(this->container.end(), queue_wrapper(time, e)); } + // cache the insertion time + this->last_change = time; + if (time < this->front_start->alive()) { + // cache the search start position for the next front() call + this->front_start = insertion_point; + } + auto ct = QueueFilterIterator>( insertion_point, this, @@ -383,7 +410,15 @@ QueueFilterIterator> Queue::insert(const time::time_t &time, template void Queue::clear(const time::time_t &time) { - auto it = this->container.begin(); + Queue::const_iterator it = this->first_alive(time); + + // no elements alive at t <= time + // so we don't have any changes + if (it == this->container.end()) { + return; + } + + // erase all elements alive at t <= time while (it->alive() <= time and it != this->container.end()) { if (it->dead() > time) { it->set_dead(time); @@ -391,6 +426,10 @@ void Queue::clear(const time::time_t &time) { ++it; } + // cache the search start position for the next front() call + this->last_change = time; + this->front_start = it; + this->changes(time); } diff --git a/libopenage/curve/queue_filter_iterator.h b/libopenage/curve/queue_filter_iterator.h index 7ecea74d36..248abb44ad 100644 --- a/libopenage/curve/queue_filter_iterator.h +++ b/libopenage/curve/queue_filter_iterator.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 From 3f6889db64a7d54d5fa4ed4ead27fde24c48e9eb Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 14 Jan 2024 02:35:15 +0100 Subject: [PATCH 202/771] doc: Update curve queue documentation. --- doc/code/curves.md | 50 ++++++++++++++++++++++++++++++++-------- libopenage/curve/queue.h | 16 ++++++++----- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/doc/code/curves.md b/doc/code/curves.md index 2ead4a8c77..376c4c938c 100644 --- a/doc/code/curves.md +++ b/doc/code/curves.md @@ -201,19 +201,51 @@ Container curves are intended for storing changes to collections and containers. The currently supported containers are `Queue` and `UnorderedMap`. The most important distinction between regular C++ containers and curve containers -is that curve containers keep track of when modifications happen and what changes -to an element are made. Deleting elements also does not erase elements from memory. -Instead, they are simply hidden for requests for time `t1` after the deletion time `t2` if -`t1 > t2`. +is that curve containers track the *lifespan* of each element, i.e. their insertion time, +modification time, and erasure time. Erasing elements also does not delete them from memory +and instead hides them for requests made after the erasure time. #### Queue -Queue curve containers store elements in first-in-first-out (FIFO) insertion order -while additionally keeping track of element insertion time. Requests for the front element -at time `t` will return the element that is in front of the queue at that time. -The queue can also be iterated over for a specific time `t` which allows access to -all elements that were in the queue at time `t`. +Queue curve containers are the equivalent to the `std::queue` C++ containers. As such, they +should be used in situations where first-in-first-out (FIFO) access patterns are desired. + +Elements in the queue are sorted by insertion time. The element with the earliest insertion time +is the *front* element of the queue. + +The front element at time `t` can be read via the `front(t)` method, which retrieves the first, +non-erased element with insertion time before or at `t`. In comparison, `pop_front(t)` returns +the same value as `front(t)` and additionally erases the element from the queue at time `t`. + +It should be stressed again that erasing an element does not delete it from memory but +simply ends its lifespan inside the curve container. `front(t)` and `pop_front(t)` always +consider elements with an active lifespan (i.e. elements that are not erased at time `t`). +As a side effect, `pop_front(t1)` and `front(t2)`/`pop_front(t2)` may return the same element +when `t2 < t1` because the element has not ended its lifespan at time `t2` yet. It is in +the responsibility of the caller to ensure that this behaviour does not cause any +side effects. + + +**Read** + +Read operations retrieve values for a specific point in time. + +| Method | Description | +| ---------- | --------------------------------------- | +| `front(t)` | Get front element at time `t` | +| `empty(t)` | Check if the queue is empty at time `t` | + +**Modify** + +Modify operations insert values for a specific point in time. + +| Method | Description | +| ------------------ | ------------------------------------------------------ | +| `insert(t, value)` | Insert a new element at time `t` | +| `pop_front(t)` | Get front element at time `t` and erase it at time `t` | +| `clear(t)` | Erase all elements inserted before time `t` | + #### Unordered Map diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index c803b4e356..7a3de26cf4 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -128,24 +128,30 @@ class Queue : public event::EventEntity { * * Does not ignore dead elements. * - * @param t The time to get the element at. + * @param t The time to get the element at (default: \p time::TIME_MAX ). + * * @return Iterator to the last element. */ QueueFilterIterator> end(const time::time_t &time = time::TIME_MAX) const; /** * Get an iterator to elements that are in the queue between two time frames. + * + * Does not ignore dead elements. * - * @param begin Start time. - * @param end End time. + * @param begin Start time (default: \p time::TIME_MIN ). + * @param end End time (default: \p time::TIME_MAX ). + * * @return Iterator to the first element in the time frame. */ QueueFilterIterator> between( - const time::time_t &begin = time::TIME_MAX, + const time::time_t &begin = time::TIME_MIN, const time::time_t &end = time::TIME_MAX) const; /** * Erase an element from the queue. + * + * Does not ignore dead elements. * * @param it The iterator to the element to erase. */ @@ -256,8 +262,6 @@ Queue::const_iterator Queue::first_alive(const time::time_t &time) const { } // Iterate until we find an alive element - bool al = hint->alive() <= time; - bool en = hint != this->container.end(); while (hint->alive() <= time and hint != this->container.end()) { if (hint->dead() > time) { From 19748fa937b2510a50ee0629217ddd1dfd958e33 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 14 Jan 2024 03:03:28 +0100 Subject: [PATCH 203/771] curve: Optimize queue for late insertions. --- libopenage/curve/queue.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index 7a3de26cf4..bb2f5154aa 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -252,7 +252,7 @@ class Queue : public event::EventEntity { template -Queue::const_iterator Queue::first_alive(const time::time_t &time) const { +typename Queue::const_iterator Queue::first_alive(const time::time_t &time) const { auto hint = this->container.begin(); // check if the access is later than the last change @@ -377,17 +377,17 @@ template QueueFilterIterator> Queue::insert(const time::time_t &time, const T &e) { const_iterator insertion_point = this->container.end(); - for (auto it = this->container.begin(); it != this->container.end(); ++it) { - if (time < it->alive()) { - insertion_point = this->container.insert(it, queue_wrapper(time, e)); + while (insertion_point != this->container.begin()) { + --insertion_point; + if (insertion_point->alive() <= time) { + ++insertion_point; break; } } + insertion_point = this->container.insert(insertion_point, queue_wrapper{time, e}); - if (insertion_point == this->container.end()) { - insertion_point = this->container.insert(this->container.end(), - queue_wrapper(time, e)); - } + // TODO: Inserting before any dead elements shoud reset their death time + // since by definition, they cannot be popped before the new element // cache the insertion time this->last_change = time; From d8d33c3684aea98863390ca1bf8fabc9690f5eb6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 14 Jan 2024 19:05:42 +0100 Subject: [PATCH 204/771] curve: Replace empty checks in front() with assertions. --- libopenage/curve/queue.h | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index bb2f5154aa..800ae08860 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -8,6 +8,8 @@ #include #include +#include "error/error.h" + #include "curve/iterator.h" #include "curve/queue_filter_iterator.h" #include "event/evententity.h" @@ -276,12 +278,8 @@ typename Queue::const_iterator Queue::first_alive(const time::time_t &time template const T &Queue::front(const time::time_t &time) const { - if (this->empty(time)) [[unlikely]] { - throw Error{MSG(err) << "Tried accessing front at " - << time << " but queue is empty."}; - } - auto it = this->first_alive(time); + ENSURE(it != this->container.end(), "Tried accessing front at " << time << " but queue is empty."); return it->value; } @@ -289,12 +287,8 @@ const T &Queue::front(const time::time_t &time) const { template const T &Queue::pop_front(const time::time_t &time) { - if (this->empty(time)) [[unlikely]] { - throw Error{MSG(err) << "Tried accessing front at " - << time << " but queue is empty."}; - } - Queue::const_iterator it = this->first_alive(time); + ENSURE(it != this->container.end(), "Tried accessing front at " << time << " but queue is empty."); // cache the search start position for the next front() call this->front_start = it; From 4bef85b12ae5e6c3e6defb6f08ac2da7ca6276e6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 16 Jan 2024 02:18:14 +0100 Subject: [PATCH 205/771] curve: Fix a memory leak in the queue cache. 'front_start' was not a valid element when the queue was empty. --- libopenage/curve/queue.h | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index 800ae08860..a255d0f091 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -3,8 +3,8 @@ #pragma once #include -#include #include +#include #include #include @@ -60,7 +60,7 @@ class Queue : public event::EventEntity { }; public: - using container_t = typename std::deque; + using container_t = typename std::list; using const_iterator = typename container_t::const_iterator; using iterator = typename container_t::iterator; @@ -71,7 +71,7 @@ class Queue : public event::EventEntity { _id{id}, _idstr{idstr}, last_change{time::TIME_ZERO}, - front_start{this->container.begin()} {} + front_start{this->container.end()} {} // prevent accidental copy of queue Queue(const Queue &) = delete; @@ -258,7 +258,7 @@ typename Queue::const_iterator Queue::first_alive(const time::time_t &time auto hint = this->container.begin(); // check if the access is later than the last change - if (this->last_change <= time) [[likely]] { + if (this->last_change <= time) { // start searching from the last front position hint = this->front_start; } @@ -385,10 +385,19 @@ QueueFilterIterator> Queue::insert(const time::time_t &time, // cache the insertion time this->last_change = time; - if (time < this->front_start->alive()) { - // cache the search start position for the next front() call + if (this->front_start == this->container.end()) [[unlikely]] { + // only true if the container is empty + // or all elements are dead this->front_start = insertion_point; } + else { + // if there are more alive elements, only cache if the + // insertion time is before the current front + if (time < this->front_start->alive()) { + // cache the search start position for the next front() call + this->front_start = insertion_point; + } + } auto ct = QueueFilterIterator>( insertion_point, From 56c6fd504a62ffd13bc1d93d6e93cacb6cef7305 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 13 Feb 2024 18:04:46 +0100 Subject: [PATCH 206/771] curve: Convert comment indent to tabs. --- libopenage/curve/queue.h | 72 ++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index a255d0f091..7698c18c5c 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -80,22 +80,22 @@ class Queue : public event::EventEntity { /** * Get the first element inserted at t <= time. - * - * Ignores dead elements. + * + * Ignores dead elements. * * @param time The time to get the element at. - * + * * @return Queue element. */ const T &front(const time::time_t &time) const; /** * Check if the queue is empty at the given time (no elements alive - * before t <= time). - * - * Ignores dead elements. - * - * @param time The time to check at. + * before t <= time). + * + * Ignores dead elements. + * + * @param time The time to check at. * * @return true if the queue is empty, false otherwise. */ @@ -105,9 +105,9 @@ class Queue : public event::EventEntity { /** * Get the first element inserted at t <= time and erase it from the - * queue. - * - * Ignores dead elements. + * queue. + * + * Ignores dead elements. * * @param time The time to get the element at. * @param value Queue element. @@ -116,34 +116,34 @@ class Queue : public event::EventEntity { /** * Get an iterator to the first element inserted at t >= time. - * - * Does not ignore dead elements. + * + * Does not ignore dead elements. * * @param time The time to get the element at (default: \p time::TIME_MIN ). - * + * * @return Iterator to the first element. */ QueueFilterIterator> begin(const time::time_t &time = time::TIME_MIN) const; /** * Get an iterator to the last element in the queue at the given time. - * - * Does not ignore dead elements. + * + * Does not ignore dead elements. * * @param t The time to get the element at (default: \p time::TIME_MAX ). - * + * * @return Iterator to the last element. */ QueueFilterIterator> end(const time::time_t &time = time::TIME_MAX) const; /** * Get an iterator to elements that are in the queue between two time frames. - * - * Does not ignore dead elements. + * + * Does not ignore dead elements. * * @param begin Start time (default: \p time::TIME_MIN ). * @param end End time (default: \p time::TIME_MAX ). - * + * * @return Iterator to the first element in the time frame. */ QueueFilterIterator> between( @@ -152,8 +152,8 @@ class Queue : public event::EventEntity { /** * Erase an element from the queue. - * - * Does not ignore dead elements. + * + * Does not ignore dead elements. * * @param it The iterator to the element to erase. */ @@ -164,7 +164,7 @@ class Queue : public event::EventEntity { * * @param time The time to insert at. * @param e The element to insert. - * + * * @return Iterator to the inserted element. */ QueueFilterIterator> insert(const time::time_t &time, const T &e); @@ -210,18 +210,18 @@ class Queue : public event::EventEntity { /** * Erase an element from the queue at the given time. * - * @param time The time to erase at. + * @param time The time to erase at. * @param it The iterator to the element to erase. */ void erase(const time::time_t &time, const_iterator &it); /** - * Get the first alive element inserted at t <= time. - * - * @param time The time to get the element at. - * - * @return Iterator to the first alive element or end() if no such element exists. - */ + * Get the first alive element inserted at t <= time. + * + * @param time The time to get the element at. + * + * @return Iterator to the first alive element or end() if no such element exists. + */ const_iterator first_alive(const time::time_t &time) const; /** @@ -240,15 +240,15 @@ class Queue : public event::EventEntity { container_t container; /** - * Simulation time of the last modifying change to the queue. - */ + * Simulation time of the last modifying change to the queue. + */ time::time_t last_change; /** - * Caches the search start position for the next front() call. - * - * All iterators before are guaranteed to be dead at t >= last_change. - */ + * Caches the search start position for the next front() call. + * + * All iterators before are guaranteed to be dead at t >= last_change. + */ mutable typename Queue::const_iterator front_start; }; From 26ae0f0ad0cab175a14078f1a7cefc39e25debcc Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 20 Feb 2024 23:56:28 +0100 Subject: [PATCH 207/771] convert: Raise errors inside multiprocessing thread. --- openage/convert/processor/export/media_exporter.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 1d824a532e..a9ffd84092 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -256,6 +256,12 @@ def _export_multithreaded( # Small optimization that saves some time for small exports worker_count = min(multiprocessing.cpu_count(), len(requests)) + def error_callback(exception: Exception): + """ + Error callback for the worker pool. + """ + raise exception + # Create a manager for sharing data between the workers and main process with multiprocessing.Manager() as manager: # Workers write the image metadata to this queue @@ -295,7 +301,8 @@ def _export_multithreaded( target_path, *itargs ), - kwds=kwargs + kwds=kwargs, + error_callback=error_callback ) # Log file information From 5b844c0808e45ae972f09e4cef8cd67649364c3c Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 21 Feb 2024 00:46:45 +0100 Subject: [PATCH 208/771] renderer: Log creation of OpenGL objects. --- libopenage/renderer/opengl/framebuffer.cpp | 7 +++- libopenage/renderer/opengl/render_pass.cpp | 9 ++++- libopenage/renderer/opengl/render_target.cpp | 12 ++++-- libopenage/renderer/opengl/texture.cpp | 14 ++++--- libopenage/renderer/opengl/texture_array.cpp | 38 ++++++++++--------- libopenage/renderer/opengl/uniform_buffer.cpp | 8 +++- 6 files changed, 57 insertions(+), 31 deletions(-) diff --git a/libopenage/renderer/opengl/framebuffer.cpp b/libopenage/renderer/opengl/framebuffer.cpp index 477c628f90..0ad46c1563 100644 --- a/libopenage/renderer/opengl/framebuffer.cpp +++ b/libopenage/renderer/opengl/framebuffer.cpp @@ -1,7 +1,9 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #include "framebuffer.h" +#include "log/log.h" + #include "renderer/opengl/context.h" #include "renderer/opengl/texture.h" @@ -12,6 +14,7 @@ GlFramebuffer::GlFramebuffer(const std::shared_ptr &context) : GlSimpleObject(context, [](GLuint /*handle*/) {}), type{gl_framebuffer_t::display} { + log::log(MSG(dbg) << "Created OpenGL framebuffer with display target"); } // TODO the validity of this object is contingent @@ -51,6 +54,8 @@ GlFramebuffer::GlFramebuffer(const std::shared_ptr &context, if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { throw Error(MSG(err) << "Could not create OpenGL framebuffer."); } + + log::log(MSG(dbg) << "Created OpenGL framebuffer with texture targets"); } gl_framebuffer_t GlFramebuffer::get_type() const { diff --git a/libopenage/renderer/opengl/render_pass.cpp b/libopenage/renderer/opengl/render_pass.cpp index 4ea9dffd44..526663d324 100644 --- a/libopenage/renderer/opengl/render_pass.cpp +++ b/libopenage/renderer/opengl/render_pass.cpp @@ -1,13 +1,18 @@ -// Copyright 2019-2023 the openage authors. See copying.md for legal info. +// Copyright 2019-2024 the openage authors. See copying.md for legal info. #include "render_pass.h" +#include "log/log.h" + + namespace openage::renderer::opengl { GlRenderPass::GlRenderPass(std::vector renderables, const std::shared_ptr &target) : RenderPass(renderables, target), - is_optimised(false) {} + is_optimised(false) { + log::log(MSG(dbg) << "Created OpenGL render pass"); +} const std::vector &GlRenderPass::get_renderables() const { return this->renderables; diff --git a/libopenage/renderer/opengl/render_target.cpp b/libopenage/renderer/opengl/render_target.cpp index ba0505adba..4a54f4c0d8 100644 --- a/libopenage/renderer/opengl/render_target.cpp +++ b/libopenage/renderer/opengl/render_target.cpp @@ -1,17 +1,21 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #include "render_target.h" #include "error/error.h" +#include "log/log.h" #include "renderer/opengl/texture.h" + namespace openage::renderer::opengl { GlRenderTarget::GlRenderTarget(const std::shared_ptr &context, size_t width, size_t height) : type(gl_render_target_t::framebuffer), size(width, height), - framebuffer(context) {} + framebuffer(context) { + log::log(MSG(dbg) << "Created OpenGL render target for default framebuffer"); +} GlRenderTarget::GlRenderTarget(const std::shared_ptr &context, const std::vector> &textures) : @@ -20,6 +24,8 @@ GlRenderTarget::GlRenderTarget(const std::shared_ptr &context, textures(textures) { // TODO: Check if the textures are all the same size this->size = this->textures.value().at(0)->get_info().get_size(); + + log::log(MSG(dbg) << "Created OpenGL render target for textures"); } resources::Texture2dData GlRenderTarget::into_data() { @@ -40,7 +46,7 @@ std::vector> GlRenderTarget::get_texture_targets() { if (this->framebuffer->get_type() == gl_framebuffer_t::display) { return textures; } - //else upcast pointers + // else upcast pointers for (auto tex : this->textures.value()) { auto new_ptr = dynamic_pointer_cast(tex); textures.push_back(new_ptr); diff --git a/libopenage/renderer/opengl/texture.cpp b/libopenage/renderer/opengl/texture.cpp index d3d83051a2..5e795bcd82 100644 --- a/libopenage/renderer/opengl/texture.cpp +++ b/libopenage/renderer/opengl/texture.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "texture.h" @@ -54,7 +54,8 @@ GlTexture2d::GlTexture2d(const std::shared_ptr &context, glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - log::log(MSG(dbg) << "Created OpenGL texture from data"); + log::log(MSG(dbg) << "Created OpenGL texture from data (size: " + << size.first << "x" << size.second << ")"); } GlTexture2d::GlTexture2d(const std::shared_ptr &context, @@ -70,7 +71,7 @@ GlTexture2d::GlTexture2d(const std::shared_ptr &context, auto fmt_in_out = GL_PIXEL_FORMAT.get(this->info.get_format()); - auto dims = this->info.get_size(); + auto size = this->info.get_size(); glPixelStorei(GL_UNPACK_ALIGNMENT, this->info.get_row_alignment()); @@ -78,8 +79,8 @@ GlTexture2d::GlTexture2d(const std::shared_ptr &context, GL_TEXTURE_2D, 0, std::get<0>(fmt_in_out), - dims.first, - dims.second, + size.first, + size.second, 0, std::get<1>(fmt_in_out), std::get<2>(fmt_in_out), @@ -89,7 +90,8 @@ GlTexture2d::GlTexture2d(const std::shared_ptr &context, glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - log::log(MSG(dbg) << "Created OpenGL texture from parameters"); + log::log(MSG(dbg) << "Created OpenGL texture from info parameters (size: " + << size.first << "x" << size.second << ")"); } resources::Texture2dData GlTexture2d::into_data() { diff --git a/libopenage/renderer/opengl/texture_array.cpp b/libopenage/renderer/opengl/texture_array.cpp index 2266145cbe..41821b5180 100644 --- a/libopenage/renderer/opengl/texture_array.cpp +++ b/libopenage/renderer/opengl/texture_array.cpp @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #include "texture_array.h" @@ -31,20 +31,22 @@ GlTexture2dArray::GlTexture2dArray(const std::shared_ptr &context, size_t i = 0; for (auto const &tex : data) { glTexSubImage3D(GL_TEXTURE_2D_ARRAY, - 0, // mipmap number - 0, - 0, - i, // xoffset, yoffset, zoffset - size.first, - size.second, - 1, // width, height, depth + 0, // mipmap number + 0, // xoffset + 0, // yoffset + i, // zoffset + size.first, // width + size.second, // height, + 1, // depth std::get<1>(fmt_in_out), // format std::get<2>(fmt_in_out), // type - tex.get_data() // data + tex.get_data() // data ); i += 1; } + + log::log(MSG(dbg) << "Created OpenGL texture array from data"); } GlTexture2dArray::GlTexture2dArray(const std::shared_ptr &context, @@ -65,22 +67,22 @@ GlTexture2dArray::GlTexture2dArray(const std::shared_ptr &context, // Create empty image glTexImage3D(GL_TEXTURE_2D_ARRAY, - 0, // mipmap level + 0, // mipmap level std::get<0>(fmt_in_out), // gpu texel format - size.first, // width - size.second, // height - n_layers, // depth - 0, // border + size.first, // width + size.second, // height + n_layers, // depth + 0, // border std::get<1>(fmt_in_out), // cpu pixel format std::get<2>(fmt_in_out), // cpu pixel type - nullptr // data + nullptr // data ); // TODO these are outdated, use sampler settings glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - log::log(MSG(dbg) << "Created an OpenGL texture array."); + log::log(MSG(dbg) << "Created OpenGL texture array from info parameters"); } void GlTexture2dArray::upload(size_t layer, resources::Texture2dData const &data) { @@ -101,10 +103,10 @@ void GlTexture2dArray::upload(size_t layer, resources::Texture2dData const &data layer, // xoffset, yoffset, zoffset size.first, size.second, - 1, // width, height, depth + 1, // width, height, depth std::get<1>(fmt_in_out), // format std::get<2>(fmt_in_out), // type - data.get_data() // data + data.get_data() // data ); } diff --git a/libopenage/renderer/opengl/uniform_buffer.cpp b/libopenage/renderer/opengl/uniform_buffer.cpp index 28f78edb18..b84b17e6e1 100644 --- a/libopenage/renderer/opengl/uniform_buffer.cpp +++ b/libopenage/renderer/opengl/uniform_buffer.cpp @@ -1,8 +1,10 @@ -// 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 "uniform_buffer.h" #include "error/error.h" +#include "log/log.h" + #include "renderer/opengl/context.h" #include "renderer/opengl/lookup.h" #include "renderer/opengl/texture.h" @@ -32,6 +34,10 @@ GlUniformBuffer::GlUniformBuffer(const std::shared_ptr &context, glBufferData(GL_UNIFORM_BUFFER, this->data_size, NULL, usage); glBindBufferRange(GL_UNIFORM_BUFFER, this->binding_point, *this->handle, 0, this->data_size); + + log::log(MSG(dbg) << "Created OpenGL uniform buffer (size: " + << this->data_size << ", binding point: " + << this->binding_point << ")"); } GLuint GlUniformBuffer::get_binding_point() const { From 6beeb2e33ce13d66bed12c76c2ff49e30f29924a Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 23 Feb 2024 00:17:17 +0100 Subject: [PATCH 209/771] convert: Log file info after multi-threaded conversion has finished. --- .../convert/processor/export/media_exporter.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index a9ffd84092..61e48b9e36 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -305,13 +305,6 @@ def error_callback(exception: Exception): error_callback=error_callback ) - # Log file information - if get_loglevel() <= logging.DEBUG: - MediaExporter.log_fileinfo( - sourcedir[request.get_type().value, request.source_filename], - exportdir[request.targetdir, request.target_filename] - ) - # Show progress MediaExporter._show_progress(outqueue.qsize(), expected_size) @@ -328,6 +321,14 @@ def error_callback(exception: Exception): if handle_outqueue_func: handle_outqueue_func(outqueue, requests) + # Log file information + if get_loglevel() <= logging.DEBUG: + for request in requests: + MediaExporter.log_fileinfo( + sourcedir[request.get_type().value, request.source_filename], + exportdir[request.targetdir, request.target_filename] + ) + @staticmethod def _get_blend_data( request: MediaExportRequest, From 4704ec34e1280a78b872f7185357ab8ecff8d04a Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 23 Feb 2024 00:18:17 +0100 Subject: [PATCH 210/771] convert: Fix file size fetch when logging asset file info. --- openage/convert/processor/export/media_exporter.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 61e48b9e36..966ebe8da6 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -608,17 +608,8 @@ def log_fileinfo( source_format = source_file.suffix[1:].upper() target_format = target_file.suffix[1:].upper() - source_path = source_file.resolve_native_path() - if source_path: - source_size = os.path.getsize(source_path) - - else: - with source_file.open('r') as src: - src.seek(0, os.SEEK_END) - source_size = src.tell() - - target_path = target_file.resolve_native_path() - target_size = os.path.getsize(target_path) + source_size = source_file.filesize + target_size = target_file.filesize log = ("Converted: " f"{source_file.name} " From 6a7ad9a2e4a083402a0656cd59f2029ff4c9ba91 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 23 Feb 2024 19:14:18 +0100 Subject: [PATCH 211/771] cli: Use CWD as DLL search path on Windows as default. --- openage/__main__.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/openage/__main__.py b/openage/__main__.py index a2bac73496..ac20017088 100644 --- a/openage/__main__.py +++ b/openage/__main__.py @@ -1,4 +1,4 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-statements """ @@ -44,12 +44,14 @@ def close_windows_dll_path_handles(dll_path_handles): for handle in dll_path_handles: handle.close() - if sys.platform == 'win32' and dll_paths is not None: - import atexit - win_dll_path_handles = [] - for addtional_path in dll_paths: - win_dll_path_handles.append(os.add_dll_directory(addtional_path)) - atexit.register(close_windows_dll_path_handles, win_dll_path_handles) + if sys.platform != 'win32' or dll_paths is None: + return + + import atexit + win_dll_path_handles = [] + for addtional_path in dll_paths: + win_dll_path_handles.append(os.add_dll_directory(addtional_path)) + atexit.register(close_windows_dll_path_handles, win_dll_path_handles) def main(argv=None): @@ -62,6 +64,7 @@ def main(argv=None): if sys.platform == 'win32': cli.add_argument( "--add-dll-search-path", action='append', dest='dll_paths', + default=[os.getcwd()], help="(Windows only) provide additional DLL search path") cli.add_argument("--version", "-V", action='store_true', dest='print_version', From 6e7a04390965098b19740a581b944fce9fbfbee6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 24 Feb 2024 16:41:07 +0100 Subject: [PATCH 212/771] cli: Use entrypoint path as DLL search path on Windows as default. --- openage/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openage/__main__.py b/openage/__main__.py index ac20017088..c724d5bc6a 100644 --- a/openage/__main__.py +++ b/openage/__main__.py @@ -62,9 +62,11 @@ def main(argv=None): ) if sys.platform == 'win32': + import inspect cli.add_argument( "--add-dll-search-path", action='append', dest='dll_paths', - default=[os.getcwd()], + # use path of current openage executable as default + default=[os.path.dirname(os.path.abspath(inspect.getsourcefile(lambda: 0)))], help="(Windows only) provide additional DLL search path") cli.add_argument("--version", "-V", action='store_true', dest='print_version', From 5814bec47ab8744bd5d4009039fd02a99011f8b8 Mon Sep 17 00:00:00 2001 From: RoboSchmied Date: Mon, 25 Mar 2024 21:58:21 +0100 Subject: [PATCH 213/771] Fix: 3 typos Signed-off-by: RoboSchmied --- openage/cabextract/cabchecksum.pyx | 2 +- openage/util/struct.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openage/cabextract/cabchecksum.pyx b/openage/cabextract/cabchecksum.pyx index f869fc383b..3c8efaddb3 100644 --- a/openage/cabextract/cabchecksum.pyx +++ b/openage/cabextract/cabchecksum.pyx @@ -74,7 +74,7 @@ def mscab_csum(bytes data): for i in range(count): result ^= data_ptr[i] - # we have so far ignored endianess issues. + # we have so far ignored endianness issues. # on a non-little endian system, the interpretation is wrong. # thus, interpret it as binary data and decode it as little endian. result = ( diff --git a/openage/util/struct.py b/openage/util/struct.py index fa092669d5..30530c55ac 100644 --- a/openage/util/struct.py +++ b/openage/util/struct.py @@ -48,7 +48,7 @@ def __new__(mcs, name, bases, classdict, **kwds): raise SyntaxError("endianness has been given multiple times") if value not in "@=<>!": - raise SyntaxError("endianess: expected one of @=<>!") + raise SyntaxError("endianness: expected one of @=<>!") specstr = value continue @@ -95,7 +95,7 @@ class NamedStruct(metaclass=NamedStructMeta): Alternatively, attributes may be set to None; those are ignored, and may be set manually at some later point. - The first member must be 'endianess'. + The first member must be 'endianness'. Example: From 5bc1abb6bfc113df7bc765e0092d45b94bb14436 Mon Sep 17 00:00:00 2001 From: RoboSchmied Date: Tue, 26 Mar 2024 00:45:38 +0100 Subject: [PATCH 214/771] fix copyright year in 2 files and add info to copying.md Signed-off-by: Michael Seibt --- copying.md | 1 + openage/cabextract/cabchecksum.pyx | 2 +- openage/util/struct.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/copying.md b/copying.md index afad82f953..13124fb993 100644 --- a/copying.md +++ b/copying.md @@ -152,6 +152,7 @@ _the openage authors_ are: | Fábio Barkoski | fabiobarkoski | fabiobarkoskii à gmail dawt com | | Astitva Kamble | askastitva | astitvakamble5 à gmail dawt com | | Haoyang Bi | AyiStar | ayistar à outlook dawt com | +| Michael Seibt | RoboSchmied | github à roboschmie dawt de | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. diff --git a/openage/cabextract/cabchecksum.pyx b/openage/cabextract/cabchecksum.pyx index 3c8efaddb3..cc5d418868 100644 --- a/openage/cabextract/cabchecksum.pyx +++ b/openage/cabextract/cabchecksum.pyx @@ -1,4 +1,4 @@ -# Copyright 2015-2016 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. """ Implements the MSCAB checksum algorithm. diff --git a/openage/util/struct.py b/openage/util/struct.py index 30530c55ac..c56d7ed023 100644 --- a/openage/util/struct.py +++ b/openage/util/struct.py @@ -1,4 +1,4 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. """ Provides some classes designed to expand the functionality of struct.struct From 9bb2432b579fab95410584f2c7ecc6845a0831d8 Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Fri, 29 Mar 2024 19:06:29 +0100 Subject: [PATCH 215/771] feat: added flake --- flake.lock | 61 ++++++++++++++++++++++++++++++++++++ flake.nix | 24 ++++++++++++++ nix/nyan.nix | 44 ++++++++++++++++++++++++++ nix/openage.nix | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/nyan.nix create mode 100644 nix/openage.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000000..dae268d7dc --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1711460390, + "narHash": "sha256-akSgjDZL6pVHEfSE6sz1DNSXuYX6hq+P/1Z5IoYWs7E=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "44733514b72e732bd49f5511bd0203dea9b9a434", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-23.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000000..13c53a8205 --- /dev/null +++ b/flake.nix @@ -0,0 +1,24 @@ +{ + description = "Free (as in freedom) open source clone of the Age of Empires II engine"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let pkgs = import nixpkgs { inherit system; }; in + { + packages = { + nyan = pkgs.callPackage ./nix/nyan.nix { }; + openage = pkgs.callPackage ./nix/openage.nix { + inherit (self.packages.${system}) nyan; + }; + }; + apps = rec { + openage = flake-utils.lib.mkApp { drv = self.packages.${system}.openage; }; + default = openage; + }; + }); +} diff --git a/nix/nyan.nix b/nix/nyan.nix new file mode 100644 index 0000000000..18ccb74288 --- /dev/null +++ b/nix/nyan.nix @@ -0,0 +1,44 @@ +{ lib +, stdenv +, fetchFromGitHub +, clang +, cmake +, flex +}: +let + pname = "nyan"; + version = "0.3"; +in +stdenv.mkDerivation +{ + inherit pname version; + + src = fetchFromGitHub { + owner = "SFTtech"; + repo = pname; + rev = "v${version}"; + hash = "sha256-bjz4aSS5RO+QuLd7kyblTPNcuQFhYK7sW1csNXHL4Qs="; + }; + + nativeBuildInputs = [ + clang + cmake + ]; + + buildInputs = [ + flex + ]; + + meta = with lib; { + description = "A data description language"; + longDescription = '' + Nyan stores hierarchical objects with key-value pairs in a database with the key idea that changes in a parent affect all children. We created nyan because there existed no suitable language to properly represent the enormous complexity of storing the data for openage. + ''; + homepage = "https://openage.sft.mx"; + license = licenses.lgpl3Plus; + platforms = platforms.unix; + mainProgram = "nyancat"; + }; +} + + diff --git a/nix/openage.nix b/nix/openage.nix new file mode 100644 index 0000000000..8fd38fe244 --- /dev/null +++ b/nix/openage.nix @@ -0,0 +1,83 @@ +{ pkgs +, lib +, stdenv +, fetchFromGitHub +, nyan +, makeWrapper +, python3 +, ... +}: +let + pname = "openage"; + version = "0.5.3"; + + # Python libraries needed at build and run time + pyEnv = python3.withPackages (ps: [ + ps.mako + ps.pillow + ps.numpy + ps.lz4 + ps.pygments + ps.cython + ps.pylint + ps.toml + ]); +in +stdenv.mkDerivation { + inherit pname version; + + src = fetchFromGitHub { + owner = "SFTtech"; + repo = pname; + rev = "v${version}"; + hash = "sha256-/ag4U7nnZbvkInnLOZ6cB2VWUb+n/HgM1wpo1y/mUHQ="; + }; + + nativeBuildInputs = [ + pkgs.qt6.wrapQtAppsHook + pkgs.toml11 + + makeWrapper + ]; + + buildInputs = with pkgs; [ + nyan + pyEnv + + gcc + clang + cmake + gnumake + qt6.full + + eigen + libepoxy + libogg + libpng + dejavu_fonts + ftgl + fontconfig + harfbuzz + opusfile + libopus + qt6.qtdeclarative + qt6.qtmultimedia + ]; + + # openage requires access to both python dependencies and openage bindings + postInstall = '' + wrapProgram $out/bin/openage --set PYTHONPATH "${pyEnv}/${pyEnv.sitePackages}:$out/lib/python3.11/site-packages/" + ''; + + meta = with lib; { + description = "Free (as in freedom) open source clone of the Age of Empires II engine"; + longDescription = '' + openage: a volunteer project to create a free engine clone of the Genie Engine used by Age of Empires, Age of Empires II (HD) and Star Wars: Galactic Battlegrounds, comparable to projects like OpenMW, OpenRA, OpenSAGE, OpenTTD and OpenRCT2. + + openage uses the original game assets (such as sounds and graphics), but (for obvious reasons) doesn't ship them + ''; + homepage = "https://openage.sft.mx"; + license = licenses.lgpl3Plus; + platforms = platforms.unix; + }; +} From 1b479eb11e9d670039d36ead975f033b4edc7f86 Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Sun, 31 Mar 2024 16:54:09 +0200 Subject: [PATCH 216/771] fix: added docs and using in-tree code --- doc/build_instructions/nix.md | 78 +++++++++++++++++++++++++++++++++++ doc/building.md | 2 +- flake.nix | 17 +++++++- nix/openage.nix | 50 +++++++++++++++++----- 4 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 doc/build_instructions/nix.md diff --git a/doc/build_instructions/nix.md b/doc/build_instructions/nix.md new file mode 100644 index 0000000000..46ff37b2d2 --- /dev/null +++ b/doc/build_instructions/nix.md @@ -0,0 +1,78 @@ +# Building and developing on Nix based systems + +The openage repository is a [nix flake](https://nixos.wiki/wiki/Flakes) that +allow Nix users to easily build, install and develop openage. + +To build openage using nix + +1. make sure you have nix with flakes enabled, either by permanent system + configuration or by using command line flags, as described on + [the wiki](https://nixos.wiki/wiki/Flakes); +2. clone this repository and `cd` into it; +3. run `nix build .#openage` to start the build process; +4. the built artifact will be in `./result`. + +Nix will configure and build the source code automatically via the +`configurePhase` and `buildPhase` scripts, automatically provided by nix. + +If you want to build the derivation and run it immediately, you can use + +``` +nix run .#openage +``` + +instead of + +``` +nix build .#openage +./result/bin/openage +``` + +## Development + +You can get a development shell, with the required dependencies, by running + +``` +nix shell +``` + +This will download the same dependencies used for build and pop you in a +shell ready for use. You can call `configurePhase` and `buildPhase` to run +cmake configuration and building. + +Please note that nyan is downloaded from github as defined in `nix/nyan.nix`, +so if you want to provide nyan source tree, you'll need to modify that file +to use a path (such as `../../nyan`, relative to `nyan.nix`) instead of the +`fetchFromGitHub` function, for example: + +``` +# Clone both +git clone https://github.com/SFTtech/nyan +git clone https://github.com/SFTtech/openage +``` + +Then edit `openage/nix/nyan.nix` to use `../../nyan`, the result will be like + +``` +$ head -n18 openage/nix/nyan.nix + +``` +{ lib +, stdenv +, fetchFromGitHub +, clang +, cmake +, flex +}: +let + pname = "nyan"; + version = "0.3"; +in +stdenv.mkDerivation +{ + inherit pname version; + + src = ../../nyan; + + nativeBuildInputs = [ +``` diff --git a/doc/building.md b/doc/building.md index e7e0339d64..619ad7a423 100644 --- a/doc/building.md +++ b/doc/building.md @@ -84,9 +84,9 @@ described below for some of the most common ones: - [Arch Linux](build_instructions/arch_linux.md) - [FreeBSD](build_instructions/freebsd.md) - [Gentoo](build_instructions/gentoo.md) +- [Nix/NixOS](build_instructions/nix.md) - [Microsoft Windows](build_instructions/windows_msvc.md) - ### nyan installation `openage` depends on [`nyan`](https://github.com/SFTtech/nyan), which is the diff --git a/flake.nix b/flake.nix index 13c53a8205..2e28a3b7dd 100644 --- a/flake.nix +++ b/flake.nix @@ -1,4 +1,8 @@ { + # This is a nix flake that contains a declarative definition of the openage + # and nyan packages, providing convenient and reproducible builds and + # development shells. + description = "Free (as in freedom) open source clone of the Age of Empires II engine"; inputs = { @@ -10,12 +14,23 @@ flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; in { - packages = { + # This output is to build the derivation with `nix build` as well as to + # get development shells using `nix develop`. + # These are the packages provided by this flake: nyan and openage. + packages = rec { + # `nix build .#nyan` to build this nyan = pkgs.callPackage ./nix/nyan.nix { }; + # `nix build .#openage` to build this openage = pkgs.callPackage ./nix/openage.nix { + # Nyan is not provided by nixpkgs, but it comes from this flake inherit (self.packages.${system}) nyan; }; + # If no path is specified, openage is the default + default = openage; }; + + # This output is to run the application directly with `nix run` + # (or `nix run .#openage` if you want to be explicit) apps = rec { openage = flake-utils.lib.mkApp { drv = self.packages.${system}.openage; }; default = openage; diff --git a/nix/openage.nix b/nix/openage.nix index 8fd38fe244..c26e51253a 100644 --- a/nix/openage.nix +++ b/nix/openage.nix @@ -1,9 +1,19 @@ +# This file contains the definition of a nix expression that builds openage. +# A similar one exists from nyan, the configuration language. +# +# The expression in this file is a function (to be called by callPackage) +# that returns a derivation. callPackage is used inside the flake.nix file. +# +# To be compliant with nixpkgs, the dependencies can be passed via function +# arguments, similarly to how python3 or nyan is used here. The available +# packages that can be used when building/developing can be found among nixpkgs +# (https://search.nixos.org/) -as done with python3- or passed from the flake +# as done with nyan in the flake.nix file. { pkgs , lib , stdenv , fetchFromGitHub , nyan -, makeWrapper , python3 , ... }: @@ -11,7 +21,15 @@ let pname = "openage"; version = "0.5.3"; - # Python libraries needed at build and run time + # Python libraries needed at build and run time. This function creates a + # python3 package configured with some packages (similar to a python venv). + # The current python3 version is used, which is python3.11 at the moment of + # writing. + # If more packages are needed, they can be added here. The available packages + # are listed at + # https://search.nixos.org/packages?channel=unstable&query=python311Packages + # For example, to add python311Packages.pybind11 to the dependencies, we could + # add to this list ps.pybind11 pyEnv = python3.withPackages (ps: [ ps.mako ps.pillow @@ -26,20 +44,26 @@ in stdenv.mkDerivation { inherit pname version; - src = fetchFromGitHub { - owner = "SFTtech"; - repo = pname; - rev = "v${version}"; - hash = "sha256-/ag4U7nnZbvkInnLOZ6cB2VWUb+n/HgM1wpo1y/mUHQ="; - }; + # This uses the current checked-out repository as source code + src = ../.; + + # This fetches the code on github: one would use this when submitting the + # package to nixpkgs. + # src = fetchFromGitHub { + # owner = "SFTtech"; + # repo = pname; + # rev = "v${version}"; + # hash = "sha256-/ag4U7nnZbvkInnLOZ6cB2VWUb+n/HgM1wpo1y/mUHQ="; + # }; + # Dependencies that are used only at build time nativeBuildInputs = [ + # This is needed for qt applications pkgs.qt6.wrapQtAppsHook pkgs.toml11 - - makeWrapper ]; + # Dependencies that are used at build and run time buildInputs = with pkgs; [ nyan pyEnv @@ -65,10 +89,16 @@ stdenv.mkDerivation { ]; # openage requires access to both python dependencies and openage bindings + # Since nix places the binary somewhere in the nix store (/nix/store/blah), + # it needs to know where to find the python environment build above. This + # is done by setting the PYTHONPATH: the first part contains the path to + # the environment, the second part contains the path to the openage library + # build with this code. postInstall = '' wrapProgram $out/bin/openage --set PYTHONPATH "${pyEnv}/${pyEnv.sitePackages}:$out/lib/python3.11/site-packages/" ''; + # Metadata meta = with lib; { description = "Free (as in freedom) open source clone of the Age of Empires II engine"; longDescription = '' From 1f3ef0733f6f346cbb2aa36825c6193c6a87e3f3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 3 Apr 2024 22:03:59 +0200 Subject: [PATCH 217/771] convert: Add freeze_support() before using multiprocessing. --- openage/__main__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openage/__main__.py b/openage/__main__.py index c724d5bc6a..4475f46518 100644 --- a/openage/__main__.py +++ b/openage/__main__.py @@ -177,6 +177,11 @@ def main(argv=None): if __name__ == '__main__': + # Required for Windows executables (and apparently macOS too) + # https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support + # https://pyinstaller.org/en/latest/common-issues-and-pitfalls.html#multi-processing + multiprocessing.freeze_support() + # openage is complicated and multithreaded; better not use fork. multiprocessing.set_start_method('spawn') From 518690a14b7fbec9898211de88b76da3d08ab895 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 6 Apr 2024 23:10:51 +0200 Subject: [PATCH 218/771] etc: Add gdb pretty printers for time::time_t. --- etc/gdb_pretty/__init__.py | 5 +++ etc/gdb_pretty/printers.py | 77 ++++++++++++++++++++++++++++++++++++++ etc/openage.gdbinit | 12 ++++++ 3 files changed, 94 insertions(+) create mode 100644 etc/gdb_pretty/__init__.py create mode 100644 etc/gdb_pretty/printers.py create mode 100644 etc/openage.gdbinit diff --git a/etc/gdb_pretty/__init__.py b/etc/gdb_pretty/__init__.py new file mode 100644 index 0000000000..0e697d0676 --- /dev/null +++ b/etc/gdb_pretty/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2024-2024 the openage authors. See copying.md for legal info. + +""" +GDB pretty printers for openage. +""" diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py new file mode 100644 index 0000000000..28d4bd7234 --- /dev/null +++ b/etc/gdb_pretty/printers.py @@ -0,0 +1,77 @@ +# Copyright 2024-2024 the openage authors. See copying.md for legal info. + +""" +Pretty printers for GDB. +""" + +import gdb + + +class PrinterControl(gdb.printing.PrettyPrinter): + """ + Exposes a pretty printer for a specific type name. + """ + + def __init__(self, type_name: str, printer): + super().__init__(type_name) + self.printer = printer + + def __call__(self, val): + if val.type.name == self.name: + return self.printer(val) + + +def printer(type_name: str): + """ + Decorator for pretty printers. + + :param type_name: The name of the type to register the printer for. + :type type_name: str + """ + def _register_printer(printer): + """ + Registers the printer with GDB. + """ + gdb.printing.register_pretty_printer( + None, + PrinterControl(type_name, printer) + ) + + return _register_printer + + +@printer('openage::time::time_t') +class TimePrinter: + """ + Pretty printer for openage::time::time_t. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + # convert the fixed point value to double + seconds = float(self.__val['raw_value']) * float(self.__val['to_double_factor']) + return f'{seconds:.5f}s' + + def children(self): + yield ('raw_value', self.__val['raw_value']) + # calculate the precision of the fixed point value + # 16 * log10(2) = 16 * 0.30103 = 4.81648 + # do this manualy because it's usually optimized out by the compiler + precision = int(16 * 0.30103 + 1) + yield ('approx_precision', precision) + + +# def add_pretty_printer(val): +# if str(val.type) == 'openage::time::time_t': +# return TimePrinter(val) + +# return None + + +# def register_openage_printers(objfile): +# """ +# Register the openage pretty printers with GDB. +# """ +# gdb.pretty_printers.append(add_pretty_printer) diff --git a/etc/openage.gdbinit b/etc/openage.gdbinit new file mode 100644 index 0000000000..69d42aeb51 --- /dev/null +++ b/etc/openage.gdbinit @@ -0,0 +1,12 @@ +python +import sys, os + +print(f".gdbinit Python: current working directory is {os.getcwd()}") +print(f".gdbinit Python: adding custom pretty-printers directory to the GDB path: {os.getcwd() + '../../etc'}") + +sys.path.insert(0, "../../etc") + +import gdb_pretty.printers +# from gdb_pretty.printers import register_openage_printers +# register_openage_printers(None) +end From 50d2f761f2899add91e4665c04054b1b9c9f1c78 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 7 Apr 2024 01:05:29 +0200 Subject: [PATCH 219/771] etc: Regex option for pretty printing types. --- etc/gdb_pretty/printers.py | 91 +++++++++++++++++++++++++++----------- etc/openage.gdbinit | 6 +-- 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index 28d4bd7234..e854427df4 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -5,23 +5,60 @@ """ import gdb +import re class PrinterControl(gdb.printing.PrettyPrinter): """ - Exposes a pretty printer for a specific type name. + Exposes a pretty printer for a specific type. + + Printer are searched in the following order: + 1. Exact type name _with_ typedefs + 2. Regex of type name _without_ typedefs """ - def __init__(self, type_name: str, printer): - super().__init__(type_name) - self.printer = printer + def __init__(self, name: str): + super().__init__(name) + + self.name_printers = {} + self.regex_printers = {} + + def add_printer(self, type_name: str, printer): + """ + Adds a printer for a specific type name. + """ + self.name_printers[type_name] = printer + + def add_printer_regex(self, regex: str, printer): + """ + Adds a printer for a specific type name. - def __call__(self, val): - if val.type.name == self.name: - return self.printer(val) + :param regex: The regex to match the type name. + :type regex: str + """ + self.regex_printers[re.compile(regex)] = printer + def __call__(self, val: gdb.Value): + # Check the exact type name with typedefa + type_name = val.type.name + if type_name in self.name_printers: + return self.name_printers[val.type.name](val) -def printer(type_name: str): + # Check the type name without typedefs and regex + type_name = val.type.unqualified().strip_typedefs().tag + if type_name is None: + return None + + for regex, printer in self.regex_printers.items(): + if regex.match(type_name): + return printer(val) + + +pp = PrinterControl('openage') +gdb.printing.register_pretty_printer(None, pp) + + +def printer(type_name: str, regex: str = None): """ Decorator for pretty printers. @@ -32,10 +69,23 @@ def _register_printer(printer): """ Registers the printer with GDB. """ - gdb.printing.register_pretty_printer( - None, - PrinterControl(type_name, printer) - ) + pp.add_printer(type_name, printer) + + return _register_printer + + +def printer_regex(regex: str): + """ + Decorator for pretty printers. + + :param regex: The regex to match the type name. + :type regex: str + """ + def _register_printer(printer): + """ + Registers the printer with GDB. + """ + pp.add_printer_regex(regex, printer) return _register_printer @@ -52,7 +102,8 @@ def __init__(self, val: gdb.Value): def to_string(self): # convert the fixed point value to double seconds = float(self.__val['raw_value']) * float(self.__val['to_double_factor']) - return f'{seconds:.5f}s' + # show as seconds with millisecond precision + return f'{seconds:.3f}s' def children(self): yield ('raw_value', self.__val['raw_value']) @@ -61,17 +112,3 @@ def children(self): # do this manualy because it's usually optimized out by the compiler precision = int(16 * 0.30103 + 1) yield ('approx_precision', precision) - - -# def add_pretty_printer(val): -# if str(val.type) == 'openage::time::time_t': -# return TimePrinter(val) - -# return None - - -# def register_openage_printers(objfile): -# """ -# Register the openage pretty printers with GDB. -# """ -# gdb.pretty_printers.append(add_pretty_printer) diff --git a/etc/openage.gdbinit b/etc/openage.gdbinit index 69d42aeb51..c0e8b86ce4 100644 --- a/etc/openage.gdbinit +++ b/etc/openage.gdbinit @@ -1,12 +1,10 @@ python import sys, os -print(f".gdbinit Python: current working directory is {os.getcwd()}") -print(f".gdbinit Python: adding custom pretty-printers directory to the GDB path: {os.getcwd() + '../../etc'}") +print("Loading openage.gdbinit") +print(f"Adding custom pretty-printers directory to the GDB path: {os.getcwd() + '../../etc'}") sys.path.insert(0, "../../etc") import gdb_pretty.printers -# from gdb_pretty.printers import register_openage_printers -# register_openage_printers(None) end From d0b742dd0cb149ee32003b0dfa09d0fdf924ceef Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 7 Apr 2024 01:06:01 +0200 Subject: [PATCH 220/771] etc: Add gdb pretty print for util::FixedPoint. --- etc/gdb_pretty/printers.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index e854427df4..daf844fe18 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -112,3 +112,21 @@ def children(self): # do this manualy because it's usually optimized out by the compiler precision = int(16 * 0.30103 + 1) yield ('approx_precision', precision) + + +@printer_regex('^openage::util::FixedPoint<.*>') +class FixedPointPrinter: + """ + Pretty printer for openage::util::FixedPoint. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + # convert the fixed point value to double + num = float(self.__val['raw_value']) * float(self.__val['to_double_factor']) + return f'{num:.5f}' + + def children(self): + yield ('raw_value', self.__val['raw_value']) From dc9f8918da9cc79758bb0146809b4932f1d47afe Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 7 Apr 2024 01:10:39 +0200 Subject: [PATCH 221/771] etc: Ignore warnings about non-existent Python gdb module. gdb module is important by gdb itself. --- etc/gdb_pretty/printers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index daf844fe18..81611ca543 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -4,7 +4,7 @@ Pretty printers for GDB. """ -import gdb +import gdb # type: ignore import re From f62c44a12ba6f90941c609531d568c74125e0312 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 7 Apr 2024 01:38:22 +0200 Subject: [PATCH 222/771] buildsys: Check pretty printers with sanity checks. --- buildsystem/codecompliance/__main__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/buildsystem/codecompliance/__main__.py b/buildsystem/codecompliance/__main__.py index dded8dd0a9..2a4f8b4084 100644 --- a/buildsystem/codecompliance/__main__.py +++ b/buildsystem/codecompliance/__main__.py @@ -1,4 +1,4 @@ -# Copyright 2014-2023 the openage authors. See copying.md for legal info. +# Copyright 2014-2024 the openage authors. See copying.md for legal info. """ Entry point for the code compliance checker. @@ -231,7 +231,7 @@ def find_all_issues(args, check_files=None): if args.pystyle: from .pystyle import find_issues - yield from find_issues(check_files, ('openage', 'buildsystem')) + yield from find_issues(check_files, ('openage', 'buildsystem', 'etc/gdb_pretty')) if args.cython: from buildsystem.codecompliance.cython import find_issues @@ -243,12 +243,12 @@ def find_all_issues(args, check_files=None): if args.pylint: from .pylint import find_issues - yield from find_issues(check_files, ('openage', 'buildsystem')) + yield from find_issues(check_files, ('openage', 'buildsystem', 'etc/gdb_pretty')) if args.textfiles: from .textfiles import find_issues yield from find_issues( - ('openage', 'libopenage', 'buildsystem', 'doc', 'legal'), + ('openage', 'libopenage', 'buildsystem', 'doc', 'legal', 'etc/gdb_pretty'), ('.pxd', '.pyx', '.pxi', '.py', '.h', '.cpp', '.template', '', '.txt', '.md', '.conf', @@ -257,13 +257,13 @@ def find_all_issues(args, check_files=None): if args.legal: from .legal import find_issues yield from find_issues(check_files, - ('openage', 'buildsystem', 'libopenage'), + ('openage', 'buildsystem', 'libopenage', 'etc/gdb_pretty'), args.test_git_change_years) if args.filemodes: from .modes import find_issues yield from find_issues(check_files, ('openage', 'buildsystem', - 'libopenage')) + 'libopenage', 'etc/gdb_pretty')) if __name__ == '__main__': From cc56b19416377c2d32ae2f15c8245dd52d5c694d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 7 Apr 2024 01:40:00 +0200 Subject: [PATCH 223/771] Fix pylint complaints. --- etc/gdb_pretty/printers.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index 81611ca543..9757a56848 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -4,8 +4,8 @@ Pretty printers for GDB. """ -import gdb # type: ignore import re +import gdb # type: ignore class PrinterControl(gdb.printing.PrettyPrinter): @@ -53,12 +53,14 @@ def __call__(self, val: gdb.Value): if regex.match(type_name): return printer(val) + return None + pp = PrinterControl('openage') gdb.printing.register_pretty_printer(None, pp) -def printer(type_name: str, regex: str = None): +def printer_typedef(type_name: str): """ Decorator for pretty printers. @@ -90,7 +92,7 @@ def _register_printer(printer): return _register_printer -@printer('openage::time::time_t') +@printer_typedef('openage::time::time_t') class TimePrinter: """ Pretty printer for openage::time::time_t. @@ -100,12 +102,20 @@ def __init__(self, val: gdb.Value): self.__val = val def to_string(self): + """ + Get the time as a string. + + Format: SS.sss (e.g. 12.345s) + """ # convert the fixed point value to double seconds = float(self.__val['raw_value']) * float(self.__val['to_double_factor']) # show as seconds with millisecond precision return f'{seconds:.3f}s' def children(self): + """ + Get the displayed children of the time value. + """ yield ('raw_value', self.__val['raw_value']) # calculate the precision of the fixed point value # 16 * log10(2) = 16 * 0.30103 = 4.81648 @@ -124,9 +134,17 @@ def __init__(self, val: gdb.Value): self.__val = val def to_string(self): + """ + Get the fixed point value as a string. + + Format: 0.12345 + """ # convert the fixed point value to double num = float(self.__val['raw_value']) * float(self.__val['to_double_factor']) return f'{num:.5f}' def children(self): + """ + Get the displayed children of the fixed point value. + """ yield ('raw_value', self.__val['raw_value']) From 6454661b0a9d237cfda069c7075934fd48e68e8e Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 7 Apr 2024 06:00:44 +0200 Subject: [PATCH 224/771] etc: Fix member value being optimized out in gcc. --- etc/gdb_pretty/printers.py | 23 ++++++++++++++++------- libopenage/gamestate/simulation.cpp | 4 ++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index 9757a56848..42078d2dca 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -107,8 +107,11 @@ def to_string(self): Format: SS.sss (e.g. 12.345s) """ + fractional_bits = int(self.__val.type.template_argument(1)) + # convert the fixed point value to double - seconds = float(self.__val['raw_value']) * float(self.__val['to_double_factor']) + to_double_factor = 1 / pow(2, fractional_bits) + seconds = float(self.__val['raw_value']) * to_double_factor # show as seconds with millisecond precision return f'{seconds:.3f}s' @@ -117,11 +120,6 @@ def children(self): Get the displayed children of the time value. """ yield ('raw_value', self.__val['raw_value']) - # calculate the precision of the fixed point value - # 16 * log10(2) = 16 * 0.30103 = 4.81648 - # do this manualy because it's usually optimized out by the compiler - precision = int(16 * 0.30103 + 1) - yield ('approx_precision', precision) @printer_regex('^openage::util::FixedPoint<.*>') @@ -139,8 +137,11 @@ def to_string(self): Format: 0.12345 """ + fractional_bits = int(self.__val.type.template_argument(1)) + # convert the fixed point value to double - num = float(self.__val['raw_value']) * float(self.__val['to_double_factor']) + to_double_factor = 1 / pow(2, fractional_bits) + num = float(self.__val['raw_value']) * to_double_factor return f'{num:.5f}' def children(self): @@ -148,3 +149,11 @@ def children(self): Get the displayed children of the fixed point value. """ yield ('raw_value', self.__val['raw_value']) + + # calculate the precision of the fixed point value + # 16 * log10(2) = 16 * 0.30103 = 4.81648 + # do this manualy because it's usually optimized out by the compiler + fractional_bits = int(self.__val.type.template_argument(1)) + + precision = int(fractional_bits * 0.30103 + 1) + yield ('approx_precision', precision) diff --git a/libopenage/gamestate/simulation.cpp b/libopenage/gamestate/simulation.cpp index e883bd56f7..ed2627c95e 100644 --- a/libopenage/gamestate/simulation.cpp +++ b/libopenage/gamestate/simulation.cpp @@ -1,4 +1,4 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #include "simulation.h" @@ -45,7 +45,7 @@ GameSimulation::GameSimulation(const util::Path &root_dir, void GameSimulation::run() { this->start(); while (this->running) { - auto current_time = this->time_loop->get_clock()->get_time(); + time::time_t current_time = this->time_loop->get_clock()->get_time(); this->event_loop->reach_time(current_time, this->game->get_state()); } log::log(MSG(info) << "Game simulation loop exited"); From d72de6176743e69cfa2e77a277b26a6bf20b2d52 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 7 Apr 2024 06:33:55 +0200 Subject: [PATCH 225/771] doc: Document that we have pretty printers. --- doc/debug.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/doc/debug.md b/doc/debug.md index e8f838e0b2..cf6c8880fc 100644 --- a/doc/debug.md +++ b/doc/debug.md @@ -21,10 +21,28 @@ gdb -ex 'set breakpoint pending on' -ex 'b openage::run_game' -ex run --args run ``` The game will be paused at the start of the function run_game() located in `libopenage/main.cpp` -#### Note: -The `run` executable is a compiled version of `run.py` that also embeds the interpreter. +**Note:** The `run` executable is a compiled version of `run.py` that also embeds the interpreter. The game is intended to be run by `run.py` but it is much easier to debug the `./run` file +### Pretty Printers + +Enabling pretty printing will make GDB's output much more readable, so we always recommend +to configure it in your setup. Your [favourite IDE](/doc/ide/) probably an option to enable pretty printers +for the standard library types. If not, you can get them from the [gcc repository](https://github.com/gcc-mirror/gcc/tree/master/libstdc%2B%2B-v3/python/libstdcxx) and register them in your local `.gdbinit` file. + +Additionally, we have created several custom GDB pretty printers for types used in `libopenage`, +the C++ library that contains the openage engine core. To enable them, you have to load the project's +own init file [openage.gdbinit](/etc/openage.gdbinit) when running GDB: + +```gdb +(gdb) source /etc/openage.gdbinit +``` + +Your IDE may be able to do this automatically for a debug run. Alternatively, you can configure +an [auto-loader](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Python-Auto_002dloading.html#Python-Auto_002dloading) +that loads the scripts for you. + + ### GDBGUI [gdbgui](https://github.com/cs01/gdbgui) is a browser-based frontend for GDB. From 0b146e27fcab9bd17c356a94ef9af11b91ade500 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 7 Apr 2024 16:53:33 +0200 Subject: [PATCH 226/771] etc: Pretty print openage::util::Vector. --- etc/gdb_pretty/printers.py | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index 42078d2dca..2a0be8b167 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -157,3 +157,48 @@ def children(self): precision = int(fractional_bits * 0.30103 + 1) yield ('approx_precision', precision) + + +@printer_regex('^openage::util::Vector<.*>') +class VectorPrinter: + """ + Pretty printer for openage::util::Vector. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the vector as a string. + """ + size = self.__val.type.template_argument(0) + int_type = self.__val.type.template_argument(1) + return f'openage::util::Vector<{size}, {int_type}>' + + def children(self): + """ + Get the displayed children of the vector. + """ + size = self.__val.type.template_argument(0) + for i in range(size): + yield (str(i), self.__val['_M_elems'][i]) + + def child(self, index): + """ + Get the child at the given index. + """ + return self.__val['_M_elems'][index] + + def num_children(self): + """ + Get the number of children of the vector. + """ + return self.__val.type.template_argument(0) + + @staticmethod + def display_hint(): + """ + Get the display hint for the vector. + """ + return 'array' From f34022c4c8984291f0f12a454449e7caeb78777c Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 7 Apr 2024 17:32:11 +0200 Subject: [PATCH 227/771] etc: Add reminder to inherit from gdb.ValuePrinter later. --- etc/gdb_pretty/printers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index 2a0be8b167..2d3c6e9fe5 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -96,6 +96,8 @@ def _register_printer(printer): class TimePrinter: """ Pretty printer for openage::time::time_t. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. """ def __init__(self, val: gdb.Value): @@ -126,6 +128,8 @@ def children(self): class FixedPointPrinter: """ Pretty printer for openage::util::FixedPoint. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. """ def __init__(self, val: gdb.Value): @@ -163,6 +167,8 @@ def children(self): class VectorPrinter: """ Pretty printer for openage::util::Vector. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. """ def __init__(self, val: gdb.Value): From 6a234ef8ee8c1221cf109e4a58648e15d09367ea Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 7 Apr 2024 18:05:46 +0200 Subject: [PATCH 228/771] etc: Pretty print openage::curve::Keyframe. --- etc/gdb_pretty/printers.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index 2d3c6e9fe5..e02ce9c292 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -208,3 +208,28 @@ def display_hint(): Get the display hint for the vector. """ return 'array' + + +@printer_regex('^openage::curve::Keyframe<.*>') +class KeyframePrinter: + """ + Pretty printer for openage::curve::Keyframe. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the keyframe as a string. + """ + return f'openage::curve::Keyframe<{self.__val.type.template_argument(0)}>' + + def children(self): + """ + Get the displayed children of the keyframe. + """ + yield ('time', self.__val['time']) + yield ('value', self.__val['value']) From e79a403ef84666e37be54000a73b0e9da811434d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 7 Apr 2024 18:34:07 +0200 Subject: [PATCH 229/771] etc: TODOs for future extensions of pretty printer. --- etc/gdb_pretty/printers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index e02ce9c292..e5bbb42d84 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -233,3 +233,9 @@ def children(self): """ yield ('time', self.__val['time']) yield ('value', self.__val['value']) + +# TODO: curve types +# TODO: coord types +# TODO: pathfinding types +# TODO: input event codes +# TODO: eigen types https://github.com/dmillard/eigengdb From 9294876c19a88921bd7ccf39d6de8951e7a55a50 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 14 Apr 2024 01:35:24 +0200 Subject: [PATCH 230/771] doc: Fix Markdown rendering of input system README. --- doc/code/input/README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/doc/code/input/README.md b/doc/code/input/README.md index ab059a668f..f7013dd4ee 100644 --- a/doc/code/input/README.md +++ b/doc/code/input/README.md @@ -86,11 +86,11 @@ the resulting GUI event. In any case, the resulting `QEvent` representing the raw input is converted to a generalized `input::Event` object containing the following information: - - **event class**: Basic event categorization, usually type of input device (e.g. keyboard, mouse, GUI) - - **code**: Unique identifier of the specific key/button that was pressed - - **modifiers**: Keyboard modifiers pressed alongside the key/button (e.g. CTRL, SHIFT, ALT) - - **state**: State of the button/key (e.g. pressed, released, double click) - - **raw event**: Reference to the original `QEvent` +- **event class**: Basic event categorization, usually type of input device (e.g. keyboard, mouse, GUI) +- **code**: Unique identifier of the specific key/button that was pressed +- **modifiers**: Keyboard modifiers pressed alongside the key/button (e.g. CTRL, SHIFT, ALT) +- **state**: State of the button/key (e.g. pressed, released, double click) +- **raw event**: Reference to the original `QEvent` The unique combination of *class*, *code*, *modifiers*, and *state* represents a specific key press and provides the **signature** of an input event. (Key) bindings are created by mapping signatures @@ -121,27 +121,27 @@ the actions taken by the input manager mainly consists of forwarding event data high-level interfaces. Therefore, these actions should not have any effect on the game simulation. `input_action` contains the following information: - - **action type**: One of the pre-defined types (see below). Used for determining a default action. - - **custom action function**: Executed instead of the default action if set (optional). - - **execution flags**: Key-value pairs for configuration settings (optional). +- **action type**: One of the pre-defined types (see below). Used for determining a default action. +- **custom action function**: Executed instead of the default action if set (optional). +- **execution flags**: Key-value pairs for configuration settings (optional). Most types have a default action that is executed unless a custom function is defined in `input_action`. These default actions are: - - **push context**: push a context on top of the stack - - **pop context**: remove the current top context - - **remove context**: remove a context from the stack - - **Controller**: forward event arguments in direction of gamestate (i.e. to high-level interface) - - **GUI**: forward event arguments to the GUI +- **push context**: push a context on top of the stack +- **pop context**: remove the current top context +- **remove context**: remove a context from the stack +- **Controller**: forward event arguments in direction of gamestate (i.e. to high-level interface) +- **GUI**: forward event arguments to the GUI In most cases, it should be sufficient to bind one of these options to an input event. Custom functions should only be used for edge cases which cannot be handled otherwise. For forwarding actions, additional arguments may be passed to the high-level interface. In the case of the controller, the input manager passes the `event_arguments` struct which contains: - - **input event** (i.e. the generalized `input::Event`) - - **mouse position** - - **mouse motion** (e.g. for calculating mouse movement direction) - - **flags**: Key-value pairs for configuration settings (optional) +- **input event** (i.e. the generalized `input::Event`) +- **mouse position** +- **mouse motion** (e.g. for calculating mouse movement direction) +- **flags**: Key-value pairs for configuration settings (optional) ### High-Level Interface (Controller) @@ -158,9 +158,9 @@ in binding context using the input event signature or class (similiar to how its interface). `binding_action` contains the following information: - - **transform function**: Transformation from event arguments to game event. - - **queue type**: Determines whether the created event is queued or passed to the gamestate immediately . - - **execution flags**: Key-value pairs for configuration settings (optional). +- **transform function**: Transformation from event arguments to game event. +- **queue type**: Determines whether the created event is queued or passed to the gamestate immediately . +- **execution flags**: Key-value pairs for configuration settings (optional). Game events can be queued before they are forwarded to the gamestate to allow chained commands, e.g. setting a bunch of waypoints before giving the final move command. From d72d9b9c716f4b56dbcf471bc4b7e7441db32cb3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 16 Apr 2024 01:03:54 +0200 Subject: [PATCH 231/771] etc: Pretty printers for fixed-point coordinates. --- etc/gdb_pretty/printers.py | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index e5bbb42d84..1b239092ac 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -92,6 +92,57 @@ def _register_printer(printer): return _register_printer +@printer_regex('^openage::coord::((phys|scene)2|(chunk|tile))(_delta)?') +class CoordNeSePrinter: + """ + Pretty printer for openage::coord::CoordNeSe. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the coord as a string. + """ + return self.__val.type.name + + def children(self): + """ + Get the displayed children of the coord. + """ + yield ('ne', self.__val['ne']) + yield ('se', self.__val['se']) + + +@printer_regex('^openage::coord::(chunk|phys|scene|tile)3(_delta)?') +class CoordNeSeUpPrinter: + """ + Pretty printer for openage::coord::CoordNeSeUp. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the coord as a string. + """ + return self.__val.type.name + + def children(self): + """ + Get the displayed children of the coord. + """ + yield ('ne', self.__val['ne']) + yield ('se', self.__val['se']) + yield ('up', self.__val['up']) + + @printer_typedef('openage::time::time_t') class TimePrinter: """ From a0de523a2985bc96a7c252f8ed0d0ea4089272bd Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 16 Apr 2024 01:10:23 +0200 Subject: [PATCH 232/771] etc: Pretty printers for integer coordinates. --- etc/gdb_pretty/printers.py | 52 +++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index 1b239092ac..1f0d354150 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -143,6 +143,57 @@ def children(self): yield ('up', self.__val['up']) +@printer_regex('^openage::coord::(camhud|viewport|input|term)(_delta)?') +class CoordXYPrinter: + """ + Pretty printer for openage::coord::CoordXY. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the coord as a string. + """ + return self.__val.type.name + + def children(self): + """ + Get the displayed children of the coord. + """ + yield ('x', self.__val['x']) + yield ('y', self.__val['y']) + + +@printer_regex('^openage::coord::(camhud|viewport|input|term)3(_delta)?') +class CoordXYZPrinter: + """ + Pretty printer for openage::coord::CoordXYZ. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the coord as a string. + """ + return self.__val.type.name + + def children(self): + """ + Get the displayed children of the coord. + """ + yield ('x', self.__val['x']) + yield ('y', self.__val['y']) + yield ('z', self.__val['z']) + + @printer_typedef('openage::time::time_t') class TimePrinter: """ @@ -286,7 +337,6 @@ def children(self): yield ('value', self.__val['value']) # TODO: curve types -# TODO: coord types # TODO: pathfinding types # TODO: input event codes # TODO: eigen types https://github.com/dmillard/eigengdb From 7e73cef274bfc0c0d70e595479a250135500b5bb Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 16 Apr 2024 01:26:36 +0200 Subject: [PATCH 233/771] etc: Flatten coord printer into single class. --- etc/gdb_pretty/printers.py | 89 ++++---------------------------------- 1 file changed, 8 insertions(+), 81 deletions(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index 1f0d354150..9a083f36ae 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -92,8 +92,8 @@ def _register_printer(printer): return _register_printer -@printer_regex('^openage::coord::((phys|scene)2|(chunk|tile))(_delta)?') -class CoordNeSePrinter: +@printer_regex('^openage::coord::(camhud|chunk|input|phys|scene|term|tile|viewport)(2|3)?(_delta)?') +class CoordPrinter: """ Pretty printer for openage::coord::CoordNeSe. @@ -113,85 +113,12 @@ def children(self): """ Get the displayed children of the coord. """ - yield ('ne', self.__val['ne']) - yield ('se', self.__val['se']) - - -@printer_regex('^openage::coord::(chunk|phys|scene|tile)3(_delta)?') -class CoordNeSeUpPrinter: - """ - Pretty printer for openage::coord::CoordNeSeUp. - - TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. - """ - - def __init__(self, val: gdb.Value): - self.__val = val - - def to_string(self): - """ - Get the coord as a string. - """ - return self.__val.type.name - - def children(self): - """ - Get the displayed children of the coord. - """ - yield ('ne', self.__val['ne']) - yield ('se', self.__val['se']) - yield ('up', self.__val['up']) - - -@printer_regex('^openage::coord::(camhud|viewport|input|term)(_delta)?') -class CoordXYPrinter: - """ - Pretty printer for openage::coord::CoordXY. - - TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. - """ - - def __init__(self, val: gdb.Value): - self.__val = val - - def to_string(self): - """ - Get the coord as a string. - """ - return self.__val.type.name - - def children(self): - """ - Get the displayed children of the coord. - """ - yield ('x', self.__val['x']) - yield ('y', self.__val['y']) - - -@printer_regex('^openage::coord::(camhud|viewport|input|term)3(_delta)?') -class CoordXYZPrinter: - """ - Pretty printer for openage::coord::CoordXYZ. - - TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. - """ - - def __init__(self, val: gdb.Value): - self.__val = val - - def to_string(self): - """ - Get the coord as a string. - """ - return self.__val.type.name - - def children(self): - """ - Get the displayed children of the coord. - """ - yield ('x', self.__val['x']) - yield ('y', self.__val['y']) - yield ('z', self.__val['z']) + # Each coord type has one parent which is either + # of CoordNeSe, CoordNeSeUp, CoordXY, CoordXYZ + # From this parent we can get the fields + parent_type = self.__val.type.fields()[0].type + for child in parent_type.fields(): + yield (child.name, self.__val[child.name]) @printer_typedef('openage::time::time_t') From b284aa812d2b11a56c70057ba3dd923d7610fd76 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 16 Apr 2024 23:11:36 +0200 Subject: [PATCH 234/771] etc: Pretty print coord values in to_string(). --- etc/gdb_pretty/printers.py | 56 +++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index 9a083f36ae..d2f19685ad 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -92,10 +92,23 @@ def _register_printer(printer): return _register_printer +def format_fixed_point(value: int, fractional_bits: int) -> float: + """ + Formats a fixed point value to a double. + + :param value: The fixed point value. + :type value: int + :param fractional_bits: The number of fractional bits. + :type fractional_bits: int + """ + to_double_factor = 1 / pow(2, fractional_bits) + return float(value) * to_double_factor + + @printer_regex('^openage::coord::(camhud|chunk|input|phys|scene|term|tile|viewport)(2|3)?(_delta)?') class CoordPrinter: """ - Pretty printer for openage::coord::CoordNeSe. + Pretty printer for openage::coord types (CoordNeSe, CoordNeSeUp, CoordXY, CoordXYZ). TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. """ @@ -103,21 +116,33 @@ class CoordPrinter: def __init__(self, val: gdb.Value): self.__val = val + # Each coord type has one parent which is either + # of CoordNeSe, CoordNeSeUp, CoordXY, CoordXYZ + # From this parent we can get the fields + self._parent_type = self.__val.type.fields()[0].type + def to_string(self): """ Get the coord as a string. """ - return self.__val.type.name + field_vals = [] + for child in self._parent_type.fields(): + # Include the fixed point coordinates in the summary + val = self.__val[child.name] + num = format_fixed_point( + int(val['raw_value']), + int(val.type.template_argument(1)) + ) + field_vals.append(f"{num:.5f}") + + # Example: phys3[1.00000, 2.00000, 3.00000] + return f"{self.__val.type.tag.split('::')[-1]}[{', '.join(field_vals)}]" def children(self): """ Get the displayed children of the coord. """ - # Each coord type has one parent which is either - # of CoordNeSe, CoordNeSeUp, CoordXY, CoordXYZ - # From this parent we can get the fields - parent_type = self.__val.type.fields()[0].type - for child in parent_type.fields(): + for child in self._parent_type.fields(): yield (child.name, self.__val[child.name]) @@ -138,11 +163,11 @@ def to_string(self): Format: SS.sss (e.g. 12.345s) """ - fractional_bits = int(self.__val.type.template_argument(1)) + seconds = format_fixed_point( + int(self.__val['raw_value']), + int(self.__val.type.template_argument(1)) + ) - # convert the fixed point value to double - to_double_factor = 1 / pow(2, fractional_bits) - seconds = float(self.__val['raw_value']) * to_double_factor # show as seconds with millisecond precision return f'{seconds:.3f}s' @@ -170,11 +195,10 @@ def to_string(self): Format: 0.12345 """ - fractional_bits = int(self.__val.type.template_argument(1)) - - # convert the fixed point value to double - to_double_factor = 1 / pow(2, fractional_bits) - num = float(self.__val['raw_value']) * to_double_factor + num = format_fixed_point( + int(self.__val['raw_value']), + int(self.__val.type.template_argument(1)) + ) return f'{num:.5f}' def children(self): From b92138d401ac8f298cfb48c399a28eded5624ed0 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 19 Apr 2024 02:36:07 +0200 Subject: [PATCH 235/771] etc: Move Pretty print TODOs to the top of the module. --- etc/gdb_pretty/printers.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index d2f19685ad..ca8f92083c 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -7,6 +7,8 @@ import re import gdb # type: ignore +# TODO: Printers should inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + class PrinterControl(gdb.printing.PrettyPrinter): """ @@ -109,8 +111,6 @@ def format_fixed_point(value: int, fractional_bits: int) -> float: class CoordPrinter: """ Pretty printer for openage::coord types (CoordNeSe, CoordNeSeUp, CoordXY, CoordXYZ). - - TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. """ def __init__(self, val: gdb.Value): @@ -150,8 +150,6 @@ def children(self): class TimePrinter: """ Pretty printer for openage::time::time_t. - - TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. """ def __init__(self, val: gdb.Value): @@ -182,8 +180,6 @@ def children(self): class FixedPointPrinter: """ Pretty printer for openage::util::FixedPoint. - - TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. """ def __init__(self, val: gdb.Value): @@ -220,8 +216,6 @@ def children(self): class VectorPrinter: """ Pretty printer for openage::util::Vector. - - TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. """ def __init__(self, val: gdb.Value): @@ -267,8 +261,6 @@ def display_hint(): class KeyframePrinter: """ Pretty printer for openage::curve::Keyframe. - - TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. """ def __init__(self, val: gdb.Value): From d79640e4bf98eeb771c7b51caaf1b6fd2d33e9c2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 20 Apr 2024 13:26:29 +0200 Subject: [PATCH 236/771] etc: Put reminders for changing pretty printers in CPP classes. --- libopenage/coord/coord.h.template | 8 +++++++- libopenage/curve/keyframe.h | 5 ++++- libopenage/util/fixed_point.h | 3 +++ libopenage/util/vector.h | 3 +++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/libopenage/coord/coord.h.template b/libopenage/coord/coord.h.template index d4af555ea0..8aeafadc5f 100644 --- a/libopenage/coord/coord.h.template +++ b/libopenage/coord/coord.h.template @@ -1,4 +1,4 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. +// Copyright 2016-2024 the openage authors. See copying.md for legal info. #pragma once @@ -24,6 +24,9 @@ namespace coord { * * 'Absolute' and 'Relative' are the absolute and relative types of the * derived class (CRTP). + * + * If you change this class, remember to update the gdb pretty printers + * in etc/gdb_pretty/printers.py. */ template struct Coord${camelcase}Absolute { @@ -87,6 +90,9 @@ struct Coord${camelcase}Absolute { * * 'Absolute' and 'Relative' are the absolute and relative types of the * derived class (CRTP). + * + * If you change this class, remember to update the gdb pretty printers + * in etc/gdb_pretty/printers.py. */ template struct Coord${camelcase}Relative { diff --git a/libopenage/curve/keyframe.h b/libopenage/curve/keyframe.h index 82e36ed147..484085cc70 100644 --- a/libopenage/curve/keyframe.h +++ b/libopenage/curve/keyframe.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 @@ -11,6 +11,9 @@ namespace openage::curve { /** * A element of the curvecontainer. This is especially used to keep track of * the value-timing. + * + * If you change this class, remember to update the gdb pretty printers + * in etc/gdb_pretty/printers.py. */ template class Keyframe { diff --git a/libopenage/util/fixed_point.h b/libopenage/util/fixed_point.h index 7558ac9cb8..4e97e77323 100644 --- a/libopenage/util/fixed_point.h +++ b/libopenage/util/fixed_point.h @@ -81,6 +81,9 @@ constexpr static * For example, * FixedPoint * can store values from -2**32 to +2**32 with a constant precision of 2**-32. + * + * If you change this class, remember to update the gdb pretty printers + * in etc/gdb_pretty/printers.py. */ template class FixedPoint { diff --git a/libopenage/util/vector.h b/libopenage/util/vector.h index 8e7c7bceb8..e79283fb64 100644 --- a/libopenage/util/vector.h +++ b/libopenage/util/vector.h @@ -20,6 +20,9 @@ namespace openage::util { * * N = dimensions * T = underlying single value type (double, float, ...) + * + * If you change this class, remember to update the gdb pretty printers + * in etc/gdb_pretty/printers.py. */ template class Vector : public std::array { From ff7d234bf9b2c943d6db52336d1293a7793efc0a Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 20 Apr 2024 19:47:11 +0200 Subject: [PATCH 237/771] renderer: Add struct for window settings. --- libopenage/presenter/presenter.cpp | 15 ++++++++++----- libopenage/renderer/types.h | 10 ++++++++++ libopenage/renderer/window.cpp | 6 +++--- libopenage/renderer/window.h | 29 +++++++++++++++++++++++------ 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index b8c034987a..084ab49c96 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -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. #include "presenter.h" @@ -93,8 +93,13 @@ std::shared_ptr Presenter::init_window_system() { void Presenter::init_graphics(bool debug) { log::log(INFO << "Presenter: Initializing graphics subsystems..."); + // Start up rendering framework this->gui_app = this->init_window_system(); - this->window = renderer::Window::create("openage presenter test", 1024, 768, debug); + + // Window and renderer + renderer::window_settings settings; + settings.debug = debug; + this->window = renderer::Window::create("openage presenter test", 1024, 768, settings); this->renderer = this->window->make_renderer(); // Asset mangement @@ -190,10 +195,10 @@ void Presenter::init_gui() { this->gui = std::make_shared( this->gui_app, // Qt application wrapper - this->window, // window for the gui + this->window, // window for the gui qml_root_file, // entry qml file, absolute path. - qml_root, // directory to watch for qml file changes - qml_assets, // qml data: Engine *, the data directory, ... + qml_root, // directory to watch for qml file changes + qml_assets, // qml data: Engine *, the data directory, ... this->renderer // openage renderer ); diff --git a/libopenage/renderer/types.h b/libopenage/renderer/types.h index cade7159f0..cf764266ed 100644 --- a/libopenage/renderer/types.h +++ b/libopenage/renderer/types.h @@ -12,4 +12,14 @@ namespace openage::renderer { */ using uniform_id_t = uint32_t; +/** + * Graphics API types. + */ +enum class graphics_api_t { + DEFAULT, + OPENGL, + VULKAN, +}; + + } // namespace openage::renderer diff --git a/libopenage/renderer/window.cpp b/libopenage/renderer/window.cpp index 5cdd3b2052..35a00e918f 100644 --- a/libopenage/renderer/window.cpp +++ b/libopenage/renderer/window.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "window.h" @@ -13,11 +13,11 @@ namespace openage::renderer { std::shared_ptr Window::create(const std::string &title, size_t width, size_t height, - bool debug) { + window_settings settings) { // currently we only have a functional GL window // TODO: support other renderer windows // and add some selection mechanism. - return std::make_shared(title, width, height, debug); + return std::make_shared(title, width, height, settings.debug); } diff --git a/libopenage/renderer/window.h b/libopenage/renderer/window.h index 18c7ce04d8..a6a9011d56 100644 --- a/libopenage/renderer/window.h +++ b/libopenage/renderer/window.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -6,11 +6,12 @@ #include #include -#include "../util/vector.h" -#include "renderer.h" - #include +#include "renderer/renderer.h" +#include "renderer/types.h" +#include "util/vector.h" + QT_FORWARD_DECLARE_CLASS(QWindow) QT_FORWARD_DECLARE_CLASS(QKeyEvent) QT_FORWARD_DECLARE_CLASS(QMouseEvent) @@ -20,6 +21,22 @@ namespace openage::renderer { class WindowEventHandler; +/** + * Settings for creating a window. + */ +struct window_settings { + // Graphics API to use in the window's renderer. + graphics_api_t backend = graphics_api_t::DEFAULT; + // If true, enable vsync. + bool vsync = true; + // If true, enable debug logging for the selected backend. + bool debug = false; +}; + + +/** + * Represents a window that can be used to display graphics. + */ class Window { public: /** @@ -28,14 +45,14 @@ class Window { * @param title Window title shown in the Desktop Environment. * @param width Width in pixels. * @param height Height in pixels. - * @param debug If true, enable OpenGL debug logging. + * @param settings Settings for creating the window. * * @return The created Window instance. */ static std::shared_ptr create(const std::string &title, size_t width, size_t height, - bool debug = false); + window_settings settings = {}); virtual ~Window() = default; From eee03afbd89b8eded9d7a324d084818a2e366357 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 20 Apr 2024 19:55:54 +0200 Subject: [PATCH 238/771] renderer: Use window settings in OpenGL backend. --- libopenage/renderer/demo/demo_0.cpp | 6 ++++-- libopenage/renderer/demo/demo_1.cpp | 6 ++++-- libopenage/renderer/demo/demo_2.cpp | 14 ++++++++------ libopenage/renderer/demo/demo_3.cpp | 6 ++++-- libopenage/renderer/demo/demo_4.cpp | 14 ++++++++------ libopenage/renderer/demo/demo_5.cpp | 6 ++++-- libopenage/renderer/demo/stresstest_0.cpp | 6 ++++-- libopenage/renderer/opengl/window.cpp | 12 ++++++++---- libopenage/renderer/opengl/window.h | 6 +++--- libopenage/renderer/window.cpp | 2 +- 10 files changed, 48 insertions(+), 30 deletions(-) diff --git a/libopenage/renderer/demo/demo_0.cpp b/libopenage/renderer/demo/demo_0.cpp index 1db9061318..cdb62fbb59 100644 --- a/libopenage/renderer/demo/demo_0.cpp +++ b/libopenage/renderer/demo/demo_0.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "demo_0.h" @@ -13,7 +13,9 @@ namespace openage::renderer::tests { void renderer_demo_0(const util::Path &path) { auto qtapp = std::make_shared(); - opengl::GlWindow window("openage renderer test", 800, 600, true); + window_settings settings; + settings.debug = true; + opengl::GlWindow window("openage renderer test", 800, 600, settings); auto renderer = window.make_renderer(); auto shaderdir = path / "assets" / "test" / "shaders"; diff --git a/libopenage/renderer/demo/demo_1.cpp b/libopenage/renderer/demo/demo_1.cpp index da6bad36d0..c193b4b35e 100644 --- a/libopenage/renderer/demo/demo_1.cpp +++ b/libopenage/renderer/demo/demo_1.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "demo_1.h" @@ -19,7 +19,9 @@ namespace openage::renderer::tests { void renderer_demo_1(const util::Path &path) { auto qtapp = std::make_shared(); - opengl::GlWindow window("openage renderer test", 800, 600, true); + window_settings settings; + settings.debug = true; + opengl::GlWindow window("openage renderer test", 800, 600, settings); auto renderer = window.make_renderer(); auto shaderdir = path / "assets" / "test" / "shaders"; diff --git a/libopenage/renderer/demo/demo_2.cpp b/libopenage/renderer/demo/demo_2.cpp index 931371c0c6..765c5e37df 100644 --- a/libopenage/renderer/demo/demo_2.cpp +++ b/libopenage/renderer/demo/demo_2.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "demo_2.h" @@ -22,7 +22,9 @@ namespace openage::renderer::tests { void renderer_demo_2(const util::Path &path) { auto qtapp = std::make_shared(); - opengl::GlWindow window("openage renderer test", 800, 600, true); + window_settings settings; + settings.debug = true; + opengl::GlWindow window("openage renderer test", 800, 600, settings); auto renderer = window.make_renderer(); /* Load texture file standalone. */ @@ -150,10 +152,10 @@ void renderer_demo_2(const util::Path &path) { 1.0f)); /* Pass uniforms to the shaders. - mv : The upscaling matrix - offset_tile : Subtexture coordinates (as floats relative to texture image size) - u_id : Identifier - tex : OpenGL texture reference + mv : The upscaling matrix + offset_tile : Subtexture coordinates (as floats relative to texture image size) + u_id : Identifier + tex : OpenGL texture reference */ auto obj1_unifs = obj_shader->new_uniform_input( "mv", diff --git a/libopenage/renderer/demo/demo_3.cpp b/libopenage/renderer/demo/demo_3.cpp index 835c13a364..ebaffaff80 100644 --- a/libopenage/renderer/demo/demo_3.cpp +++ b/libopenage/renderer/demo/demo_3.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "demo_3.h" @@ -28,7 +28,9 @@ namespace openage::renderer::tests { void renderer_demo_3(const util::Path &path) { auto qtapp = std::make_shared(); - auto window = std::make_shared("openage renderer test", 800, 600, true); + window_settings settings; + settings.debug = true; + auto window = std::make_shared("openage renderer test", 800, 600, settings); auto renderer = window->make_renderer(); // Clock required by world renderer for timing animation frames diff --git a/libopenage/renderer/demo/demo_4.cpp b/libopenage/renderer/demo/demo_4.cpp index 82250cd1dc..abdac51c26 100644 --- a/libopenage/renderer/demo/demo_4.cpp +++ b/libopenage/renderer/demo/demo_4.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "demo_4.h" @@ -20,7 +20,9 @@ namespace openage::renderer::tests { void renderer_demo_4(const util::Path &path) { auto qtapp = std::make_shared(); - opengl::GlWindow window("openage renderer test", 800, 600, true); + window_settings settings; + settings.debug = true; + opengl::GlWindow window("openage renderer test", 800, 600, settings); auto renderer = window.make_renderer(); /* Clock for timed display */ @@ -97,10 +99,10 @@ void renderer_demo_4(const util::Path &path) { 1.0f)); /* Pass uniforms to the shaders. - mv : The upscaling matrix - offset_tile : Subtexture coordinates (as floats relative to texture image size) - u_id : Identifier - tex : OpenGL texture reference + mv : The upscaling matrix + offset_tile : Subtexture coordinates (as floats relative to texture image size) + u_id : Identifier + tex : OpenGL texture reference */ auto obj1_unifs = obj_shader->new_uniform_input( "mv", diff --git a/libopenage/renderer/demo/demo_5.cpp b/libopenage/renderer/demo/demo_5.cpp index afea99a30b..5fddd537ca 100644 --- a/libopenage/renderer/demo/demo_5.cpp +++ b/libopenage/renderer/demo/demo_5.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 "demo_5.h" @@ -22,7 +22,9 @@ namespace openage::renderer::tests { void renderer_demo_5(const util::Path &path) { auto qtapp = std::make_shared(); - opengl::GlWindow window("openage renderer test", 800, 600, true); + window_settings settings; + settings.debug = true; + opengl::GlWindow window("openage renderer test", 800, 600, settings); auto renderer = window.make_renderer(); auto size = window.get_size(); diff --git a/libopenage/renderer/demo/stresstest_0.cpp b/libopenage/renderer/demo/stresstest_0.cpp index 7167150ac9..afa9a53dcf 100644 --- a/libopenage/renderer/demo/stresstest_0.cpp +++ b/libopenage/renderer/demo/stresstest_0.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 "stresstest_0.h" @@ -28,7 +28,9 @@ namespace openage::renderer::tests { void renderer_stresstest_0(const util::Path &path) { auto qtapp = std::make_shared(); - auto window = std::make_shared("openage renderer test", 1024, 768, true); + window_settings settings; + settings.debug = true; + auto window = std::make_shared("openage renderer test", 1024, 768, settings); auto renderer = window->make_renderer(); // Clock required by world renderer for timing animation frames diff --git a/libopenage/renderer/opengl/window.cpp b/libopenage/renderer/opengl/window.cpp index d169bff00b..ad8699149d 100644 --- a/libopenage/renderer/opengl/window.cpp +++ b/libopenage/renderer/opengl/window.cpp @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #include "window.h" @@ -20,7 +20,7 @@ namespace openage::renderer::opengl { GlWindow::GlWindow(const std::string &title, size_t width, size_t height, - bool debug) : + window_settings settings) : Window{width, height} { if (QGuiApplication::instance() == nullptr) { // Qt windows need to attach to a QtGuiApplication @@ -40,6 +40,10 @@ GlWindow::GlWindow(const std::string &title, format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); format.setSwapBehavior(QSurfaceFormat::SwapBehavior::DoubleBuffer); + if (not settings.vsync) { + format.setSwapInterval(0); + } + format.setMajorVersion(gl_specs.major_version); format.setMinorVersion(gl_specs.minor_version); @@ -47,7 +51,7 @@ GlWindow::GlWindow(const std::string &title, format.setDepthBufferSize(24); format.setStencilBufferSize(8); - if (debug) { + if (settings.debug) { format.setOption(QSurfaceFormat::DebugContext); } @@ -55,7 +59,7 @@ GlWindow::GlWindow(const std::string &title, this->window->setFormat(format); this->window->create(); - this->context = std::make_shared(this->window, debug); + this->context = std::make_shared(this->window, settings.debug); if (not this->context->get_raw_context()->isValid()) { throw Error{MSG(err) << "Failed to create Qt OpenGL context."}; } diff --git a/libopenage/renderer/opengl/window.h b/libopenage/renderer/opengl/window.h index 9d1600a329..38bc32a046 100644 --- a/libopenage/renderer/opengl/window.h +++ b/libopenage/renderer/opengl/window.h @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #pragma once @@ -27,12 +27,12 @@ class GlWindow final : public Window { * @param title The window title. * @param width Width (in pixels). * @param height Height (in pixels). - * @param debug If true, enable OpenGL debug logging. + * @param settings Settings for creating the window. */ GlWindow(const std::string &title, size_t width, size_t height, - bool debug = false); + window_settings settings = {}); ~GlWindow(); void set_size(size_t width, size_t height) override; diff --git a/libopenage/renderer/window.cpp b/libopenage/renderer/window.cpp index 35a00e918f..ba05ac7511 100644 --- a/libopenage/renderer/window.cpp +++ b/libopenage/renderer/window.cpp @@ -17,7 +17,7 @@ std::shared_ptr Window::create(const std::string &title, // currently we only have a functional GL window // TODO: support other renderer windows // and add some selection mechanism. - return std::make_shared(title, width, height, settings.debug); + return std::make_shared(title, width, height, settings); } From a2552f13b32adb43617ee4a8e2560ae6ea405bec Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 20 Apr 2024 20:02:54 +0200 Subject: [PATCH 239/771] renderer: Turn off vsync in stresstest. --- libopenage/renderer/demo/stresstest_0.cpp | 1 + libopenage/renderer/opengl/context.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/libopenage/renderer/demo/stresstest_0.cpp b/libopenage/renderer/demo/stresstest_0.cpp index afa9a53dcf..cb3c2cdb90 100644 --- a/libopenage/renderer/demo/stresstest_0.cpp +++ b/libopenage/renderer/demo/stresstest_0.cpp @@ -29,6 +29,7 @@ void renderer_stresstest_0(const util::Path &path) { auto qtapp = std::make_shared(); window_settings settings; + settings.vsync = false; settings.debug = true; auto window = std::make_shared("openage renderer test", 1024, 768, settings); auto renderer = window->make_renderer(); diff --git a/libopenage/renderer/opengl/context.h b/libopenage/renderer/opengl/context.h index 42c7564118..1b9cd936ff 100644 --- a/libopenage/renderer/opengl/context.h +++ b/libopenage/renderer/opengl/context.h @@ -86,7 +86,7 @@ class GlContext { * Activate or deactivate VSync for this context. * * TODO: This currently does not work at runtime. vsync must be set before - * the QApplication is created. + * the QWindow is created. * * @param on \p true to activate VSync, \p false to deactivate. */ From 6688fa382cd97ab7e147372c198305f5693fe422 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Apr 2024 16:27:48 +0200 Subject: [PATCH 240/771] renderer: Move window size to settings struct. --- libopenage/input/tests.cpp | 5 +++-- libopenage/main/demo/pong/gui.cpp | 10 +++++----- libopenage/presenter/presenter.cpp | 4 +++- libopenage/renderer/demo/demo_0.cpp | 4 +++- libopenage/renderer/demo/demo_1.cpp | 4 +++- libopenage/renderer/demo/demo_2.cpp | 4 +++- libopenage/renderer/demo/demo_3.cpp | 4 +++- libopenage/renderer/demo/demo_4.cpp | 4 +++- libopenage/renderer/demo/demo_5.cpp | 4 +++- libopenage/renderer/demo/stresstest_0.cpp | 4 +++- libopenage/renderer/opengl/window.cpp | 6 ++---- libopenage/renderer/opengl/window.h | 4 ---- libopenage/renderer/window.cpp | 4 +--- libopenage/renderer/window.h | 8 ++++---- 14 files changed, 39 insertions(+), 30 deletions(-) diff --git a/libopenage/input/tests.cpp b/libopenage/input/tests.cpp index 735ee3ddce..b97efb9219 100644 --- a/libopenage/input/tests.cpp +++ b/libopenage/input/tests.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 "error/error.h" #include "log/log.h" @@ -14,7 +14,8 @@ void action_demo() { auto qtapp = std::make_shared(); // create a window where we get our inputs from - renderer::opengl::GlWindow window("openage input test", 800, 600); + + renderer::opengl::GlWindow window("openage input test"); // manager that receives window inputs // the manager creates its own global context with ID "main" diff --git a/libopenage/main/demo/pong/gui.cpp b/libopenage/main/demo/pong/gui.cpp index 9800357ec0..bc2d334b3e 100644 --- a/libopenage/main/demo/pong/gui.cpp +++ b/libopenage/main/demo/pong/gui.cpp @@ -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. #include "gui.h" @@ -6,8 +6,8 @@ #include #include -#include "main/demo/pong/gamestate.h" #include "log/log.h" +#include "main/demo/pong/gamestate.h" #include "renderer/geometry.h" #include "renderer/opengl/context.h" #include "renderer/opengl/shader.h" @@ -34,7 +34,7 @@ const std::vector &Gui::get_inputs(const std::shared_ptr /* for (all inputs from window) { - add key to inputs vector; + add key to inputs vector; } */ @@ -78,7 +78,7 @@ constexpr const int max_log_msgs = 10; Gui::Gui() : - window{"openage engine test", 800, 600}, + window{"openage engine test", {800, 600}}, renderer{window.make_renderer()} { auto vshader_src = renderer::resources::ShaderSource( renderer::resources::shader_lang_t::glsl, @@ -199,7 +199,7 @@ void Gui::draw(const std::shared_ptr &state, const time::time_t &now) auto ball_pos = state->ball->position->get(now); auto ball_pos_matrix = Eigen::Affine3f::Identity(); ball_pos_matrix.prescale(Eigen::Vector3f(ball_size, ball_size, 1.0f)); - //ball_pos_matrix.prerotate(Eigen::AngleAxisf(45.0f * math::PI / 180.0f, Eigen::Vector3f::UnitZ())); + // ball_pos_matrix.prerotate(Eigen::AngleAxisf(45.0f * math::PI / 180.0f, Eigen::Vector3f::UnitZ())); ball_pos_matrix.pretranslate(Eigen::Vector3f(ball_pos[0], ball_pos[1], 0.0f)); this->ball.uniform->update("pos", ball_pos_matrix.matrix()); diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index 084ab49c96..38c741f8fd 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -98,8 +98,10 @@ void Presenter::init_graphics(bool debug) { // Window and renderer renderer::window_settings settings; + settings.width = 1024; + settings.height = 768; settings.debug = debug; - this->window = renderer::Window::create("openage presenter test", 1024, 768, settings); + this->window = renderer::Window::create("openage presenter test", settings); this->renderer = this->window->make_renderer(); // Asset mangement diff --git a/libopenage/renderer/demo/demo_0.cpp b/libopenage/renderer/demo/demo_0.cpp index cdb62fbb59..617b7c6f75 100644 --- a/libopenage/renderer/demo/demo_0.cpp +++ b/libopenage/renderer/demo/demo_0.cpp @@ -14,8 +14,10 @@ void renderer_demo_0(const util::Path &path) { auto qtapp = std::make_shared(); window_settings settings; + settings.width = 800; + settings.height = 600; settings.debug = true; - opengl::GlWindow window("openage renderer test", 800, 600, settings); + opengl::GlWindow window("openage renderer test", settings); auto renderer = window.make_renderer(); auto shaderdir = path / "assets" / "test" / "shaders"; diff --git a/libopenage/renderer/demo/demo_1.cpp b/libopenage/renderer/demo/demo_1.cpp index c193b4b35e..312efb47c4 100644 --- a/libopenage/renderer/demo/demo_1.cpp +++ b/libopenage/renderer/demo/demo_1.cpp @@ -20,8 +20,10 @@ void renderer_demo_1(const util::Path &path) { auto qtapp = std::make_shared(); window_settings settings; + settings.width = 800; + settings.height = 600; settings.debug = true; - opengl::GlWindow window("openage renderer test", 800, 600, settings); + opengl::GlWindow window("openage renderer test", settings); auto renderer = window.make_renderer(); auto shaderdir = path / "assets" / "test" / "shaders"; diff --git a/libopenage/renderer/demo/demo_2.cpp b/libopenage/renderer/demo/demo_2.cpp index 765c5e37df..ac30adc78a 100644 --- a/libopenage/renderer/demo/demo_2.cpp +++ b/libopenage/renderer/demo/demo_2.cpp @@ -23,8 +23,10 @@ void renderer_demo_2(const util::Path &path) { auto qtapp = std::make_shared(); window_settings settings; + settings.width = 800; + settings.height = 600; settings.debug = true; - opengl::GlWindow window("openage renderer test", 800, 600, settings); + opengl::GlWindow window("openage renderer test", settings); auto renderer = window.make_renderer(); /* Load texture file standalone. */ diff --git a/libopenage/renderer/demo/demo_3.cpp b/libopenage/renderer/demo/demo_3.cpp index ebaffaff80..c143bd0a13 100644 --- a/libopenage/renderer/demo/demo_3.cpp +++ b/libopenage/renderer/demo/demo_3.cpp @@ -29,8 +29,10 @@ void renderer_demo_3(const util::Path &path) { auto qtapp = std::make_shared(); window_settings settings; + settings.width = 800; + settings.height = 600; settings.debug = true; - auto window = std::make_shared("openage renderer test", 800, 600, settings); + auto window = std::make_shared("openage renderer test", settings); auto renderer = window->make_renderer(); // Clock required by world renderer for timing animation frames diff --git a/libopenage/renderer/demo/demo_4.cpp b/libopenage/renderer/demo/demo_4.cpp index abdac51c26..7bac7a1654 100644 --- a/libopenage/renderer/demo/demo_4.cpp +++ b/libopenage/renderer/demo/demo_4.cpp @@ -21,8 +21,10 @@ void renderer_demo_4(const util::Path &path) { auto qtapp = std::make_shared(); window_settings settings; + settings.width = 800; + settings.height = 600; settings.debug = true; - opengl::GlWindow window("openage renderer test", 800, 600, settings); + opengl::GlWindow window("openage renderer test", settings); auto renderer = window.make_renderer(); /* Clock for timed display */ diff --git a/libopenage/renderer/demo/demo_5.cpp b/libopenage/renderer/demo/demo_5.cpp index 5fddd537ca..73c2c15226 100644 --- a/libopenage/renderer/demo/demo_5.cpp +++ b/libopenage/renderer/demo/demo_5.cpp @@ -23,8 +23,10 @@ void renderer_demo_5(const util::Path &path) { auto qtapp = std::make_shared(); window_settings settings; + settings.width = 800; + settings.height = 600; settings.debug = true; - opengl::GlWindow window("openage renderer test", 800, 600, settings); + opengl::GlWindow window("openage renderer test", settings); auto renderer = window.make_renderer(); auto size = window.get_size(); diff --git a/libopenage/renderer/demo/stresstest_0.cpp b/libopenage/renderer/demo/stresstest_0.cpp index cb3c2cdb90..ce7da14948 100644 --- a/libopenage/renderer/demo/stresstest_0.cpp +++ b/libopenage/renderer/demo/stresstest_0.cpp @@ -29,9 +29,11 @@ void renderer_stresstest_0(const util::Path &path) { auto qtapp = std::make_shared(); window_settings settings; + settings.width = 1024; + settings.height = 768; settings.vsync = false; settings.debug = true; - auto window = std::make_shared("openage renderer test", 1024, 768, settings); + auto window = std::make_shared("openage renderer test", settings); auto renderer = window->make_renderer(); // Clock required by world renderer for timing animation frames diff --git a/libopenage/renderer/opengl/window.cpp b/libopenage/renderer/opengl/window.cpp index ad8699149d..8389c6e241 100644 --- a/libopenage/renderer/opengl/window.cpp +++ b/libopenage/renderer/opengl/window.cpp @@ -18,10 +18,8 @@ namespace openage::renderer::opengl { GlWindow::GlWindow(const std::string &title, - size_t width, - size_t height, window_settings settings) : - Window{width, height} { + Window{settings.width, settings.height} { if (QGuiApplication::instance() == nullptr) { // Qt windows need to attach to a QtGuiApplication throw Error{MSG(err) << "Failed to create Qt window: QGuiApplication has not been created yet."}; @@ -31,7 +29,7 @@ GlWindow::GlWindow(const std::string &title, this->window = std::make_shared(); this->window->setTitle(QString::fromStdString(title)); - this->window->resize(width, height); + this->window->resize(settings.width, settings.height); this->window->setSurfaceType(QSurface::OpenGLSurface); diff --git a/libopenage/renderer/opengl/window.h b/libopenage/renderer/opengl/window.h index 38bc32a046..7bf5ca7af2 100644 --- a/libopenage/renderer/opengl/window.h +++ b/libopenage/renderer/opengl/window.h @@ -25,13 +25,9 @@ class GlWindow final : public Window { * Create a shiny window with the given title. * * @param title The window title. - * @param width Width (in pixels). - * @param height Height (in pixels). * @param settings Settings for creating the window. */ GlWindow(const std::string &title, - size_t width, - size_t height, window_settings settings = {}); ~GlWindow(); diff --git a/libopenage/renderer/window.cpp b/libopenage/renderer/window.cpp index ba05ac7511..497f36c535 100644 --- a/libopenage/renderer/window.cpp +++ b/libopenage/renderer/window.cpp @@ -11,13 +11,11 @@ namespace openage::renderer { std::shared_ptr Window::create(const std::string &title, - size_t width, - size_t height, window_settings settings) { // currently we only have a functional GL window // TODO: support other renderer windows // and add some selection mechanism. - return std::make_shared(title, width, height, settings); + return std::make_shared(title, settings); } diff --git a/libopenage/renderer/window.h b/libopenage/renderer/window.h index a6a9011d56..2ac9c13430 100644 --- a/libopenage/renderer/window.h +++ b/libopenage/renderer/window.h @@ -25,6 +25,10 @@ class WindowEventHandler; * Settings for creating a window. */ struct window_settings { + // Width of the window in pixels. + size_t width = 1024; + // Height of the window in pixels. + size_t height = 768; // Graphics API to use in the window's renderer. graphics_api_t backend = graphics_api_t::DEFAULT; // If true, enable vsync. @@ -43,15 +47,11 @@ class Window { * Create a new Window instance for displaying stuff. * * @param title Window title shown in the Desktop Environment. - * @param width Width in pixels. - * @param height Height in pixels. * @param settings Settings for creating the window. * * @return The created Window instance. */ static std::shared_ptr create(const std::string &title, - size_t width, - size_t height, window_settings settings = {}); virtual ~Window() = default; From d2d9c33c7911f14395ea3b60d3c0ea9f9dcb1d01 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 20 Apr 2024 21:08:25 +0200 Subject: [PATCH 241/771] renderer: Compute camera scaling variables for uniform buffer, --- assets/shaders/terrain.vert.glsl | 12 ++++++++-- assets/shaders/world2d.vert.glsl | 19 ++++++++++----- libopenage/renderer/camera/camera.cpp | 23 +++++++++++-------- libopenage/renderer/camera/camera.h | 9 ++++++++ libopenage/renderer/stages/camera/manager.cpp | 20 +++++++++++++--- 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/assets/shaders/terrain.vert.glsl b/assets/shaders/terrain.vert.glsl index aef9b45867..614f6f382b 100644 --- a/assets/shaders/terrain.vert.glsl +++ b/assets/shaders/terrain.vert.glsl @@ -7,9 +7,17 @@ out vec2 tex_pos; uniform mat4 model; +// camera parameters for transforming the object position +// and scaling the subtex to the correct size layout (std140) uniform camera { - mat4 view; - mat4 proj; + // view matrix (world to view space) + mat4 view; + // projection matrix (view to clip space) + mat4 proj; + // inverse zoom factor (1.0 / zoom) + float inv_zoom; + // inverse viewport size (1.0 / viewport size) + vec2 inv_viewport_size; }; void main() { diff --git a/assets/shaders/world2d.vert.glsl b/assets/shaders/world2d.vert.glsl index 76cd6db34d..edb557e4a9 100644 --- a/assets/shaders/world2d.vert.glsl +++ b/assets/shaders/world2d.vert.glsl @@ -5,10 +5,17 @@ layout(location=1) in vec2 uv; out vec2 vert_uv; -// transformation for object (not vertex!) position to clip space +// camera parameters for transforming the object position +// and scaling the subtex to the correct size layout (std140) uniform camera { - mat4 view; - mat4 proj; + // view matrix (world to view space) + mat4 view; + // projection matrix (view to clip space) + mat4 proj; + // inverse zoom factor (1.0 / zoom) + float inv_zoom; + // inverse viewport size (1.0 / viewport size) + vec2 inv_viewport_size; }; // can be used to move the object position in world space _before_ @@ -51,9 +58,9 @@ void main() { // create a move matrix for positioning the vertices // uses the scale and the transformed object position in clip space - mat4 move = mat4(scale.x, 0.0, 0.0, 0.0, - 0.0, scale.y, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, + mat4 move = mat4(scale.x, 0.0, 0.0, 0.0, + 0.0, scale.y, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, obj_clip_pos.x, obj_clip_pos.y, obj_clip_pos.z, 1.0); // calculate the final vertex position diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index 604ddf440f..884aceca1b 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #include "camera.h" @@ -28,10 +28,7 @@ Camera::Camera(const std::shared_ptr &renderer, proj{Eigen::Matrix4f::Identity()} { this->look_at_scene(Eigen::Vector3f(0.0f, 0.0f, 0.0f)); - resources::UBOInput view_input{"view", resources::ubo_input_t::M4F32}; - resources::UBOInput proj_input{"proj", resources::ubo_input_t::M4F32}; - auto ubo_info = resources::UniformBufferInfo{resources::ubo_layout_t::STD140, {view_input, proj_input}}; - this->uniform_buffer = renderer->add_uniform_buffer(ubo_info); + this->init_uniform_buffer(renderer); log::log(INFO << "Created new camera at position " << "(" << this->scene_pos[0] @@ -55,10 +52,7 @@ Camera::Camera(const std::shared_ptr &renderer, viewport_changed{true}, view{Eigen::Matrix4f::Identity()}, proj{Eigen::Matrix4f::Identity()} { - resources::UBOInput view_input{"view", resources::ubo_input_t::M4F32}; - resources::UBOInput proj_input{"proj", resources::ubo_input_t::M4F32}; - auto ubo_info = resources::UniformBufferInfo{resources::ubo_layout_t::STD140, {view_input, proj_input}}; - this->uniform_buffer = renderer->add_uniform_buffer(ubo_info); + this->init_uniform_buffer(renderer); log::log(INFO << "Created new camera at position " << "(" << this->scene_pos[0] @@ -269,4 +263,15 @@ const std::shared_ptr &Camera::get_uniform_buffer() con return this->uniform_buffer; } +void Camera::init_uniform_buffer(const std::shared_ptr &renderer) { + resources::UBOInput view_input{"view", resources::ubo_input_t::M4F32}; + resources::UBOInput proj_input{"proj", resources::ubo_input_t::M4F32}; + resources::UBOInput inv_zoom_input{"inv_zoom", resources::ubo_input_t::F32}; + resources::UBOInput inv_viewport_size{"inv_viewport_size", resources::ubo_input_t::V2F32}; + auto ubo_info = resources::UniformBufferInfo{ + resources::ubo_layout_t::STD140, + {view_input, proj_input, inv_zoom_input, inv_viewport_size}}; + this->uniform_buffer = renderer->add_uniform_buffer(ubo_info); +} + } // namespace openage::renderer::camera diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index 9cdd559e6f..c8c5c23076 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -43,6 +43,7 @@ class Camera { * The camera uses default values. Its centered on the origin of the scene (0.0f, 0.0f, 0.0f) * and has a zoom level of 1.0f. * + * @param renderer openage renderer instance. * @param viewport_size Initial viewport size of the camera (width x height). */ Camera(const std::shared_ptr &renderer, @@ -51,6 +52,7 @@ class Camera { /** * Create a new camera for the renderer. * + * @param renderer openage renderer instance. * @param viewport_size Viewport size of the camera (width x height). * @param scene_pos Position of the camera in the scene. * @param zoom Zoom level of the camera (defaults to 1.0f). @@ -187,6 +189,13 @@ class Camera { const std::shared_ptr &get_uniform_buffer() const; private: + /** + * Create the uniform buffer for the camera. + * + * @param renderer openage renderer instance. + */ + void init_uniform_buffer(const std::shared_ptr &renderer); + /** * Position in the 3D scene. */ diff --git a/libopenage/renderer/stages/camera/manager.cpp b/libopenage/renderer/stages/camera/manager.cpp index 580f7db597..39eddd2b52 100644 --- a/libopenage/renderer/stages/camera/manager.cpp +++ b/libopenage/renderer/stages/camera/manager.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 "manager.h" @@ -97,11 +97,25 @@ void CameraManager::update_motion() { } void CameraManager::update_uniforms() { + // transformation matrices this->uniforms->update( "view", - camera->get_view_matrix(), + this->camera->get_view_matrix(), "proj", - camera->get_projection_matrix()); + this->camera->get_projection_matrix()); + + // zoom scaling + this->uniforms->update( + "inv_zoom", + 1.0f / this->camera->get_zoom()); + + auto viewport_size = this->camera->get_viewport_size(); + Eigen::Vector2f viewport_size_vec{ + 1.0f / static_cast(viewport_size[0]), + 1.0f / static_cast(viewport_size[1])}; + this->uniforms->update("inv_viewport_size", viewport_size_vec); + + // update the uniform buffer this->camera->get_uniform_buffer()->update_uniforms(this->uniforms); } From f8d0f08a2c6d3a10b1ff106c658066cfe7db7094 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Apr 2024 00:05:03 +0200 Subject: [PATCH 242/771] renderer: Calculate zoom scale from uniform buffer values. --- assets/shaders/world2d.vert.glsl | 28 +++++++++++++++---- libopenage/renderer/stages/world/object.cpp | 19 +++++++++++-- .../renderer/stages/world/render_stage.cpp | 4 +-- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/assets/shaders/world2d.vert.glsl b/assets/shaders/world2d.vert.glsl index edb557e4a9..3c763d4ae3 100644 --- a/assets/shaders/world2d.vert.glsl +++ b/assets/shaders/world2d.vert.glsl @@ -36,21 +36,37 @@ uniform bool flip_y; // offset from the subtex anchor // moves the subtex relative to the subtex center -uniform vec2 anchor_offset; +// uniform vec2 anchor_offset; // scales the vertex positions so that they // match the subtex dimensions -uniform vec2 scale; +// uniform vec2 scale; + +uniform float scalefactor; +uniform float zoom; +uniform vec2 screen_size; +uniform vec2 subtex_size; +uniform vec2 anchor; void main() { // translate the position of the object from world space to clip space // this is the position where we want to draw the subtex in 2D vec4 obj_clip_pos = proj * view * model * vec4(obj_world_position, 1.0); + float obj_scale = scalefactor * inv_zoom; + vec2 obj_scale_vec = vec2( + obj_scale * (subtex_size.x / screen_size.x), + obj_scale * (subtex_size.y / screen_size.y) + ); + vec2 obj_anchor_vec = vec2( + obj_scale * (anchor.x / screen_size.x), + obj_scale * (anchor.y / screen_size.y) + ); + // if the subtex is flipped, we also need to flip the anchor offset // essentially, we invert the coordinates for the flipped axis - float anchor_x = float(flip_x) * -1.0 * anchor_offset.x + float(!flip_x) * anchor_offset.x; - float anchor_y = float(flip_y) * -1.0 * anchor_offset.y + float(!flip_y) * anchor_offset.y; + float anchor_x = float(flip_x) * -1.0 * obj_anchor_vec.x + float(!flip_x) * obj_anchor_vec.x; + float anchor_y = float(flip_y) * -1.0 * obj_anchor_vec.y + float(!flip_y) * obj_anchor_vec.y; // offset the clip position by the offset of the subtex anchor // imagine this as pinning the subtex to the object position at the subtex anchor point @@ -58,8 +74,8 @@ void main() { // create a move matrix for positioning the vertices // uses the scale and the transformed object position in clip space - mat4 move = mat4(scale.x, 0.0, 0.0, 0.0, - 0.0, scale.y, 0.0, 0.0, + mat4 move = mat4(obj_scale_vec.x, 0.0, 0.0, 0.0, + 0.0, obj_scale_vec.y, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, obj_clip_pos.x, obj_clip_pos.y, obj_clip_pos.z, 1.0); diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index 23915d420f..7709d2223a 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -148,14 +148,29 @@ void WorldObject::update_uniforms(const time::time_t &time) { auto scale_vec = Eigen::Vector2f{ scale * (static_cast(subtex_size[0]) / screen_size[0]), scale * (static_cast(subtex_size[1]) / screen_size[1])}; - this->uniforms->update(this->scale, scale_vec); + // this->uniforms->update(this->scale, scale_vec); // Move subtexture in scene so that its anchor point is at the object's position auto anchor = tex_info->get_subtex_info(subtex_idx).get_anchor_params(); auto anchor_offset = Eigen::Vector2f{ scale * (static_cast(anchor[0]) / screen_size[0]), scale * (static_cast(anchor[1]) / screen_size[1])}; - this->uniforms->update(this->anchor_offset, anchor_offset); + // this->uniforms->update(this->anchor_offset, anchor_offset); + + this->uniforms->update("scalefactor", animation_info->get_scalefactor()); + // this->uniforms->update("zoom", this->camera->get_zoom()); + Eigen::Vector2f screen_size_vec{ + static_cast(screen_size[0]), + static_cast(screen_size[1])}; + this->uniforms->update("screen_size", screen_size_vec); + Eigen::Vector2f subtex_size_vec{ + static_cast(subtex_size[0]), + static_cast(subtex_size[1])}; + this->uniforms->update("subtex_size", subtex_size_vec); + Eigen::Vector2f anchor_vec{ + static_cast(anchor[0]), + static_cast(anchor[1])}; + this->uniforms->update("anchor", anchor_vec); } uint32_t WorldObject::get_id() { diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 85e5da7048..e97b52cabf 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -135,8 +135,8 @@ void WorldRenderStage::init_uniform_ids() { WorldObject::flip_y = this->display_shader->get_uniform_id("flip_y"); WorldObject::tex = this->display_shader->get_uniform_id("tex"); WorldObject::tile_params = this->display_shader->get_uniform_id("tile_params"); - WorldObject::scale = this->display_shader->get_uniform_id("scale"); - WorldObject::anchor_offset = this->display_shader->get_uniform_id("anchor_offset"); + // WorldObject::scale = this->display_shader->get_uniform_id("scale"); + // WorldObject::anchor_offset = this->display_shader->get_uniform_id("anchor_offset"); } } // namespace openage::renderer::world From 81124bea3935d8713c99dd98b68400e83bdf6068 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Apr 2024 15:17:38 +0200 Subject: [PATCH 243/771] renderer: Check if uniform buffer conforms to uniform block definition. --- libopenage/renderer/opengl/shader_program.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/libopenage/renderer/opengl/shader_program.cpp b/libopenage/renderer/opengl/shader_program.cpp index 3ccd49ce80..31b0d17778 100644 --- a/libopenage/renderer/opengl/shader_program.cpp +++ b/libopenage/renderer/opengl/shader_program.cpp @@ -1,4 +1,4 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #include "shader_program.h" @@ -445,7 +445,12 @@ void GlShaderProgram::bind_uniform_buffer(const char *block_name, std::shared_pt auto gl_buffer = std::dynamic_pointer_cast(buffer); auto &block = this->uniform_blocks[block_name]; - // TODO: Check if the uniform buffer matches the block definition + // Check if the uniform buffer matches the block definition + for (auto const &pair : block.uniforms) { + auto const &unif = pair.second; + ENSURE(gl_buffer->has_uniform(pair.first.c_str()), + "Uniform buffer does not contain uniform '" << pair.first << "' required by block " << block_name); + } block.binding_point = gl_buffer->get_binding_point(); glUniformBlockBinding(*this->handle, block.index, block.binding_point); From 24da277e08bed71bdf99acecf6557c59278c43dc Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Apr 2024 15:19:48 +0200 Subject: [PATCH 244/771] renderer: Fix alignment of uniform buffer inputs. --- libopenage/renderer/opengl/renderer.cpp | 16 +++++++++++----- libopenage/renderer/resources/buffer_info.cpp | 10 ++++++++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/libopenage/renderer/opengl/renderer.cpp b/libopenage/renderer/opengl/renderer.cpp index a06cc16edf..d26ba7b96c 100644 --- a/libopenage/renderer/opengl/renderer.cpp +++ b/libopenage/renderer/opengl/renderer.cpp @@ -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. #include "renderer.h" @@ -33,10 +33,10 @@ GlRenderer::GlRenderer(const std::shared_ptr &ctx, // global GL alpha blending settings // glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFuncSeparate( - GL_SRC_ALPHA, // source (overlaying) RGB factor + GL_SRC_ALPHA, // source (overlaying) RGB factor GL_ONE_MINUS_SRC_ALPHA, // destination (underlying) RGB factor - GL_ONE, // source (overlaying) alpha factor - GL_ONE_MINUS_SRC_ALPHA // destination (underlying) alpha factor + GL_ONE, // source (overlaying) alpha factor + GL_ONE_MINUS_SRC_ALPHA // destination (underlying) alpha factor ); // global GL depth testing settings @@ -90,6 +90,11 @@ std::shared_ptr GlRenderer::add_uniform_buffer(resources::Uniform size_t offset = 0; for (auto const &input : inputs) { auto type = GL_UBO_INPUT_TYPE.get(input.type); + auto size = resources::UniformBufferInfo::get_size(input, info.get_layout()); + + // align offset to the size of the type + offset += offset % size; + uniforms.emplace( std::make_pair(input.name, GlInBlockUniform{type, @@ -97,7 +102,8 @@ std::shared_ptr GlRenderer::add_uniform_buffer(resources::Uniform resources::UniformBufferInfo::get_size(input, info.get_layout()), resources::UniformBufferInfo::get_stride_size(input.type, info.get_layout()), input.count})); - offset += resources::UniformBufferInfo::get_size(input, info.get_layout()); + + offset += size; } return std::make_shared(this->gl_context, diff --git a/libopenage/renderer/resources/buffer_info.cpp b/libopenage/renderer/resources/buffer_info.cpp index c3215d63ba..de9a1751c8 100644 --- a/libopenage/renderer/resources/buffer_info.cpp +++ b/libopenage/renderer/resources/buffer_info.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 "buffer_info.h" @@ -27,7 +27,13 @@ const std::vector &UniformBufferInfo::get_inputs() const { size_t UniformBufferInfo::get_size() const { size_t size = 0; for (const auto &input : this->inputs) { - size += this->get_size(input, this->layout); + // size of the input type + size_t input_size = this->get_size(input, this->layout); + + // inputs must additionally be aligned to a multiple of their size + size_t align_size = size % input_size; + + size += input_size + align_size; } return size; } From 67b0fa4b60f37c340f9cb13d1a4594985e769daa Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Apr 2024 15:20:36 +0200 Subject: [PATCH 245/771] renderer: Calculate subtex/anchor scale from uniform buffer values. --- assets/shaders/world2d.vert.glsl | 8 ++++---- libopenage/renderer/stages/world/object.cpp | 4 ++-- libopenage/renderer/stages/world/render_stage.cpp | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/assets/shaders/world2d.vert.glsl b/assets/shaders/world2d.vert.glsl index 3c763d4ae3..e3a8e4a773 100644 --- a/assets/shaders/world2d.vert.glsl +++ b/assets/shaders/world2d.vert.glsl @@ -55,12 +55,12 @@ void main() { float obj_scale = scalefactor * inv_zoom; vec2 obj_scale_vec = vec2( - obj_scale * (subtex_size.x / screen_size.x), - obj_scale * (subtex_size.y / screen_size.y) + obj_scale * (subtex_size.x * inv_viewport_size.x), + obj_scale * (subtex_size.y * inv_viewport_size.y) ); vec2 obj_anchor_vec = vec2( - obj_scale * (anchor.x / screen_size.x), - obj_scale * (anchor.y / screen_size.y) + obj_scale * (anchor.x * inv_viewport_size.x), + obj_scale * (anchor.y * inv_viewport_size.y) ); // if the subtex is flipped, we also need to flip the anchor offset diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index 7709d2223a..93f7c7a8bb 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #include "object.h" @@ -162,7 +162,7 @@ void WorldObject::update_uniforms(const time::time_t &time) { Eigen::Vector2f screen_size_vec{ static_cast(screen_size[0]), static_cast(screen_size[1])}; - this->uniforms->update("screen_size", screen_size_vec); + // this->uniforms->update("screen_size", screen_size_vec); Eigen::Vector2f subtex_size_vec{ static_cast(subtex_size[0]), static_cast(subtex_size[1])}; diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index e97b52cabf..b107f8d863 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #include "render_stage.h" From 154a2eef7d7cead774160dea258c3cd72f44c50f Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Apr 2024 15:30:18 +0200 Subject: [PATCH 246/771] renderer: Use uniform IDs to set new uniforms in world stage. --- assets/shaders/world2d.vert.glsl | 14 +++++++------- libopenage/renderer/stages/world/object.cpp | 6 +++--- libopenage/renderer/stages/world/object.h | 3 +++ libopenage/renderer/stages/world/render_stage.cpp | 3 +++ 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/assets/shaders/world2d.vert.glsl b/assets/shaders/world2d.vert.glsl index e3a8e4a773..86a6565656 100644 --- a/assets/shaders/world2d.vert.glsl +++ b/assets/shaders/world2d.vert.glsl @@ -42,25 +42,25 @@ uniform bool flip_y; // match the subtex dimensions // uniform vec2 scale; -uniform float scalefactor; +uniform float scale; uniform float zoom; uniform vec2 screen_size; uniform vec2 subtex_size; -uniform vec2 anchor; +uniform vec2 anchor_offset; void main() { // translate the position of the object from world space to clip space // this is the position where we want to draw the subtex in 2D vec4 obj_clip_pos = proj * view * model * vec4(obj_world_position, 1.0); - float obj_scale = scalefactor * inv_zoom; + float obj_scale = scale * inv_zoom; vec2 obj_scale_vec = vec2( - obj_scale * (subtex_size.x * inv_viewport_size.x), - obj_scale * (subtex_size.y * inv_viewport_size.y) + obj_scale * subtex_size.x * inv_viewport_size.x, + obj_scale * subtex_size.y * inv_viewport_size.y ); vec2 obj_anchor_vec = vec2( - obj_scale * (anchor.x * inv_viewport_size.x), - obj_scale * (anchor.y * inv_viewport_size.y) + obj_scale * anchor_offset.x * inv_viewport_size.x, + obj_scale * anchor_offset.y * inv_viewport_size.y ); // if the subtex is flipped, we also need to flip the anchor offset diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index 93f7c7a8bb..e26aaab9e7 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -157,7 +157,7 @@ void WorldObject::update_uniforms(const time::time_t &time) { scale * (static_cast(anchor[1]) / screen_size[1])}; // this->uniforms->update(this->anchor_offset, anchor_offset); - this->uniforms->update("scalefactor", animation_info->get_scalefactor()); + this->uniforms->update(this->scale, animation_info->get_scalefactor()); // this->uniforms->update("zoom", this->camera->get_zoom()); Eigen::Vector2f screen_size_vec{ static_cast(screen_size[0]), @@ -166,11 +166,11 @@ void WorldObject::update_uniforms(const time::time_t &time) { Eigen::Vector2f subtex_size_vec{ static_cast(subtex_size[0]), static_cast(subtex_size[1])}; - this->uniforms->update("subtex_size", subtex_size_vec); + this->uniforms->update(this->subtex_size, subtex_size_vec); Eigen::Vector2f anchor_vec{ static_cast(anchor[0]), static_cast(anchor[1])}; - this->uniforms->update("anchor", anchor_vec); + this->uniforms->update(this->anchor_offset, anchor_vec); } uint32_t WorldObject::get_id() { diff --git a/libopenage/renderer/stages/world/object.h b/libopenage/renderer/stages/world/object.h index e2fc865dd6..eb075dd4c3 100644 --- a/libopenage/renderer/stages/world/object.h +++ b/libopenage/renderer/stages/world/object.h @@ -131,7 +131,10 @@ class WorldObject { inline static uniform_id_t flip_y; inline static uniform_id_t tex; inline static uniform_id_t tile_params; + // inline static uniform_id_t scale; + // inline static uniform_id_t anchor_offset; inline static uniform_id_t scale; + inline static uniform_id_t subtex_size; inline static uniform_id_t anchor_offset; private: diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index b107f8d863..c8c8770414 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -137,6 +137,9 @@ void WorldRenderStage::init_uniform_ids() { WorldObject::tile_params = this->display_shader->get_uniform_id("tile_params"); // WorldObject::scale = this->display_shader->get_uniform_id("scale"); // WorldObject::anchor_offset = this->display_shader->get_uniform_id("anchor_offset"); + WorldObject::scale = this->display_shader->get_uniform_id("scale"); + WorldObject::subtex_size = this->display_shader->get_uniform_id("subtex_size"); + WorldObject::anchor_offset = this->display_shader->get_uniform_id("anchor_offset"); } } // namespace openage::renderer::world From 080849f5d9689f52d07b13c13246cc296f4b68b2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Apr 2024 16:01:46 +0200 Subject: [PATCH 247/771] renderer: Remove unused uniform code from world stage. --- assets/shaders/world2d.vert.glsl | 54 ++++++++++++------- libopenage/renderer/stages/world/object.cpp | 38 +++++-------- libopenage/renderer/stages/world/object.h | 2 - .../renderer/stages/world/render_stage.cpp | 2 - 4 files changed, 46 insertions(+), 50 deletions(-) diff --git a/assets/shaders/world2d.vert.glsl b/assets/shaders/world2d.vert.glsl index 86a6565656..3b698b39c1 100644 --- a/assets/shaders/world2d.vert.glsl +++ b/assets/shaders/world2d.vert.glsl @@ -13,6 +13,8 @@ layout (std140) uniform camera { // projection matrix (view to clip space) mat4 proj; // inverse zoom factor (1.0 / zoom) + // high zoom = upscale subtex + // low zoom = downscale subtex float inv_zoom; // inverse viewport size (1.0 / viewport size) vec2 inv_viewport_size; @@ -34,18 +36,21 @@ uniform bool flip_y; // parameters for scaling and moving the subtex // to the correct position in clip space -// offset from the subtex anchor -// moves the subtex relative to the subtex center -// uniform vec2 anchor_offset; - +// animation scalefactor // scales the vertex positions so that they // match the subtex dimensions -// uniform vec2 scale; - +// +// high animation scale = downscale subtex +// low animation scale = upscale subtex uniform float scale; -uniform float zoom; -uniform vec2 screen_size; + +// size of the subtex (in pixels) uniform vec2 subtex_size; + +// offset of the subtex anchor point +// from the subtex center (in pixels) +// used to move the subtex so that the anchor point +// is at the object position uniform vec2 anchor_offset; void main() { @@ -53,29 +58,38 @@ void main() { // this is the position where we want to draw the subtex in 2D vec4 obj_clip_pos = proj * view * model * vec4(obj_world_position, 1.0); - float obj_scale = scale * inv_zoom; - vec2 obj_scale_vec = vec2( - obj_scale * subtex_size.x * inv_viewport_size.x, - obj_scale * subtex_size.y * inv_viewport_size.y + // subtex has to be scaled to account for the zoom factor + // and the animation scale factor. essentially this is (animation scale / zoom). + float zoom_scale = scale * inv_zoom; + + // Scale the subtex vertices + // we have to account for the viewport size to get the correct dimensions + // and then scale the subtex to the zoom factor to get the correct size + vec2 vert_scale = vec2( + zoom_scale * subtex_size.x * inv_viewport_size.x, + zoom_scale * subtex_size.y * inv_viewport_size.y ); - vec2 obj_anchor_vec = vec2( - obj_scale * anchor_offset.x * inv_viewport_size.x, - obj_scale * anchor_offset.y * inv_viewport_size.y + + // Scale the anchor offset with the same method as above + // to get the correct anchor position in the viewport + vec2 anchor_scale = vec2( + zoom_scale * anchor_offset.x * inv_viewport_size.x, + zoom_scale * anchor_offset.y * inv_viewport_size.y ); // if the subtex is flipped, we also need to flip the anchor offset // essentially, we invert the coordinates for the flipped axis - float anchor_x = float(flip_x) * -1.0 * obj_anchor_vec.x + float(!flip_x) * obj_anchor_vec.x; - float anchor_y = float(flip_y) * -1.0 * obj_anchor_vec.y + float(!flip_y) * obj_anchor_vec.y; + float anchor_x = float(flip_x) * -1.0 * anchor_scale.x + float(!flip_x) * anchor_scale.x; + float anchor_y = float(flip_y) * -1.0 * anchor_scale.y + float(!flip_y) * anchor_scale.y; // offset the clip position by the offset of the subtex anchor // imagine this as pinning the subtex to the object position at the subtex anchor point obj_clip_pos += vec4(anchor_x, anchor_y, 0.0, 0.0); // create a move matrix for positioning the vertices - // uses the scale and the transformed object position in clip space - mat4 move = mat4(obj_scale_vec.x, 0.0, 0.0, 0.0, - 0.0, obj_scale_vec.y, 0.0, 0.0, + // uses the vert scale and the transformed object position in clip space + mat4 move = mat4(vert_scale.x, 0.0, 0.0, 0.0, + 0.0, vert_scale.y, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, obj_clip_pos.x, obj_clip_pos.y, obj_clip_pos.z, 1.0); diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index e26aaab9e7..c58e9faa98 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -138,39 +138,25 @@ void WorldObject::update_uniforms(const time::time_t &time) { auto coords = tex_info->get_subtex_info(subtex_idx).get_tile_params(); this->uniforms->update(this->tile_params, coords); - // scale and keep width x height ratio of texture - // when the viewport size changes - auto scale = animation_info->get_scalefactor() / this->camera->get_zoom(); - auto screen_size = this->camera->get_viewport_size(); - auto subtex_size = tex_info->get_subtex_info(subtex_idx).get_size(); - - // Scaling with viewport size and zoom - auto scale_vec = Eigen::Vector2f{ - scale * (static_cast(subtex_size[0]) / screen_size[0]), - scale * (static_cast(subtex_size[1]) / screen_size[1])}; - // this->uniforms->update(this->scale, scale_vec); + // Animation scale factor + // Scales the subtex up or down in the shader + auto scale = animation_info->get_scalefactor(); + this->uniforms->update(this->scale, scale); - // Move subtexture in scene so that its anchor point is at the object's position - auto anchor = tex_info->get_subtex_info(subtex_idx).get_anchor_params(); - auto anchor_offset = Eigen::Vector2f{ - scale * (static_cast(anchor[0]) / screen_size[0]), - scale * (static_cast(anchor[1]) / screen_size[1])}; - // this->uniforms->update(this->anchor_offset, anchor_offset); - - this->uniforms->update(this->scale, animation_info->get_scalefactor()); - // this->uniforms->update("zoom", this->camera->get_zoom()); - Eigen::Vector2f screen_size_vec{ - static_cast(screen_size[0]), - static_cast(screen_size[1])}; - // this->uniforms->update("screen_size", screen_size_vec); + // Subtexture size in pixels + auto subtex_size = tex_info->get_subtex_info(subtex_idx).get_size(); Eigen::Vector2f subtex_size_vec{ static_cast(subtex_size[0]), static_cast(subtex_size[1])}; this->uniforms->update(this->subtex_size, subtex_size_vec); - Eigen::Vector2f anchor_vec{ + + // Anchor point offset (in pixels) + // moves the subtex in the shader so that the anchor point is at the object's position + auto anchor = tex_info->get_subtex_info(subtex_idx).get_anchor_params(); + Eigen::Vector2f anchor_offset{ static_cast(anchor[0]), static_cast(anchor[1])}; - this->uniforms->update(this->anchor_offset, anchor_vec); + this->uniforms->update(this->anchor_offset, anchor_offset); } uint32_t WorldObject::get_id() { diff --git a/libopenage/renderer/stages/world/object.h b/libopenage/renderer/stages/world/object.h index eb075dd4c3..a34a0c690c 100644 --- a/libopenage/renderer/stages/world/object.h +++ b/libopenage/renderer/stages/world/object.h @@ -131,8 +131,6 @@ class WorldObject { inline static uniform_id_t flip_y; inline static uniform_id_t tex; inline static uniform_id_t tile_params; - // inline static uniform_id_t scale; - // inline static uniform_id_t anchor_offset; inline static uniform_id_t scale; inline static uniform_id_t subtex_size; inline static uniform_id_t anchor_offset; diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index c8c8770414..d24bb5f896 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -135,8 +135,6 @@ void WorldRenderStage::init_uniform_ids() { WorldObject::flip_y = this->display_shader->get_uniform_id("flip_y"); WorldObject::tex = this->display_shader->get_uniform_id("tex"); WorldObject::tile_params = this->display_shader->get_uniform_id("tile_params"); - // WorldObject::scale = this->display_shader->get_uniform_id("scale"); - // WorldObject::anchor_offset = this->display_shader->get_uniform_id("anchor_offset"); WorldObject::scale = this->display_shader->get_uniform_id("scale"); WorldObject::subtex_size = this->display_shader->get_uniform_id("subtex_size"); WorldObject::anchor_offset = this->display_shader->get_uniform_id("anchor_offset"); From a19b42b4aaa98524a44856bfbfb6cdf345c2bb89 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Apr 2024 16:03:57 +0200 Subject: [PATCH 248/771] renderer: Use more compact shader code. --- assets/shaders/world2d.vert.glsl | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/assets/shaders/world2d.vert.glsl b/assets/shaders/world2d.vert.glsl index 3b698b39c1..7987d40ec3 100644 --- a/assets/shaders/world2d.vert.glsl +++ b/assets/shaders/world2d.vert.glsl @@ -65,17 +65,11 @@ void main() { // Scale the subtex vertices // we have to account for the viewport size to get the correct dimensions // and then scale the subtex to the zoom factor to get the correct size - vec2 vert_scale = vec2( - zoom_scale * subtex_size.x * inv_viewport_size.x, - zoom_scale * subtex_size.y * inv_viewport_size.y - ); + vec2 vert_scale = zoom_scale * subtex_size * inv_viewport_size; // Scale the anchor offset with the same method as above // to get the correct anchor position in the viewport - vec2 anchor_scale = vec2( - zoom_scale * anchor_offset.x * inv_viewport_size.x, - zoom_scale * anchor_offset.y * inv_viewport_size.y - ); + vec2 anchor_scale = zoom_scale * anchor_offset * inv_viewport_size; // if the subtex is flipped, we also need to flip the anchor offset // essentially, we invert the coordinates for the flipped axis From 185dd98ed9e01c97740e67e89a72feaa3c95a41f Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Apr 2024 16:08:59 +0200 Subject: [PATCH 249/771] renderer: Add new camera uniforms in stresstest 0. --- libopenage/renderer/demo/stresstest_0.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libopenage/renderer/demo/stresstest_0.cpp b/libopenage/renderer/demo/stresstest_0.cpp index ce7da14948..01169b0849 100644 --- a/libopenage/renderer/demo/stresstest_0.cpp +++ b/libopenage/renderer/demo/stresstest_0.cpp @@ -49,7 +49,14 @@ void renderer_stresstest_0(const util::Path &path) { "view", camera->get_view_matrix(), "proj", - camera->get_projection_matrix()); + camera->get_projection_matrix(), + "inv_zoom", + 1.0f / camera->get_zoom()); + auto viewport_size = camera->get_viewport_size(); + Eigen::Vector2f viewport_size_vec{ + 1.0f / static_cast(viewport_size[0]), + 1.0f / static_cast(viewport_size[1])}; + cam_unifs->update("inv_viewport_size", viewport_size_vec); camera->get_uniform_buffer()->update_uniforms(cam_unifs); // Render stages From 0590bf20c150b1bd175a6c7bcef1cf2f61485208 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 27 Apr 2024 17:17:33 +0200 Subject: [PATCH 250/771] ci: Force install pip packages for macOS. --- .github/workflows/macosx-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/macosx-ci.yml b/.github/workflows/macosx-ci.yml index eba3c5fd98..253a5f359b 100644 --- a/.github/workflows/macosx-ci.yml +++ b/.github/workflows/macosx-ci.yml @@ -66,7 +66,7 @@ jobs: # cython, numpy and pygments are in homebrew, # but "cython is keg-only, which means it was not symlinked into /usr/local" # numpy pulls gcc as dep? and pygments doesn't work. - run: pip3 install --upgrade cython numpy mako lz4 pillow pygments setuptools toml + run: pip3 install --upgrade --break-system-packages cython numpy mako lz4 pillow pygments setuptools toml - name: Configure run: | CLANG_PATH="$HOME/clang-15.0.0/bin/clang++" From 993408dac51c351a34600cd3beba17f5b99da9ce Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Apr 2024 17:17:35 +0200 Subject: [PATCH 251/771] renderer: Seperate callback queue for mouse move events. --- libopenage/presenter/presenter.cpp | 3 +++ libopenage/renderer/opengl/window.cpp | 7 ++++++- libopenage/renderer/window.cpp | 4 ++++ libopenage/renderer/window.h | 13 +++++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index 38c741f8fd..64df9bc05a 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -217,6 +217,9 @@ void Presenter::init_input() { this->input_manager->process(ev); }); this->window->add_mouse_button_callback([&](const QMouseEvent &ev) { + this->input_manager->process(ev); + }); + this->window->add_mouse_move_callback([&](const QMouseEvent &ev) { this->input_manager->set_mouse(ev.position().x(), ev.position().y()); this->input_manager->process(ev); }); diff --git a/libopenage/renderer/opengl/window.cpp b/libopenage/renderer/opengl/window.cpp index 8389c6e241..5f75717540 100644 --- a/libopenage/renderer/opengl/window.cpp +++ b/libopenage/renderer/opengl/window.cpp @@ -122,13 +122,18 @@ void GlWindow::update() { } break; case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: - case QEvent::MouseMove: case QEvent::MouseButtonDblClick: { auto const ev = std::dynamic_pointer_cast(event); for (auto &cb : this->on_mouse_button) { cb(*ev); } } break; + case QEvent::MouseMove: { + auto const ev = std::dynamic_pointer_cast(event); + for (auto &cb : this->on_mouse_move) { + cb(*ev); + } + } break; case QEvent::Wheel: { auto const ev = std::dynamic_pointer_cast(event); for (auto &cb : this->on_mouse_wheel) { diff --git a/libopenage/renderer/window.cpp b/libopenage/renderer/window.cpp index 497f36c535..88dbefb92e 100644 --- a/libopenage/renderer/window.cpp +++ b/libopenage/renderer/window.cpp @@ -39,6 +39,10 @@ void Window::add_mouse_button_callback(const mouse_button_cb_t &cb) { this->on_mouse_button.push_back(cb); } +void Window::add_mouse_move_callback(const mouse_move_cb_t &cb) { + this->on_mouse_move.push_back(cb); +} + void Window::add_mouse_wheel_callback(const mouse_wheel_cb_t &cb) { this->on_mouse_wheel.push_back(cb); } diff --git a/libopenage/renderer/window.h b/libopenage/renderer/window.h index 2ac9c13430..71960e2d17 100644 --- a/libopenage/renderer/window.h +++ b/libopenage/renderer/window.h @@ -79,6 +79,7 @@ class Window { using key_cb_t = std::function; using mouse_button_cb_t = std::function; + using mouse_move_cb_t = std::function; using mouse_wheel_cb_t = std::function; using resize_cb_t = std::function; @@ -96,6 +97,13 @@ class Window { */ void add_mouse_button_callback(const mouse_button_cb_t &cb); + /** + * Register a function that executes when the mouse is moved. + * + * @param cb Callback function. + */ + void add_mouse_move_callback(const mouse_move_cb_t &cb); + /** * Register a function that executes when a mouse wheel action is used. * @@ -173,6 +181,11 @@ class Window { */ std::vector on_mouse_button; + /** + * Callbacks for mouse move actions. + */ + std::vector on_mouse_move; + /** * Callbacks for mouse wheel actions. */ From 175332a6e2bd3fb87ddea0bde23d2e7fc2ed14e5 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Apr 2024 17:59:44 +0200 Subject: [PATCH 252/771] input: Fix order of processing contexts. --- libopenage/input/input_manager.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libopenage/input/input_manager.cpp b/libopenage/input/input_manager.cpp index 58bca7fb9e..4857863eb1 100644 --- a/libopenage/input/input_manager.cpp +++ b/libopenage/input/input_manager.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "input_manager.h" @@ -9,6 +9,7 @@ #include "input/input_context.h" #include "renderer/gui/guisys/public/gui_input.h" + namespace openage::input { InputManager::InputManager() : @@ -131,7 +132,8 @@ bool InputManager::process(const QEvent &ev) { input::Event input_ev{ev}; // Check context list on top of the stack (most recent bound first) - for (auto const &ctx : this->active_contexts) { + for (size_t i = this->active_contexts.size(); i > 0; --i) { + auto &ctx = this->active_contexts.at(i - 1); if (ctx->is_bound(input_ev)) { auto &actions = ctx->lookup(input_ev); for (auto const &action : actions) { From 51c8c4f1ebca3627f285785d87180bab595ade69 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Apr 2024 18:00:03 +0200 Subject: [PATCH 253/771] input: Fix input demo keybindings- --- libopenage/input/tests.cpp | 44 +++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/libopenage/input/tests.cpp b/libopenage/input/tests.cpp index b97efb9219..11860e6449 100644 --- a/libopenage/input/tests.cpp +++ b/libopenage/input/tests.cpp @@ -87,16 +87,40 @@ void action_demo() { input_action catch_all{input_action_t::CUSTOM, nop}; // events that map to specific keys/buttons - Event ev_up{event_class::KEYBOARD, Qt::Key_Up, Qt::NoModifier, QEvent::KeyRelease}; - Event ev_down{event_class::KEYBOARD, Qt::Key_Down, Qt::NoModifier, QEvent::KeyRelease}; - - Event ev_w{event_class::KEYBOARD, Qt::Key_W, Qt::NoModifier, QEvent::KeyRelease}; - Event ev_a{event_class::KEYBOARD, Qt::Key_A, Qt::NoModifier, QEvent::KeyRelease}; - Event ev_s{event_class::KEYBOARD, Qt::Key_S, Qt::NoModifier, QEvent::KeyRelease}; - Event ev_d{event_class::KEYBOARD, Qt::Key_D, Qt::NoModifier, QEvent::KeyRelease}; - - Event ev_lmb{event_class::MOUSE, Qt::LeftButton, Qt::NoModifier, QEvent::MouseButtonRelease}; - Event ev_rmb{event_class::MOUSE, Qt::RightButton, Qt::NoModifier, QEvent::MouseButtonRelease}; + Event ev_up{event_class::KEYBOARD, + Qt::Key::Key_Up, + Qt::KeyboardModifier::NoModifier, + QEvent::KeyRelease}; + Event ev_down{event_class::KEYBOARD, + Qt::Key::Key_Down, + Qt::KeyboardModifier::NoModifier, + QEvent::KeyRelease}; + + Event ev_w{event_class::KEYBOARD, + Qt::Key::Key_W, + Qt::KeyboardModifier::NoModifier, + QEvent::KeyRelease}; + Event ev_a{event_class::KEYBOARD, + Qt::Key::Key_A, + Qt::KeyboardModifier::NoModifier, + QEvent::KeyRelease}; + Event ev_s{event_class::KEYBOARD, + Qt::Key::Key_S, + Qt::KeyboardModifier::NoModifier, + QEvent::KeyRelease}; + Event ev_d{event_class::KEYBOARD, + Qt::Key::Key_D, + Qt::KeyboardModifier::NoModifier, + QEvent::KeyRelease}; + + Event ev_lmb{event_class::MOUSE_BUTTON, + Qt::MouseButton::LeftButton, + Qt::KeyboardModifier::NoModifier, + QEvent::MouseButtonRelease}; + Event ev_rmb{event_class::MOUSE_BUTTON, + Qt::MouseButton::RightButton, + Qt::KeyboardModifier::NoModifier, + QEvent::MouseButtonRelease}; // bind events to actions in the contexts mgr.get_global_context()->bind(ev_up, push_a); From d1dba83cba709ae1aa432d756b8de51e9872529c Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Jan 2024 23:08:28 +0100 Subject: [PATCH 254/771] curve: Use vector for keyframe container. --- libopenage/curve/base_curve.h | 27 +-- libopenage/curve/continuous.h | 4 +- libopenage/curve/discrete.h | 13 +- libopenage/curve/discrete_mod.h | 2 +- libopenage/curve/interpolated.h | 19 +- libopenage/curve/keyframe.h | 16 +- libopenage/curve/keyframe_container.h | 289 +++++++++++++------------ libopenage/curve/segmented.h | 4 +- libopenage/curve/tests/curve_types.cpp | 174 +++++++-------- 9 files changed, 284 insertions(+), 264 deletions(-) diff --git a/libopenage/curve/base_curve.h b/libopenage/curve/base_curve.h index 0e8d2d8100..a9e1ba779f 100644 --- a/libopenage/curve/base_curve.h +++ b/libopenage/curve/base_curve.h @@ -4,7 +4,6 @@ #include #include -#include #include #include #include @@ -40,7 +39,7 @@ class BaseCurve : public event::EventEntity { _id{id}, _idstr{idstr}, loop{loop}, - last_element{this->container.begin()} {} + last_element{this->container.size()} {} virtual ~BaseCurve() = default; @@ -193,9 +192,9 @@ class BaseCurve : public event::EventEntity { const std::shared_ptr loop; /** - * Cache the iterator for quickly finding the last accessed element (usually the end) + * Cache the index of the last accessed element (usually the end). */ - mutable typename KeyframeContainer::iterator last_element; + mutable typename KeyframeContainer::index_t last_element; }; @@ -204,7 +203,7 @@ void BaseCurve::set_last(const time::time_t &at, const T &value) { auto hint = this->container.last(at, this->last_element); // erase max one same-time value - if (hint->time == at) { + if (this->container.get(hint).time() == at) { hint--; } @@ -221,7 +220,7 @@ template void BaseCurve::set_insert(const time::time_t &at, const T &value) { auto hint = this->container.insert_after(at, value, this->last_element); // check if this is now the final keyframe - if (hint->time > this->last_element->time) { + if (this->container.get(hint).time() > this->container.get(this->last_element).time()) { this->last_element = hint; } this->changes(at); @@ -244,16 +243,18 @@ void BaseCurve::erase(const time::time_t &at) { template std::pair BaseCurve::frame(const time::time_t &time) const { - auto e = this->container.last(time, this->container.end()); - return std::make_pair(e->time, e->value); + auto e = this->container.last(time, this->container.size()); + auto elem = this->container.get(e); + return std::make_pair(elem.time(), elem.val()); } template std::pair BaseCurve::next_frame(const time::time_t &time) const { - auto e = this->container.last(time, this->container.end()); + auto e = this->container.last(time, this->container.size()); e++; - return std::make_pair(e->time, e->value); + auto elem = this->container.get(e); + return std::make_pair(elem.time(), elem.val()); } template @@ -261,7 +262,7 @@ std::string BaseCurve::str() const { std::stringstream ss; ss << "Curve[" << this->idstr() << "]{" << std::endl; for (const auto &keyframe : this->container) { - ss << " " << keyframe.time << ": " << keyframe.value << "," << std::endl; + ss << " " << keyframe.time() << ": " << keyframe.val() << "," << std::endl; } ss << "}"; @@ -272,10 +273,10 @@ template void BaseCurve::check_integrity() const { time::time_t last_time = time::TIME_MIN; for (const auto &keyframe : this->container) { - if (keyframe.time < last_time) { + if (keyframe.time() < last_time) { throw Error{MSG(err) << "curve is broken after t=" << last_time << ": " << this->str()}; } - last_time = keyframe.time; + last_time = keyframe.time(); } } diff --git a/libopenage/curve/continuous.h b/libopenage/curve/continuous.h index 3eb7170ddb..0cf438f237 100644 --- a/libopenage/curve/continuous.h +++ b/libopenage/curve/continuous.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 @@ -48,7 +48,7 @@ void Continuous::set_last(const time::time_t &at, const T &value) { auto hint = this->container.last(at, this->last_element); // erase all same-time entries - while (hint->time == at) { + while (this->container.get(hint).time() == at) { hint--; } diff --git a/libopenage/curve/discrete.h b/libopenage/curve/discrete.h index cdcb3daa31..44fe132de6 100644 --- a/libopenage/curve/discrete.h +++ b/libopenage/curve/discrete.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 @@ -55,7 +55,7 @@ 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 e->value; + return this->container.get(e).val(); } @@ -78,7 +78,9 @@ 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; - return std::make_pair(e->time, e->value); + + auto elem = this->container.get(e); + return std::make_pair(elem.time, elem.value); } @@ -89,12 +91,13 @@ std::optional> Discrete::get_previous(const time:: // if we're not at the container head // go back one entry. - if (e == std::begin(this->container)) { + if (e == 0) { return {}; } e--; - return std::make_pair(e->time, e->value); + auto elem = this->container.get(e); + return std::make_pair(elem.time(), elem.val()); } } // namespace openage::curve diff --git a/libopenage/curve/discrete_mod.h b/libopenage/curve/discrete_mod.h index de036bbf10..953939f975 100644 --- a/libopenage/curve/discrete_mod.h +++ b/libopenage/curve/discrete_mod.h @@ -93,7 +93,7 @@ void DiscreteMod::erase(const time::time_t &at) { BaseCurve::erase(at); if (this->time_length == at) { - this->time_length = this->last_element->time; + this->time_length = this->container.get(this->container.size() - 1).time(); } } diff --git a/libopenage/curve/interpolated.h b/libopenage/curve/interpolated.h index d5d4a1dedf..564ba1d0af 100644 --- a/libopenage/curve/interpolated.h +++ b/libopenage/curve/interpolated.h @@ -40,7 +40,7 @@ class Interpolated : public BaseCurve { template T Interpolated::get(const time::time_t &time) const { - const auto &e = this->container.last(time, this->last_element); + const auto e = this->container.last(time, this->last_element); this->last_element = e; auto nxt = e; @@ -48,21 +48,21 @@ T Interpolated::get(const time::time_t &time) const { time::time_t interval = 0; - auto offset = time - e->time; + auto offset = time - this->container.get(e).time(); - if (nxt != this->container.end()) { - interval = nxt->time - e->time; + if (nxt != this->container.size()) { + interval = this->container.get(nxt).time() - 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.end() // 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 + 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 - return e->value; + return this->container.get(e).val(); } else { // Interpolation between time(now) and time(next) that has elapsed @@ -72,7 +72,8 @@ T Interpolated::get(const time::time_t &time) const { // 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 - return e->value + (nxt->value - e->value) * elapsed_frac; + auto diff_value = (this->container.get(nxt).val() - this->container.get(e).val()) * elapsed_frac; + return this->container.get(e).val() + diff_value; } } diff --git a/libopenage/curve/keyframe.h b/libopenage/curve/keyframe.h index 484085cc70..cb4c4dc727 100644 --- a/libopenage/curve/keyframe.h +++ b/libopenage/curve/keyframe.h @@ -27,16 +27,26 @@ class Keyframe { * New, default-constructed element at the given time */ Keyframe(const time::time_t &time) : - time{time} {} + timestamp{time} {} /** * New element fron time and value */ Keyframe(const time::time_t &time, const T &value) : - time{time}, + timestamp{time}, value{value} {} - const time::time_t time = time::TIME_MIN; + const time::time_t &time() const { + return this->timestamp; + } + + const T &val() const { + return this->value; + } + +private: + time::time_t timestamp = time::TIME_MIN; + T value = T{}; }; diff --git a/libopenage/curve/keyframe_container.h b/libopenage/curve/keyframe_container.h index b9e6850257..67bb0d0cc3 100644 --- a/libopenage/curve/keyframe_container.h +++ b/libopenage/curve/keyframe_container.h @@ -34,17 +34,18 @@ class KeyframeContainer { /** * The underlaying container type. - * - * The most important property of this container is the iterator validity on - * insert and remove. */ - using container_t = std::list; + using container_t = std::vector; + + /** + * The index type to access elements in the container + */ + using index_t = typename container_t::size_type; /** * The iterator type to access elements in the container */ using iterator = typename container_t::const_iterator; - using const_iterator = typename container_t::const_iterator; /** * Create a new container. @@ -75,12 +76,16 @@ class KeyframeContainer { */ size_t size() const; + const keyframe_t &get(const index_t &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. */ - iterator last(const time::time_t &time, - const iterator &hint) const; + index_t last(const time::time_t &time, + const index_t &hint) const; /** * Get the last element with elem->time <= time, without a hint where to start @@ -90,23 +95,23 @@ class KeyframeContainer { * no chance for you to have a hint (or the container is known to be nearly * empty) */ - iterator last(const time::time_t &time) const { - return this->last(time, std::end(this->container)); + index_t 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. */ - iterator last_before(const time::time_t &time, - const iterator &hint) const; + index_t last_before(const time::time_t &time, + const index_t &hint) const; /** * Get the last element with elem->time < time, without a hint where to start * searching. */ - iterator last_before(const time::time_t &time) const { - return this->last_before(time, std::end(this->container)); + index_t last_before(const time::time_t &time) const { + return this->last_before(time, this->container.size()); } /** @@ -116,8 +121,8 @@ class KeyframeContainer { * This function is not recommended for use, whenever possible, keep a hint * to insert the data. */ - iterator insert_before(const keyframe_t &value) { - return this->insert_before(value, std::end(this->container)); + index_t insert_before(const keyframe_t &value) { + return this->insert_before(value, this->container.size()); } /** @@ -127,7 +132,7 @@ class KeyframeContainer { * history size. If there is a keyframe with identical time, this will * insert the new keyframe before the old one. */ - iterator insert_before(const keyframe_t &value, const iterator &hint); + index_t insert_before(const keyframe_t &value, const index_t &hint); /** * Create and insert a new element without submitting a hint. The search is @@ -135,8 +140,8 @@ class KeyframeContainer { * discouraged, use it only, if your really do not have the possibility to * get a hint. */ - iterator insert_before(const time::time_t &time, const T &value) { - return this->insert_before(keyframe_t(time, value), std::end(this->container)); + index_t insert_before(const time::time_t &time, const T &value) { + return this->insert_before(keyframe_t(time, value), this->container.size()); } /** @@ -144,7 +149,7 @@ class KeyframeContainer { * If there is a value with identical time, this will insert the new value * before the old one. */ - iterator insert_before(const time::time_t &time, const T &value, const iterator &hint) { + index_t insert_before(const time::time_t &time, const T &value, const index_t &hint) { return this->insert_before(keyframe_t(time, value), hint); } @@ -155,9 +160,9 @@ class KeyframeContainer { * `overwrite_all` == true -> overwrite all same-time elements. * `overwrite_all` == false -> overwrite the last of the time-conflict elements. */ - iterator insert_overwrite(const keyframe_t &value, - const iterator &hint, - bool overwrite_all = false); + index_t insert_overwrite(const keyframe_t &value, + const index_t &hint, + bool overwrite_all = false); /** * Insert a new value at given time which will overwrite the last of the @@ -165,9 +170,9 @@ class KeyframeContainer { * 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. */ - iterator insert_overwrite(const time::time_t &time, const T &value) { + index_t insert_overwrite(const time::time_t &time, const T &value) { return this->insert_overwrite(keyframe_t(time, value), - std::end(this->container)); + this->container.size()); } /** @@ -177,10 +182,10 @@ class KeyframeContainer { * Provide a insertion hint to abbreviate the search for the * insertion point. */ - iterator insert_overwrite(const time::time_t &time, - const T &value, - const iterator &hint, - bool overwrite_all = false) { + index_t insert_overwrite(const time::time_t &time, + const T &value, + const index_t &hint, + bool overwrite_all = false) { return this->insert_overwrite(keyframe_t(time, value), hint, overwrite_all); } @@ -189,7 +194,7 @@ class KeyframeContainer { * conflict. Give an approximate insertion location to minimize runtime on * big-history curves. */ - iterator insert_after(const keyframe_t &value, const iterator &hint); + index_t insert_after(const keyframe_t &value, const index_t &hint); /** * Insert a new value at given time which will be prepended to the block of @@ -197,37 +202,37 @@ class KeyframeContainer { * 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. */ - iterator insert_after(const time::time_t &time, const T &value) { + index_t insert_after(const time::time_t &time, const T &value) { return this->insert_after(keyframe_t(time, value), - std::end(this->container)); + this->container.size()); } /** * 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. */ - iterator insert_after(const time::time_t &time, const T &value, const iterator &hint) { + index_t insert_after(const time::time_t &time, const T &value, const index_t &hint) { return this->insert_after(keyframe_t(time, value), hint); } /** * Erase all elements that come after this last valid element. */ - iterator erase_after(iterator last_valid); + index_t erase_after(index_t last_valid); /** * Erase a single element from the curve. * Returns the element after the deleted one. */ - iterator erase(iterator it); + index_t erase(index_t 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. */ - iterator erase(const time::time_t &time) { - return this->erase(time, std::end(this->container)); + index_t erase(const time::time_t &time) { + return this->erase(time, this->container.size()); } /** @@ -239,8 +244,8 @@ class KeyframeContainer { * Or, if no elements with this time exist, * the iterator to the first element after the requested time is returned */ - iterator erase(const time::time_t &time, - const iterator &hint) { + index_t erase(const time::time_t &time, + const index_t &hint) { return this->erase_group(time, this->last(time, hint)); } @@ -280,8 +285,8 @@ class KeyframeContainer { * Using the default value replaces ALL keyframes of \p this with * the keyframes of \p other. */ - iterator sync(const KeyframeContainer &other, - const time::time_t &start = time::TIME_MIN); + index_t sync(const KeyframeContainer &other, + const time::time_t &start = time::TIME_MIN); /** * Copy keyframes from another container (with a different element type) to this container. @@ -296,9 +301,9 @@ class KeyframeContainer { * the keyframes of \p other. */ template - iterator sync(const KeyframeContainer &other, - const std::function &converter, - const time::time_t &start = time::TIME_MIN); + index_t sync(const KeyframeContainer &other, + const std::function &converter, + const time::time_t &start = time::TIME_MIN); /** * Debugging method to be used from gdb to understand bugs better. @@ -314,8 +319,8 @@ class KeyframeContainer { * Erase elements with this time. * The iterator has to point to the last element of the same-time group. */ - iterator erase_group(const time::time_t &time, - const iterator &last_elem); + index_t erase_group(const time::time_t &time, + const index_t &last_elem); /** * The data store. @@ -355,28 +360,28 @@ size_t KeyframeContainer::size() const { * that determines the curve value for a searched time. */ template -typename KeyframeContainer::iterator KeyframeContainer::last(const time::time_t &time, - const iterator &hint) const { - iterator e = hint; - auto end = std::end(this->container); +typename KeyframeContainer::index_t +KeyframeContainer::last(const time::time_t &time, + const KeyframeContainer::index_t &hint) const { + index_t at = hint; + const index_t end = this->container.size(); - if (e != end and e->time <= time) { + if (at != end and this->container.at(at).time() <= time) { // walk to the right until the time is larget than the searched - // then go one to the left to get the last item with <= requested time - while (e != end && e->time <= time) { - e++; + while (at != end and this->container.at(at).time() <= time) { + ++at; } - e--; + // go one back, because we want the last element that is <= time + --at; } - else { // e == end or e->time > time - // walk to the left until the element time is smaller than or equal to the searched time - auto begin = std::begin(this->container); - while (e != begin and (e == end or e->time > time)) { - e--; + else { // idx == end or idx->time > time + // walk to the left until the element time is smaller than the searched time + while (at > 0 and (at == end or this->container.at(at).time() > time)) { + --at; } } - return e; + return at; } @@ -389,28 +394,28 @@ typename KeyframeContainer::iterator KeyframeContainer::last(const time::t * first element that matches the search time. */ template -typename KeyframeContainer::iterator KeyframeContainer::last_before(const time::time_t &time, - const iterator &hint) const { - iterator e = hint; - auto end = std::end(this->container); +typename KeyframeContainer::index_t +KeyframeContainer::last_before(const time::time_t &time, + const KeyframeContainer::index_t &hint) const { + index_t at = hint; + const index_t end = this->container.size(); - if (e != end and e->time < time) { + if (at != end and this->container.at(at).time() < time) { // walk to the right until the time is larget than the searched - // then go one to the left to get the last item with <= requested time - while (e != end && e->time <= time) { - e++; + while (at != end and this->container.at(at).time() <= time) { + ++at; } - e--; + // go one back, because we want the last element that is <= time + --at; } - else { // e == end or e->time > time + else { // idx == end or idx->time > time // walk to the left until the element time is smaller than the searched time - auto begin = std::begin(this->container); - while (e != begin and (e == end or e->time >= time)) { - e--; + while (at > 0 and (at == end or this->container.at(at).time() >= time)) { + --at; } } - return e; + return at; } @@ -418,21 +423,25 @@ typename KeyframeContainer::iterator KeyframeContainer::last_before(const * Determine where to insert based on time, and insert. */ template -typename KeyframeContainer::iterator +typename KeyframeContainer::index_t KeyframeContainer::insert_before(const KeyframeContainer::keyframe_t &e, - const KeyframeContainer::iterator &hint) { - iterator at = this->last(e.time, hint); - // seek over all same-time elements, so we can insert before the first one - while (at != std::begin(this->container) and at->time == e.time) { - --at; + const KeyframeContainer::index_t &hint) { + index_t at = this->last(e.time(), hint); + + if (at == this->container.size()) { + this->container.push_back(e); + return at; } - // the above while-loop overshoots and selects the first non-equal element. - // set iterator one to the right, i.e. on the first same-value - if (at != std::end(this->container)) { - ++at; + // seek over all same-time elements, so we can insert before the first one + while (this->container.at(at).time() == e.time() and at > 0) { + at--; } - return this->container.insert(at, e); + + ++at; + + this->container.insert(this->begin() + at, e); + return at; } @@ -440,26 +449,28 @@ KeyframeContainer::insert_before(const KeyframeContainer::keyframe_t &e, * Determine where to insert based on time, and insert, overwriting value(s) with same time. */ template -typename KeyframeContainer::iterator -KeyframeContainer::insert_overwrite( - const KeyframeContainer::keyframe_t &e, - const KeyframeContainer::iterator &hint, - bool overwrite_all) { - iterator at = this->last(e.time, hint); +typename KeyframeContainer::index_t +KeyframeContainer::insert_overwrite(const KeyframeContainer::keyframe_t &e, + const KeyframeContainer::index_t &hint, + bool overwrite_all) { + index_t at = this->last(e.time(), hint); + const index_t end = this->container.size(); if (overwrite_all) { - at = this->erase_group(e.time, at); + at = this->erase_group(e.time(), at); } - else if (at != std::end(this->container)) { + else if (at != end) { // overwrite the same-time element - if (at->time == e.time) { - at = this->container.erase(at); + if (this->get(at).time() == e.time()) { + this->container.erase(this->begin() + at); } else { ++at; } } - return this->container.insert(at, e); + + this->container.insert(this->begin() + at, e); + return at; } @@ -468,16 +479,18 @@ KeyframeContainer::insert_overwrite( * If there is a time conflict, insert after the existing element. */ template -typename KeyframeContainer::iterator -KeyframeContainer::insert_after( - const KeyframeContainer::keyframe_t &e, - const KeyframeContainer::iterator &hint) { - iterator at = this->last(e.time, hint); +typename KeyframeContainer::index_t +KeyframeContainer::insert_after(const KeyframeContainer::keyframe_t &e, + const KeyframeContainer::index_t &hint) { + index_t at = this->last(e.time(), hint); + const index_t end = this->container.size(); - if (at != std::end(this->container)) { + if (at != end) { ++at; } - return this->container.insert(at, e); + + this->container.insert(this->begin() + at, e); + return at; } @@ -485,17 +498,15 @@ KeyframeContainer::insert_after( * Go from the end to the last_valid element, and call erase on all of them */ template -typename KeyframeContainer::iterator -KeyframeContainer::erase_after(KeyframeContainer::iterator last_valid) { +typename KeyframeContainer::index_t +KeyframeContainer::erase_after(KeyframeContainer::index_t last_valid) { // exclude the last_valid element from deletion - if (last_valid != this->container.end()) { - ++last_valid; + if (last_valid != this->container.size()) { + // Delete everything to the end. + const index_t delete_start = last_valid + 1; + this->container.erase(this->begin() + delete_start, this->end()); } - // Delete everything to the end. - while (last_valid != this->container.end()) { - last_valid = this->container.erase(last_valid); - } return last_valid; } @@ -504,79 +515,73 @@ KeyframeContainer::erase_after(KeyframeContainer::iterator last_valid) { * Delete the element from the list and call delete on it. */ template -typename KeyframeContainer::iterator -KeyframeContainer::erase(KeyframeContainer::iterator e) { - return this->container.erase(e); +typename KeyframeContainer::index_t +KeyframeContainer::erase(KeyframeContainer::index_t e) { + this->container.erase(this->begin() + e); + return e; } template -typename KeyframeContainer::iterator +typename KeyframeContainer::index_t KeyframeContainer::sync(const KeyframeContainer &other, const time::time_t &start) { // Delete elements after start time - iterator at = this->last_before(start, this->end()); + index_t at = this->last_before(start, this->container.size()); at = this->erase_after(at); - auto at_other = other.begin(); - ++at_other; // always skip the first element (because it's the default value) + auto at_other = 1; // always skip the first element (because it's the default value) // Copy all elements from other with time >= start - while (at_other != other.end()) { - if (at_other->time >= start) { - at = this->insert_after(*at_other, at); + for (size_t i = at_other; i < other.size(); i++) { + if (other.get(i).time() >= start) { + at = this->insert_after(other.get(i), at); } - ++at_other; } - return at; + return this->container.size(); } template template -typename KeyframeContainer::iterator +typename KeyframeContainer::index_t KeyframeContainer::sync(const KeyframeContainer &other, const std::function &converter, const time::time_t &start) { // Delete elements after start time - iterator at = this->last_before(start, this->end()); + index_t at = this->last_before(start, this->container.size()); at = this->erase_after(at); - auto at_other = other.begin(); - ++at_other; // always skip the first element (because it's the default value) + auto at_other = 1; // always skip the first element (because it's the default value) // Copy all elements from other with time >= start - while (at_other != other.end()) { - if (at_other->time >= start) { - // Convert the value to the type of this container - at = this->insert_after(at_other->time, converter(at_other->value), at); + 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); } - ++at_other; } - return at; + return this->container.size(); } template -typename KeyframeContainer::iterator +typename KeyframeContainer::index_t KeyframeContainer::erase_group(const time::time_t &time, - const iterator &last_elem) { - iterator at = last_elem; + const KeyframeContainer::index_t &last_elem) { + size_t at = last_elem; // if the time what we're looking for // erase elements until all element with that time are purged - while (at != std::end(this->container) and at->time == time) { - at = this->container.erase(at); - if (at != std::begin(this->container)) [[likely]] { - --at; - } + while (at != this->container.size() and this->container.at(at).time() == time) { + this->container.erase(this->container.begin() + at); + --at; } // we have to cancel one --at in order to return // the element after the group we deleted. - if (at != std::end(this->container)) { + if (at != this->container.size()) { ++at; } diff --git a/libopenage/curve/segmented.h b/libopenage/curve/segmented.h index e02572060f..98add2995e 100644 --- a/libopenage/curve/segmented.h +++ b/libopenage/curve/segmented.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 @@ -62,7 +62,7 @@ void Segmented::set_last_jump(const time::time_t &at, const T &leftval, const auto hint = this->container.last(at, this->last_element); // erase all one same-time values - while (hint->time == at) { + while (this->container.get(hint).time() == at) { hint--; } diff --git a/libopenage/curve/tests/curve_types.cpp b/libopenage/curve/tests/curve_types.cpp index 40f20395ee..421d3fb4d7 100644 --- a/libopenage/curve/tests/curve_types.cpp +++ b/libopenage/curve/tests/curve_types.cpp @@ -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. #include #include @@ -27,8 +27,8 @@ void curve_types() { auto p0 = c.insert_before(0, 0); auto p1 = c.insert_before(1, 1); auto p2 = c.insert_before(10, 2); - auto pa = std::begin(c); - auto pe = std::end(c); + auto ib = 0; + auto ie = c.size(); // now contains: [-inf: 0, 0:0, 1:1, 10:2] @@ -36,133 +36,133 @@ void curve_types() { { auto it = c.begin(); - TESTEQUALS(it->value, 0); - TESTEQUALS(it->time, time::TIME_MIN); - TESTEQUALS((++it)->time, 0); - TESTEQUALS(it->value, 0); - TESTEQUALS((++it)->time, 1); - TESTEQUALS(it->value, 1); - TESTEQUALS((++it)->time, 10); - TESTEQUALS(it->value, 2); + TESTEQUALS(it->val(), 0); + TESTEQUALS(it->time(), time::TIME_MIN); + TESTEQUALS((++it)->time(), 0); + TESTEQUALS(it->val(), 0); + TESTEQUALS((++it)->time(), 1); + TESTEQUALS(it->val(), 1); + TESTEQUALS((++it)->time(), 10); + TESTEQUALS(it->val(), 2); } // last function tests without hints - TESTEQUALS(c.last(0)->value, 0); - TESTEQUALS(c.last(1)->value, 1); - TESTEQUALS(c.last(5)->value, 1); - TESTEQUALS(c.last(10)->value, 2); - TESTEQUALS(c.last(47)->value, 2); + TESTEQUALS(c.get(c.last(0)).val(), 0); + TESTEQUALS(c.get(c.last(1)).val(), 1); + TESTEQUALS(c.get(c.last(5)).val(), 1); + TESTEQUALS(c.get(c.last(10)).val(), 2); + TESTEQUALS(c.get(c.last(47)).val(), 2); // last() with hints. - TESTEQUALS(c.last(0, pa)->value, 0); - TESTEQUALS(c.last(1, pa)->value, 1); - TESTEQUALS(c.last(5, pa)->value, 1); - TESTEQUALS(c.last(10, pa)->value, 2); - TESTEQUALS(c.last(47, pa)->value, 2); - - TESTEQUALS(c.last(0, p0)->value, 0); - TESTEQUALS(c.last(1, p0)->value, 1); - TESTEQUALS(c.last(5, p0)->value, 1); - TESTEQUALS(c.last(10, p0)->value, 2); - TESTEQUALS(c.last(47, p0)->value, 2); - - TESTEQUALS(c.last(0, p1)->value, 0); - TESTEQUALS(c.last(1, p1)->value, 1); - TESTEQUALS(c.last(5, p1)->value, 1); - TESTEQUALS(c.last(10, p1)->value, 2); - TESTEQUALS(c.last(47, p1)->value, 2); - - TESTEQUALS(c.last(0, p2)->value, 0); - TESTEQUALS(c.last(1, p2)->value, 1); - TESTEQUALS(c.last(5, p2)->value, 1); - TESTEQUALS(c.last(10, p2)->value, 2); - TESTEQUALS(c.last(47, p2)->value, 2); - - TESTEQUALS(c.last(0, pe)->value, 0); - TESTEQUALS(c.last(1, pe)->value, 1); - TESTEQUALS(c.last(5, pe)->value, 1); - TESTEQUALS(c.last(10, pe)->value, 2); - TESTEQUALS(c.last(47, pe)->value, 2); + TESTEQUALS(c.get(c.last(0, ib)).val(), 0); + TESTEQUALS(c.get(c.last(1, ib)).val(), 1); + TESTEQUALS(c.get(c.last(5, ib)).val(), 1); + TESTEQUALS(c.get(c.last(10, ib)).val(), 2); + TESTEQUALS(c.get(c.last(47, ib)).val(), 2); + + TESTEQUALS(c.get(c.last(0, p0)).val(), 0); + TESTEQUALS(c.get(c.last(1, p0)).val(), 1); + TESTEQUALS(c.get(c.last(5, p0)).val(), 1); + TESTEQUALS(c.get(c.last(10, p0)).val(), 2); + TESTEQUALS(c.get(c.last(47, p0)).val(), 2); + + TESTEQUALS(c.get(c.last(0, p1)).val(), 0); + TESTEQUALS(c.get(c.last(1, p1)).val(), 1); + TESTEQUALS(c.get(c.last(5, p1)).val(), 1); + TESTEQUALS(c.get(c.last(10, p1)).val(), 2); + TESTEQUALS(c.get(c.last(47, p1)).val(), 2); + + TESTEQUALS(c.get(c.last(0, p2)).val(), 0); + TESTEQUALS(c.get(c.last(1, p2)).val(), 1); + TESTEQUALS(c.get(c.last(5, p2)).val(), 1); + TESTEQUALS(c.get(c.last(10, p2)).val(), 2); + TESTEQUALS(c.get(c.last(47, p2)).val(), 2); + + TESTEQUALS(c.get(c.last(0, ie)).val(), 0); + TESTEQUALS(c.get(c.last(1, ie)).val(), 1); + TESTEQUALS(c.get(c.last(5, ie)).val(), 1); + TESTEQUALS(c.get(c.last(10, ie)).val(), 2); + TESTEQUALS(c.get(c.last(47, ie)).val(), 2); // Now test the basic erase() function // Delete the 1-element, new values should be [-inf:0, 0:0, 10:2] c.erase(c.last(1)); - TESTEQUALS(c.last(1)->value, 0); - TESTEQUALS(c.last(5)->value, 0); - TESTEQUALS(c.last(47)->value, 2); + TESTEQUALS(c.get(c.last(1)).val(), 0); + TESTEQUALS(c.get(c.last(5)).val(), 0); + TESTEQUALS(c.get(c.last(47)).val(), 2); // should do nothing, since we delete all at > 99, // but the last element is at 10. should still be [-inf:0, 0:0, 10:2] c.erase_after(c.last(99)); - TESTEQUALS(c.last(47)->value, 2); + TESTEQUALS(c.get(c.last(47)).val(), 2); // now since 5 < 10, element with value 2 has to be gone // result should be [-inf:0, 0:0] c.erase_after(c.last(5)); - TESTEQUALS(c.last(47)->value, 0); + TESTEQUALS(c.get(c.last(47)).val(), 0); c.insert_overwrite(0, 42); - TESTEQUALS(c.last(100)->value, 42); - TESTEQUALS(c.last(100)->time, 0); + TESTEQUALS(c.get(c.last(100)).val(), 42); + TESTEQUALS(c.get(c.last(100)).time(), 0); // the curve now contains [-inf:0, 0:42] // let's change/add some more elements c.insert_overwrite(0, 10); - TESTEQUALS(c.last(100)->value, 10); + TESTEQUALS(c.get(c.last(100)).val(), 10); c.insert_after(0, 11); c.insert_after(0, 12); // now: [-inf:0, 0:10, 0:11, 0:12] - TESTEQUALS(c.last(0)->value, 12); - TESTEQUALS(c.last(10)->value, 12); + TESTEQUALS(c.get(c.last(0)).val(), 12); + TESTEQUALS(c.get(c.last(10)).val(), 12); c.insert_before(0, 2); // all the values at t=0 should be 2, 10, 11, 12 c.insert_after(1, 15); - TESTEQUALS(c.last(1)->value, 15); - TESTEQUALS(c.last(10)->value, 15); + TESTEQUALS(c.get(c.last(1)).val(), 15); + TESTEQUALS(c.get(c.last(10)).val(), 15); c.insert_overwrite(2, 20); - TESTEQUALS(c.last(1)->value, 15); - TESTEQUALS(c.last(2)->value, 20); - TESTEQUALS(c.last(10)->value, 20); + TESTEQUALS(c.get(c.last(1)).val(), 15); + TESTEQUALS(c.get(c.last(2)).val(), 20); + TESTEQUALS(c.get(c.last(10)).val(), 20); c.insert_before(3, 25); - TESTEQUALS(c.last(1)->value, 15); - TESTEQUALS(c.last(2)->value, 20); - TESTEQUALS(c.last(3)->value, 25); - TESTEQUALS(c.last(10)->value, 25); + TESTEQUALS(c.get(c.last(1)).val(), 15); + TESTEQUALS(c.get(c.last(2)).val(), 20); + TESTEQUALS(c.get(c.last(3)).val(), 25); + TESTEQUALS(c.get(c.last(10)).val(), 25); // now it should be [-inf: 0, 0: 2, 0: 10, 0: 11, 0: 12, 1: 15, 2: 20, // 3: 25] { auto it = c.begin(); - TESTEQUALS(it->time, time::TIME_MIN); - TESTEQUALS(it->value, 0); + TESTEQUALS(it->time(), time::TIME_MIN); + TESTEQUALS(it->val(), 0); - TESTEQUALS((++it)->time, 0); - TESTEQUALS(it->value, 2); + TESTEQUALS((++it)->time(), 0); + TESTEQUALS(it->val(), 2); - TESTEQUALS((++it)->time, 0); - TESTEQUALS(it->value, 10); + TESTEQUALS((++it)->time(), 0); + TESTEQUALS(it->val(), 10); - TESTEQUALS((++it)->time, 0); - TESTEQUALS(it->value, 11); + TESTEQUALS((++it)->time(), 0); + TESTEQUALS(it->val(), 11); - TESTEQUALS((++it)->time, 0); - TESTEQUALS(it->value, 12); + TESTEQUALS((++it)->time(), 0); + TESTEQUALS(it->val(), 12); - TESTEQUALS((++it)->time, 1); - TESTEQUALS(it->value, 15); + TESTEQUALS((++it)->time(), 1); + TESTEQUALS(it->val(), 15); - TESTEQUALS((++it)->time, 2); - TESTEQUALS(it->value, 20); + TESTEQUALS((++it)->time(), 2); + TESTEQUALS(it->val(), 20); - TESTEQUALS((++it)->time, 3); - TESTEQUALS(it->value, 25); + TESTEQUALS((++it)->time(), 3); + TESTEQUALS(it->val(), 25); } // TODO: test c.insert_overwrite and c.insert_after @@ -170,17 +170,17 @@ void curve_types() { KeyframeContainer c2; c2.sync(c, 1); // now c2 should be [-inf: 0, 1: 15, 2: 20, 3: 25] - TESTEQUALS(c2.last(0)->value, 0); - TESTEQUALS(c2.last(1)->value, 15); - TESTEQUALS(c2.last(2)->value, 20); - TESTEQUALS(c2.last(3)->value, 25); - TESTEQUALS(c2.last(10)->value, 25); + TESTEQUALS(c2.get(c2.last(0)).val(), 0); + TESTEQUALS(c2.get(c2.last(1)).val(), 15); + TESTEQUALS(c2.get(c2.last(2)).val(), 20); + TESTEQUALS(c2.get(c2.last(3)).val(), 25); + TESTEQUALS(c2.get(c2.last(10)).val(), 25); TESTEQUALS(c2.size(), 4); c.clear(); // now it should be [-inf: 0] - TESTEQUALS(c.last(0)->value, 0); - TESTEQUALS(c.last(1)->value, 0); + TESTEQUALS(c.get(c.last(0)).val(), 0); + TESTEQUALS(c.get(c.last(1)).val(), 0); TESTEQUALS(c.size(), 1); } From 5b5ce24639b8672faad8b135d6e002a1f393da9e Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 14 Apr 2024 14:41:05 +0200 Subject: [PATCH 255/771] curve: Use vector for queue container. --- libopenage/curve/queue.h | 100 +++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 42 deletions(-) diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index 7698c18c5c..20fcbbd2e3 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -60,8 +61,19 @@ class Queue : public event::EventEntity { }; public: - using container_t = typename std::list; - using const_iterator = typename container_t::const_iterator; + /** + * The underlaying container type. + */ + using container_t = typename std::vector; + + /** + * The index type to access elements in the container + */ + using index_t = typename container_t::size_type; + + /** + * The iterator type to access elements in the container + */ using iterator = typename container_t::iterator; Queue(const std::shared_ptr &loop, @@ -71,7 +83,7 @@ class Queue : public event::EventEntity { _id{id}, _idstr{idstr}, last_change{time::TIME_ZERO}, - front_start{this->container.end()} {} + front_start{0} {} // prevent accidental copy of queue Queue(const Queue &) = delete; @@ -211,18 +223,18 @@ class Queue : public event::EventEntity { * Erase an element from the queue at the given time. * * @param time The time to erase at. - * @param it The iterator to the element to erase. + * @param at The index of the element to erase. */ - void erase(const time::time_t &time, const_iterator &it); + void erase(const time::time_t &time, index_t at); /** * Get the first alive element inserted at t <= time. * * @param time The time to get the element at. * - * @return Iterator to the first alive element or end() if no such element exists. + * @return Index of the first alive element or end() if no such element exists. */ - const_iterator first_alive(const time::time_t &time) const; + index_t first_alive(const time::time_t &time) const; /** * Identifier for the container @@ -247,15 +259,15 @@ class Queue : public event::EventEntity { /** * Caches the search start position for the next front() call. * - * All iterators before are guaranteed to be dead at t >= last_change. + * All positions before the index are guaranteed to be dead at t >= last_change. */ - mutable typename Queue::const_iterator front_start; + index_t front_start; }; template -typename Queue::const_iterator Queue::first_alive(const time::time_t &time) const { - auto hint = this->container.begin(); +typename Queue::index_t Queue::first_alive(const time::time_t &time) const { + index_t hint = 0; // check if the access is later than the last change if (this->last_change <= time) { @@ -264,42 +276,42 @@ typename Queue::const_iterator Queue::first_alive(const time::time_t &time } // Iterate until we find an alive element - while (hint->alive() <= time - and hint != this->container.end()) { - if (hint->dead() > time) { + while (hint != this->container.size() + and this->container.at(hint).alive() <= time) { + if (this->container.at(hint).dead() > time) { return hint; } ++hint; } - return this->container.end(); + return this->container.size(); } template const T &Queue::front(const time::time_t &time) const { - auto it = this->first_alive(time); - ENSURE(it != this->container.end(), "Tried accessing front at " << time << " but queue is empty."); + auto at = this->first_alive(time); + ENSURE(at != this->container.size(), "Tried accessing front at " << time << " but queue is empty."); - return it->value; + return this->container.at(at).value; } template const T &Queue::pop_front(const time::time_t &time) { - Queue::const_iterator it = this->first_alive(time); - ENSURE(it != this->container.end(), "Tried accessing front at " << time << " but queue is empty."); + auto at = this->first_alive(time); + ENSURE(at != this->container.size(), "Tried accessing front at " << time << " but queue is empty."); // cache the search start position for the next front() call - this->front_start = it; + this->front_start = at; this->last_change = time; // erase the element - this->erase(time, it); + this->erase(time, at); this->changes(time); - return it->value; + return this->container.at(at).value; } @@ -309,7 +321,7 @@ bool Queue::empty(const time::time_t &time) const { return true; } - return this->first_alive(time) == this->container.end(); + return this->first_alive(time) == this->container.size(); } @@ -362,22 +374,25 @@ void Queue::erase(const CurveIterator> &it) { template void Queue::erase(const time::time_t &time, - const_iterator &it) { - it->set_dead(time); + index_t at) { + this->container[at].set_dead(time); } template QueueFilterIterator> Queue::insert(const time::time_t &time, const T &e) { - const_iterator insertion_point = this->container.end(); - while (insertion_point != this->container.begin()) { - --insertion_point; - if (insertion_point->alive() <= time) { - ++insertion_point; + index_t at = this->container.size(); + while (at != 0) { + --at; + if (this->container.at(at).alive() <= time) { + ++at; break; } } + + // Get the iterator to the insertion point + iterator insertion_point = std::next(this->container.begin(), at); insertion_point = this->container.insert(insertion_point, queue_wrapper{time, e}); // TODO: Inserting before any dead elements shoud reset their death time @@ -385,17 +400,17 @@ QueueFilterIterator> Queue::insert(const time::time_t &time, // cache the insertion time this->last_change = time; - if (this->front_start == this->container.end()) [[unlikely]] { + if (this->front_start == this->container.size()) [[unlikely]] { // only true if the container is empty // or all elements are dead - this->front_start = insertion_point; + this->front_start = at; } else { // if there are more alive elements, only cache if the // insertion time is before the current front - if (time < this->front_start->alive()) { + if (time < this->container.at(this->front_start).alive()) { // cache the search start position for the next front() call - this->front_start = insertion_point; + this->front_start = at; } } @@ -417,25 +432,26 @@ QueueFilterIterator> Queue::insert(const time::time_t &time, template void Queue::clear(const time::time_t &time) { - Queue::const_iterator it = this->first_alive(time); + index_t at = this->first_alive(time); // no elements alive at t <= time // so we don't have any changes - if (it == this->container.end()) { + if (at == this->container.size()) { return; } // erase all elements alive at t <= time - while (it->alive() <= time and it != this->container.end()) { - if (it->dead() > time) { - it->set_dead(time); + while (this->container.at(at).alive() <= time + and at != this->container.size()) { + if (this->container.at(at).dead() > time) { + this->container[at].set_dead(time); } - ++it; + ++at; } // cache the search start position for the next front() call this->last_change = time; - this->front_start = it; + this->front_start = at; this->changes(time); } From b2d9916dd9a2a998bcc286acfc12592db4ea9278 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 14 Apr 2024 16:19:28 +0200 Subject: [PATCH 256/771] curve: Simplify queue operations. --- libopenage/curve/queue.h | 64 ++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index 20fcbbd2e3..e2094aba1e 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -74,6 +74,7 @@ class Queue : public event::EventEntity { /** * The iterator type to access elements in the container */ + using const_iterator = typename container_t::const_iterator; using iterator = typename container_t::iterator; Queue(const std::shared_ptr &loop, @@ -220,12 +221,15 @@ class Queue : public event::EventEntity { private: /** - * Erase an element from the queue at the given time. + * Kill an element from the queue at the given time. * - * @param time The time to erase at. - * @param at The index of the element to erase. + * The element is set to dead at the given time and is not accessible + * for pops with t > time. + * + * @param time The time to kill at. + * @param at The index of the element to kill. */ - void erase(const time::time_t &time, index_t at); + void kill(const time::time_t &time, index_t at); /** * Get the first alive element inserted at t <= time. @@ -274,6 +278,7 @@ typename Queue::index_t Queue::first_alive(const time::time_t &time) const // start searching from the last front position hint = this->front_start; } + // else search from the beginning // Iterate until we find an alive element while (hint != this->container.size() @@ -290,8 +295,14 @@ typename Queue::index_t Queue::first_alive(const time::time_t &time) const template const T &Queue::front(const time::time_t &time) const { - auto at = this->first_alive(time); - ENSURE(at != this->container.size(), "Tried accessing front at " << time << " but queue is empty."); + index_t at = this->first_alive(time); + ENSURE(at < this->container.size(), + "Tried accessing front at " << time << " but index " << at << " is invalid. " + << "The queue may be empty." + << "(last_change: " << this->last_change + << ", front_start: " << this->front_start + << ", container size: " << this->container.size() + << ")"); return this->container.at(at).value; } @@ -299,15 +310,23 @@ const T &Queue::front(const time::time_t &time) const { template const T &Queue::pop_front(const time::time_t &time) { - auto at = this->first_alive(time); - ENSURE(at != this->container.size(), "Tried accessing front at " << time << " but queue is empty."); + index_t at = this->first_alive(time); + ENSURE(at < this->container.size(), + "Tried accessing front at " << time << " but index " << at << " is invalid. " + << "The queue may be empty." + << "(last_change: " << this->last_change + << ", front_start: " << this->front_start + << ", container size: " << this->container.size() + << ")"); + + // kill the element at time t + this->kill(time, at); // cache the search start position for the next front() call - this->front_start = at; + // for pop time t, there should be no more elements alive before t + // so we can advance the front to the next element this->last_change = time; - - // erase the element - this->erase(time, at); + this->front_start = at + 1; this->changes(time); @@ -373,8 +392,8 @@ void Queue::erase(const CurveIterator> &it) { template -void Queue::erase(const time::time_t &time, - index_t at) { +void Queue::kill(const time::time_t &time, + index_t at) { this->container[at].set_dead(time); } @@ -383,7 +402,7 @@ template QueueFilterIterator> Queue::insert(const time::time_t &time, const T &e) { index_t at = this->container.size(); - while (at != 0) { + while (at > 0) { --at; if (this->container.at(at).alive() <= time) { ++at; @@ -400,19 +419,12 @@ QueueFilterIterator> Queue::insert(const time::time_t &time, // cache the insertion time this->last_change = time; - if (this->front_start == this->container.size()) [[unlikely]] { - // only true if the container is empty - // or all elements are dead + + // if the new element is inserted before the current front element + // cache it as the new front element + if (at < this->front_start) { this->front_start = at; } - else { - // if there are more alive elements, only cache if the - // insertion time is before the current front - if (time < this->container.at(this->front_start).alive()) { - // cache the search start position for the next front() call - this->front_start = at; - } - } auto ct = QueueFilterIterator>( insertion_point, From 32ddddb26ae7086edbaf88276595ff19f67868b9 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 27 Apr 2024 03:54:19 +0200 Subject: [PATCH 257/771] curve: Make Keyframe's 'value' member public. --- libopenage/curve/keyframe.h | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/libopenage/curve/keyframe.h b/libopenage/curve/keyframe.h index cb4c4dc727..e868783cbb 100644 --- a/libopenage/curve/keyframe.h +++ b/libopenage/curve/keyframe.h @@ -33,21 +33,40 @@ class Keyframe { * New element fron time and value */ Keyframe(const time::time_t &time, const T &value) : - timestamp{time}, - value{value} {} + value{value}, + timestamp{time} {} + /** + * Get the time of this keyframe. + * + * @return Keyframe time. + */ const time::time_t &time() const { return this->timestamp; } + /** + * Get the value of this keyframe. + * + * @return Keyframe value. + */ const T &val() const { return this->value; } +public: + /** + * Value of the keyframe. + * + * Can be modified by the curve if necessary. + */ + T value = T{}; + private: + /** + * Time of the keyframe. + */ time::time_t timestamp = time::TIME_MIN; - - T value = T{}; }; } // namespace openage::curve From d2349d5ef90dde9ff0618516f8940ba2ea631139 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 6 May 2024 19:45:40 +0200 Subject: [PATCH 258/771] curve: Rename 'index_t' to 'elem_ptr'. --- libopenage/curve/base_curve.h | 2 +- libopenage/curve/keyframe_container.h | 130 +++++++++++++------------- libopenage/curve/queue.h | 22 ++--- 3 files changed, 77 insertions(+), 77 deletions(-) diff --git a/libopenage/curve/base_curve.h b/libopenage/curve/base_curve.h index a9e1ba779f..dc58885b98 100644 --- a/libopenage/curve/base_curve.h +++ b/libopenage/curve/base_curve.h @@ -194,7 +194,7 @@ class BaseCurve : public event::EventEntity { /** * Cache the index of the last accessed element (usually the end). */ - mutable typename KeyframeContainer::index_t last_element; + mutable typename KeyframeContainer::elem_ptr last_element; }; diff --git a/libopenage/curve/keyframe_container.h b/libopenage/curve/keyframe_container.h index 67bb0d0cc3..d9bb9a0bbd 100644 --- a/libopenage/curve/keyframe_container.h +++ b/libopenage/curve/keyframe_container.h @@ -40,7 +40,7 @@ class KeyframeContainer { /** * The index type to access elements in the container */ - using index_t = typename container_t::size_type; + using elem_ptr = typename container_t::size_type; /** * The iterator type to access elements in the container @@ -76,7 +76,7 @@ class KeyframeContainer { */ size_t size() const; - const keyframe_t &get(const index_t &idx) const { + const keyframe_t &get(const elem_ptr &idx) const { return this->container.at(idx); } @@ -84,8 +84,8 @@ class KeyframeContainer { * 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. */ - index_t last(const time::time_t &time, - const index_t &hint) const; + 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 @@ -95,7 +95,7 @@ class KeyframeContainer { * no chance for you to have a hint (or the container is known to be nearly * empty) */ - index_t last(const time::time_t &time) const { + elem_ptr last(const time::time_t &time) const { return this->last(time, this->container.size()); } @@ -103,14 +103,14 @@ class KeyframeContainer { * 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. */ - index_t last_before(const time::time_t &time, - const index_t &hint) const; + 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. */ - index_t last_before(const time::time_t &time) const { + elem_ptr last_before(const time::time_t &time) const { return this->last_before(time, this->container.size()); } @@ -121,7 +121,7 @@ class KeyframeContainer { * This function is not recommended for use, whenever possible, keep a hint * to insert the data. */ - index_t insert_before(const keyframe_t &value) { + elem_ptr insert_before(const keyframe_t &value) { return this->insert_before(value, this->container.size()); } @@ -132,7 +132,7 @@ class KeyframeContainer { * history size. If there is a keyframe with identical time, this will * insert the new keyframe before the old one. */ - index_t insert_before(const keyframe_t &value, const index_t &hint); + elem_ptr insert_before(const keyframe_t &value, const elem_ptr &hint); /** * Create and insert a new element without submitting a hint. The search is @@ -140,7 +140,7 @@ class KeyframeContainer { * discouraged, use it only, if your really do not have the possibility to * get a hint. */ - index_t insert_before(const time::time_t &time, const T &value) { + elem_ptr insert_before(const time::time_t &time, const T &value) { return this->insert_before(keyframe_t(time, value), this->container.size()); } @@ -149,7 +149,7 @@ class KeyframeContainer { * If there is a value with identical time, this will insert the new value * before the old one. */ - index_t insert_before(const time::time_t &time, const T &value, const index_t &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); } @@ -160,9 +160,9 @@ class KeyframeContainer { * `overwrite_all` == true -> overwrite all same-time elements. * `overwrite_all` == false -> overwrite the last of the time-conflict elements. */ - index_t insert_overwrite(const keyframe_t &value, - const index_t &hint, - bool overwrite_all = false); + elem_ptr insert_overwrite(const keyframe_t &value, + const elem_ptr &hint, + bool overwrite_all = false); /** * Insert a new value at given time which will overwrite the last of the @@ -170,7 +170,7 @@ class KeyframeContainer { * 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. */ - index_t insert_overwrite(const time::time_t &time, const T &value) { + elem_ptr insert_overwrite(const time::time_t &time, const T &value) { return this->insert_overwrite(keyframe_t(time, value), this->container.size()); } @@ -182,10 +182,10 @@ class KeyframeContainer { * Provide a insertion hint to abbreviate the search for the * insertion point. */ - index_t insert_overwrite(const time::time_t &time, - const T &value, - const index_t &hint, - bool overwrite_all = false) { + elem_ptr insert_overwrite(const time::time_t &time, + const T &value, + const elem_ptr &hint, + bool overwrite_all = false) { return this->insert_overwrite(keyframe_t(time, value), hint, overwrite_all); } @@ -194,7 +194,7 @@ class KeyframeContainer { * conflict. Give an approximate insertion location to minimize runtime on * big-history curves. */ - index_t insert_after(const keyframe_t &value, const index_t &hint); + elem_ptr insert_after(const keyframe_t &value, const elem_ptr &hint); /** * Insert a new value at given time which will be prepended to the block of @@ -202,7 +202,7 @@ class KeyframeContainer { * 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. */ - index_t 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()); } @@ -211,27 +211,27 @@ 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. */ - index_t insert_after(const time::time_t &time, const T &value, const index_t &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. */ - index_t erase_after(index_t last_valid); + elem_ptr erase_after(elem_ptr last_valid); /** * Erase a single element from the curve. * Returns the element after the deleted one. */ - index_t erase(index_t it); + 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. */ - index_t erase(const time::time_t &time) { + elem_ptr erase(const time::time_t &time) { return this->erase(time, this->container.size()); } @@ -244,8 +244,8 @@ class KeyframeContainer { * Or, if no elements with this time exist, * the iterator to the first element after the requested time is returned */ - index_t erase(const time::time_t &time, - const index_t &hint) { + elem_ptr erase(const time::time_t &time, + const elem_ptr &hint) { return this->erase_group(time, this->last(time, hint)); } @@ -285,8 +285,8 @@ class KeyframeContainer { * Using the default value replaces ALL keyframes of \p this with * the keyframes of \p other. */ - index_t sync(const KeyframeContainer &other, - const time::time_t &start = time::TIME_MIN); + elem_ptr sync(const KeyframeContainer &other, + const time::time_t &start = time::TIME_MIN); /** * Copy keyframes from another container (with a different element type) to this container. @@ -301,9 +301,9 @@ class KeyframeContainer { * the keyframes of \p other. */ template - index_t sync(const KeyframeContainer &other, - const std::function &converter, - const time::time_t &start = time::TIME_MIN); + elem_ptr sync(const KeyframeContainer &other, + const std::function &converter, + const time::time_t &start = time::TIME_MIN); /** * Debugging method to be used from gdb to understand bugs better. @@ -319,8 +319,8 @@ class KeyframeContainer { * Erase elements with this time. * The iterator has to point to the last element of the same-time group. */ - index_t erase_group(const time::time_t &time, - const index_t &last_elem); + elem_ptr erase_group(const time::time_t &time, + const elem_ptr &last_elem); /** * The data store. @@ -360,11 +360,11 @@ size_t KeyframeContainer::size() const { * that determines the curve value for a searched time. */ template -typename KeyframeContainer::index_t +typename KeyframeContainer::elem_ptr KeyframeContainer::last(const time::time_t &time, - const KeyframeContainer::index_t &hint) const { - index_t at = hint; - const index_t end = this->container.size(); + const KeyframeContainer::elem_ptr &hint) const { + elem_ptr at = hint; + const elem_ptr end = this->container.size(); if (at != end and this->container.at(at).time() <= time) { // walk to the right until the time is larget than the searched @@ -394,11 +394,11 @@ KeyframeContainer::last(const time::time_t &time, * first element that matches the search time. */ template -typename KeyframeContainer::index_t +typename KeyframeContainer::elem_ptr KeyframeContainer::last_before(const time::time_t &time, - const KeyframeContainer::index_t &hint) const { - index_t at = hint; - const index_t end = this->container.size(); + const KeyframeContainer::elem_ptr &hint) const { + elem_ptr at = hint; + const elem_ptr end = this->container.size(); if (at != end and this->container.at(at).time() < time) { // walk to the right until the time is larget than the searched @@ -423,10 +423,10 @@ KeyframeContainer::last_before(const time::time_t &time, * Determine where to insert based on time, and insert. */ template -typename KeyframeContainer::index_t +typename KeyframeContainer::elem_ptr KeyframeContainer::insert_before(const KeyframeContainer::keyframe_t &e, - const KeyframeContainer::index_t &hint) { - index_t at = this->last(e.time(), hint); + const KeyframeContainer::elem_ptr &hint) { + elem_ptr at = this->last(e.time(), hint); if (at == this->container.size()) { this->container.push_back(e); @@ -449,12 +449,12 @@ KeyframeContainer::insert_before(const KeyframeContainer::keyframe_t &e, * Determine where to insert based on time, and insert, overwriting value(s) with same time. */ template -typename KeyframeContainer::index_t +typename KeyframeContainer::elem_ptr KeyframeContainer::insert_overwrite(const KeyframeContainer::keyframe_t &e, - const KeyframeContainer::index_t &hint, + const KeyframeContainer::elem_ptr &hint, bool overwrite_all) { - index_t at = this->last(e.time(), hint); - const index_t end = this->container.size(); + elem_ptr at = this->last(e.time(), hint); + const elem_ptr end = this->container.size(); if (overwrite_all) { at = this->erase_group(e.time(), at); @@ -479,11 +479,11 @@ KeyframeContainer::insert_overwrite(const KeyframeContainer::keyframe_t &e * If there is a time conflict, insert after the existing element. */ template -typename KeyframeContainer::index_t +typename KeyframeContainer::elem_ptr KeyframeContainer::insert_after(const KeyframeContainer::keyframe_t &e, - const KeyframeContainer::index_t &hint) { - index_t at = this->last(e.time(), hint); - const index_t end = this->container.size(); + const KeyframeContainer::elem_ptr &hint) { + elem_ptr at = this->last(e.time(), hint); + const elem_ptr end = this->container.size(); if (at != end) { ++at; @@ -498,12 +498,12 @@ 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 -typename KeyframeContainer::index_t -KeyframeContainer::erase_after(KeyframeContainer::index_t last_valid) { +typename KeyframeContainer::elem_ptr +KeyframeContainer::erase_after(KeyframeContainer::elem_ptr last_valid) { // exclude the last_valid element from deletion if (last_valid != this->container.size()) { // Delete everything to the end. - const index_t delete_start = last_valid + 1; + const elem_ptr delete_start = last_valid + 1; this->container.erase(this->begin() + delete_start, this->end()); } @@ -515,19 +515,19 @@ KeyframeContainer::erase_after(KeyframeContainer::index_t last_valid) { * Delete the element from the list and call delete on it. */ template -typename KeyframeContainer::index_t -KeyframeContainer::erase(KeyframeContainer::index_t e) { +typename KeyframeContainer::elem_ptr +KeyframeContainer::erase(KeyframeContainer::elem_ptr e) { this->container.erase(this->begin() + e); return e; } template -typename KeyframeContainer::index_t +typename KeyframeContainer::elem_ptr KeyframeContainer::sync(const KeyframeContainer &other, const time::time_t &start) { // Delete elements after start time - index_t at = this->last_before(start, this->container.size()); + 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) @@ -545,12 +545,12 @@ KeyframeContainer::sync(const KeyframeContainer &other, template template -typename KeyframeContainer::index_t +typename KeyframeContainer::elem_ptr KeyframeContainer::sync(const KeyframeContainer &other, const std::function &converter, const time::time_t &start) { // Delete elements after start time - index_t at = this->last_before(start, this->container.size()); + 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) @@ -567,9 +567,9 @@ KeyframeContainer::sync(const KeyframeContainer &other, template -typename KeyframeContainer::index_t +typename KeyframeContainer::elem_ptr KeyframeContainer::erase_group(const time::time_t &time, - const KeyframeContainer::index_t &last_elem) { + const KeyframeContainer::elem_ptr &last_elem) { size_t at = last_elem; // if the time what we're looking for diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index e2094aba1e..9604a76308 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -69,7 +69,7 @@ class Queue : public event::EventEntity { /** * The index type to access elements in the container */ - using index_t = typename container_t::size_type; + using elem_ptr = typename container_t::size_type; /** * The iterator type to access elements in the container @@ -229,7 +229,7 @@ class Queue : public event::EventEntity { * @param time The time to kill at. * @param at The index of the element to kill. */ - void kill(const time::time_t &time, index_t at); + void kill(const time::time_t &time, elem_ptr at); /** * Get the first alive element inserted at t <= time. @@ -238,7 +238,7 @@ class Queue : public event::EventEntity { * * @return Index of the first alive element or end() if no such element exists. */ - index_t first_alive(const time::time_t &time) const; + elem_ptr first_alive(const time::time_t &time) const; /** * Identifier for the container @@ -265,13 +265,13 @@ class Queue : public event::EventEntity { * * All positions before the index are guaranteed to be dead at t >= last_change. */ - index_t front_start; + elem_ptr front_start; }; template -typename Queue::index_t Queue::first_alive(const time::time_t &time) const { - index_t hint = 0; +typename Queue::elem_ptr Queue::first_alive(const time::time_t &time) const { + elem_ptr hint = 0; // check if the access is later than the last change if (this->last_change <= time) { @@ -295,7 +295,7 @@ typename Queue::index_t Queue::first_alive(const time::time_t &time) const template const T &Queue::front(const time::time_t &time) const { - index_t at = this->first_alive(time); + elem_ptr at = this->first_alive(time); ENSURE(at < this->container.size(), "Tried accessing front at " << time << " but index " << at << " is invalid. " << "The queue may be empty." @@ -310,7 +310,7 @@ const T &Queue::front(const time::time_t &time) const { template const T &Queue::pop_front(const time::time_t &time) { - index_t at = this->first_alive(time); + elem_ptr at = this->first_alive(time); ENSURE(at < this->container.size(), "Tried accessing front at " << time << " but index " << at << " is invalid. " << "The queue may be empty." @@ -393,7 +393,7 @@ void Queue::erase(const CurveIterator> &it) { template void Queue::kill(const time::time_t &time, - index_t at) { + elem_ptr at) { this->container[at].set_dead(time); } @@ -401,7 +401,7 @@ void Queue::kill(const time::time_t &time, template QueueFilterIterator> Queue::insert(const time::time_t &time, const T &e) { - index_t at = this->container.size(); + elem_ptr at = this->container.size(); while (at > 0) { --at; if (this->container.at(at).alive() <= time) { @@ -444,7 +444,7 @@ QueueFilterIterator> Queue::insert(const time::time_t &time, template void Queue::clear(const time::time_t &time) { - index_t at = this->first_alive(time); + elem_ptr at = this->first_alive(time); // no elements alive at t <= time // so we don't have any changes From ae6c34fb880b60d6579c21cec55da0096751b5e8 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 28 Apr 2024 15:52:12 +0200 Subject: [PATCH 259/771] renderer: Store terrain tiles in render entity. --- libopenage/renderer/stages/terrain/chunk.cpp | 43 +++++++++++----- libopenage/renderer/stages/terrain/chunk.h | 2 +- .../renderer/stages/terrain/render_entity.cpp | 49 +++++++++++++++---- .../renderer/stages/terrain/render_entity.h | 36 ++++++++++++-- 4 files changed, 105 insertions(+), 25 deletions(-) diff --git a/libopenage/renderer/stages/terrain/chunk.cpp b/libopenage/renderer/stages/terrain/chunk.cpp index 2bb836a673..70322283bb 100644 --- a/libopenage/renderer/stages/terrain/chunk.cpp +++ b/libopenage/renderer/stages/terrain/chunk.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 "chunk.h" @@ -33,10 +33,17 @@ void TerrainChunk::fetch_updates(const time::time_t & /* time */) { } // TODO: Change mesh instead of recreating it // TODO: Multiple meshes - auto new_mesh = this->create_mesh(); - new_mesh->create_model_matrix(this->offset); this->meshes.clear(); - this->meshes.push_back(new_mesh); + for (const auto &terrain_path : this->render_entity->get_terrain_paths()) { + auto new_mesh = this->create_mesh(terrain_path); + new_mesh->create_model_matrix(this->offset); + this->meshes.push_back(new_mesh); + } + + // auto new_mesh = this->create_mesh(); + // new_mesh->create_model_matrix(this->offset); + // this->meshes.clear(); + // this->meshes.push_back(new_mesh); // Indicate to the render entity that its updates have been processed. this->render_entity->clear_changed_flag(); @@ -52,11 +59,25 @@ const std::vector> &TerrainChunk::get_meshes( return this->meshes; } -std::shared_ptr TerrainChunk::create_mesh() { - // Update mesh +std::shared_ptr TerrainChunk::create_mesh(const std::string &texture_path) { auto size = this->render_entity->get_size(); + auto tiles = this->render_entity->get_tiles(); auto src_verts = this->render_entity->get_vertices(); + // Filter tiles by texture path + std::vector> tile_coords; + for (size_t i = 0; i < tiles.size(); ++i) { + auto tile = tiles.at(i); + if (tile.second != texture_path) { + continue; + } + + // Convert tile index to 2D coordinates + auto x = i % size[0]; + auto y = i / size[0]; + tile_coords.push_back({x, y}); + } + // dst_verts places vertices in order // (left to right, bottom to top) std::vector dst_verts{}; @@ -82,12 +103,12 @@ std::shared_ptr TerrainChunk::create_mesh() { for (size_t j = 0; j < size[1] - 1; ++j) { // since we are working on tiles, we split each tile into two triangles // with counter-clockwise vertex order - idxs.push_back(j + i * size[1]); // bottom left - idxs.push_back(j + 1 + i * size[1]); // bottom right - idxs.push_back(j + size[1] + i * size[1]); // top left - idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + 1 + i * size[1]); // bottom right idxs.push_back(j + size[1] + 1 + i * size[1]); // top right - idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + size[1] + i * size[1]); // top left } } diff --git a/libopenage/renderer/stages/terrain/chunk.h b/libopenage/renderer/stages/terrain/chunk.h index 9dc7f028e0..b64133e5b6 100644 --- a/libopenage/renderer/stages/terrain/chunk.h +++ b/libopenage/renderer/stages/terrain/chunk.h @@ -87,7 +87,7 @@ class TerrainChunk { * * @return New terrain mesh. */ - std::shared_ptr create_mesh(); + std::shared_ptr create_mesh(const std::string &texture_path); /** * Size of the chunk in tiles (width x height). diff --git a/libopenage/renderer/stages/terrain/render_entity.cpp b/libopenage/renderer/stages/terrain/render_entity.cpp index fdfd7e486d..9a38681b7f 100644 --- a/libopenage/renderer/stages/terrain/render_entity.cpp +++ b/libopenage/renderer/stages/terrain/render_entity.cpp @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #include "render_entity.h" @@ -12,8 +12,12 @@ namespace openage::renderer::terrain { TerrainRenderEntity::TerrainRenderEntity() : changed{false}, size{0, 0}, - vertices{}, - terrain_path{nullptr, 0} { + tiles{}, + last_update{0.0}, + terrain_paths{}, + vertices{} +// terrain_path{nullptr, 0}, +{ } void TerrainRenderEntity::update_tile(const util::Vector2s size, @@ -31,13 +35,22 @@ void TerrainRenderEntity::update_tile(const util::Vector2s size, auto left_corner = pos.ne * size[0] + pos.se; // update the 4 vertices of the tile - this->vertices[left_corner].up = elevation.to_float(); - this->vertices[left_corner + 1].up = elevation.to_float(); // bottom corner + this->vertices[left_corner].up = elevation.to_float(); // left corner + this->vertices[left_corner + 1].up = elevation.to_float(); // bottom corner this->vertices[left_corner + (size[0] + 1)].up = elevation.to_float(); // top corner this->vertices[left_corner + (size[0] + 2)].up = elevation.to_float(); // right corner + // update tile + this->tiles[left_corner] = {elevation, terrain_path}; + + // update the last update time + this->last_update = time; + + // update the terrain paths + this->terrain_paths.insert(terrain_path); + // set texture path - this->terrain_path.set_last(time, terrain_path); + // this->terrain_path.set_last(time, terrain_path); this->changed = true; } @@ -83,8 +96,20 @@ void TerrainRenderEntity::update(const util::Vector2s size, } } + // update tiles + this->tiles = tiles; + + // update the last update time + this->last_update = time; + + // update the terrain paths + this->terrain_paths.clear(); + for (const auto &tile : this->tiles) { + this->terrain_paths.insert(tile.second); + } + // set texture path - this->terrain_path.set_last(time, tiles[0].second); + // this->terrain_path.set_last(time, tiles[0].second); this->changed = true; } @@ -95,10 +120,16 @@ const std::vector &TerrainRenderEntity::get_vertices() { return this->vertices; } -const curve::Discrete &TerrainRenderEntity::get_terrain_path() { +// const curve::Discrete &TerrainRenderEntity::get_terrain_path() { +// std::shared_lock lock{this->mutex}; + +// return this->terrain_path; +// } + +const TerrainRenderEntity::tiles_t &TerrainRenderEntity::get_tiles() { std::shared_lock lock{this->mutex}; - return this->terrain_path; + return this->tiles; } const util::Vector2s &TerrainRenderEntity::get_size() { diff --git a/libopenage/renderer/stages/terrain/render_entity.h b/libopenage/renderer/stages/terrain/render_entity.h index 054ee73931..2b80c634a0 100644 --- a/libopenage/renderer/stages/terrain/render_entity.h +++ b/libopenage/renderer/stages/terrain/render_entity.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "coord/scene.h" @@ -69,7 +70,21 @@ class TerrainRenderEntity { * * @return Texture mapping of textures to vertex area. */ - const curve::Discrete &get_terrain_path(); + // const curve::Discrete &get_terrain_path(); + + /** + * Get the tiles of the terrain. + * + * @return Terrain tiles. + */ + const tiles_t &get_tiles(); + + /** + * Get the terrain paths used in the terrain. + * + * @return Terrain paths. + */ + const std::unordered_set &get_terrain_paths(); /** * Get the number of vertices on each side of the terrain. @@ -104,6 +119,21 @@ class TerrainRenderEntity { */ util::Vector2s size; + /** + * Terrain tile information (elevation, terrain path). + */ + tiles_t tiles; + + /** + * Time of the last update call. + */ + time::time_t last_update; + + /** + * Terrain texture paths used in \p tiles . + */ + std::unordered_set terrain_paths; + /** * Terrain vertices (ingame coordinates). */ @@ -112,9 +142,7 @@ class TerrainRenderEntity { /** * Path to the terrain definition file. */ - curve::Discrete terrain_path; - - // std::unordered_map texture_map; // texture -> vertex indices + // curve::Discrete terrain_path; /** * Mutex for protecting threaded access. From 56994af7c56fe877a882954739ef4d5709685c63 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 1 May 2024 11:07:26 +0200 Subject: [PATCH 260/771] renderer: Don't use curve for terrain mesh. --- libopenage/renderer/stages/terrain/mesh.cpp | 25 ++++++++------------- libopenage/renderer/stages/terrain/mesh.h | 7 +++--- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/libopenage/renderer/stages/terrain/mesh.cpp b/libopenage/renderer/stages/terrain/mesh.cpp index ca8b19937c..b5af5bee69 100644 --- a/libopenage/renderer/stages/terrain/mesh.cpp +++ b/libopenage/renderer/stages/terrain/mesh.cpp @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #include "mesh.h" @@ -20,21 +20,21 @@ TerrainRenderMesh::TerrainRenderMesh(const std::shared_ptr &asset_manager, - const curve::Discrete &terrain_path, + const std::shared_ptr &info, renderer::resources::MeshData &&mesh) : require_renderable{true}, changed{true}, asset_manager{asset_manager}, - terrain_info{nullptr, 0}, + terrain_info{nullptr}, uniforms{nullptr}, mesh{std::move(mesh)} { - this->set_terrain_path(terrain_path); + this->set_terrain_info(info); } void TerrainRenderMesh::set_mesh(renderer::resources::MeshData &&mesh) { @@ -47,19 +47,12 @@ const renderer::resources::MeshData &TerrainRenderMesh::get_mesh() { return this->mesh; } -void TerrainRenderMesh::set_terrain_path(const curve::Discrete &terrain_path) { +void TerrainRenderMesh::set_terrain_info(const std::shared_ptr &info) { this->changed = true; - this->terrain_info.sync(terrain_path, - std::function(const std::string &)>( - [&](const std::string &path) { - if (path.empty()) { - return std::shared_ptr{nullptr}; - } - return this->asset_manager->request_terrain(path); - })); + this->terrain_info = info; } -void TerrainRenderMesh::update_uniforms(const time::time_t &time) { +void TerrainRenderMesh::update_uniforms(const time::time_t & /* time */) { // TODO: Only update uniforms that changed since last update if (this->uniforms == nullptr) [[unlikely]] { return; @@ -72,7 +65,7 @@ void TerrainRenderMesh::update_uniforms(const time::time_t &time) { // local space -> world space this->uniforms->update("model", this->model_matrix); - auto tex_info = this->terrain_info.get(time)->get_texture(0); + auto tex_info = this->terrain_info->get_texture(0); auto tex_manager = this->asset_manager->get_texture_manager(); auto texture = tex_manager->request(tex_info->get_image_path().value()); diff --git a/libopenage/renderer/stages/terrain/mesh.h b/libopenage/renderer/stages/terrain/mesh.h index 118af0ca00..00bab1b1ed 100644 --- a/libopenage/renderer/stages/terrain/mesh.h +++ b/libopenage/renderer/stages/terrain/mesh.h @@ -8,7 +8,6 @@ #include #include "coord/scene.h" -#include "curve/discrete.h" #include "renderer/resources/mesh_data.h" #include "time/time.h" #include "util/vector.h" @@ -46,7 +45,7 @@ class TerrainRenderMesh { * @param mesh Vertex data of the mesh. */ TerrainRenderMesh(const std::shared_ptr &asset_manager, - const curve::Discrete &terrain_path, + const std::shared_ptr &info, renderer::resources::MeshData &&mesh); ~TerrainRenderMesh() = default; @@ -86,7 +85,7 @@ class TerrainRenderMesh { * * @param texture Terrain info. */ - void set_terrain_path(const curve::Discrete &info); + void set_terrain_info(const std::shared_ptr &info); /** * Update the uniforms of the renderable associated with this object. @@ -150,7 +149,7 @@ class TerrainRenderMesh { /** * Terrain information for the renderables. */ - curve::Discrete> terrain_info; + std::shared_ptr terrain_info; /** * Shader uniforms for the renderable in the terrain render pass. From 86040276ce654c8dad90bb8334c84d3585011256 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 1 May 2024 11:08:35 +0200 Subject: [PATCH 261/771] renderer: Create mesh for individual terrain textures. --- libopenage/renderer/stages/terrain/chunk.cpp | 123 ++++++++++-------- .../renderer/stages/terrain/render_entity.cpp | 6 + 2 files changed, 72 insertions(+), 57 deletions(-) diff --git a/libopenage/renderer/stages/terrain/chunk.cpp b/libopenage/renderer/stages/terrain/chunk.cpp index 70322283bb..12b207bdc9 100644 --- a/libopenage/renderer/stages/terrain/chunk.cpp +++ b/libopenage/renderer/stages/terrain/chunk.cpp @@ -61,54 +61,66 @@ const std::vector> &TerrainChunk::get_meshes( std::shared_ptr TerrainChunk::create_mesh(const std::string &texture_path) { auto size = this->render_entity->get_size(); - auto tiles = this->render_entity->get_tiles(); - auto src_verts = this->render_entity->get_vertices(); - - // Filter tiles by texture path - std::vector> tile_coords; - for (size_t i = 0; i < tiles.size(); ++i) { - auto tile = tiles.at(i); - if (tile.second != texture_path) { - continue; - } + auto v_width = size[0]; + auto v_height = size[1]; - // Convert tile index to 2D coordinates - auto x = i % size[0]; - auto y = i / size[0]; - tile_coords.push_back({x, y}); - } - - // dst_verts places vertices in order - // (left to right, bottom to top) - std::vector dst_verts{}; - dst_verts.reserve(src_verts.size() * 5); - for (auto v : src_verts) { - // Transform to scene coords - auto v_vec = v.to_world_space(); - dst_verts.push_back(v_vec[0]); - dst_verts.push_back(v_vec[1]); - dst_verts.push_back(v_vec[2]); - // TODO: Texture scaling - dst_verts.push_back((v.ne / 10).to_float()); - dst_verts.push_back((v.se / 10).to_float()); - } - - // split the grid into triangles using an index array - std::vector idxs; - idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); - // iterate over all tiles in the grid by columns, i.e. starting - // from the left corner to the bottom corner if you imagine it from - // the camera's point of view - for (size_t i = 0; i < size[0] - 1; ++i) { - for (size_t j = 0; j < size[1] - 1; ++j) { - // since we are working on tiles, we split each tile into two triangles - // with counter-clockwise vertex order - idxs.push_back(j + i * size[1]); // bottom left - idxs.push_back(j + 1 + i * size[1]); // bottom right - idxs.push_back(j + size[1] + i * size[1]); // top left - idxs.push_back(j + 1 + i * size[1]); // bottom right - idxs.push_back(j + size[1] + 1 + i * size[1]); // top right - idxs.push_back(j + size[1] + i * size[1]); // top left + auto tiles = this->render_entity->get_tiles(); + auto heightmap_verts = this->render_entity->get_vertices(); + + // vertex data for the mesh + std::vector mesh_verts{}; + + // vertex indices for the mesh + std::vector idxs{}; + + // maps indices of verts in the heightmap to indices in the vertex data vector + std::unordered_map index_map; + + for (size_t i = 0; i < v_width - 1; ++i) { + for (size_t j = 0; j < v_height - 1; ++j) { + auto tile = tiles.at(j + i * (v_height - 1)); + if (tile.second != texture_path) { + // Skip tiles with different textures + continue; + } + + // indices of the vertices of the current tile + // in the hightmap + std::array tile_verts{ + j + i * v_height, // top left + j + (i + 1) * v_height, // bottom left + j + 1 + (i + 1) * v_height, // bottom right + j + 1 + i * v_height, // top right + }; + + // add the vertices of the current tile to the vertex data vector + for (size_t v_idx : tile_verts) { + // skip if the vertex is already in the vertex data vector + if (not index_map.contains(v_idx)) { + auto v = heightmap_verts[v_idx]; + auto v_vec = v.to_world_space(); + mesh_verts.push_back(v_vec[0]); + mesh_verts.push_back(v_vec[1]); + mesh_verts.push_back(v_vec[2]); + mesh_verts.push_back((v.ne / 10).to_float()); + mesh_verts.push_back((v.se / 10).to_float()); + + // update the index map + // since new verts are added to the end of the vertex data vector + // the mapped index is the current size of the index map + index_map[v_idx] = index_map.size(); + } + } + + // first triangle + idxs.push_back(index_map[tile_verts[0]]); // top left + idxs.push_back(index_map[tile_verts[1]]); // bottom left + idxs.push_back(index_map[tile_verts[2]]); // bottom right + + // second triangle + idxs.push_back(index_map[tile_verts[0]]); // top left + idxs.push_back(index_map[tile_verts[2]]); // bottom right + idxs.push_back(index_map[tile_verts[3]]); // top right } } @@ -118,24 +130,21 @@ std::shared_ptr TerrainChunk::create_mesh(const std::string & resources::vertex_primitive_t::TRIANGLES, resources::index_t::U16}; - auto const vert_data_size = dst_verts.size() * sizeof(float); - std::vector vert_data(vert_data_size); - std::memcpy(vert_data.data(), dst_verts.data(), vert_data_size); + auto const vert_data_size_new = mesh_verts.size() * sizeof(float); + std::vector vert_data_new(vert_data_size_new); + std::memcpy(vert_data_new.data(), mesh_verts.data(), vert_data_size_new); auto const idx_data_size = idxs.size() * sizeof(uint16_t); std::vector idx_data(idx_data_size); std::memcpy(idx_data.data(), idxs.data(), idx_data_size); - resources::MeshData meshdata{std::move(vert_data), std::move(idx_data), info}; - - // Update textures - auto tex_manager = this->asset_manager->get_texture_manager(); - - // TODO: Support multiple textures per chunk + resources::MeshData meshdata{std::move(vert_data_new), std::move(idx_data), info}; + // Create the terrain mesh + auto terrain_info = this->asset_manager->request_terrain(texture_path); auto terrain_mesh = std::make_shared( this->asset_manager, - this->render_entity->get_terrain_path(), + terrain_info, std::move(meshdata)); return terrain_mesh; diff --git a/libopenage/renderer/stages/terrain/render_entity.cpp b/libopenage/renderer/stages/terrain/render_entity.cpp index 9a38681b7f..ba6da4faaa 100644 --- a/libopenage/renderer/stages/terrain/render_entity.cpp +++ b/libopenage/renderer/stages/terrain/render_entity.cpp @@ -132,6 +132,12 @@ const TerrainRenderEntity::tiles_t &TerrainRenderEntity::get_tiles() { return this->tiles; } +const std::unordered_set &TerrainRenderEntity::get_terrain_paths() { + std::shared_lock lock{this->mutex}; + + return this->terrain_paths; +} + const util::Vector2s &TerrainRenderEntity::get_size() { std::shared_lock lock{this->mutex}; From 2233c713cc11ae1bb40925e67771e8128d8b7123 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 1 May 2024 13:47:10 +0200 Subject: [PATCH 262/771] renderer: Remove old terrain paths from render entity. --- .../renderer/stages/terrain/render_entity.cpp | 12 ------------ libopenage/renderer/stages/terrain/render_entity.h | 14 -------------- 2 files changed, 26 deletions(-) diff --git a/libopenage/renderer/stages/terrain/render_entity.cpp b/libopenage/renderer/stages/terrain/render_entity.cpp index ba6da4faaa..e5eb0ba784 100644 --- a/libopenage/renderer/stages/terrain/render_entity.cpp +++ b/libopenage/renderer/stages/terrain/render_entity.cpp @@ -49,9 +49,6 @@ void TerrainRenderEntity::update_tile(const util::Vector2s size, // update the terrain paths this->terrain_paths.insert(terrain_path); - // set texture path - // this->terrain_path.set_last(time, terrain_path); - this->changed = true; } @@ -108,9 +105,6 @@ void TerrainRenderEntity::update(const util::Vector2s size, this->terrain_paths.insert(tile.second); } - // set texture path - // this->terrain_path.set_last(time, tiles[0].second); - this->changed = true; } @@ -120,12 +114,6 @@ const std::vector &TerrainRenderEntity::get_vertices() { return this->vertices; } -// const curve::Discrete &TerrainRenderEntity::get_terrain_path() { -// std::shared_lock lock{this->mutex}; - -// return this->terrain_path; -// } - const TerrainRenderEntity::tiles_t &TerrainRenderEntity::get_tiles() { std::shared_lock lock{this->mutex}; diff --git a/libopenage/renderer/stages/terrain/render_entity.h b/libopenage/renderer/stages/terrain/render_entity.h index 2b80c634a0..4136c7c81a 100644 --- a/libopenage/renderer/stages/terrain/render_entity.h +++ b/libopenage/renderer/stages/terrain/render_entity.h @@ -63,15 +63,6 @@ class TerrainRenderEntity { */ const std::vector &get_vertices(); - /** - * Get the texture mapping for the terrain. - * - * TODO: Return the actual mapping. - * - * @return Texture mapping of textures to vertex area. - */ - // const curve::Discrete &get_terrain_path(); - /** * Get the tiles of the terrain. * @@ -139,11 +130,6 @@ class TerrainRenderEntity { */ std::vector vertices; - /** - * Path to the terrain definition file. - */ - // curve::Discrete terrain_path; - /** * Mutex for protecting threaded access. */ From fc44940f27d539cfdd2037518d7d1b7001d686bc Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 1 May 2024 14:54:27 +0200 Subject: [PATCH 263/771] assets: Add second test terrain asset. --- assets/test/textures/test_terrain2.png | Bin 0 -> 9596 bytes assets/test/textures/test_terrain2.terrain | 12 ++++++++++++ assets/test/textures/test_terrain2.texture | 12 ++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 assets/test/textures/test_terrain2.png create mode 100644 assets/test/textures/test_terrain2.terrain create mode 100644 assets/test/textures/test_terrain2.texture diff --git a/assets/test/textures/test_terrain2.png b/assets/test/textures/test_terrain2.png new file mode 100644 index 0000000000000000000000000000000000000000..100205bfa2cf5eb44c7e1aaabbeeb01c766063a1 GIT binary patch literal 9596 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 Date: Wed, 1 May 2024 15:15:28 +0200 Subject: [PATCH 264/771] gamestate: Use tile's asset path when updating renderer. --- libopenage/gamestate/terrain_chunk.cpp | 24 ++------------------ libopenage/gamestate/terrain_chunk.h | 29 +++++++----------------- libopenage/gamestate/terrain_factory.cpp | 7 ++---- 3 files changed, 12 insertions(+), 48 deletions(-) diff --git a/libopenage/gamestate/terrain_chunk.cpp b/libopenage/gamestate/terrain_chunk.cpp index 9fc5422b65..e988260333 100644 --- a/libopenage/gamestate/terrain_chunk.cpp +++ b/libopenage/gamestate/terrain_chunk.cpp @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #include "terrain_chunk.h" @@ -22,22 +22,6 @@ void TerrainChunk::set_render_entity(const std::shared_ptrrender_entity = entity; } -void TerrainChunk::render_update(const time::time_t &time, - const std::string &terrain_path) { - if (this->render_entity != nullptr) { - // TODO: Update individual tiles instead of the whole chunk - std::vector> tiles; - tiles.reserve(this->tiles.size()); - for (const auto &tile : this->tiles) { - tiles.emplace_back(tile.elevation, terrain_path); - } - - this->render_entity->update(this->size, - tiles, - time); - } -} - const util::Vector2s &TerrainChunk::get_size() const { return this->size; } @@ -46,17 +30,13 @@ const coord::tile_delta &TerrainChunk::get_offset() const { return this->offset; } -void TerrainChunk::set_terrain_path(const std::string &terrain_path) { - this->terrain_path = terrain_path; -} - void TerrainChunk::render_update(const time::time_t &time) { if (this->render_entity != nullptr) { // TODO: Update individual tiles instead of the whole chunk std::vector> tiles; tiles.reserve(this->tiles.size()); for (const auto &tile : this->tiles) { - tiles.emplace_back(tile.elevation, terrain_path); + tiles.emplace_back(tile.elevation, tile.terrain_asset_path); } this->render_entity->update(this->size, diff --git a/libopenage/gamestate/terrain_chunk.h b/libopenage/gamestate/terrain_chunk.h index 0cd1840028..684d3af357 100644 --- a/libopenage/gamestate/terrain_chunk.h +++ b/libopenage/gamestate/terrain_chunk.h @@ -34,15 +34,6 @@ class TerrainChunk { */ void set_render_entity(const std::shared_ptr &entity); - /** - * Update the render entity. - * - * @param time Simulation time of the update. - * @param terrain_path Path to the terrain definition used at \p time. - */ - void render_update(const time::time_t &time, - const std::string &terrain_path); - /** * Get the size of this terrain chunk. * @@ -57,14 +48,11 @@ class TerrainChunk { */ const coord::tile_delta &get_offset() const; - // TODO: Remove test texture references - - // Set the terrain path of this terrain chunk. - // TODO: Remove later - void set_terrain_path(const std::string &terrain_path); - - // Send the current texture to the renderer. - // TODO: Replace later with render_update(time, terrain_path) + /** + * Update the render entity. + * + * @param time Simulation time of the update. + */ void render_update(const time::time_t &time); private: @@ -81,7 +69,9 @@ class TerrainChunk { coord::tile_delta offset; /** - * Height map of the terrain chunk. + * Terrain tile info of the terrain chunk. + * + * Layout is row-major. */ std::vector tiles; @@ -89,9 +79,6 @@ class TerrainChunk { * Render entity for pushing updates to the renderer. Can be \p nullptr. */ std::shared_ptr render_entity; - - // TODO: Remove test texture references - std::string terrain_path; }; } // namespace openage::gamestate diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp index 128b92a043..974cf19de6 100644 --- a/libopenage/gamestate/terrain_factory.cpp +++ b/libopenage/gamestate/terrain_factory.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "terrain_factory.h" @@ -131,12 +131,9 @@ std::shared_ptr TerrainFactory::add_chunk(const std::shared_ptrrender_factory->add_terrain_render_entity(size, offset); chunk->set_render_entity(render_entity); - chunk->render_update(time::TIME_ZERO, - test_texture_path); + chunk->render_update(time::TIME_ZERO); } - chunk->set_terrain_path(test_texture_path); - return chunk; } From 7a02427a618fc141288c919080f01cc6c03a4a6a Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 1 May 2024 17:27:05 +0200 Subject: [PATCH 265/771] gamestate: Add more choices to terrain examples. --- libopenage/gamestate/terrain_factory.cpp | 40 ++++++++++++++++-------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp index 974cf19de6..afeb4ae52a 100644 --- a/libopenage/gamestate/terrain_factory.cpp +++ b/libopenage/gamestate/terrain_factory.cpp @@ -20,33 +20,33 @@ namespace openage::gamestate { +// TODO: Remove test terrain references +static const std::string test_terrain_path = "../test/textures/test_terrain.terrain"; +static const std::string test_terrain_path2 = "../test/textures/test_terrain2.terrain"; + static const std::vector aoe1_test_terrain = {}; static const std::vector de1_test_terrain = {}; static const std::vector aoe2_test_terrain = { "aoe2_base.data.terrain.foundation.foundation.Foundation", "aoe2_base.data.terrain.grass.grass.Grass", "aoe2_base.data.terrain.dirt.dirt.Dirt", + "aoe2_base.data.terrain.water.water.Water", }; static const std::vector de2_test_terrain = {}; static const std::vector hd_test_terrain = { "hd_base.data.terrain.foundation.foundation.Foundation", "hd_base.data.terrain.grass.grass.Grass", "hd_base.data.terrain.dirt.dirt.Dirt", + "hd_base.data.terrain.water.water.Water", }; static const std::vector swgb_test_terrain = { - "swgb_base.data.terrain.desert0.desert0.Desert0", - "swgb_base.data.terrain.grass2.grass2.Grass2", "swgb_base.data.terrain.foundation.foundation.Foundation", + "swgb_base.data.terrain.grass2.grass2.Grass2", + "swgb_base.data.terrain.desert0.desert0.Desert0", + "swgb_base.data.terrain.water1.water1.Water1", }; static const std::vector trial_test_terrain = {}; -std::shared_ptr TerrainFactory::add_terrain() { - // TODO: Replace this with a proper terrain generator. - auto terrain = std::make_shared(); - - return terrain; -} - // TODO: Remove hardcoded test texture references static std::vector test_terrains; // declare static so we only have to do this once @@ -91,12 +91,17 @@ void build_test_terrains(const std::shared_ptr &gstate) { } } +std::shared_ptr TerrainFactory::add_terrain() { + // TODO: Replace this with a proper terrain generator. + auto terrain = std::make_shared(); + + return terrain; +} std::shared_ptr TerrainFactory::add_chunk(const std::shared_ptr &gstate, const util::Vector2s size, const coord::tile_delta offset) { - // TODO: Remove test texture references - std::string test_texture_path = "../test/textures/test_terrain.terrain"; + std::string terrain_info_path; // TODO: Remove test texture references // ========== @@ -112,7 +117,16 @@ std::shared_ptr TerrainFactory::add_chunk(const std::shared_ptrget_db_view()->get_object(test_terrains[test_terrain_index]); - test_texture_path = api::APITerrain::get_terrain_path(terrain_obj.value()); + terrain_info_path = api::APITerrain::get_terrain_path(terrain_obj.value()); + + test_terrain_index += 1; + } + else { + // use a test texture + if (test_terrain_index >= test_terrains.size()) { + test_terrain_index = 0; + } + terrain_info_path = test_terrain_path; test_terrain_index += 1; } @@ -122,7 +136,7 @@ std::shared_ptr TerrainFactory::add_chunk(const std::shared_ptr tiles{}; tiles.reserve(size[0] * size[1]); for (size_t i = 0; i < size[0] * size[1]; ++i) { - tiles.push_back({terrain_obj, test_texture_path, terrain_elevation_t::zero()}); + tiles.push_back({terrain_obj, terrain_info_path, terrain_elevation_t::zero()}); } auto chunk = std::make_shared(size, offset, std::move(tiles)); From 87346675a69a36c3c017c91bb4b7d6eae3129bc6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 1 May 2024 18:00:26 +0200 Subject: [PATCH 266/771] gamestate: Add terrain test layouts to terrain factory. --- libopenage/gamestate/terrain_factory.cpp | 123 +++++++++++++++++++---- 1 file changed, 104 insertions(+), 19 deletions(-) diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp index afeb4ae52a..17b5d452cb 100644 --- a/libopenage/gamestate/terrain_factory.cpp +++ b/libopenage/gamestate/terrain_factory.cpp @@ -21,8 +21,10 @@ namespace openage::gamestate { // TODO: Remove test terrain references -static const std::string test_terrain_path = "../test/textures/test_terrain.terrain"; -static const std::string test_terrain_path2 = "../test/textures/test_terrain2.terrain"; +static const std::vector test_terrain_paths = { + "../test/textures/test_terrain.terrain", + "../test/textures/test_terrain2.terrain", +}; static const std::vector aoe1_test_terrain = {}; static const std::vector de1_test_terrain = {}; @@ -91,6 +93,83 @@ void build_test_terrains(const std::shared_ptr &gstate) { } } +// Layout of terrain tiles on chunk 0 +// values are the terrain index +static const std::array layout_chunk0{ + // clang-format off + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, + 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, + 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, + 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, + 0, 0, 0, 1, 3, 3, 1, 0, 0, 0, + 0, 0, 1, 1, 3, 3, 1, 1, 0, 0, + 0, 1, 1, 1, 3, 3, 1, 1, 1, 0, + 0, 0, 1, 1, 3, 3, 1, 0, 0, 0, + // clang-format on +}; + +// Layout of terrain tiles on chunk 1 +// values are the terrain index +static const std::array layout_chunk1{ + // clang-format off + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, + 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, + 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, + // clang-format on +}; + +// Layout of terrain tiles on chunk 2 +// values are the terrain index +static const std::array layout_chunk2{ + // clang-format off + 1, 1, 1, 1, 3, 3, 1, 0, 0, 0, + 1, 1, 1, 1, 3, 3, 1, 1, 0, 0, + 1, 1, 1, 1, 3, 3, 1, 1, 0, 0, + 1, 1, 1, 3, 3, 3, 3, 1, 0, 0, + 1, 3, 3, 3, 3, 3, 3, 3, 1, 2, + 1, 3, 3, 3, 2, 2, 2, 3, 1, 2, + 3, 3, 3, 3, 2, 2, 2, 3, 1, 2, + 1, 3, 3, 3, 2, 2, 2, 3, 1, 1, + 1, 3, 3, 3, 3, 3, 3, 3, 3, 1, + 1, 3, 3, 3, 3, 3, 3, 3, 1, 0, + // clang-format on +}; + +// Layout of terrain tiles on chunk 3 +// values are the terrain index +static const std::array layout_chunk3{ + // clang-format off + 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, + 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, + 0, 0, 2, 2, 2, 2, 1, 1, 2, 0, + 0, 2, 2, 2, 2, 2, 1, 2, 0, 0, + 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, + 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, + 0, 0, 2, 2, 2, 2, 2, 0, 0, 0, + 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, + 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, + 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, + // clang-format on +}; + + +static const std::vector> layout_chunks{ + layout_chunk0, + layout_chunk1, + layout_chunk2, + layout_chunk3, +}; + + std::shared_ptr TerrainFactory::add_terrain() { // TODO: Replace this with a proper terrain generator. auto terrain = std::make_shared(); @@ -110,34 +189,40 @@ std::shared_ptr TerrainFactory::add_chunk(const std::shared_ptr tiles{}; + tiles.reserve(size[0] * size[1]); + + static size_t test_chunk_index = 0; if (not test_terrains.empty()) { // use one of the modpack terrain textures - if (test_terrain_index >= test_terrains.size()) { - test_terrain_index = 0; + if (test_chunk_index >= layout_chunks.size()) { + test_chunk_index = 0; } - terrain_obj = gstate->get_db_view()->get_object(test_terrains[test_terrain_index]); - terrain_info_path = api::APITerrain::get_terrain_path(terrain_obj.value()); - test_terrain_index += 1; + for (size_t i = 0; i < size[0] * size[1]; ++i) { + size_t terrain_index = layout_chunks.at(test_chunk_index).at(i); + terrain_obj = gstate->get_db_view()->get_object(test_terrains.at(terrain_index)); + terrain_info_path = api::APITerrain::get_terrain_path(terrain_obj.value()); + tiles.push_back({terrain_obj, terrain_info_path, terrain_elevation_t::zero()}); + } + + test_chunk_index += 1; } else { // use a test texture - if (test_terrain_index >= test_terrains.size()) { - test_terrain_index = 0; + if (test_chunk_index >= test_terrain_paths.size()) { + test_chunk_index = 0; } - terrain_info_path = test_terrain_path; + terrain_info_path = test_terrain_paths.at(test_chunk_index); - test_terrain_index += 1; - } - // ========== + for (size_t i = 0; i < size[0] * size[1]; ++i) { + tiles.push_back({terrain_obj, terrain_info_path, terrain_elevation_t::zero()}); + } - // fill the chunk with tiles - std::vector tiles{}; - tiles.reserve(size[0] * size[1]); - for (size_t i = 0; i < size[0] * size[1]; ++i) { - tiles.push_back({terrain_obj, terrain_info_path, terrain_elevation_t::zero()}); + test_chunk_index += 1; } + // ========== auto chunk = std::make_shared(size, offset, std::move(tiles)); From a46451e852f5744757721d2fbfde392cfd9154f7 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 1 May 2024 18:12:12 +0200 Subject: [PATCH 267/771] renderer: Fix vertex generation from tiles. Generation used column-major instead of row-major ordering. --- libopenage/renderer/stages/terrain/render_entity.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/renderer/stages/terrain/render_entity.cpp b/libopenage/renderer/stages/terrain/render_entity.cpp index e5eb0ba784..7f2e6d825e 100644 --- a/libopenage/renderer/stages/terrain/render_entity.cpp +++ b/libopenage/renderer/stages/terrain/render_entity.cpp @@ -66,8 +66,8 @@ void TerrainRenderEntity::update(const util::Vector2s size, auto vert_count = this->size[0] * this->size[1]; this->vertices.clear(); this->vertices.reserve(vert_count); - for (int i = 0; i < (int)this->size[0]; ++i) { - for (int j = 0; j < (int)this->size[1]; ++j) { + for (int j = 0; j < (int)this->size[1]; ++j) { + for (int i = 0; i < (int)this->size[0]; ++i) { // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { From 9329095014edfce577d298dcc4a1aa9fa18dae56 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 3 May 2024 18:12:55 +0200 Subject: [PATCH 268/771] renderer: Order tayers by position. --- libopenage/renderer/resources/parser/parse_sprite.cpp | 11 +++++++++-- .../renderer/resources/parser/parse_terrain.cpp | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/libopenage/renderer/resources/parser/parse_sprite.cpp b/libopenage/renderer/resources/parser/parse_sprite.cpp index 2156cc8422..9ea4afac1c 100644 --- a/libopenage/renderer/resources/parser/parse_sprite.cpp +++ b/libopenage/renderer/resources/parser/parse_sprite.cpp @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the openage authors. See copying.md for legal info. +// Copyright 2021-2024 the openage authors. See copying.md for legal info. #include "parse_sprite.h" @@ -188,7 +188,7 @@ Animation2dInfo parse_sprite_file(const util::Path &file, frames.at(frame.angle).push_back(frame); // check for the largest index, so we can use it to - // interpolate the total animation length + // interpolate the total animation length if (frame.index > largest_frame_idx) { largest_frame_idx = frame.index; } @@ -228,6 +228,13 @@ Animation2dInfo parse_sprite_file(const util::Path &file, return a1.degree < a2.degree; }); + // Order layers by position + std::sort(layers.begin(), + layers.end(), + [](LayerData &l1, LayerData &l2) { + return l1.position < l2.position; + }); + // Create ID map. Resolves IDs used in the file to array indices std::unordered_map texture_id_map; for (size_t i = 0; i < textures.size(); ++i) { diff --git a/libopenage/renderer/resources/parser/parse_terrain.cpp b/libopenage/renderer/resources/parser/parse_terrain.cpp index cc7b2f4814..4330606091 100644 --- a/libopenage/renderer/resources/parser/parse_terrain.cpp +++ b/libopenage/renderer/resources/parser/parse_terrain.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 "parse_terrain.h" @@ -193,7 +193,7 @@ TerrainInfo parse_terrain_file(const util::Path &file, frames.at(frame.layer_id).push_back(frame); // check for the largest index, so we can use it to - // interpolate the total animation length + // interpolate the total animation length if (frame.index > largest_frame_idx) { largest_frame_idx = frame.index; } @@ -226,6 +226,13 @@ TerrainInfo parse_terrain_file(const util::Path &file, }); } + // Order layers by position + std::sort(layers.begin(), + layers.end(), + [](TerrainLayerData &l1, TerrainLayerData &l2) { + return l1.position < l2.position; + }); + // Create ID map. Resolves IDs used in the file to array indices std::unordered_map texture_id_map; for (size_t i = 0; i < textures.size(); ++i) { From f5177907102fe8b3d8012bf82d141b8d414eace6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 3 May 2024 19:04:34 +0200 Subject: [PATCH 269/771] renderer: Remove camera from world object. --- libopenage/renderer/stages/world/object.cpp | 5 ----- libopenage/renderer/stages/world/object.h | 12 ------------ libopenage/renderer/stages/world/render_stage.cpp | 1 - 3 files changed, 18 deletions(-) diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index c58e9faa98..95ddf21ae4 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -33,7 +33,6 @@ namespace openage::renderer::world { WorldObject::WorldObject(const std::shared_ptr &asset_manager) : require_renderable{true}, changed{false}, - camera{nullptr}, asset_manager{asset_manager}, render_entity{nullptr}, ref_id{0}, @@ -49,10 +48,6 @@ void WorldObject::set_render_entity(const std::shared_ptr &en this->fetch_updates(); } -void WorldObject::set_camera(const std::shared_ptr &camera) { - this->camera = camera; -} - void WorldObject::fetch_updates(const time::time_t &time) { if (not this->render_entity->is_changed()) { // exit early because there is nothing to do diff --git a/libopenage/renderer/stages/world/object.h b/libopenage/renderer/stages/world/object.h index a34a0c690c..1516a99be0 100644 --- a/libopenage/renderer/stages/world/object.h +++ b/libopenage/renderer/stages/world/object.h @@ -51,13 +51,6 @@ class WorldObject { */ void set_render_entity(const std::shared_ptr &entity); - /** - * Set the current camera of the scene. - * - * @param camera Camera object viewing the scene. - */ - void set_camera(const std::shared_ptr &camera); - /** * Fetch updates from the render entity. * @@ -147,11 +140,6 @@ class WorldObject { */ bool changed; - /** - * Camera for model uniforms. - */ - std::shared_ptr camera; - /** * Asset manager for central accessing and loading asset resources. */ diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index d24bb5f896..0201fd598f 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -50,7 +50,6 @@ void WorldRenderStage::add_render_entity(const std::shared_ptr(this->asset_manager); world_object->set_render_entity(entity); - world_object->set_camera(this->camera); this->render_objects.push_back(world_object); } From fcfc5963f902995be88200ce44c01173ff80edf1 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 3 May 2024 19:21:36 +0200 Subject: [PATCH 270/771] renderer: Store uniforms for multiple layers in world object. --- libopenage/renderer/stages/world/object.cpp | 133 ++++++++++---------- libopenage/renderer/stages/world/object.h | 14 ++- 2 files changed, 80 insertions(+), 67 deletions(-) diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index 95ddf21ae4..3636fb99d8 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -39,7 +39,7 @@ WorldObject::WorldObject(const std::shared_ptruniforms == nullptr) [[unlikely]] { + if (this->layer_uniforms.empty()) [[unlikely]] { return; } // Object world position auto current_pos = this->position.get(time); - this->uniforms->update(this->obj_world_position, current_pos.to_world_space()); // Direction angle the object is facing towards currently auto angle_degrees = this->angle.get(time).to_float(); - // Frame subtexture + // Animation information auto animation_info = this->animation_info.get(time); - auto &layer = animation_info->get_layer(0); // TODO: Support multiple layers - auto &angle = layer.get_direction_angle(angle_degrees); - // Flip subtexture horizontally if angle is mirrored - if (angle->is_mirrored()) { - this->uniforms->update(this->flip_x, true); + for (size_t layer_idx = 0; layer_idx < this->layer_uniforms.size(); ++layer_idx) { + auto &layer_unifs = this->layer_uniforms.at(layer_idx); + layer_unifs->update(this->obj_world_position, current_pos.to_world_space()); + + // Frame subtexture + auto &layer = animation_info->get_layer(layer_idx); + auto &angle = layer.get_direction_angle(angle_degrees); + + // Flip subtexture horizontally if angle is mirrored + if (angle->is_mirrored()) { + layer_unifs->update(this->flip_x, true); + } + else { + layer_unifs->update(this->flip_x, false); + } + + // Current frame index considering current time + size_t frame_idx; + switch (layer.get_display_mode()) { + case renderer::resources::display_mode::ONCE: + case renderer::resources::display_mode::LOOP: { + // ONCE and LOOP are animated based on time + auto &timing = layer.get_frame_timing(); + frame_idx = timing->get_frame(time, this->render_entity->get_update_time()); + } break; + case renderer::resources::display_mode::OFF: + default: + // OFF only shows the first frame + frame_idx = 0; + break; + } + + // Index of texture and subtexture where the frame's pixels are located + auto &frame_info = angle->get_frame(frame_idx); + auto tex_idx = frame_info->get_texture_idx(); + auto subtex_idx = frame_info->get_subtexture_idx(); + + auto &tex_info = animation_info->get_texture(tex_idx); + auto &tex_manager = this->asset_manager->get_texture_manager(); + auto &texture = tex_manager->request(tex_info->get_image_path().value()); + layer_unifs->update(this->tex, texture); + + // Subtexture coordinates.inside texture + auto coords = tex_info->get_subtex_info(subtex_idx).get_tile_params(); + layer_unifs->update(this->tile_params, coords); + + // Animation scale factor + // Scales the subtex up or down in the shader + auto scale = animation_info->get_scalefactor(); + layer_unifs->update(this->scale, scale); + + // Subtexture size in pixels + auto subtex_size = tex_info->get_subtex_info(subtex_idx).get_size(); + Eigen::Vector2f subtex_size_vec{ + static_cast(subtex_size[0]), + static_cast(subtex_size[1])}; + layer_unifs->update(this->subtex_size, subtex_size_vec); + + // Anchor point offset (in pixels) + // moves the subtex in the shader so that the anchor point is at the object's position + auto anchor = tex_info->get_subtex_info(subtex_idx).get_anchor_params(); + Eigen::Vector2f anchor_offset{ + static_cast(anchor[0]), + static_cast(anchor[1])}; + layer_unifs->update(this->anchor_offset, anchor_offset); } - else { - this->uniforms->update(this->flip_x, false); - } - - // Current frame index considering current time - size_t frame_idx; - switch (layer.get_display_mode()) { - case renderer::resources::display_mode::ONCE: - case renderer::resources::display_mode::LOOP: { - // ONCE and LOOP are animated based on time - auto &timing = layer.get_frame_timing(); - frame_idx = timing->get_frame(time, this->render_entity->get_update_time()); - } break; - case renderer::resources::display_mode::OFF: - default: - // OFF only shows the first frame - frame_idx = 0; - break; - } - - // Index of texture and subtexture where the frame's pixels are located - auto &frame_info = angle->get_frame(frame_idx); - auto tex_idx = frame_info->get_texture_idx(); - auto subtex_idx = frame_info->get_subtexture_idx(); - - auto &tex_info = animation_info->get_texture(tex_idx); - auto &tex_manager = this->asset_manager->get_texture_manager(); - auto &texture = tex_manager->request(tex_info->get_image_path().value()); - this->uniforms->update(this->tex, texture); - - // Subtexture coordinates.inside texture - auto coords = tex_info->get_subtex_info(subtex_idx).get_tile_params(); - this->uniforms->update(this->tile_params, coords); - - // Animation scale factor - // Scales the subtex up or down in the shader - auto scale = animation_info->get_scalefactor(); - this->uniforms->update(this->scale, scale); - - // Subtexture size in pixels - auto subtex_size = tex_info->get_subtex_info(subtex_idx).get_size(); - Eigen::Vector2f subtex_size_vec{ - static_cast(subtex_size[0]), - static_cast(subtex_size[1])}; - this->uniforms->update(this->subtex_size, subtex_size_vec); - - // Anchor point offset (in pixels) - // moves the subtex in the shader so that the anchor point is at the object's position - auto anchor = tex_info->get_subtex_info(subtex_idx).get_anchor_params(); - Eigen::Vector2f anchor_offset{ - static_cast(anchor[0]), - static_cast(anchor[1])}; - this->uniforms->update(this->anchor_offset, anchor_offset); } uint32_t WorldObject::get_id() { @@ -179,7 +185,8 @@ void WorldObject::clear_changed_flag() { } void WorldObject::set_uniforms(const std::shared_ptr &uniforms) { - this->uniforms = uniforms; + this->layer_uniforms.clear(); // ASDF: Update instead of clear + this->layer_uniforms.push_back(uniforms); } } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/object.h b/libopenage/renderer/stages/world/object.h index 1516a99be0..19684330f8 100644 --- a/libopenage/renderer/stages/world/object.h +++ b/libopenage/renderer/stages/world/object.h @@ -146,7 +146,8 @@ class WorldObject { std::shared_ptr asset_manager; /** - * Source for positional and texture data. + * Entity that gets updates from the gamestate, e.g. the position and + * requested animation data. */ std::shared_ptr render_entity; @@ -167,14 +168,19 @@ class WorldObject { curve::Segmented angle; /** - * Animation information for the renderables. + * Animation information for the layers. */ curve::Discrete> animation_info; /** - * Shader uniforms for the renderable in the terrain render pass. + * Shader uniforms for the layers of the object. Each layer corresponds to a + * renderable in the render pass. + * + * Since all layers are sprites and share the same geometry, we can reuse the layer uniforms and + * their renderables even if the animation changes. Therefore, we only need to add/remove + * layers when the _number_ of layers in the animation changes. */ - std::shared_ptr uniforms; + std::vector> layer_uniforms; /** * Time of the last update call. From b576532121de23b47dc1cb75e70504111065d49e Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 3 May 2024 20:27:32 +0200 Subject: [PATCH 271/771] renderer: Draw multiple layers per animation. --- libopenage/renderer/stages/world/object.cpp | 30 ++++++++++-- libopenage/renderer/stages/world/object.h | 31 ++++++++---- .../renderer/stages/world/render_stage.cpp | 49 ++++++++++--------- 3 files changed, 73 insertions(+), 37 deletions(-) diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index 3636fb99d8..949cb23395 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -49,10 +49,18 @@ void WorldObject::set_render_entity(const std::shared_ptr &en } void WorldObject::fetch_updates(const time::time_t &time) { + // TODO: Calling this once per frame is very expensive + auto layer_count = this->get_required_layer_count(time); + if (this->layer_uniforms.size() != layer_count) { + // The number of layers changed, so we need to update the renderables + this->require_renderable = true; + } + if (not this->render_entity->is_changed()) { - // exit early because there is nothing to do + // exit early because there is nothing to update return; } + // Get data from render entity this->ref_id = this->render_entity->get_id(); this->position.sync(this->render_entity->get_position()); @@ -168,7 +176,11 @@ const renderer::resources::MeshData WorldObject::get_mesh() { return resources::MeshData::make_quad(); } -bool WorldObject::requires_renderable() { +const Eigen::Matrix4f WorldObject::get_model_matrix() { + return Eigen::Matrix4f::Identity(); +} + +bool WorldObject::requires_renderable() const { return this->require_renderable; } @@ -176,6 +188,15 @@ void WorldObject::clear_requires_renderable() { this->require_renderable = false; } +size_t WorldObject::get_required_layer_count(const time::time_t &time) const { + auto animation_info = this->animation_info.get(time); + if (not animation_info) { + return 0; + } + + return animation_info->get_layer_count(); +} + bool WorldObject::is_changed() { return this->changed; } @@ -184,9 +205,8 @@ void WorldObject::clear_changed_flag() { this->changed = false; } -void WorldObject::set_uniforms(const std::shared_ptr &uniforms) { - this->layer_uniforms.clear(); // ASDF: Update instead of clear - this->layer_uniforms.push_back(uniforms); +void WorldObject::set_uniforms(std::vector> &&uniforms) { + this->layer_uniforms = std::move(uniforms); } } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/object.h b/libopenage/renderer/stages/world/object.h index 19684330f8..bec405a69d 100644 --- a/libopenage/renderer/stages/world/object.h +++ b/libopenage/renderer/stages/world/object.h @@ -75,10 +75,19 @@ class WorldObject { /** * Get the quad for creating the geometry. * + * Since the object is a bunch of sprite layers, the mesh is always a quad. + * * @return Mesh for creating a renderer geometry object. */ static const renderer::resources::MeshData get_mesh(); + /** + * Get the model matrix for the uniform input of a layer. + * + * @return Model matrix. + */ + static const Eigen::Matrix4f get_model_matrix(); + /** * Check whether a new renderable needs to be created for this mesh. * @@ -88,13 +97,20 @@ class WorldObject { * * @return true if a new renderable is required, else false. */ - bool requires_renderable(); + bool requires_renderable() const; /** * Indicate to this mesh that a new renderable has been created. */ void clear_requires_renderable(); + /** + * Get the number of layers required by this object. + * + * @return Number of layers. + */ + size_t get_required_layer_count(const time::time_t &time) const; + /** * Check whether the object was changed by \p update(). * @@ -108,13 +124,12 @@ class WorldObject { void clear_changed_flag(); /** - * Set the reference to the uniform inputs of the renderable - * associated with this object. Relevant uniforms are updated - * when calling \p update(). + * Set the uniform inputs for the layers of this object. + * Layer uniforms are updated on every update call. * - * @param uniforms Uniform inputs of this object's renderable. + * @param uniforms Uniform inputs of this object's layers. */ - void set_uniforms(const std::shared_ptr &uniforms); + void set_uniforms(std::vector> &&uniforms); /** * Shader uniform IDs for setting uniform values. @@ -175,10 +190,6 @@ class WorldObject { /** * Shader uniforms for the layers of the object. Each layer corresponds to a * renderable in the render pass. - * - * Since all layers are sprites and share the same geometry, we can reuse the layer uniforms and - * their renderables even if the animation changes. Therefore, we only need to add/remove - * layers when the _number_ of layers in the animation changes. */ std::vector> layer_uniforms; diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 0201fd598f..147e3fbd1b 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -60,31 +60,36 @@ void WorldRenderStage::update() { obj->fetch_updates(current_time); if (obj->is_changed()) { if (obj->requires_renderable()) { - Eigen::Matrix4f model_m = Eigen::Matrix4f::Identity(); - - // Set uniforms that don't change or are not changed often - auto transform_unifs = this->display_shader->new_uniform_input( - "model", - model_m, - "flip_x", - false, - "flip_y", - false, - "u_id", - obj->get_id()); - - Renderable display_obj{ - transform_unifs, - this->default_geometry, - true, - true, - }; - - this->render_pass->add_renderables(display_obj); + auto layer_count = obj->get_required_layer_count(current_time); + Eigen::Matrix4f model_m = obj->get_model_matrix(); + + std::vector> transform_unifs; + for (size_t i = 0; i < layer_count; i++) { + // Set uniforms that don't change or are not changed often + auto layer_unifs = this->display_shader->new_uniform_input( + "model", + model_m, + "flip_x", + false, + "flip_y", + false, + "u_id", + obj->get_id()); + + Renderable display_obj{ + layer_unifs, + this->default_geometry, + true, + true, + }; + this->render_pass->add_renderables(display_obj); + transform_unifs.push_back(layer_unifs); + } + obj->clear_requires_renderable(); // update remaining uniforms for the object - obj->set_uniforms(transform_unifs); + obj->set_uniforms(std::move(transform_unifs)); } } obj->update_uniforms(current_time); From d11e9267eeec6475c6dea12b6a158c1a1e34ca9d Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 3 May 2024 21:09:31 +0200 Subject: [PATCH 272/771] renderer: Split renderer.h into more source files. --- libopenage/presenter/presenter.cpp | 3 + libopenage/renderer/CMakeLists.txt | 3 + libopenage/renderer/demo/demo_0.cpp | 3 + libopenage/renderer/demo/demo_1.cpp | 3 + libopenage/renderer/demo/demo_2.cpp | 3 + libopenage/renderer/demo/demo_3.cpp | 2 + libopenage/renderer/demo/demo_4.cpp | 3 + libopenage/renderer/demo/demo_5.cpp | 2 + libopenage/renderer/demo/stresstest_0.cpp | 2 + libopenage/renderer/gui/gui.cpp | 13 ++- libopenage/renderer/opengl/render_pass.h | 6 +- libopenage/renderer/opengl/render_target.cpp | 4 + libopenage/renderer/opengl/render_target.h | 8 ++ libopenage/renderer/render_pass.cpp | 40 +++++++ libopenage/renderer/render_pass.h | 44 ++++++++ libopenage/renderer/render_target.cpp | 8 ++ libopenage/renderer/render_target.h | 38 +++++++ libopenage/renderer/renderable.cpp | 8 ++ libopenage/renderer/renderable.h | 62 +++++++++++ libopenage/renderer/renderer.cpp | 34 +----- libopenage/renderer/renderer.h | 101 +----------------- .../renderer/stages/hud/render_stage.cpp | 4 +- .../renderer/stages/screen/render_stage.cpp | 4 +- .../renderer/stages/screen/screenshot.cpp | 5 +- .../renderer/stages/skybox/render_stage.cpp | 4 +- .../renderer/stages/terrain/render_stage.cpp | 4 +- .../renderer/stages/world/render_stage.cpp | 2 + libopenage/renderer/vulkan/render_target.h | 2 +- 28 files changed, 272 insertions(+), 143 deletions(-) create mode 100644 libopenage/renderer/render_pass.cpp create mode 100644 libopenage/renderer/render_pass.h create mode 100644 libopenage/renderer/render_target.cpp create mode 100644 libopenage/renderer/render_target.h create mode 100644 libopenage/renderer/renderable.cpp create mode 100644 libopenage/renderer/renderable.h diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index 64df9bc05a..62435074d4 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -21,6 +21,8 @@ #include "renderer/gui/gui.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/render_factory.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/resources/assets/asset_manager.h" #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" @@ -34,6 +36,7 @@ #include "time/time_loop.h" #include "util/path.h" + namespace openage::presenter { Presenter::Presenter(const util::Path &root_dir, diff --git a/libopenage/renderer/CMakeLists.txt b/libopenage/renderer/CMakeLists.txt index 756e9e4d98..dfcb5e8b9e 100644 --- a/libopenage/renderer/CMakeLists.txt +++ b/libopenage/renderer/CMakeLists.txt @@ -3,6 +3,9 @@ add_sources(libopenage definitions.cpp geometry.cpp render_factory.cpp + render_pass.cpp + render_target.cpp + renderable.cpp renderer.cpp shader_program.cpp texture.cpp diff --git a/libopenage/renderer/demo/demo_0.cpp b/libopenage/renderer/demo/demo_0.cpp index 617b7c6f75..d82eb62381 100644 --- a/libopenage/renderer/demo/demo_0.cpp +++ b/libopenage/renderer/demo/demo_0.cpp @@ -4,10 +4,13 @@ #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/resources/mesh_data.h" #include "renderer/resources/shader_source.h" #include "renderer/shader_program.h" + namespace openage::renderer::tests { void renderer_demo_0(const util::Path &path) { diff --git a/libopenage/renderer/demo/demo_1.cpp b/libopenage/renderer/demo/demo_1.cpp index 312efb47c4..1ecd7815a1 100644 --- a/libopenage/renderer/demo/demo_1.cpp +++ b/libopenage/renderer/demo/demo_1.cpp @@ -8,12 +8,15 @@ #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_data.h" #include "renderer/shader_program.h" #include "renderer/texture.h" #include "util/math_constants.h" + namespace openage::renderer::tests { void renderer_demo_1(const util::Path &path) { diff --git a/libopenage/renderer/demo/demo_2.cpp b/libopenage/renderer/demo/demo_2.cpp index ac30adc78a..7f6f398e12 100644 --- a/libopenage/renderer/demo/demo_2.cpp +++ b/libopenage/renderer/demo/demo_2.cpp @@ -8,6 +8,8 @@ #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/resources/animation/angle_info.h" #include "renderer/resources/animation/frame_info.h" #include "renderer/resources/parser/parse_sprite.h" @@ -17,6 +19,7 @@ #include "renderer/shader_program.h" #include "renderer/texture.h" + namespace openage::renderer::tests { void renderer_demo_2(const util::Path &path) { diff --git a/libopenage/renderer/demo/demo_3.cpp b/libopenage/renderer/demo/demo_3.cpp index c143bd0a13..23de242fd7 100644 --- a/libopenage/renderer/demo/demo_3.cpp +++ b/libopenage/renderer/demo/demo_3.cpp @@ -10,6 +10,8 @@ #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" #include "renderer/render_factory.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/resources/assets/asset_manager.h" #include "renderer/resources/shader_source.h" #include "renderer/stages/camera/manager.h" diff --git a/libopenage/renderer/demo/demo_4.cpp b/libopenage/renderer/demo/demo_4.cpp index 7bac7a1654..fb39057e5d 100644 --- a/libopenage/renderer/demo/demo_4.cpp +++ b/libopenage/renderer/demo/demo_4.cpp @@ -7,6 +7,8 @@ #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/resources/animation/angle_info.h" #include "renderer/resources/animation/frame_info.h" #include "renderer/resources/frame_timing.h" @@ -16,6 +18,7 @@ #include "renderer/shader_program.h" #include "time/clock.h" + namespace openage::renderer::tests { void renderer_demo_4(const util::Path &path) { auto qtapp = std::make_shared(); diff --git a/libopenage/renderer/demo/demo_5.cpp b/libopenage/renderer/demo/demo_5.cpp index 73c2c15226..85752db18c 100644 --- a/libopenage/renderer/demo/demo_5.cpp +++ b/libopenage/renderer/demo/demo_5.cpp @@ -9,6 +9,8 @@ #include "renderer/camera/camera.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/resources/buffer_info.h" #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_data.h" diff --git a/libopenage/renderer/demo/stresstest_0.cpp b/libopenage/renderer/demo/stresstest_0.cpp index 01169b0849..76c53d7b21 100644 --- a/libopenage/renderer/demo/stresstest_0.cpp +++ b/libopenage/renderer/demo/stresstest_0.cpp @@ -9,6 +9,8 @@ #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" #include "renderer/render_factory.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/resources/assets/asset_manager.h" #include "renderer/resources/shader_source.h" #include "renderer/stages/camera/manager.h" diff --git a/libopenage/renderer/gui/gui.cpp b/libopenage/renderer/gui/gui.cpp index f51320c24c..879a709d0c 100644 --- a/libopenage/renderer/gui/gui.cpp +++ b/libopenage/renderer/gui/gui.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "gui.h" @@ -7,6 +7,8 @@ #include "renderer/gui/guisys/public/gui_renderer.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/context.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/renderer.h" #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" @@ -14,6 +16,7 @@ #include "renderer/window.h" #include "util/path.h" + namespace openage::renderer::gui { GUI::GUI(std::shared_ptr app, @@ -31,10 +34,10 @@ GUI::GUI(std::shared_ptr app, engine, source.resolve_native_path(), rootdir.resolve_native_path()}, - //input{&gui_renderer, &game_logic_updater} - //image_provider_by_filename{ - // &render_updater, - // openage::gui::GuiGameSpecImageProvider::Type::ByFilename}, + // input{&gui_renderer, &game_logic_updater} + // image_provider_by_filename{ + // &render_updater, + // openage::gui::GuiGameSpecImageProvider::Type::ByFilename}, renderer{renderer} { // everything alright before we create the gui stuff? renderer::opengl::GlContext::check_error(); diff --git a/libopenage/renderer/opengl/render_pass.h b/libopenage/renderer/opengl/render_pass.h index 2994a0e7f7..b5f317f585 100644 --- a/libopenage/renderer/opengl/render_pass.h +++ b/libopenage/renderer/opengl/render_pass.h @@ -1,8 +1,10 @@ -// 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 -#include "../renderer.h" +#include "renderer/render_pass.h" +#include "renderer/renderable.h" + namespace openage::renderer::opengl { diff --git a/libopenage/renderer/opengl/render_target.cpp b/libopenage/renderer/opengl/render_target.cpp index 4a54f4c0d8..b965dc58ea 100644 --- a/libopenage/renderer/opengl/render_target.cpp +++ b/libopenage/renderer/opengl/render_target.cpp @@ -41,6 +41,10 @@ resources::Texture2dData GlRenderTarget::into_data() { return resources::Texture2dData{info, std::move(pxdata)}; } +gl_render_target_t GlRenderTarget::get_type() const { + return this->type; +} + std::vector> GlRenderTarget::get_texture_targets() { std::vector> textures{}; if (this->framebuffer->get_type() == gl_framebuffer_t::display) { diff --git a/libopenage/renderer/opengl/render_target.h b/libopenage/renderer/opengl/render_target.h index ccf67bfd87..5c6061e070 100644 --- a/libopenage/renderer/opengl/render_target.h +++ b/libopenage/renderer/opengl/render_target.h @@ -5,6 +5,7 @@ #include #include "renderer/opengl/framebuffer.h" +#include "renderer/render_target.h" #include "renderer/renderer.h" @@ -63,6 +64,13 @@ class GlRenderTarget final : public RenderTarget { */ resources::Texture2dData into_data() override; + /** + * Get the type of this render target. + * + * @return Render target type. + */ + gl_render_target_t get_type() const; + /** * Get the targeted textures. * diff --git a/libopenage/renderer/render_pass.cpp b/libopenage/renderer/render_pass.cpp new file mode 100644 index 0000000000..f326fdf0bb --- /dev/null +++ b/libopenage/renderer/render_pass.cpp @@ -0,0 +1,40 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "render_pass.h" + + +namespace openage::renderer { + +RenderPass::RenderPass(std::vector renderables, + const std::shared_ptr &target) : + renderables(std::move(renderables)), + target{target} {} + + +const std::shared_ptr &RenderPass::get_target() const { + return this->target; +} + + +void RenderPass::set_target(const std::shared_ptr &target) { + this->target = target; +} + +void RenderPass::set_renderables(std::vector renderables) { + this->renderables = std::move(renderables); +} +void RenderPass::add_renderables(std::vector renderables) { + for (auto item : renderables) { + this->renderables.push_back(item); + } +} + +void RenderPass::add_renderables(Renderable renderable) { + this->renderables.push_back(std::move(renderable)); +} + +void RenderPass::clear_renderables() { + this->renderables.clear(); +} + +} // namespace openage::renderer diff --git a/libopenage/renderer/render_pass.h b/libopenage/renderer/render_pass.h new file mode 100644 index 0000000000..5849c97a26 --- /dev/null +++ b/libopenage/renderer/render_pass.h @@ -0,0 +1,44 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "renderer/renderable.h" + + +namespace openage { +namespace renderer { +class RenderTarget; + +/// A render pass is a series of draw calls represented by renderables that output into the given render target. +class RenderPass { +protected: + /// Create a new RenderPass. This is called from Renderer::add_render_pass, + /// which then creates the proper subclass of RenderPass, depending on the backend. + RenderPass(std::vector, const std::shared_ptr &); + /// The renderables to parse and possibly execute. + std::vector renderables; + +public: + virtual ~RenderPass() = default; + void set_target(const std::shared_ptr &); + const std::shared_ptr &get_target() const; + + // Replace the current renderables + void set_renderables(std::vector); + // Append renderables to the end of the list of renderables + void add_renderables(std::vector); + // Append a single renderable to the end of the list of renderables + void add_renderables(Renderable); + // Clear the list of renderables + void clear_renderables(); + +private: + /// The render target to write into. + std::shared_ptr target; +}; + +} // namespace renderer +} // namespace openage diff --git a/libopenage/renderer/render_target.cpp b/libopenage/renderer/render_target.cpp new file mode 100644 index 0000000000..158687cc42 --- /dev/null +++ b/libopenage/renderer/render_target.cpp @@ -0,0 +1,8 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "render_target.h" + + +namespace openage::renderer { + +} // namespace openage::renderer diff --git a/libopenage/renderer/render_target.h b/libopenage/renderer/render_target.h new file mode 100644 index 0000000000..4db95701f2 --- /dev/null +++ b/libopenage/renderer/render_target.h @@ -0,0 +1,38 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + + +namespace openage { +namespace renderer { +class Texture2d; + +namespace resources { +class Texture2dData; +} // namespace resources + +/// The abstract base for a render target. +class RenderTarget : public std::enable_shared_from_this { +protected: + RenderTarget() = default; + +public: + virtual ~RenderTarget() = default; + + /** + * Get an image from the pixels in the render target's framebuffer. + * + * This should only be called _after_ rendering to the framebuffer has finished. + * + * @return RGBA texture data. + */ + virtual resources::Texture2dData into_data() = 0; + + virtual std::vector> get_texture_targets() = 0; +}; + +} // namespace renderer +} // namespace openage diff --git a/libopenage/renderer/renderable.cpp b/libopenage/renderer/renderable.cpp new file mode 100644 index 0000000000..213abdf36e --- /dev/null +++ b/libopenage/renderer/renderable.cpp @@ -0,0 +1,8 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "renderable.h" + + +namespace openage::renderer { + +} // namespace openage::renderer diff --git a/libopenage/renderer/renderable.h b/libopenage/renderer/renderable.h new file mode 100644 index 0000000000..a122db9679 --- /dev/null +++ b/libopenage/renderer/renderable.h @@ -0,0 +1,62 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + + +namespace openage { +namespace renderer { +class Geometry; +class UniformInput; + +/** + * A renderable is a set of a shader UniformInput and a possible draw call. + * Usually it is one "step" in a RenderPass. + * + * The UniformInput only stores the values the CPU at first. When the renderer + * "executes" the Renderable in a pass, the UniformInput values are uploaded to + * the shader on the GPU they were created with. + * + * If the geometry is nullptr, the uniform values are uploaded to the shader, + * but no draw call is performed. This can be used to, for example, first set + * the values of uniforms that many objects have in common, and then only + * upload the uniforms that vary between them in each draw call. This works + * because uniform values in any given shader are preserved across a render + * pass. + * + * If geometry is set (i.e. it is not nullptr), the renderer draws the geometry + * with the shader and other settings in the renderable. The result is written + * into the render target, defined in the RenderPass. + */ +struct Renderable { + /// Uniform values to be set in the appropriate shader. Contains a reference + /// to the correct shader, and this is the shader that will be used for + /// drawing if geometry is present. + std::shared_ptr uniform; + /// The geometry. It can be a simple primitive or a complex mesh. + /// Can be nullptr to only set uniforms but do not perform draw call. + std::shared_ptr geometry; + /// Whether to perform alpha-based color blending with whatever was in the + /// render target before. + bool alpha_blending = true; + /// Whether to perform depth testing and discard occluded fragments. + bool depth_test = true; +}; + +/** + * Simplified form of Renderable, which is just an update for a shader. + * When the ShaderUpdate is processed in a RenderPass, + * the new uniform values (set on the CPU first with unif_in.update(...)) + * are uploaded to the GPU. + */ +struct ShaderUpdate : Renderable { + ShaderUpdate(std::shared_ptr const &uniform) : + Renderable{uniform, nullptr} {} + + ShaderUpdate(std::shared_ptr &&uniform) : + Renderable{std::move(uniform), nullptr} {} +}; + +} // namespace renderer +} // namespace openage diff --git a/libopenage/renderer/renderer.cpp b/libopenage/renderer/renderer.cpp index 63f61da76e..e59d1ff421 100644 --- a/libopenage/renderer/renderer.cpp +++ b/libopenage/renderer/renderer.cpp @@ -1,39 +1,7 @@ -// Copyright 2019-2023 the openage authors. See copying.md for legal info. +// Copyright 2019-2024 the openage authors. See copying.md for legal info. #include "renderer.h" namespace openage::renderer { -RenderPass::RenderPass(std::vector renderables, - const std::shared_ptr &target) : - renderables(std::move(renderables)), - target{target} {} - - -const std::shared_ptr &RenderPass::get_target() const { - return this->target; -} - - -void RenderPass::set_target(const std::shared_ptr &target) { - this->target = target; -} - -void RenderPass::set_renderables(std::vector renderables) { - this->renderables = std::move(renderables); -} -void RenderPass::add_renderables(std::vector renderables) { - for (auto item : renderables) { - this->renderables.push_back(item); - } -} - -void RenderPass::add_renderables(Renderable renderable) { - this->renderables.push_back(std::move(renderable)); -} - -void RenderPass::clear_renderables() { - this->renderables.clear(); -} - } // namespace openage::renderer diff --git a/libopenage/renderer/renderer.h b/libopenage/renderer/renderer.h index 8f6e4ed3b9..907d9079df 100644 --- a/libopenage/renderer/renderer.h +++ b/libopenage/renderer/renderer.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -7,6 +7,8 @@ #include #include +#include "renderer/renderable.h" + namespace openage { namespace renderer { @@ -21,106 +23,13 @@ class UniformBufferInfo; class ShaderProgram; class Geometry; +class RenderPass; +class RenderTarget; class Texture2d; class UniformBuffer; class UniformInput; -/// The abstract base for a render target. -class RenderTarget : public std::enable_shared_from_this { -protected: - RenderTarget() = default; - -public: - virtual ~RenderTarget() = default; - - /** - * Get an image from the pixels in the render target's framebuffer. - * - * This should only be called _after_ rendering to the framebuffer has finished. - * - * @return RGBA texture data. - */ - virtual resources::Texture2dData into_data() = 0; - - virtual std::vector> get_texture_targets() = 0; -}; - -/// A renderable is a set of a shader UniformInput and a possible draw call. -/// Usually it is one "step" in a RenderPass. -/// -/// The UniformInput only stores the values the CPU at first. When the renderer -/// "executes" the Renderable in a pass, the UniformInput values are uploaded to -/// the shader on the GPU they were created with. -/// -/// If the geometry is nullptr, the uniform values are uploaded to the shader, -/// but no draw call is performed. This can be used to, for example, first set -/// the values of uniforms that many objects have in common, and then only -/// upload the uniforms that vary between them in each draw call. This works -/// because uniform values in any given shader are preserved across a render -/// pass. -/// -/// If geometry is set (i.e. it is not nullptr), the renderer draws the geometry -/// with the shader and other settings in the renderable. The result is written -/// into the render target, defined in the RenderPass. -struct Renderable { - /// Uniform values to be set in the appropriate shader. Contains a reference - /// to the correct shader, and this is the shader that will be used for - /// drawing if geometry is present. - std::shared_ptr uniform; - /// The geometry. It can be a simple primitive or a complex mesh. - /// Can be nullptr to only set uniforms but do not perform draw call. - std::shared_ptr geometry; - /// Whether to perform alpha-based color blending with whatever was in the - /// render target before. - bool alpha_blending = true; - /// Whether to perform depth testing and discard occluded fragments. - bool depth_test = true; -}; - - -/// Simplified form of Renderable, which is just an update for a shader. -/// When the ShaderUpdate is processed in a RenderPass, -/// the new uniform values (set on the CPU first with unif_in.update(...)) -/// are uploaded to the GPU. -struct ShaderUpdate : Renderable { - ShaderUpdate(std::shared_ptr const &uniform) : - Renderable{uniform, nullptr} {} - - ShaderUpdate(std::shared_ptr &&uniform) : - Renderable{std::move(uniform), nullptr} {} -}; - - -/// A render pass is a series of draw calls represented by renderables that output into the given render target. -class RenderPass { -protected: - /// Create a new RenderPass. This is called from Renderer::add_render_pass, - /// which then creates the proper subclass of RenderPass, depending on the backend. - RenderPass(std::vector, const std::shared_ptr &); - /// The renderables to parse and possibly execute. - std::vector renderables; - -public: - virtual ~RenderPass() = default; - void set_target(const std::shared_ptr &); - const std::shared_ptr &get_target() const; - - // Replace the current renderables - void set_renderables(std::vector); - // Append renderables to the end of the list of renderables - void add_renderables(std::vector); - // Append a single renderable to the end of the list of renderables - void add_renderables(Renderable); - // Clear the list of renderables - void clear_renderables(); - -private: - /// The render target to write into. - std::shared_ptr target; -}; - - /// The renderer. This class is used for performing all graphics operations. It is abstract and has implementations /// for various low-level graphics APIs like OpenGL. class Renderer { diff --git a/libopenage/renderer/stages/hud/render_stage.cpp b/libopenage/renderer/stages/hud/render_stage.cpp index b7ecaef31e..ad115bd2f6 100644 --- a/libopenage/renderer/stages/hud/render_stage.cpp +++ b/libopenage/renderer/stages/hud/render_stage.cpp @@ -1,9 +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 "render_stage.h" #include "renderer/camera/camera.h" #include "renderer/opengl/context.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/resources/assets/asset_manager.h" #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" diff --git a/libopenage/renderer/stages/screen/render_stage.cpp b/libopenage/renderer/stages/screen/render_stage.cpp index 786c547f62..a55419e0e1 100644 --- a/libopenage/renderer/stages/screen/render_stage.cpp +++ b/libopenage/renderer/stages/screen/render_stage.cpp @@ -1,8 +1,10 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #include "render_stage.h" #include "renderer/opengl/context.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/renderer.h" #include "renderer/resources/mesh_data.h" #include "renderer/resources/shader_source.h" diff --git a/libopenage/renderer/stages/screen/screenshot.cpp b/libopenage/renderer/stages/screen/screenshot.cpp index e910fb34ec..583641b6c0 100644 --- a/libopenage/renderer/stages/screen/screenshot.cpp +++ b/libopenage/renderer/stages/screen/screenshot.cpp @@ -1,4 +1,4 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #include "screenshot.h" @@ -13,7 +13,8 @@ #include "job/job_manager.h" #include "log/log.h" -#include "renderer/renderer.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/resources/texture_data.h" #include "renderer/stages/screen/render_stage.h" #include "util/strings.h" diff --git a/libopenage/renderer/stages/skybox/render_stage.cpp b/libopenage/renderer/stages/skybox/render_stage.cpp index c8b2fe76d2..b6fe47a7db 100644 --- a/libopenage/renderer/stages/skybox/render_stage.cpp +++ b/libopenage/renderer/stages/skybox/render_stage.cpp @@ -1,8 +1,10 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #include "render_stage.h" #include "renderer/opengl/context.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/renderer.h" #include "renderer/resources/mesh_data.h" #include "renderer/resources/shader_source.h" diff --git a/libopenage/renderer/stages/terrain/render_stage.cpp b/libopenage/renderer/stages/terrain/render_stage.cpp index 518ebd7c9b..332b80dfd9 100644 --- a/libopenage/renderer/stages/terrain/render_stage.cpp +++ b/libopenage/renderer/stages/terrain/render_stage.cpp @@ -1,9 +1,11 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #include "render_stage.h" #include "renderer/camera/camera.h" #include "renderer/opengl/context.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/renderer.h" #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 147e3fbd1b..203b974d4d 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -4,6 +4,8 @@ #include "renderer/camera/camera.h" #include "renderer/opengl/context.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" #include "renderer/resources/assets/asset_manager.h" #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" diff --git a/libopenage/renderer/vulkan/render_target.h b/libopenage/renderer/vulkan/render_target.h index f25c684fd4..32cc4784d7 100644 --- a/libopenage/renderer/vulkan/render_target.h +++ b/libopenage/renderer/vulkan/render_target.h @@ -9,7 +9,7 @@ #include "log/log.h" -#include "renderer/renderer.h" +#include "renderer/render_target.h" #include "renderer/vulkan/graphics_device.h" From 253f94b70574a16a0e5fe422377535faa51980eb Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 3 May 2024 21:33:31 +0200 Subject: [PATCH 273/771] renderer: Remove unused variable. --- libopenage/renderer/opengl/shader_program.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libopenage/renderer/opengl/shader_program.cpp b/libopenage/renderer/opengl/shader_program.cpp index 31b0d17778..cf59be1edb 100644 --- a/libopenage/renderer/opengl/shader_program.cpp +++ b/libopenage/renderer/opengl/shader_program.cpp @@ -447,7 +447,6 @@ void GlShaderProgram::bind_uniform_buffer(const char *block_name, std::shared_pt // Check if the uniform buffer matches the block definition for (auto const &pair : block.uniforms) { - auto const &unif = pair.second; ENSURE(gl_buffer->has_uniform(pair.first.c_str()), "Uniform buffer does not contain uniform '" << pair.first << "' required by block " << block_name); } From 85c4787195e46766296efbc860696cca5018f1f4 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 4 May 2024 00:03:07 +0200 Subject: [PATCH 274/771] renderer: Priority sorting in render pass. --- libopenage/renderer/opengl/render_pass.cpp | 2 +- libopenage/renderer/render_pass.cpp | 67 +++++++++++++- libopenage/renderer/render_pass.h | 87 ++++++++++++++++--- libopenage/renderer/stages/world/object.cpp | 14 +++ libopenage/renderer/stages/world/object.h | 2 + .../renderer/stages/world/render_stage.cpp | 6 +- 6 files changed, 159 insertions(+), 19 deletions(-) diff --git a/libopenage/renderer/opengl/render_pass.cpp b/libopenage/renderer/opengl/render_pass.cpp index 526663d324..f0904d9bb2 100644 --- a/libopenage/renderer/opengl/render_pass.cpp +++ b/libopenage/renderer/opengl/render_pass.cpp @@ -19,7 +19,7 @@ const std::vector &GlRenderPass::get_renderables() const { } void GlRenderPass::set_renderables(std::vector renderables) { - this->renderables = renderables; + RenderPass::set_renderables(renderables); this->is_optimised = false; } diff --git a/libopenage/renderer/render_pass.cpp b/libopenage/renderer/render_pass.cpp index f326fdf0bb..0be3bdc941 100644 --- a/libopenage/renderer/render_pass.cpp +++ b/libopenage/renderer/render_pass.cpp @@ -2,6 +2,8 @@ #include "render_pass.h" +#include + namespace openage::renderer { @@ -23,14 +25,75 @@ void RenderPass::set_target(const std::shared_ptr &target) { void RenderPass::set_renderables(std::vector renderables) { this->renderables = std::move(renderables); } + void RenderPass::add_renderables(std::vector renderables) { - for (auto item : renderables) { - this->renderables.push_back(item); + this->renderables.insert(this->renderables.end(), renderables.begin(), renderables.end()); + + if (this->priority_slices.empty() or this->priority_slices.back().priority != std::numeric_limits::max()) { + this->priority_slices.push_back(priority_slice{std::numeric_limits::max(), renderables.size()}); + return; } + + this->priority_slices.back().length += renderables.size(); } void RenderPass::add_renderables(Renderable renderable) { this->renderables.push_back(std::move(renderable)); + + if (this->priority_slices.empty() or this->priority_slices.back().priority != std::numeric_limits::max()) { + this->priority_slices.push_back(priority_slice{std::numeric_limits::max(), 1}); + return; + } + + this->priority_slices.back().length += 1; +} + +void RenderPass::add_renderables(std::vector renderables, int64_t priority) { + size_t renderables_index = 0; + size_t slice_index = 0; + int64_t current_priority = std::numeric_limits::min(); + for (auto i = 0; i < this->priority_slices.size(); i++) { + auto &slice = this->priority_slices.at(i); + if (slice.priority > priority) { + slice_index = i; + break; + } + renderables_index += slice.length; + current_priority = slice.priority; + } + + this->renderables.insert(this->renderables.begin() + renderables_index, renderables.begin(), renderables.end()); + + if (current_priority == priority) { + this->priority_slices[slice_index].length += renderables.size(); + } + else { + this->priority_slices.insert(this->priority_slices.begin() + slice_index, priority_slice{priority, renderables.size()}); + } +} + +void RenderPass::add_renderables(Renderable renderable, int64_t priority) { + size_t renderables_index = 0; + size_t slice_index = 0; + int64_t current_priority = std::numeric_limits::min(); + for (auto i = 0; i < this->priority_slices.size(); i++) { + auto &slice = this->priority_slices.at(i); + if (slice.priority > priority) { + slice_index = i; + break; + } + renderables_index += slice.length; + current_priority = slice.priority; + } + + this->renderables.insert(this->renderables.begin() + renderables_index, std::move(renderable)); + + if (current_priority == priority) { + this->priority_slices[slice_index].length += 1; + } + else { + this->priority_slices.insert(this->priority_slices.begin() + slice_index, priority_slice{priority, 1}); + } } void RenderPass::clear_renderables() { diff --git a/libopenage/renderer/render_pass.h b/libopenage/renderer/render_pass.h index 5849c97a26..8f4e304cfd 100644 --- a/libopenage/renderer/render_pass.h +++ b/libopenage/renderer/render_pass.h @@ -2,6 +2,7 @@ #pragma once +#include #include #include @@ -12,32 +13,92 @@ namespace openage { namespace renderer { class RenderTarget; -/// A render pass is a series of draw calls represented by renderables that output into the given render target. -class RenderPass { -protected: - /// Create a new RenderPass. This is called from Renderer::add_render_pass, - /// which then creates the proper subclass of RenderPass, depending on the backend. - RenderPass(std::vector, const std::shared_ptr &); - /// The renderables to parse and possibly execute. - std::vector renderables; +/** + * A slice of renderables with the same priority. + */ +struct priority_slice { + /// The priority of the renderables in this slice. + int64_t priority; + /// The number of renderables in this slice. + size_t length; +}; + +/** + * A render pass is a series of draw calls represented by renderables that output + * into the given render target. + */ +class RenderPass { public: virtual ~RenderPass() = default; + + /** + * Set the render target to write to. + */ void set_target(const std::shared_ptr &); + + /** + * Get the render target of the render pass. + */ const std::shared_ptr &get_target() const; - // Replace the current renderables + /** + * Replace the current renderables with the given list of renderables. + */ void set_renderables(std::vector); - // Append renderables to the end of the list of renderables + + /** + * Append renderables to the render pass. + */ void add_renderables(std::vector); - // Append a single renderable to the end of the list of renderables + + /** + * Append a single renderable to the render pass. + */ void add_renderables(Renderable); - // Clear the list of renderables + + /** + * Append renderables to the render pass with a given priority. + */ + void add_renderables(std::vector, int64_t priority); + + /** + * Append a single renderable to the render pass with a given priority. + */ + void add_renderables(Renderable, int64_t priority); + + /** + * Clear the list of renderables + */ void clear_renderables(); +protected: + /** + * Create a new RenderPass. This is called from Renderer::add_render_pass, + * which then creates the proper subclass of RenderPass, depending on the backend. + */ + RenderPass(std::vector, const std::shared_ptr &); + + /** + * The renderables to parse and possibly execute. + */ + std::vector renderables; + private: - /// The render target to write into. + /** + * Render target to write to. + */ std::shared_ptr target; + + /** + * Stores the number of renderables in the \p renderables member that + * have the same priority. + * + * The vector is sorted by priority, so the index of the first renderable + * with a given priority can be retrieved by adding up the lengths of all + * priority slices with a lower priority. + */ + std::vector priority_slices; }; } // namespace renderer diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index 949cb23395..7131d42601 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -197,6 +197,20 @@ size_t WorldObject::get_required_layer_count(const time::time_t &time) const { return animation_info->get_layer_count(); } +std::vector WorldObject::get_layer_positions(const time::time_t &time) const { + auto animation_info = this->animation_info.get(time); + if (not animation_info) { + return {}; + } + + std::vector positions; + for (size_t i = 0; i < animation_info->get_layer_count(); ++i) { + positions.push_back(i); + } + + return positions; +} + bool WorldObject::is_changed() { return this->changed; } diff --git a/libopenage/renderer/stages/world/object.h b/libopenage/renderer/stages/world/object.h index bec405a69d..c5e0aa8197 100644 --- a/libopenage/renderer/stages/world/object.h +++ b/libopenage/renderer/stages/world/object.h @@ -111,6 +111,8 @@ class WorldObject { */ size_t get_required_layer_count(const time::time_t &time) const; + std::vector get_layer_positions(const time::time_t &time) const; + /** * Check whether the object was changed by \p update(). * diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 203b974d4d..083b2461db 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -62,11 +62,11 @@ void WorldRenderStage::update() { obj->fetch_updates(current_time); if (obj->is_changed()) { if (obj->requires_renderable()) { - auto layer_count = obj->get_required_layer_count(current_time); + auto layer_positions = obj->get_layer_positions(current_time); Eigen::Matrix4f model_m = obj->get_model_matrix(); std::vector> transform_unifs; - for (size_t i = 0; i < layer_count; i++) { + for (auto layer_pos : layer_positions) { // Set uniforms that don't change or are not changed often auto layer_unifs = this->display_shader->new_uniform_input( "model", @@ -84,7 +84,7 @@ void WorldRenderStage::update() { true, true, }; - this->render_pass->add_renderables(display_obj); + this->render_pass->add_renderables(display_obj, layer_pos); transform_unifs.push_back(layer_unifs); } From e33d4815ebcdee80a3fab3bd1649ae4a37547e78 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 7 May 2024 22:56:45 +0200 Subject: [PATCH 275/771] renderer: Remove code duplication in render pass operations. --- libopenage/renderer/render_pass.cpp | 35 +++------------------ libopenage/renderer/stages/world/object.cpp | 3 +- 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/libopenage/renderer/render_pass.cpp b/libopenage/renderer/render_pass.cpp index 0be3bdc941..eaa218536f 100644 --- a/libopenage/renderer/render_pass.cpp +++ b/libopenage/renderer/render_pass.cpp @@ -38,28 +38,21 @@ void RenderPass::add_renderables(std::vector renderables) { } void RenderPass::add_renderables(Renderable renderable) { - this->renderables.push_back(std::move(renderable)); - - if (this->priority_slices.empty() or this->priority_slices.back().priority != std::numeric_limits::max()) { - this->priority_slices.push_back(priority_slice{std::numeric_limits::max(), 1}); - return; - } - - this->priority_slices.back().length += 1; + this->add_renderables(std::vector{std::move(renderable)}); } void RenderPass::add_renderables(std::vector renderables, int64_t priority) { size_t renderables_index = 0; size_t slice_index = 0; int64_t current_priority = std::numeric_limits::min(); - for (auto i = 0; i < this->priority_slices.size(); i++) { + for (size_t i = 0; i < this->priority_slices.size(); i++) { auto &slice = this->priority_slices.at(i); if (slice.priority > priority) { - slice_index = i; break; } renderables_index += slice.length; current_priority = slice.priority; + slice_index = i; } this->renderables.insert(this->renderables.begin() + renderables_index, renderables.begin(), renderables.end()); @@ -73,27 +66,7 @@ void RenderPass::add_renderables(std::vector renderables, int64_t pr } void RenderPass::add_renderables(Renderable renderable, int64_t priority) { - size_t renderables_index = 0; - size_t slice_index = 0; - int64_t current_priority = std::numeric_limits::min(); - for (auto i = 0; i < this->priority_slices.size(); i++) { - auto &slice = this->priority_slices.at(i); - if (slice.priority > priority) { - slice_index = i; - break; - } - renderables_index += slice.length; - current_priority = slice.priority; - } - - this->renderables.insert(this->renderables.begin() + renderables_index, std::move(renderable)); - - if (current_priority == priority) { - this->priority_slices[slice_index].length += 1; - } - else { - this->priority_slices.insert(this->priority_slices.begin() + slice_index, priority_slice{priority, 1}); - } + this->add_renderables(std::vector{std::move(renderable)}, priority); } void RenderPass::clear_renderables() { diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index 7131d42601..3b9edd6d3d 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -205,7 +205,8 @@ std::vector WorldObject::get_layer_positions(const time::time_t &time) c std::vector positions; for (size_t i = 0; i < animation_info->get_layer_count(); ++i) { - positions.push_back(i); + auto layer = animation_info->get_layer(i); + positions.push_back(layer.get_position()); } return positions; From 2b3262d674522e74c1bf1195433b373a547ca541 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 8 May 2024 14:59:22 +0200 Subject: [PATCH 276/771] renderer: Autosort renderables into layers on insert. --- libopenage/renderer/opengl/render_pass.cpp | 4 -- libopenage/renderer/opengl/render_pass.h | 1 - libopenage/renderer/render_pass.cpp | 63 ++++++++++++++-------- libopenage/renderer/render_pass.h | 47 ++++++++++++---- 4 files changed, 79 insertions(+), 36 deletions(-) diff --git a/libopenage/renderer/opengl/render_pass.cpp b/libopenage/renderer/opengl/render_pass.cpp index f0904d9bb2..ad8340a8b3 100644 --- a/libopenage/renderer/opengl/render_pass.cpp +++ b/libopenage/renderer/opengl/render_pass.cpp @@ -14,10 +14,6 @@ GlRenderPass::GlRenderPass(std::vector renderables, log::log(MSG(dbg) << "Created OpenGL render pass"); } -const std::vector &GlRenderPass::get_renderables() const { - return this->renderables; -} - void GlRenderPass::set_renderables(std::vector renderables) { RenderPass::set_renderables(renderables); this->is_optimised = false; diff --git a/libopenage/renderer/opengl/render_pass.h b/libopenage/renderer/opengl/render_pass.h index b5f317f585..4977c8e2e9 100644 --- a/libopenage/renderer/opengl/render_pass.h +++ b/libopenage/renderer/opengl/render_pass.h @@ -14,7 +14,6 @@ class GlRenderPass final : public RenderPass { const std::shared_ptr &); void set_renderables(std::vector); - const std::vector &get_renderables() const; void set_is_optimised(bool); bool get_is_optimised() const; diff --git a/libopenage/renderer/render_pass.cpp b/libopenage/renderer/render_pass.cpp index eaa218536f..5101a03b06 100644 --- a/libopenage/renderer/render_pass.cpp +++ b/libopenage/renderer/render_pass.cpp @@ -10,14 +10,16 @@ namespace openage::renderer { RenderPass::RenderPass(std::vector renderables, const std::shared_ptr &target) : renderables(std::move(renderables)), - target{target} {} - + target{target}, + layers{} { + // Add a default layer with the lowest priority + this->add_layer(0, std::numeric_limits::max()); +} const std::shared_ptr &RenderPass::get_target() const { return this->target; } - void RenderPass::set_target(const std::shared_ptr &target) { this->target = target; } @@ -29,12 +31,7 @@ void RenderPass::set_renderables(std::vector renderables) { void RenderPass::add_renderables(std::vector renderables) { this->renderables.insert(this->renderables.end(), renderables.begin(), renderables.end()); - if (this->priority_slices.empty() or this->priority_slices.back().priority != std::numeric_limits::max()) { - this->priority_slices.push_back(priority_slice{std::numeric_limits::max(), renderables.size()}); - return; - } - - this->priority_slices.back().length += renderables.size(); + this->layers.front().length += renderables.size(); } void RenderPass::add_renderables(Renderable renderable) { @@ -43,32 +40,56 @@ void RenderPass::add_renderables(Renderable renderable) { void RenderPass::add_renderables(std::vector renderables, int64_t priority) { size_t renderables_index = 0; - size_t slice_index = 0; + size_t layer_index = 0; int64_t current_priority = std::numeric_limits::min(); - for (size_t i = 0; i < this->priority_slices.size(); i++) { - auto &slice = this->priority_slices.at(i); - if (slice.priority > priority) { + for (size_t i = 0; i < this->layers.size(); i++) { + auto &layer = this->layers.at(i); + if (layer.priority < priority) { break; } - renderables_index += slice.length; - current_priority = slice.priority; - slice_index = i; + renderables_index += layer.length; + current_priority = layer.priority; + layer_index = i; } this->renderables.insert(this->renderables.begin() + renderables_index, renderables.begin(), renderables.end()); - if (current_priority == priority) { - this->priority_slices[slice_index].length += renderables.size(); - } - else { - this->priority_slices.insert(this->priority_slices.begin() + slice_index, priority_slice{priority, renderables.size()}); + if (current_priority != priority) { + layer_index += 1; + this->add_layer(layer_index, priority); } + + this->layers.at(layer_index).length += renderables.size(); } void RenderPass::add_renderables(Renderable renderable, int64_t priority) { this->add_renderables(std::vector{std::move(renderable)}, priority); } +void RenderPass::add_layer(int64_t priority) { + size_t layer_index = 0; + for (const auto &layer : this->layers) { + if (layer.priority < priority) { + break; + } + layer_index++; + } + + this->add_layer(layer_index, priority); +} + +const std::vector &RenderPass::get_renderables() const { + return this->renderables; +} + +const std::vector &RenderPass::get_layers() const { + return this->layers; +} + +void RenderPass::add_layer(size_t index, int64_t priority) { + this->layers.insert(this->layers.begin() + index, Layer{priority, 0}); +} + void RenderPass::clear_renderables() { this->renderables.clear(); } diff --git a/libopenage/renderer/render_pass.h b/libopenage/renderer/render_pass.h index 8f4e304cfd..82581d824a 100644 --- a/libopenage/renderer/render_pass.h +++ b/libopenage/renderer/render_pass.h @@ -14,12 +14,13 @@ namespace renderer { class RenderTarget; /** - * A slice of renderables with the same priority. + * Defines a layer in the render pass. A layer is a slice of the renderables + * that have the same priority. Each layer can have its own settings. */ -struct priority_slice { - /// The priority of the renderables in this slice. +struct Layer { + /// Priority of the renderables in this slice. int64_t priority; - /// The number of renderables in this slice. + /// Number of renderables in this slice. size_t length; }; @@ -32,6 +33,16 @@ class RenderPass { public: virtual ~RenderPass() = default; + /** + * Get the renderables of the render pass. + */ + const std::vector &get_renderables() const; + + /** + * Get the layers of the render pass. + */ + const std::vector &get_layers() const; + /** * Set the render target to write to. */ @@ -67,6 +78,14 @@ class RenderPass { */ void add_renderables(Renderable, int64_t priority); + /** + * Add a new layer to the render pass. + * + * @param priority Priority of the layer. Layers with higher priority are drawn first. + * @param clear_depth Whether to clear the depth buffer before rendering this layer. + */ + void add_layer(int64_t priority); + /** * Clear the list of renderables */ @@ -85,20 +104,28 @@ class RenderPass { std::vector renderables; private: + /** + * Add a new layer to the render pass at the given index. + * + * @param index Index in \p layers member to insert the new layer. + * @param priority Priority of the layer. Layers with higher priority are drawn first. + */ + void add_layer(size_t index, int64_t priority); + /** * Render target to write to. */ std::shared_ptr target; /** - * Stores the number of renderables in the \p renderables member that - * have the same priority. + * Stores the layers of the render pass. + * + * Layers are slices of the renderables that have the same priority. + * They can assign different settings to the renderables in the slice. * - * The vector is sorted by priority, so the index of the first renderable - * with a given priority can be retrieved by adding up the lengths of all - * priority slices with a lower priority. + * Sorted from lowest to highest priority. */ - std::vector priority_slices; + std::vector layers; }; } // namespace renderer From 10140c8102886af928d7562b34a016604a8e200d Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 8 May 2024 16:18:26 +0200 Subject: [PATCH 277/771] renderer: Make inserion by priority the default. --- libopenage/renderer/definitions.h | 10 ++++- libopenage/renderer/opengl/renderer.cpp | 2 +- libopenage/renderer/render_pass.cpp | 26 ++++++------- libopenage/renderer/render_pass.h | 49 ++++++++++++++++--------- 4 files changed, 53 insertions(+), 34 deletions(-) diff --git a/libopenage/renderer/definitions.h b/libopenage/renderer/definitions.h index 594f817d1a..49e07c30c1 100644 --- a/libopenage/renderer/definitions.h +++ b/libopenage/renderer/definitions.h @@ -1,9 +1,12 @@ -// 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 +#include + #include "coord/scene.h" + /** * Hardcoded definitions for parameters used in the renderer. * @@ -16,4 +19,9 @@ namespace openage::renderer { */ constexpr coord::scene3 SCENE_ORIGIN = coord::scene3{0, 0, 0}; +/** + * Maximum priority value for a render pass layer. + */ +static constexpr int64_t LAYER_PRIORITY_MAX = std::numeric_limits::max(); + } // namespace openage::renderer diff --git a/libopenage/renderer/opengl/renderer.cpp b/libopenage/renderer/opengl/renderer.cpp index d26ba7b96c..bf4740f935 100644 --- a/libopenage/renderer/opengl/renderer.cpp +++ b/libopenage/renderer/opengl/renderer.cpp @@ -178,7 +178,7 @@ void GlRenderer::render(const std::shared_ptr &pass) { // glEnable(GL_CULL_FACE); auto gl_pass = std::dynamic_pointer_cast(pass); - GlRenderer::optimise(gl_pass); + // GlRenderer::optimise(gl_pass); for (auto const &obj : gl_pass->get_renderables()) { if (obj.alpha_blending) { diff --git a/libopenage/renderer/render_pass.cpp b/libopenage/renderer/render_pass.cpp index 5101a03b06..e2eade4ded 100644 --- a/libopenage/renderer/render_pass.cpp +++ b/libopenage/renderer/render_pass.cpp @@ -2,18 +2,19 @@ #include "render_pass.h" -#include - namespace openage::renderer { RenderPass::RenderPass(std::vector renderables, const std::shared_ptr &target) : - renderables(std::move(renderables)), + renderables{}, target{target}, layers{} { // Add a default layer with the lowest priority this->add_layer(0, std::numeric_limits::max()); + + // Add the renderables to the pass + this->add_renderables(renderables); } const std::shared_ptr &RenderPass::get_target() const { @@ -25,17 +26,8 @@ void RenderPass::set_target(const std::shared_ptr &target) { } void RenderPass::set_renderables(std::vector renderables) { - this->renderables = std::move(renderables); -} - -void RenderPass::add_renderables(std::vector renderables) { - this->renderables.insert(this->renderables.end(), renderables.begin(), renderables.end()); - - this->layers.front().length += renderables.size(); -} - -void RenderPass::add_renderables(Renderable renderable) { - this->add_renderables(std::vector{std::move(renderable)}); + this->clear_renderables(); + this->add_renderables(renderables); } void RenderPass::add_renderables(std::vector renderables, int64_t priority) { @@ -91,7 +83,13 @@ void RenderPass::add_layer(size_t index, int64_t priority) { } void RenderPass::clear_renderables() { + // Erase the renderables this->renderables.clear(); + + // Keep layers, but reset the length of each layer + for (auto &layer : this->layers) { + layer.length = 0; + } } } // namespace openage::renderer diff --git a/libopenage/renderer/render_pass.h b/libopenage/renderer/render_pass.h index 82581d824a..6eb99963ac 100644 --- a/libopenage/renderer/render_pass.h +++ b/libopenage/renderer/render_pass.h @@ -6,6 +6,7 @@ #include #include +#include "renderer/definitions.h" #include "renderer/renderable.h" @@ -24,7 +25,6 @@ struct Layer { size_t length; }; - /** * A render pass is a series of draw calls represented by renderables that output * into the given render target. @@ -35,48 +35,56 @@ class RenderPass { /** * Get the renderables of the render pass. + * + * @return Renderables of the render pass. */ const std::vector &get_renderables() const; /** * Get the layers of the render pass. + * + * @return Layers of the render pass. */ const std::vector &get_layers() const; /** * Set the render target to write to. + * + * @param target Render target. */ - void set_target(const std::shared_ptr &); + void set_target(const std::shared_ptr &target); /** * Get the render target of the render pass. + * + * @return Render target. */ const std::shared_ptr &get_target() const; /** * Replace the current renderables with the given list of renderables. + * + * @param renderables New renderables. */ - void set_renderables(std::vector); - - /** - * Append renderables to the render pass. - */ - void add_renderables(std::vector); - - /** - * Append a single renderable to the render pass. - */ - void add_renderables(Renderable); + void set_renderables(std::vector renderables); /** * Append renderables to the render pass with a given priority. + * + * @param renderables New renderables. + * @param priority Priority of the renderables. Layers with higher priority are drawn first. */ - void add_renderables(std::vector, int64_t priority); + void add_renderables(std::vector renderables, + int64_t priority = LAYER_PRIORITY_MAX); /** * Append a single renderable to the render pass with a given priority. + * + * @param renderable New renderable. + * @param priority Priority of the renderable. Layers with higher priority are drawn first. */ - void add_renderables(Renderable, int64_t priority); + void add_renderables(Renderable renderable, + int64_t priority = LAYER_PRIORITY_MAX); /** * Add a new layer to the render pass. @@ -95,11 +103,16 @@ class RenderPass { /** * Create a new RenderPass. This is called from Renderer::add_render_pass, * which then creates the proper subclass of RenderPass, depending on the backend. + * + * @param renderables The renderables to draw. + * @param target Render target to write to. */ - RenderPass(std::vector, const std::shared_ptr &); + RenderPass(std::vector renderables, const std::shared_ptr &target); /** - * The renderables to parse and possibly execute. + * The renderables to draw. + * + * Kept sorted by layer priorities (highest to lowest priority). */ std::vector renderables; @@ -123,7 +136,7 @@ class RenderPass { * Layers are slices of the renderables that have the same priority. * They can assign different settings to the renderables in the slice. * - * Sorted from lowest to highest priority. + * Sorted from highest to lowest priority. */ std::vector layers; }; From bf9b2f2f2834df0a6a318f8d89e300ae2dba771e Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 8 May 2024 16:39:57 +0200 Subject: [PATCH 278/771] renderer: Move sorting of renderables to render pass. --- libopenage/renderer/opengl/render_pass.cpp | 13 ++++++------- libopenage/renderer/opengl/render_pass.h | 6 +++--- libopenage/renderer/opengl/renderer.cpp | 12 +++++------- libopenage/renderer/opengl/renderer.h | 2 +- libopenage/renderer/render_pass.cpp | 14 ++++++++++++++ libopenage/renderer/render_pass.h | 11 +++++++++++ 6 files changed, 40 insertions(+), 18 deletions(-) diff --git a/libopenage/renderer/opengl/render_pass.cpp b/libopenage/renderer/opengl/render_pass.cpp index ad8340a8b3..79a1773b1a 100644 --- a/libopenage/renderer/opengl/render_pass.cpp +++ b/libopenage/renderer/opengl/render_pass.cpp @@ -10,20 +10,19 @@ namespace openage::renderer::opengl { GlRenderPass::GlRenderPass(std::vector renderables, const std::shared_ptr &target) : RenderPass(renderables, target), - is_optimised(false) { - log::log(MSG(dbg) << "Created OpenGL render pass"); + is_optimized(false) { } void GlRenderPass::set_renderables(std::vector renderables) { RenderPass::set_renderables(renderables); - this->is_optimised = false; + this->is_optimized = false; } -bool GlRenderPass::get_is_optimised() const { - return this->is_optimised; +bool GlRenderPass::get_is_optimized() const { + return this->is_optimized; } -void GlRenderPass::set_is_optimised(bool flag) { - this->is_optimised = flag; +void GlRenderPass::set_is_optimized(bool flag) { + this->is_optimized = flag; } } // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/render_pass.h b/libopenage/renderer/opengl/render_pass.h index 4977c8e2e9..d29ad6e227 100644 --- a/libopenage/renderer/opengl/render_pass.h +++ b/libopenage/renderer/opengl/render_pass.h @@ -14,12 +14,12 @@ class GlRenderPass final : public RenderPass { const std::shared_ptr &); void set_renderables(std::vector); - void set_is_optimised(bool); - bool get_is_optimised() const; + void set_is_optimized(bool); + bool get_is_optimized() const; private: /// Whether the renderables order is optimised - bool is_optimised; + bool is_optimized; }; } // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/renderer.cpp b/libopenage/renderer/opengl/renderer.cpp index bf4740f935..6e8c865194 100644 --- a/libopenage/renderer/opengl/renderer.cpp +++ b/libopenage/renderer/opengl/renderer.cpp @@ -145,10 +145,9 @@ void GlRenderer::resize_display_target(size_t width, size_t height) { this->display->resize(width, height); } -void GlRenderer::optimise(const std::shared_ptr &pass) { - if (!pass->get_is_optimised()) { - auto renderables = pass->get_renderables(); - std::stable_sort(renderables.begin(), renderables.end(), [](const Renderable &a, const Renderable &b) { +void GlRenderer::optimize(const std::shared_ptr &pass) { + if (!pass->get_is_optimized()) { + pass->sort([](const Renderable &a, const Renderable &b) { GLuint shader_a = std::dynamic_pointer_cast( std::dynamic_pointer_cast(a.uniform)->get_program()) ->get_handle(); @@ -158,8 +157,7 @@ void GlRenderer::optimise(const std::shared_ptr &pass) { return shader_a < shader_b; }); - pass->set_renderables(renderables); - pass->set_is_optimised(true); + pass->set_is_optimized(true); } } @@ -178,7 +176,7 @@ void GlRenderer::render(const std::shared_ptr &pass) { // glEnable(GL_CULL_FACE); auto gl_pass = std::dynamic_pointer_cast(pass); - // GlRenderer::optimise(gl_pass); + GlRenderer::optimize(gl_pass); for (auto const &obj : gl_pass->get_renderables()) { if (obj.alpha_blending) { diff --git a/libopenage/renderer/opengl/renderer.h b/libopenage/renderer/opengl/renderer.h index 4d9038808a..aa3d3769a3 100644 --- a/libopenage/renderer/opengl/renderer.h +++ b/libopenage/renderer/opengl/renderer.h @@ -60,7 +60,7 @@ class GlRenderer final : public Renderer { private: /// Optimize the render pass by reordering stuff - static void optimise(const std::shared_ptr &); + static void optimize(const std::shared_ptr &pass); /// The GL context. std::shared_ptr gl_context; diff --git a/libopenage/renderer/render_pass.cpp b/libopenage/renderer/render_pass.cpp index e2eade4ded..b9a638cd76 100644 --- a/libopenage/renderer/render_pass.cpp +++ b/libopenage/renderer/render_pass.cpp @@ -2,6 +2,8 @@ #include "render_pass.h" +#include "log/log.h" + namespace openage::renderer { @@ -15,6 +17,8 @@ RenderPass::RenderPass(std::vector renderables, // Add the renderables to the pass this->add_renderables(renderables); + + log::log(MSG(dbg) << "Created render pass"); } const std::shared_ptr &RenderPass::get_target() const { @@ -92,4 +96,14 @@ void RenderPass::clear_renderables() { } } +void RenderPass::sort(const compare_func &compare) { + size_t offset = 0; + for (auto &layer : this->layers) { + std::stable_sort(this->renderables.begin() + offset, + this->renderables.begin() + offset + layer.length, + compare); + offset += layer.length; + } +} + } // namespace openage::renderer diff --git a/libopenage/renderer/render_pass.h b/libopenage/renderer/render_pass.h index 6eb99963ac..d424c413c7 100644 --- a/libopenage/renderer/render_pass.h +++ b/libopenage/renderer/render_pass.h @@ -99,6 +99,17 @@ class RenderPass { */ void clear_renderables(); + using compare_func = std::function; + + /** + * Sort the renderables using the given comparison function. + * + * Layers are sorted individually, so the order of layers is not changed. + * + * @param compare Comparison function. + */ + void sort(const compare_func &compare); + protected: /** * Create a new RenderPass. This is called from Renderer::add_render_pass, From b328def2f657952f9e9e5655b9ea24885326d71f Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 8 May 2024 16:50:37 +0200 Subject: [PATCH 279/771] renderer: Add more functions for optimization to GLRenderPass. --- libopenage/renderer/opengl/render_pass.cpp | 10 ++++++++++ libopenage/renderer/opengl/render_pass.h | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/libopenage/renderer/opengl/render_pass.cpp b/libopenage/renderer/opengl/render_pass.cpp index 79a1773b1a..e97fd922c1 100644 --- a/libopenage/renderer/opengl/render_pass.cpp +++ b/libopenage/renderer/opengl/render_pass.cpp @@ -18,6 +18,16 @@ void GlRenderPass::set_renderables(std::vector renderables) { this->is_optimized = false; } +void GlRenderPass::add_renderables(std::vector renderables, int64_t priority) { + RenderPass::add_renderables(renderables, priority); + this->is_optimized = false; +} + +void GlRenderPass::add_renderables(Renderable renderable, int64_t priority) { + RenderPass::add_renderables(renderable, priority); + this->is_optimized = false; +} + bool GlRenderPass::get_is_optimized() const { return this->is_optimized; } diff --git a/libopenage/renderer/opengl/render_pass.h b/libopenage/renderer/opengl/render_pass.h index d29ad6e227..4ac5e31c44 100644 --- a/libopenage/renderer/opengl/render_pass.h +++ b/libopenage/renderer/opengl/render_pass.h @@ -13,7 +13,10 @@ class GlRenderPass final : public RenderPass { GlRenderPass(std::vector, const std::shared_ptr &); - void set_renderables(std::vector); + void set_renderables(std::vector renderables); + void add_renderables(std::vector renderables, int64_t priority = LAYER_PRIORITY_MAX); + void add_renderables(Renderable renderable, int64_t priority = LAYER_PRIORITY_MAX); + void set_is_optimized(bool); bool get_is_optimized() const; From 017c3bec44dc4483f926a63808c36f89b0c5d879 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 8 May 2024 16:52:35 +0200 Subject: [PATCH 280/771] renderer: Turn off optimization for OpenGL. Because it could be very expensive. --- libopenage/renderer/opengl/renderer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libopenage/renderer/opengl/renderer.cpp b/libopenage/renderer/opengl/renderer.cpp index 6e8c865194..41496db679 100644 --- a/libopenage/renderer/opengl/renderer.cpp +++ b/libopenage/renderer/opengl/renderer.cpp @@ -176,7 +176,8 @@ void GlRenderer::render(const std::shared_ptr &pass) { // glEnable(GL_CULL_FACE); auto gl_pass = std::dynamic_pointer_cast(pass); - GlRenderer::optimize(gl_pass); + // TODO: Optimization is disabled for now. Figure out how to do this without calling it every frame + // GlRenderer::optimize(gl_pass); for (auto const &obj : gl_pass->get_renderables()) { if (obj.alpha_blending) { From 5746a5bc5c0784146718e2e8ed5675cfc976e8f3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 8 May 2024 17:21:21 +0200 Subject: [PATCH 281/771] renderer: Use move semantics for adding renderables to pass. --- libopenage/renderer/opengl/render_pass.cpp | 16 ++++++++-------- libopenage/renderer/opengl/render_pass.h | 10 +++++----- libopenage/renderer/opengl/renderer.h | 2 +- libopenage/renderer/render_pass.cpp | 16 +++++++++------- libopenage/renderer/render_pass.h | 8 ++++---- libopenage/renderer/stages/hud/render_stage.cpp | 2 +- .../renderer/stages/screen/render_stage.cpp | 2 +- .../renderer/stages/terrain/render_stage.cpp | 2 +- .../renderer/stages/world/render_stage.cpp | 2 +- 9 files changed, 31 insertions(+), 29 deletions(-) diff --git a/libopenage/renderer/opengl/render_pass.cpp b/libopenage/renderer/opengl/render_pass.cpp index e97fd922c1..6d2ab052c1 100644 --- a/libopenage/renderer/opengl/render_pass.cpp +++ b/libopenage/renderer/opengl/render_pass.cpp @@ -7,24 +7,24 @@ namespace openage::renderer::opengl { -GlRenderPass::GlRenderPass(std::vector renderables, +GlRenderPass::GlRenderPass(std::vector &&renderables, const std::shared_ptr &target) : - RenderPass(renderables, target), + RenderPass(std::move(renderables), target), is_optimized(false) { } -void GlRenderPass::set_renderables(std::vector renderables) { - RenderPass::set_renderables(renderables); +void GlRenderPass::set_renderables(std::vector &&renderables) { + RenderPass::set_renderables(std::move(renderables)); this->is_optimized = false; } -void GlRenderPass::add_renderables(std::vector renderables, int64_t priority) { - RenderPass::add_renderables(renderables, priority); +void GlRenderPass::add_renderables(std::vector &&renderables, int64_t priority) { + RenderPass::add_renderables(std::move(renderables), priority); this->is_optimized = false; } -void GlRenderPass::add_renderables(Renderable renderable, int64_t priority) { - RenderPass::add_renderables(renderable, priority); +void GlRenderPass::add_renderables(Renderable &&renderable, int64_t priority) { + RenderPass::add_renderables(std::move(renderable), priority); this->is_optimized = false; } diff --git a/libopenage/renderer/opengl/render_pass.h b/libopenage/renderer/opengl/render_pass.h index 4ac5e31c44..87ea8448c9 100644 --- a/libopenage/renderer/opengl/render_pass.h +++ b/libopenage/renderer/opengl/render_pass.h @@ -10,12 +10,12 @@ namespace openage::renderer::opengl { class GlRenderPass final : public RenderPass { public: - GlRenderPass(std::vector, - const std::shared_ptr &); + GlRenderPass(std::vector &&renderables, + const std::shared_ptr &target); - void set_renderables(std::vector renderables); - void add_renderables(std::vector renderables, int64_t priority = LAYER_PRIORITY_MAX); - void add_renderables(Renderable renderable, int64_t priority = LAYER_PRIORITY_MAX); + void set_renderables(std::vector &&renderables); + void add_renderables(std::vector &&renderables, int64_t priority = LAYER_PRIORITY_MAX); + void add_renderables(Renderable &&renderable, int64_t priority = LAYER_PRIORITY_MAX); void set_is_optimized(bool); bool get_is_optimized() const; diff --git a/libopenage/renderer/opengl/renderer.h b/libopenage/renderer/opengl/renderer.h index aa3d3769a3..458af24358 100644 --- a/libopenage/renderer/opengl/renderer.h +++ b/libopenage/renderer/opengl/renderer.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 diff --git a/libopenage/renderer/render_pass.cpp b/libopenage/renderer/render_pass.cpp index b9a638cd76..931f5519ac 100644 --- a/libopenage/renderer/render_pass.cpp +++ b/libopenage/renderer/render_pass.cpp @@ -7,7 +7,7 @@ namespace openage::renderer { -RenderPass::RenderPass(std::vector renderables, +RenderPass::RenderPass(std::vector &&renderables, const std::shared_ptr &target) : renderables{}, target{target}, @@ -16,7 +16,7 @@ RenderPass::RenderPass(std::vector renderables, this->add_layer(0, std::numeric_limits::max()); // Add the renderables to the pass - this->add_renderables(renderables); + this->add_renderables(std::move(renderables)); log::log(MSG(dbg) << "Created render pass"); } @@ -29,12 +29,12 @@ void RenderPass::set_target(const std::shared_ptr &target) { this->target = target; } -void RenderPass::set_renderables(std::vector renderables) { +void RenderPass::set_renderables(std::vector &&renderables) { this->clear_renderables(); - this->add_renderables(renderables); + this->add_renderables(std::move(renderables)); } -void RenderPass::add_renderables(std::vector renderables, int64_t priority) { +void RenderPass::add_renderables(std::vector &&renderables, int64_t priority) { size_t renderables_index = 0; size_t layer_index = 0; int64_t current_priority = std::numeric_limits::min(); @@ -48,7 +48,9 @@ void RenderPass::add_renderables(std::vector renderables, int64_t pr layer_index = i; } - this->renderables.insert(this->renderables.begin() + renderables_index, renderables.begin(), renderables.end()); + this->renderables.insert(this->renderables.begin() + renderables_index, + std::make_move_iterator(renderables.begin()), + std::make_move_iterator(renderables.end())); if (current_priority != priority) { layer_index += 1; @@ -58,7 +60,7 @@ void RenderPass::add_renderables(std::vector renderables, int64_t pr this->layers.at(layer_index).length += renderables.size(); } -void RenderPass::add_renderables(Renderable renderable, int64_t priority) { +void RenderPass::add_renderables(Renderable &&renderable, int64_t priority) { this->add_renderables(std::vector{std::move(renderable)}, priority); } diff --git a/libopenage/renderer/render_pass.h b/libopenage/renderer/render_pass.h index d424c413c7..33526e7f7b 100644 --- a/libopenage/renderer/render_pass.h +++ b/libopenage/renderer/render_pass.h @@ -66,7 +66,7 @@ class RenderPass { * * @param renderables New renderables. */ - void set_renderables(std::vector renderables); + void set_renderables(std::vector &&renderables); /** * Append renderables to the render pass with a given priority. @@ -74,7 +74,7 @@ class RenderPass { * @param renderables New renderables. * @param priority Priority of the renderables. Layers with higher priority are drawn first. */ - void add_renderables(std::vector renderables, + void add_renderables(std::vector &&renderables, int64_t priority = LAYER_PRIORITY_MAX); /** @@ -83,7 +83,7 @@ class RenderPass { * @param renderable New renderable. * @param priority Priority of the renderable. Layers with higher priority are drawn first. */ - void add_renderables(Renderable renderable, + void add_renderables(Renderable &&renderable, int64_t priority = LAYER_PRIORITY_MAX); /** @@ -118,7 +118,7 @@ class RenderPass { * @param renderables The renderables to draw. * @param target Render target to write to. */ - RenderPass(std::vector renderables, const std::shared_ptr &target); + RenderPass(std::vector &&renderables, const std::shared_ptr &target); /** * The renderables to draw. diff --git a/libopenage/renderer/stages/hud/render_stage.cpp b/libopenage/renderer/stages/hud/render_stage.cpp index ad115bd2f6..a55751ec5f 100644 --- a/libopenage/renderer/stages/hud/render_stage.cpp +++ b/libopenage/renderer/stages/hud/render_stage.cpp @@ -80,7 +80,7 @@ void HudRenderStage::update() { true, }; - this->render_pass->add_renderables(display_obj); + this->render_pass->add_renderables(std::move(display_obj)); this->drag_object->clear_requires_renderable(); this->drag_object->set_uniforms(transform_unifs); diff --git a/libopenage/renderer/stages/screen/render_stage.cpp b/libopenage/renderer/stages/screen/render_stage.cpp index a55419e0e1..3d07e4ed3e 100644 --- a/libopenage/renderer/stages/screen/render_stage.cpp +++ b/libopenage/renderer/stages/screen/render_stage.cpp @@ -89,7 +89,7 @@ void ScreenRenderStage::update_render_pass() { output_layers.push_back(display_obj); } this->render_pass->clear_renderables(); - this->render_pass->add_renderables(output_layers); + this->render_pass->add_renderables(std::move(output_layers)); } } // namespace openage::renderer::screen diff --git a/libopenage/renderer/stages/terrain/render_stage.cpp b/libopenage/renderer/stages/terrain/render_stage.cpp index 332b80dfd9..740fe260f6 100644 --- a/libopenage/renderer/stages/terrain/render_stage.cpp +++ b/libopenage/renderer/stages/terrain/render_stage.cpp @@ -77,7 +77,7 @@ void TerrainRenderStage::update() { }; // TODO: Remove old renderable instead of clearing everything - this->render_pass->add_renderables(display_obj); + this->render_pass->add_renderables(std::move(display_obj)); mesh->clear_requires_renderable(); mesh->set_uniforms(transform_unifs); diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 083b2461db..05827eb645 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -84,7 +84,7 @@ void WorldRenderStage::update() { true, true, }; - this->render_pass->add_renderables(display_obj, layer_pos); + this->render_pass->add_renderables(std::move(display_obj), layer_pos); transform_unifs.push_back(layer_unifs); } From fb93335cce9c21f0f4b7cdd1949e2bf9086a8400 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 8 May 2024 17:28:36 +0200 Subject: [PATCH 282/771] renderer: Add more comments to explain render pass logic. --- libopenage/renderer/render_pass.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/libopenage/renderer/render_pass.cpp b/libopenage/renderer/render_pass.cpp index 931f5519ac..11bad2493e 100644 --- a/libopenage/renderer/render_pass.cpp +++ b/libopenage/renderer/render_pass.cpp @@ -35,12 +35,21 @@ void RenderPass::set_renderables(std::vector &&renderables) { } void RenderPass::add_renderables(std::vector &&renderables, int64_t priority) { + // Insertion index for the renderables size_t renderables_index = 0; + + // Index of the layer where the renderables will be inserted size_t layer_index = 0; + + // Priority of the last observed layer int64_t current_priority = std::numeric_limits::min(); + + // Find the index in renderables to insert the renderables for (size_t i = 0; i < this->layers.size(); i++) { auto &layer = this->layers.at(i); if (layer.priority < priority) { + // Priority of the next layer is lower than the desired priority + // Insert the renderables directly before this layer break; } renderables_index += layer.length; @@ -53,10 +62,12 @@ void RenderPass::add_renderables(std::vector &&renderables, int64_t std::make_move_iterator(renderables.end())); if (current_priority != priority) { + // Lazily add a new layer with the desired priority layer_index += 1; this->add_layer(layer_index, priority); } + // Update the length of the layer with the number of added renderables this->layers.at(layer_index).length += renderables.size(); } @@ -89,10 +100,10 @@ void RenderPass::add_layer(size_t index, int64_t priority) { } void RenderPass::clear_renderables() { - // Erase the renderables + // Erase all renderables this->renderables.clear(); - // Keep layers, but reset the length of each layer + // Keep layer definitions, but reset the length of each layer to 0 for (auto &layer : this->layers) { layer.length = 0; } From 82356fd28d8834079ade65d832df93f58a8c2104 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 8 May 2024 19:26:45 +0200 Subject: [PATCH 283/771] renderer: Revere ordering of layers in pass. --- libopenage/renderer/opengl/renderer.cpp | 96 ++++++++++++++++++------- libopenage/renderer/render_pass.cpp | 18 +++-- libopenage/renderer/render_pass.h | 10 +-- 3 files changed, 90 insertions(+), 34 deletions(-) diff --git a/libopenage/renderer/opengl/renderer.cpp b/libopenage/renderer/opengl/renderer.cpp index 41496db679..a7546cc404 100644 --- a/libopenage/renderer/opengl/renderer.cpp +++ b/libopenage/renderer/opengl/renderer.cpp @@ -33,9 +33,9 @@ GlRenderer::GlRenderer(const std::shared_ptr &ctx, // global GL alpha blending settings // glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFuncSeparate( - GL_SRC_ALPHA, // source (overlaying) RGB factor + GL_SRC_ALPHA, // source (incoming) RGB factor GL_ONE_MINUS_SRC_ALPHA, // destination (underlying) RGB factor - GL_ONE, // source (overlaying) alpha factor + GL_ONE, // source (incoming) alpha factor GL_ONE_MINUS_SRC_ALPHA // destination (underlying) alpha factor ); @@ -179,34 +179,80 @@ void GlRenderer::render(const std::shared_ptr &pass) { // TODO: Optimization is disabled for now. Figure out how to do this without calling it every frame // GlRenderer::optimize(gl_pass); - for (auto const &obj : gl_pass->get_renderables()) { - if (obj.alpha_blending) { - glEnable(GL_BLEND); - } - else { - glDisable(GL_BLEND); - } + // render all objects in the pass + auto &layers = gl_pass->get_layers(); + auto &renderables = gl_pass->get_renderables(); - if (obj.depth_test) { - glEnable(GL_DEPTH_TEST); - } - else { - glDisable(GL_DEPTH_TEST); + // Draw by layers + size_t offset = 0; + size_t next_offset = 0; + for (auto const &layer : layers) { + if (layer.clear_depth) { + glClear(GL_DEPTH_BUFFER_BIT); } - auto in = std::dynamic_pointer_cast(obj.uniform); - auto program = std::static_pointer_cast(in->get_program()); - - // this also calls program->use() - program->update_uniforms(in); - - // draw the geometry - if (obj.geometry != nullptr) { - auto geom = std::dynamic_pointer_cast(obj.geometry); - // TODO read obj.blend + family - geom->draw(); + next_offset = offset + layer.length; + for (size_t i = offset; i < next_offset; i++) { + const auto &obj = renderables[i]; + if (obj.alpha_blending) { + glEnable(GL_BLEND); + } + else { + glDisable(GL_BLEND); + } + + if (obj.depth_test) { + glEnable(GL_DEPTH_TEST); + } + else { + glDisable(GL_DEPTH_TEST); + } + + auto in = std::dynamic_pointer_cast(obj.uniform); + auto program = std::static_pointer_cast(in->get_program()); + + // this also calls program->use() + program->update_uniforms(in); + + // draw the geometry + if (obj.geometry != nullptr) { + auto geom = std::dynamic_pointer_cast(obj.geometry); + // TODO read obj.blend + family + geom->draw(); + } } + + offset = next_offset; } + + // for (auto const &obj : gl_pass->get_renderables()) { + // if (obj.alpha_blending) { + // glEnable(GL_BLEND); + // } + // else { + // glDisable(GL_BLEND); + // } + + // if (obj.depth_test) { + // glEnable(GL_DEPTH_TEST); + // } + // else { + // glDisable(GL_DEPTH_TEST); + // } + + // auto in = std::dynamic_pointer_cast(obj.uniform); + // auto program = std::static_pointer_cast(in->get_program()); + + // // this also calls program->use() + // program->update_uniforms(in); + + // // draw the geometry + // if (obj.geometry != nullptr) { + // auto geom = std::dynamic_pointer_cast(obj.geometry); + // // TODO read obj.blend + family + // geom->draw(); + // } + // } } } // namespace openage::renderer::opengl diff --git a/libopenage/renderer/render_pass.cpp b/libopenage/renderer/render_pass.cpp index 11bad2493e..18e15b8c68 100644 --- a/libopenage/renderer/render_pass.cpp +++ b/libopenage/renderer/render_pass.cpp @@ -13,7 +13,7 @@ RenderPass::RenderPass(std::vector &&renderables, target{target}, layers{} { // Add a default layer with the lowest priority - this->add_layer(0, std::numeric_limits::max()); + this->add_layer(0, LAYER_PRIORITY_MAX); // Add the renderables to the pass this->add_renderables(std::move(renderables)); @@ -42,12 +42,21 @@ void RenderPass::add_renderables(std::vector &&renderables, int64_t size_t layer_index = 0; // Priority of the last observed layer - int64_t current_priority = std::numeric_limits::min(); + int64_t current_priority = LAYER_PRIORITY_MAX; + + if (priority == LAYER_PRIORITY_MAX) { + // Add the renderables to the last (default) layer + this->renderables.insert(this->renderables.end(), + std::make_move_iterator(renderables.begin()), + std::make_move_iterator(renderables.end())); + this->layers.back().length += renderables.size(); + return; + } // Find the index in renderables to insert the renderables for (size_t i = 0; i < this->layers.size(); i++) { auto &layer = this->layers.at(i); - if (layer.priority < priority) { + if (layer.priority > priority) { // Priority of the next layer is lower than the desired priority // Insert the renderables directly before this layer break; @@ -63,7 +72,6 @@ void RenderPass::add_renderables(std::vector &&renderables, int64_t if (current_priority != priority) { // Lazily add a new layer with the desired priority - layer_index += 1; this->add_layer(layer_index, priority); } @@ -78,7 +86,7 @@ void RenderPass::add_renderables(Renderable &&renderable, int64_t priority) { void RenderPass::add_layer(int64_t priority) { size_t layer_index = 0; for (const auto &layer : this->layers) { - if (layer.priority < priority) { + if (layer.priority > priority) { break; } layer_index++; diff --git a/libopenage/renderer/render_pass.h b/libopenage/renderer/render_pass.h index 33526e7f7b..95eb4a47ea 100644 --- a/libopenage/renderer/render_pass.h +++ b/libopenage/renderer/render_pass.h @@ -23,6 +23,8 @@ struct Layer { int64_t priority; /// Number of renderables in this slice. size_t length; + /// Whether to clear the depth buffer before rendering this layer. + bool clear_depth = true; }; /** @@ -72,7 +74,7 @@ class RenderPass { * Append renderables to the render pass with a given priority. * * @param renderables New renderables. - * @param priority Priority of the renderables. Layers with higher priority are drawn first. + * @param priority Priority of the renderables. Layers with higher priority are drawn later. */ void add_renderables(std::vector &&renderables, int64_t priority = LAYER_PRIORITY_MAX); @@ -81,7 +83,7 @@ class RenderPass { * Append a single renderable to the render pass with a given priority. * * @param renderable New renderable. - * @param priority Priority of the renderable. Layers with higher priority are drawn first. + * @param priority Priority of the renderable. Layers with higher priority are drawn later. */ void add_renderables(Renderable &&renderable, int64_t priority = LAYER_PRIORITY_MAX); @@ -123,7 +125,7 @@ class RenderPass { /** * The renderables to draw. * - * Kept sorted by layer priorities (highest to lowest priority). + * Kept sorted by layer priorities (lowest to highest priority). */ std::vector renderables; @@ -147,7 +149,7 @@ class RenderPass { * Layers are slices of the renderables that have the same priority. * They can assign different settings to the renderables in the slice. * - * Sorted from highest to lowest priority. + * Sorted from lowest to highest priority. */ std::vector layers; }; From 5f74b7212051b673b7ba6a53b645bb3747e9b640 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 8 May 2024 23:08:08 +0200 Subject: [PATCH 284/771] renderer: Allow configuring depth of added layers. --- libopenage/renderer/render_pass.cpp | 24 ++++++++++++------------ libopenage/renderer/render_pass.h | 7 ++++--- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/libopenage/renderer/render_pass.cpp b/libopenage/renderer/render_pass.cpp index 18e15b8c68..efabcbf445 100644 --- a/libopenage/renderer/render_pass.cpp +++ b/libopenage/renderer/render_pass.cpp @@ -21,6 +21,14 @@ RenderPass::RenderPass(std::vector &&renderables, log::log(MSG(dbg) << "Created render pass"); } +const std::vector &RenderPass::get_renderables() const { + return this->renderables; +} + +const std::vector &RenderPass::get_layers() const { + return this->layers; +} + const std::shared_ptr &RenderPass::get_target() const { return this->target; } @@ -83,7 +91,7 @@ void RenderPass::add_renderables(Renderable &&renderable, int64_t priority) { this->add_renderables(std::vector{std::move(renderable)}, priority); } -void RenderPass::add_layer(int64_t priority) { +void RenderPass::add_layer(int64_t priority, bool clear_depth) { size_t layer_index = 0; for (const auto &layer : this->layers) { if (layer.priority > priority) { @@ -92,19 +100,11 @@ void RenderPass::add_layer(int64_t priority) { layer_index++; } - this->add_layer(layer_index, priority); -} - -const std::vector &RenderPass::get_renderables() const { - return this->renderables; -} - -const std::vector &RenderPass::get_layers() const { - return this->layers; + this->add_layer(layer_index, priority, clear_depth); } -void RenderPass::add_layer(size_t index, int64_t priority) { - this->layers.insert(this->layers.begin() + index, Layer{priority, 0}); +void RenderPass::add_layer(size_t index, int64_t priority, bool clear_depth) { + this->layers.insert(this->layers.begin() + index, Layer{priority, 0, clear_depth}); } void RenderPass::clear_renderables() { diff --git a/libopenage/renderer/render_pass.h b/libopenage/renderer/render_pass.h index 95eb4a47ea..56473d9952 100644 --- a/libopenage/renderer/render_pass.h +++ b/libopenage/renderer/render_pass.h @@ -92,9 +92,9 @@ class RenderPass { * Add a new layer to the render pass. * * @param priority Priority of the layer. Layers with higher priority are drawn first. - * @param clear_depth Whether to clear the depth buffer before rendering this layer. + * @param clear_depth If true clears the depth buffer before rendering this layer. */ - void add_layer(int64_t priority); + void add_layer(int64_t priority, bool clear_depth = true); /** * Clear the list of renderables @@ -135,8 +135,9 @@ class RenderPass { * * @param index Index in \p layers member to insert the new layer. * @param priority Priority of the layer. Layers with higher priority are drawn first. + * @param clear_depth If true clears the depth buffer before rendering this layer. */ - void add_layer(size_t index, int64_t priority); + void add_layer(size_t index, int64_t priority, bool clear_depth = true); /** * Render target to write to. From ac1be0fb72e374c2aab79ec453b60ee1e2b99862 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 9 May 2024 00:51:32 +0200 Subject: [PATCH 285/771] doc: Document layer usage in level 1 renderer. --- doc/code/renderer/level1.md | 74 +++++++++++++++++++++++++++++-- libopenage/renderer/render_pass.h | 4 ++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/doc/code/renderer/level1.md b/doc/code/renderer/level1.md index 8a7d4e1eb0..659c51036c 100644 --- a/doc/code/renderer/level1.md +++ b/doc/code/renderer/level1.md @@ -6,7 +6,7 @@ Low-level renderer for communicating with the OpenGL and Vulkan APIs. 1. [Overview](#overview) 2. [Architecture](#architecture) -3. [Basic Usage](#basic--usage) +3. [Basic Usage](#basic-usage) 1. [Window/Renderer Creation](#windowrenderer-creation) 2. [Adding a Shader Program](#adding-a-shader-program) 3. [Creating a Renderable](#creating-a-renderable) @@ -14,8 +14,9 @@ Low-level renderer for communicating with the OpenGL and Vulkan APIs. 4. [Advanced Usage](#advanced-usage) 1. [Addressing Uniforms via numeric IDs](#addressing-uniforms-via-numeric-ids) 2. [Framebuffers / Multiple Render Passes](#framebuffers--multiple-render-passes) - 3. [Complex Geometry](#complex-geometry) - 4. [Uniform Buffers](#uniform-buffers) + 3. [Defining Layers in a Render Pass](#defining-layers-in-a-render-pass) + 4. [Complex Geometry](#complex-geometry) + 5. [Uniform Buffers](#uniform-buffers) 5. [Thread-safety](#thread-safety) @@ -44,7 +45,7 @@ The `resources` namespace provides classes for initializing and loading meshes, These classes are independent from the specific OpenGL/Vulkan backends and can thus be passed to the abstract interface of the renderer renderer to make them usable with graphics hardware. -## Basic Usage +## Basic Usage Code examples can be found in the [renderer demos](/libopenage/renderer/demo/). See the [testing docs](/doc/code/testing.md#python-demos) on how to try them out. @@ -200,9 +201,11 @@ Finally, we can execute the rendering pipeline for all objects in the render pas renderer->render(pass); ``` + After rendering is finished, the window has to be updated to display the rendered result. @@ -312,6 +315,69 @@ Renderable obj { obj.depth_test = true; ``` + +### Defining Layers in a Render Pass + +Layers give more fine-grained control over the draw order of renderables in a render pass. Every +layer has a priority that determines when associated renderables are drawn. Lower priority +renderables are drawn earlier, higher priority renderables are drawn later. + +In comparison to using multiple render passes, layers do not require the (expensive) switching +of framebuffers between passes. The tradeoff is a slight overhead when inserting new renderables +into the render pass. + +To assign renderables to a layer, we have to specify the priority in the `RenderPass::add_renderables(..)` +function call. + +```c++ +Renderable obj { + input, + geom +}; +pass->add_renderables({ obj }, 42); +``` + +For existing layers, new renderables are always appended to the end of the layer. Renderables +are sorted into the correct position automatically when they are added: + +```c++ +pass->add_renderables({ obj1, obj2, obj3 }, 42); +pass->add_renderables({ obj4 }, 0); +pass->add_renderables({ obj5, obj6 }, 1337); +pass->add_renderables({ obj7 }, 0); +// draw order: obj4, obj7, obj1, obj2, obj3, obj5, obj6 +// layers: prio 0 | prio 42 | prio 1337 +``` + +When no priority is specified when calling `RenderPass::add_renderables(..)`, the highest +priority is assumed (which is `std::numeric_limits::max()`). Therefore, +objects added like this are always drawn last. It also means that these two calls are equal: + +```c++ +pass->add_renderables({ obj }); +pass->add_renderables({ obj }, std::numeric_limits::max()); +``` + + +Layers are created lazily during insertion if no layer with the specified priority exists yet. +We can also create layers explicitly for a specific priority: + +```c++ +pass->add_layer(42); +``` + +When executing the rendering pipeline for a specific pass, renderables are drawn layer by layer. +By default, the renderer clears the depth buffer when switching to a new layer. This is done +under the assumption that layers with higher priority should always draw over layers with lower +priority, even when depth tests are active. This behavior can be deactivated when explicitly +creating a layer: + +```c++ +// keep depth testing +pass->add_layer(42, false); +``` + + ### Complex Geometry For displaying complex geometry like 3D objects or non-rectangular surfaces, the renderer diff --git a/libopenage/renderer/render_pass.h b/libopenage/renderer/render_pass.h index 56473d9952..393cfd4f6a 100644 --- a/libopenage/renderer/render_pass.h +++ b/libopenage/renderer/render_pass.h @@ -17,6 +17,10 @@ class RenderTarget; /** * Defines a layer in the render pass. A layer is a slice of the renderables * that have the same priority. Each layer can have its own settings. + * + * // TODO: We could also move these settings to the render pass itself and use + * // multiple render passes to achieve the same effect. Then we might + * // not need layers at all. */ struct Layer { /// Priority of the renderables in this slice. From 52663d60c5c3bec6bfad2d34d48df38b0359f686 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 9 May 2024 01:05:10 +0200 Subject: [PATCH 286/771] renderer: Use vector of vectors for storing renderables. --- libopenage/renderer/opengl/renderer.cpp | 46 ++++-------------------- libopenage/renderer/render_pass.cpp | 48 ++++++++++--------------- libopenage/renderer/render_pass.h | 8 ++--- 3 files changed, 28 insertions(+), 74 deletions(-) diff --git a/libopenage/renderer/opengl/renderer.cpp b/libopenage/renderer/opengl/renderer.cpp index a7546cc404..48639dc368 100644 --- a/libopenage/renderer/opengl/renderer.cpp +++ b/libopenage/renderer/opengl/renderer.cpp @@ -180,20 +180,19 @@ void GlRenderer::render(const std::shared_ptr &pass) { // GlRenderer::optimize(gl_pass); // render all objects in the pass - auto &layers = gl_pass->get_layers(); - auto &renderables = gl_pass->get_renderables(); + const auto &layers = gl_pass->get_layers(); + const auto &renderables = gl_pass->get_renderables(); // Draw by layers - size_t offset = 0; - size_t next_offset = 0; - for (auto const &layer : layers) { + for (size_t i = 0; i < layers.size(); i++) { + const auto &layer = layers[i]; + const auto &objects = renderables[i]; + if (layer.clear_depth) { glClear(GL_DEPTH_BUFFER_BIT); } - next_offset = offset + layer.length; - for (size_t i = offset; i < next_offset; i++) { - const auto &obj = renderables[i]; + for (auto const &obj : objects) { if (obj.alpha_blending) { glEnable(GL_BLEND); } @@ -221,38 +220,7 @@ void GlRenderer::render(const std::shared_ptr &pass) { geom->draw(); } } - - offset = next_offset; } - - // for (auto const &obj : gl_pass->get_renderables()) { - // if (obj.alpha_blending) { - // glEnable(GL_BLEND); - // } - // else { - // glDisable(GL_BLEND); - // } - - // if (obj.depth_test) { - // glEnable(GL_DEPTH_TEST); - // } - // else { - // glDisable(GL_DEPTH_TEST); - // } - - // auto in = std::dynamic_pointer_cast(obj.uniform); - // auto program = std::static_pointer_cast(in->get_program()); - - // // this also calls program->use() - // program->update_uniforms(in); - - // // draw the geometry - // if (obj.geometry != nullptr) { - // auto geom = std::dynamic_pointer_cast(obj.geometry); - // // TODO read obj.blend + family - // geom->draw(); - // } - // } } } // namespace openage::renderer::opengl diff --git a/libopenage/renderer/render_pass.cpp b/libopenage/renderer/render_pass.cpp index efabcbf445..c925fc0a89 100644 --- a/libopenage/renderer/render_pass.cpp +++ b/libopenage/renderer/render_pass.cpp @@ -21,7 +21,7 @@ RenderPass::RenderPass(std::vector &&renderables, log::log(MSG(dbg) << "Created render pass"); } -const std::vector &RenderPass::get_renderables() const { +const std::vector> &RenderPass::get_renderables() const { return this->renderables; } @@ -43,8 +43,13 @@ void RenderPass::set_renderables(std::vector &&renderables) { } void RenderPass::add_renderables(std::vector &&renderables, int64_t priority) { - // Insertion index for the renderables - size_t renderables_index = 0; + if (priority == LAYER_PRIORITY_MAX) { + // Add the renderables to the last (default) layer + this->renderables.back().insert(this->renderables.back().end(), + std::make_move_iterator(renderables.begin()), + std::make_move_iterator(renderables.end())); + return; + } // Index of the layer where the renderables will be inserted size_t layer_index = 0; @@ -52,15 +57,6 @@ void RenderPass::add_renderables(std::vector &&renderables, int64_t // Priority of the last observed layer int64_t current_priority = LAYER_PRIORITY_MAX; - if (priority == LAYER_PRIORITY_MAX) { - // Add the renderables to the last (default) layer - this->renderables.insert(this->renderables.end(), - std::make_move_iterator(renderables.begin()), - std::make_move_iterator(renderables.end())); - this->layers.back().length += renderables.size(); - return; - } - // Find the index in renderables to insert the renderables for (size_t i = 0; i < this->layers.size(); i++) { auto &layer = this->layers.at(i); @@ -69,22 +65,18 @@ void RenderPass::add_renderables(std::vector &&renderables, int64_t // Insert the renderables directly before this layer break; } - renderables_index += layer.length; current_priority = layer.priority; layer_index = i; } - this->renderables.insert(this->renderables.begin() + renderables_index, - std::make_move_iterator(renderables.begin()), - std::make_move_iterator(renderables.end())); - if (current_priority != priority) { // Lazily add a new layer with the desired priority this->add_layer(layer_index, priority); } - // Update the length of the layer with the number of added renderables - this->layers.at(layer_index).length += renderables.size(); + this->renderables[layer_index].insert(this->renderables[layer_index].end(), + std::make_move_iterator(renderables.begin()), + std::make_move_iterator(renderables.end())); } void RenderPass::add_renderables(Renderable &&renderable, int64_t priority) { @@ -104,26 +96,22 @@ void RenderPass::add_layer(int64_t priority, bool clear_depth) { } void RenderPass::add_layer(size_t index, int64_t priority, bool clear_depth) { - this->layers.insert(this->layers.begin() + index, Layer{priority, 0, clear_depth}); + this->layers.insert(this->layers.begin() + index, Layer{priority, clear_depth}); + this->renderables.insert(this->renderables.begin() + index, std::vector{}); } void RenderPass::clear_renderables() { - // Erase all renderables - this->renderables.clear(); - // Keep layer definitions, but reset the length of each layer to 0 - for (auto &layer : this->layers) { - layer.length = 0; + for (size_t i = 0; i < this->layers.size(); i++) { + this->renderables[i].clear(); } } void RenderPass::sort(const compare_func &compare) { - size_t offset = 0; - for (auto &layer : this->layers) { - std::stable_sort(this->renderables.begin() + offset, - this->renderables.begin() + offset + layer.length, + for (size_t i = 0; i < this->layers.size(); i++) { + std::stable_sort(this->renderables[i].begin(), + this->renderables[i].end(), compare); - offset += layer.length; } } diff --git a/libopenage/renderer/render_pass.h b/libopenage/renderer/render_pass.h index 393cfd4f6a..43c99bd659 100644 --- a/libopenage/renderer/render_pass.h +++ b/libopenage/renderer/render_pass.h @@ -23,10 +23,8 @@ class RenderTarget; * // not need layers at all. */ struct Layer { - /// Priority of the renderables in this slice. + /// Priority of the renderables. int64_t priority; - /// Number of renderables in this slice. - size_t length; /// Whether to clear the depth buffer before rendering this layer. bool clear_depth = true; }; @@ -44,7 +42,7 @@ class RenderPass { * * @return Renderables of the render pass. */ - const std::vector &get_renderables() const; + const std::vector> &get_renderables() const; /** * Get the layers of the render pass. @@ -131,7 +129,7 @@ class RenderPass { * * Kept sorted by layer priorities (lowest to highest priority). */ - std::vector renderables; + std::vector> renderables; private: /** From 8b0a96e3f78e8634af56a5bd827892448829d6f1 Mon Sep 17 00:00:00 2001 From: NACAMURA Mitsuhiro Date: Sat, 11 May 2024 18:35:36 +0900 Subject: [PATCH 287/771] docs: update NixOS wiki link --- doc/build_instructions/nix.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/build_instructions/nix.md b/doc/build_instructions/nix.md index 46ff37b2d2..e73497ebd6 100644 --- a/doc/build_instructions/nix.md +++ b/doc/build_instructions/nix.md @@ -1,13 +1,13 @@ # Building and developing on Nix based systems -The openage repository is a [nix flake](https://nixos.wiki/wiki/Flakes) that +The openage repository is a [nix flake](https://wiki.nixos.org/wiki/Flakes) that allow Nix users to easily build, install and develop openage. To build openage using nix 1. make sure you have nix with flakes enabled, either by permanent system configuration or by using command line flags, as described on - [the wiki](https://nixos.wiki/wiki/Flakes); + [the wiki](https://wiki.nixos.org/wiki/Flakes); 2. clone this repository and `cd` into it; 3. run `nix build .#openage` to start the build process; 4. the built artifact will be in `./result`. From 36714ee3606bd66b11a91118046f52ad764d5480 Mon Sep 17 00:00:00 2001 From: Christoph Heine Date: Sat, 18 May 2024 00:12:57 +0200 Subject: [PATCH 288/771] main: Add several default DLL search paths for Python. --- openage/__main__.py | 30 +++++------- openage/util/CMakeLists.txt | 1 + openage/util/dll.py | 95 +++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 openage/util/dll.py diff --git a/openage/__main__.py b/openage/__main__.py index 4475f46518..c133ed05cb 100644 --- a/openage/__main__.py +++ b/openage/__main__.py @@ -31,27 +31,15 @@ def print_version(): sys.exit(0) -def add_dll_search_paths(dll_paths): +def add_dll_search_paths(dll_paths: list[str]): """ This function adds DLL search paths. - This function does nothing if current OS is not Windows. """ + from .util.dll import DllDirectoryManager - def close_windows_dll_path_handles(dll_path_handles): - """ - This function calls close() method on each of the handles. - """ - for handle in dll_path_handles: - handle.close() + manager = DllDirectoryManager(dll_paths) - if sys.platform != 'win32' or dll_paths is None: - return - - import atexit - win_dll_path_handles = [] - for addtional_path in dll_paths: - win_dll_path_handles.append(os.add_dll_directory(addtional_path)) - atexit.register(close_windows_dll_path_handles, win_dll_path_handles) + return manager def main(argv=None): @@ -62,11 +50,11 @@ def main(argv=None): ) if sys.platform == 'win32': - import inspect + from .util.dll import default_paths cli.add_argument( "--add-dll-search-path", action='append', dest='dll_paths', # use path of current openage executable as default - default=[os.path.dirname(os.path.abspath(inspect.getsourcefile(lambda: 0)))], + default=default_paths(), help="(Windows only) provide additional DLL search path") cli.add_argument("--version", "-V", action='store_true', dest='print_version', @@ -142,7 +130,11 @@ def main(argv=None): args = cli.parse_args(argv) if sys.platform == 'win32': - add_dll_search_paths(args.dll_paths) + args.dll_manager = add_dll_search_paths(args.dll_paths) + args.dll_manager.add_directories() + + else: + args.dll_manager = None if args.print_version: print_version() diff --git a/openage/util/CMakeLists.txt b/openage/util/CMakeLists.txt index 3d58384dee..79e7309cb3 100644 --- a/openage/util/CMakeLists.txt +++ b/openage/util/CMakeLists.txt @@ -3,6 +3,7 @@ add_py_modules( bytequeue.py context.py decorators.py + dll.py files.py fsprinting.py hash.py diff --git a/openage/util/dll.py b/openage/util/dll.py new file mode 100644 index 0000000000..bc06e88dd9 --- /dev/null +++ b/openage/util/dll.py @@ -0,0 +1,95 @@ +# Copyright 2024-2024 the openage authors. See copying.md for legal info. + +""" +Windows-specific loading of compiled Python modules and DLLs. +""" + +import inspect +import os +import sys + +# python.dll location +DEFAULT_PYTHON_DLL_DIR = os.path.dirname(sys.executable) + +# openage.dll locations (relative to this file) +DEFAULT_OPENAGE_DLL_DIRs = [ + "../../libopenage/RelWithDebInfo", +] + +# nyan.dll locations (relative to this file) +DEFAULT_NYAN_DLL_DIRS = [ + "../../../../nyan/build/nyan/RelWithDebInfo", + "../../nyan-external/bin/nyan/RelWithDebInfo", +] + + +class DllDirectoryManager: + """ + Manages directories that should be added to/removed from Python's DLL search path. + + All dependent DLLs or compiled cython modules that are not in Python's default search path + mst be added manually at runtime. Basically, this applies to all openage-specific libraries. + """ + + def __init__(self, directory_paths: list[str]): + """ + Create a new DLL directory manager. + + :param directory_paths: Absolute paths to the directories that are added. + """ + # Directory paths + self.directories = directory_paths + + # Store handles for added directories + self.handles = [] + + def add_directories(self): + """ + Add the manager's directories to Python's DLL search path. + """ + for directory in self.directories: + handle = os.add_dll_directory(directory) + self.handles.append(handle) + + def remove_directories(self): + """ + Remove the manager's directories from Python's DLL search path. + """ + for handle in self.handles: + handle.close() + + self.handles = [] + + def __del__(self): + """ + Ensure that DLL paths are removed when the object is deleted. + """ + self.remove_directories() + + def __enter__(self): + self.add_directories() + + def __exit__(self, exc_type, exc_value, traceback): + self.remove_directories() + + +def default_paths() -> list[str]: + """ + Create a list of default paths. + """ + directory_paths = [] + + # Add Python DLL search path + directory_paths.append(DEFAULT_PYTHON_DLL_DIR) + + file_dir = os.path.dirname(os.path.abspath(inspect.getsourcefile(lambda: 0))) + + # Add openage DLL search paths + for path in DEFAULT_OPENAGE_DLL_DIRs: + directory_paths.append(os.path.join(file_dir, path)) + + # Add nyan DLL search paths + for path in DEFAULT_NYAN_DLL_DIRS: + directory_paths.append(os.path.join(file_dir, path)) + + return directory_paths From 42d28490d2e96530bcc62d3b8f6095d89bdd957e Mon Sep 17 00:00:00 2001 From: Christoph Heine Date: Sat, 18 May 2024 00:43:24 +0200 Subject: [PATCH 289/771] util: Do not pickle DLL directory handles. --- openage/util/dll.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openage/util/dll.py b/openage/util/dll.py index bc06e88dd9..bfc365c781 100644 --- a/openage/util/dll.py +++ b/openage/util/dll.py @@ -67,11 +67,25 @@ def __del__(self): self.remove_directories() def __enter__(self): + """ + Enter a context guard. + """ self.add_directories() def __exit__(self, exc_type, exc_value, traceback): + """ + Exit a context guard. + """ self.remove_directories() + def __getstate__(self): + """ + Change pickling behavior so that directory handles are not serialized. + """ + content = self.__dict__ + content["handles"] = [] + return content + def default_paths() -> list[str]: """ From 5774f9f549248c2fd019587403b45d84e6d0d511 Mon Sep 17 00:00:00 2001 From: Christoph Heine Date: Sat, 18 May 2024 00:43:48 +0200 Subject: [PATCH 290/771] convert: Load DLLs in subprocesses. --- .../processor/export/media_exporter.py | 69 +++++++++---------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/openage/convert/processor/export/media_exporter.py b/openage/convert/processor/export/media_exporter.py index 966ebe8da6..ace7e1f83d 100644 --- a/openage/convert/processor/export/media_exporter.py +++ b/openage/convert/processor/export/media_exporter.py @@ -11,6 +11,7 @@ import os import multiprocessing import queue +import sys from openage.convert.entity_object.export.texture import Texture from openage.convert.service import debug_info @@ -27,6 +28,7 @@ from openage.convert.value_object.read.media.colortable import ColorTable from openage.convert.value_object.init.game_version import GameVersion from openage.util.fslike.path import Path + from openage.util.dll import DllDirectoryManager class MediaExporter: @@ -126,7 +128,8 @@ def export( handle_outqueue_func, itargs, kwargs, - args.jobs + args.jobs, + args.dll_manager, ) if args.debug_info > 5: @@ -198,6 +201,7 @@ def _export_singlethreaded( idx, source_data, single_queue, + None, request.source_filename, target_path, *itargs, @@ -225,7 +229,8 @@ def _export_multithreaded( handle_outqueue_func: typing.Callable | None, itargs: tuple, kwargs: dict, - job_count: int = None + job_count: int = None, + dll_manager: DllDirectoryManager = None, ): """ Export media files in multiple threads. @@ -241,15 +246,7 @@ def _export_multithreaded( :param itargs: Arguments for the export function. :param kwargs: Keyword arguments for the export function. :param job_count: Number of worker processes to use. - :type requests: list[MediaExportRequest] - :type sourcedir: Path - :type exportdir: Path - :type read_data_func: typing.Callable - :type export_func: typing.Callable - :type handle_outqueue_func: typing.Callable - :type itargs: tuple - :type kwargs: dict - :type job_count: int + :param dll_manager: Adds DLL search paths for the subrocesses (Windows-only). """ worker_count = job_count if worker_count is None: @@ -297,6 +294,7 @@ def error_callback(exception: Exception): idx, source_data, outqueue, + dll_manager, request.source_filename, target_path, *itargs @@ -625,6 +623,7 @@ def _export_blend( request_id: int, blendfile_data: bytes, outqueue: multiprocessing.Queue, + dll_manager: DllDirectoryManager, source_filename: str, # pylint: disable=unused-argument targetdir: Path, target_filename: str, @@ -633,15 +632,16 @@ def _export_blend( """ Convert and export a blending mode. + :param request_id: ID of the export request. :param blendfile_data: Raw file data of the blending mask. :param outqueue: Queue for passing metadata to the main process. + :param dll_manager: Adds DLL search paths for the subrocesses (Windows-only). :param target_path: Path to the resulting image file. :param blend_mode_count: Number of blending modes extracted from the source file. - :type blendfile_data: bytes - :type outqueue: multiprocessing.Queue - :type target_path: openage.util.fslike.path.Path - :type blend_mode_count: int """ + if sys.platform == "win32" and dll_manager is not None: + dll_manager.add_directories() + blend_data = Blendomatic(blendfile_data, blend_mode_count) from .texture_merge import merge_frames @@ -661,6 +661,7 @@ def _export_sound( request_id: int, sound_data: bytes, outqueue: multiprocessing.Queue, + dll_manager: DllDirectoryManager, source_filename: str, # pylint: disable=unused-argument target_path: Path, **kwargs # pylint: disable=unused-argument @@ -668,13 +669,15 @@ def _export_sound( """ Convert and export a sound file. + :param request_id: ID of the export request. :param sound_data: Raw file data of the sound file. :param outqueue: Queue for passing metadata to the main process. + :param dll_manager: Adds DLL search paths for the subrocesses (Windows-only). :param target_path: Path to the resulting sound file. - :type sound_data: bytes - :type outqueue: multiprocessing.Queue - :type target_path: openage.util.fslike.path.Path """ + if sys.platform == "win32" and dll_manager is not None: + dll_manager.add_directories() + from ...service.export.opus.opusenc import encode encoded = encode(sound_data) @@ -691,6 +694,7 @@ def _export_terrain( request_id: int, graphics_data: bytes, outqueue: multiprocessing.Queue, + dll_manager: DllDirectoryManager, source_filename: str, target_path: Path, palettes: dict[int, ColorTable], @@ -700,21 +704,19 @@ def _export_terrain( """ Convert and export a terrain graphics file. + :param request_id: ID of the export request. :param graphics_data: Raw file data of the graphics file. :param outqueue: Queue for passing the image metadata to the main process. + :param dll_manager: Adds DLL search paths for the subrocesses (Windows-only). :param source_filename: Filename of the source file. :param target_path: Path to the resulting image file. :param palettes: Palettes used by the game. :param compression_level: PNG compression level for the resulting image file. :param game_version: Game edition and expansion info. - :type graphics_data: bytes - :type outqueue: multiprocessing.Queue - :type source_filename: str - :type target_path: openage.util.fslike.path.Path - :type palettes: dict - :type compression_level: int - :type game_version: GameVersion """ + if sys.platform == "win32" and dll_manager is not None: + dll_manager.add_directories() + file_ext = source_filename.split('.')[-1].lower() if file_ext == "slp": from ...value_object.read.media.slp import SLP @@ -758,6 +760,7 @@ def _export_texture( request_id: int, graphics_data: bytes, outqueue: multiprocessing.Queue, + dll_manager: DllDirectoryManager, source_filename: str, target_path: Path, palettes: dict[int, ColorTable], @@ -770,20 +773,16 @@ def _export_texture( :param request_id: ID of the export request. :param graphics_data: Raw file data of the graphics file. :param outqueue: Queue for passing the image metadata to the main process. + :param dll_manager: Adds DLL search paths for the subrocesses (Windows-only). :param source_filename: Filename of the source file. :param target_path: Path to the resulting image file. :param palettes: Palettes used by the game. :param compression_level: PNG compression level for the resulting image file. :param cache_info: Media cache information with compression parameters from a previous run. - :type request_id: int - :type graphics_data: bytes - :type outqueue: multiprocessing.Queue - :type source_filename: str - :type target_path: openage.util.fslike.path.Path - :type palettes: dict - :type compression_level: int - :type cache_info: tuple """ + if sys.platform == "win32" and dll_manager is not None: + dll_manager.add_directories() + file_ext = source_filename.split('.')[-1].lower() if file_ext == "slp": from ...value_object.read.media.slp import SLP @@ -844,10 +843,6 @@ def _save_png( :param target_path: Path to the resulting image file. :param compression_level: PNG compression level used for the resulting image file. :param dry_run: If True, create the PNG but don't save it as a file. - :type texture: Texture - :type target_path: openage.util.fslike.path.Path - :type compression_level: int - :type dry_run: bool """ from ...service.export.png import png_create From 50522776b53a61f8a64e8cbc2a97dc87df354488 Mon Sep 17 00:00:00 2001 From: Christoph Heine Date: Sat, 18 May 2024 00:48:08 +0200 Subject: [PATCH 291/771] convert: Add default DLL paths to singlefile converter, --- openage/convert/tool/singlefile.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openage/convert/tool/singlefile.py b/openage/convert/tool/singlefile.py index 78b67380b0..9d7f1b4583 100644 --- a/openage/convert/tool/singlefile.py +++ b/openage/convert/tool/singlefile.py @@ -1,10 +1,11 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. """ Convert a single slp/wav file from some drs archive to a png/opus file. """ from __future__ import annotations +import sys from pathlib import Path @@ -59,6 +60,11 @@ def main(args, error): file_path = Path(args.filename) file_extension = file_path.suffix[1:].lower() + if sys.platform == "win32": + from openage.util.dll import DllDirectoryManager, default_paths + dll_manager = DllDirectoryManager(default_paths()) + dll_manager.add_directories() + if not (args.mode in ("sld", "drs-wav", "wav") or file_extension in ("sld", "wav")): if not args.palettes_path: raise RuntimeError("palettes-path needs to be specified for " From 1ad51ba4c1579e0b4fb5206845626b4cea6ee667 Mon Sep 17 00:00:00 2001 From: Christoph Heine Date: Sat, 18 May 2024 02:36:19 +0200 Subject: [PATCH 292/771] cli: Store DLL manager for fallback subparser. --- openage/__main__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openage/__main__.py b/openage/__main__.py index c133ed05cb..970643bb51 100644 --- a/openage/__main__.py +++ b/openage/__main__.py @@ -129,12 +129,10 @@ def main(argv=None): args = cli.parse_args(argv) + dll_manager = None if sys.platform == 'win32': - args.dll_manager = add_dll_search_paths(args.dll_paths) - args.dll_manager.add_directories() - - else: - args.dll_manager = None + dll_manager = add_dll_search_paths(args.dll_paths) + dll_manager.add_directories() if args.print_version: print_version() @@ -143,6 +141,8 @@ def main(argv=None): # the user didn't specify a subcommand. default to 'main'. args = main_cli.parse_args(argv) + args.dll_manager = dll_manager + # process the shared args set_loglevel(verbosity_to_level(args.verbose - args.quiet)) From 74457691107656d52097dc66272604c8f43665cc Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 18 May 2024 22:20:46 +0200 Subject: [PATCH 293/771] util: Check if paths exist before adding them as defaults. --- openage/util/dll.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openage/util/dll.py b/openage/util/dll.py index bfc365c781..748e503bf0 100644 --- a/openage/util/dll.py +++ b/openage/util/dll.py @@ -99,11 +99,15 @@ def default_paths() -> list[str]: file_dir = os.path.dirname(os.path.abspath(inspect.getsourcefile(lambda: 0))) # Add openage DLL search paths - for path in DEFAULT_OPENAGE_DLL_DIRs: - directory_paths.append(os.path.join(file_dir, path)) + for candidate in DEFAULT_OPENAGE_DLL_DIRs: + path = os.path.join(file_dir, candidate) + if os.path.exists(path): + directory_paths.append(path) # Add nyan DLL search paths - for path in DEFAULT_NYAN_DLL_DIRS: - directory_paths.append(os.path.join(file_dir, path)) + for candidate in DEFAULT_NYAN_DLL_DIRS: + path = os.path.join(file_dir, candidate) + if os.path.exists(path): + directory_paths.append(path) return directory_paths From e5311c1f9b917810c1d3e5835e64ac1526198734 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 18 May 2024 22:23:11 +0200 Subject: [PATCH 294/771] util: Add more path locations for different build types. --- openage/util/dll.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openage/util/dll.py b/openage/util/dll.py index 748e503bf0..a3d23be7f1 100644 --- a/openage/util/dll.py +++ b/openage/util/dll.py @@ -13,13 +13,22 @@ # openage.dll locations (relative to this file) DEFAULT_OPENAGE_DLL_DIRs = [ + "../../libopenage/Debug", + "../../libopenage/Release", "../../libopenage/RelWithDebInfo", + "../../libopenage/MinSizeRel", ] # nyan.dll locations (relative to this file) DEFAULT_NYAN_DLL_DIRS = [ + "../../../../nyan/build/nyan/Debug", + "../../../../nyan/build/nyan/Release", "../../../../nyan/build/nyan/RelWithDebInfo", + "../../../../nyan/build/nyan/MinSizeRel", + "../../nyan-external/bin/nyan/Debug", + "../../nyan-external/bin/nyan/Release", "../../nyan-external/bin/nyan/RelWithDebInfo", + "../../nyan-external/bin/nyan/MinSizeRel", ] From 0b3a0e600a6ae8bf481b3fa0874ab3d70c942f86 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 27 Jan 2024 22:29:37 +0100 Subject: [PATCH 295/771] path: Initial cost field implementation. --- libopenage/pathfinding/CMakeLists.txt | 2 + libopenage/pathfinding/cost_field.cpp | 36 ++++++++++++++ libopenage/pathfinding/cost_field.h | 69 +++++++++++++++++++++++++++ libopenage/pathfinding/types.cpp | 10 ++++ libopenage/pathfinding/types.h | 29 +++++++++++ 5 files changed, 146 insertions(+) create mode 100644 libopenage/pathfinding/cost_field.cpp create mode 100644 libopenage/pathfinding/cost_field.h create mode 100644 libopenage/pathfinding/types.cpp create mode 100644 libopenage/pathfinding/types.h diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index ad828995c9..77ff7ce051 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -1,6 +1,8 @@ add_sources(libopenage a_star.cpp + cost_field.cpp heuristics.cpp path.cpp tests.cpp + types.cpp ) diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp new file mode 100644 index 0000000000..976c55b3c1 --- /dev/null +++ b/libopenage/pathfinding/cost_field.cpp @@ -0,0 +1,36 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "cost_field.h" + +#include "error/error.h" + + +namespace openage::path { + +CostField::CostField(size_t size) : + size{size}, + cells(this->size * this->size, 1) { +} + +cost_t CostField::get_cost(size_t x, size_t y) const { + return this->cells[x + y * this->size]; +} + +void CostField::set_cost(size_t x, size_t y, cost_t cost) { + this->cells[x + y * this->size] = cost; +} + +const std::vector &CostField::get_costs() const { + return this->cells; +} + +void CostField::set_costs(std::vector &&cells) { + ENSURE(cells.size() == this->cells.size(), + "cells vector has wrong size: " << cells.size() + << "; expected: " + << this->cells.size()); + + this->cells = std::move(cells); +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h new file mode 100644 index 0000000000..1231517bdd --- /dev/null +++ b/libopenage/pathfinding/cost_field.h @@ -0,0 +1,69 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "pathfinding/types.h" + + +namespace openage::path { + +/** + * Cost field in the flow-field pathfinding algorithm. + */ +class CostField { +public: + /** + * Create a square cost field with a specified size. + * + * @param size Length of one side of the square field. + */ + CostField(size_t size); + + /** + * Get the cost at a specified position. + * + * @param x X coordinate. + * @param y Y coordinate. + * @return Cost at the specified position. + */ + cost_t get_cost(size_t x, size_t y) const; + + /** + * Set the cost at a specified position. + * + * @param x X coordinate. + * @param y Y coordinate. + * @param cost Cost to set. + */ + void set_cost(size_t x, size_t y, cost_t cost); + + /** + * Get the cost field values. + * + * @return Cost field values. + */ + const std::vector &get_costs() const; + + /** + * Set the cost field values. + * + * @param cells Cost field values. + */ + void set_costs(std::vector &&cells); + +private: + /** + * Length of one side of the square cost field. + */ + size_t size; + + /** + * Cost field values. + */ + std::vector cells; +}; + +} // namespace openage::path diff --git a/libopenage/pathfinding/types.cpp b/libopenage/pathfinding/types.cpp new file mode 100644 index 0000000000..6428dd7ded --- /dev/null +++ b/libopenage/pathfinding/types.cpp @@ -0,0 +1,10 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "types.h" + + +namespace openage::path { + +// this file is intentionally empty + +} // namespace openage::path diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h new file mode 100644 index 0000000000..cfc23941ef --- /dev/null +++ b/libopenage/pathfinding/types.h @@ -0,0 +1,29 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + + +namespace openage::path { + +/** + * Movement cost in the cost field. + * + * 0: uninitialized + * 1-254: normal cost + * 255: impassable + */ +using cost_t = uint8_t; + +/** + * Total integrated cost in the integration field. + */ +using integrate_t = uint16_t; + +/** + * Flow field direction value. + */ +using flow_dir_t = uint8_t; + +} // namespace openage::path From c4b1094d8958ce4bd3c5026b3064b04012e6c02d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 4 Feb 2024 18:11:15 +0100 Subject: [PATCH 296/771] path: Define common cost values. --- libopenage/pathfinding/CMakeLists.txt | 1 + libopenage/pathfinding/definitions.cpp | 10 +++++++ libopenage/pathfinding/definitions.h | 40 ++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 libopenage/pathfinding/definitions.cpp create mode 100644 libopenage/pathfinding/definitions.h diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index 77ff7ce051..e8337b7e59 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -1,6 +1,7 @@ add_sources(libopenage a_star.cpp cost_field.cpp + definitions.cpp heuristics.cpp path.cpp tests.cpp diff --git a/libopenage/pathfinding/definitions.cpp b/libopenage/pathfinding/definitions.cpp new file mode 100644 index 0000000000..6428dd7ded --- /dev/null +++ b/libopenage/pathfinding/definitions.cpp @@ -0,0 +1,10 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "types.h" + + +namespace openage::path { + +// this file is intentionally empty + +} // namespace openage::path diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h new file mode 100644 index 0000000000..47660a15bb --- /dev/null +++ b/libopenage/pathfinding/definitions.h @@ -0,0 +1,40 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include "pathfinding/types.h" + + +namespace openage::path { + +/** + * Init value for a cells in the cost grid. + * + * Should not be used for actual costs. + */ +constexpr cost_t COST_INIT = 0; + +/** + * Minimum possible cost for a passable cell in the cost grid. + */ +constexpr cost_t COST_MIN = 1; + +/** + * Maximum possible cost for a passable cell in the cost grid. + */ +constexpr cost_t COST_MAX = 254; + +/** + * Cost value for an impassable cell in the cost grid. + */ +constexpr cost_t COST_IMPASSABLE = 255; + + +/** + * Unreachable value for a cells in the integration grid. + */ +constexpr integrate_t INTEGRATE_UNREACHABLE = std::numeric_limits::max(); + +} // namespace openage::path From dea91a752423dfaf2a0fda25f0bcac4b16bba26a Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 4 Feb 2024 19:14:37 +0100 Subject: [PATCH 297/771] path: Initial integration field implementation. --- libopenage/pathfinding/CMakeLists.txt | 1 + libopenage/pathfinding/cost_field.cpp | 12 +- libopenage/pathfinding/cost_field.h | 15 ++ libopenage/pathfinding/definitions.h | 5 + libopenage/pathfinding/integration_field.cpp | 146 +++++++++++++++++++ libopenage/pathfinding/integration_field.h | 75 ++++++++++ 6 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 libopenage/pathfinding/integration_field.cpp create mode 100644 libopenage/pathfinding/integration_field.h diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index e8337b7e59..b40027bdaa 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -3,6 +3,7 @@ add_sources(libopenage cost_field.cpp definitions.cpp heuristics.cpp + integration_field.cpp path.cpp tests.cpp types.cpp diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index 976c55b3c1..d00f51decf 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -4,18 +4,28 @@ #include "error/error.h" +#include "pathfinding/definitions.h" + namespace openage::path { CostField::CostField(size_t size) : size{size}, - cells(this->size * this->size, 1) { + cells(this->size * this->size, COST_MIN) { +} + +size_t CostField::get_size() const { + return this->size; } cost_t CostField::get_cost(size_t x, size_t y) const { return this->cells[x + y * this->size]; } +cost_t CostField::get_cost(size_t idx) const { + return this->cells.at(idx); +} + void CostField::set_cost(size_t x, size_t y, cost_t cost) { this->cells[x + y * this->size] = cost; } diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index 1231517bdd..e9afee27ac 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -22,6 +22,13 @@ class CostField { */ CostField(size_t size); + /** + * Get the size of the cost field. + * + * @return Size of the cost field. + */ + size_t get_size() const; + /** * Get the cost at a specified position. * @@ -31,6 +38,14 @@ class CostField { */ cost_t get_cost(size_t x, size_t y) const; + /** + * Get the cost at a specified position. + * + * @param idx Index of the cell. + * @return Cost at the specified position. + */ + cost_t get_cost(size_t idx) const; + /** * Set the cost at a specified position. * diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h index 47660a15bb..f90e4c9799 100644 --- a/libopenage/pathfinding/definitions.h +++ b/libopenage/pathfinding/definitions.h @@ -32,6 +32,11 @@ constexpr cost_t COST_MAX = 254; constexpr cost_t COST_IMPASSABLE = 255; +/** + * Start value for goal cells. + */ +const integrate_t INTEGRATE_START = 0; + /** * Unreachable value for a cells in the integration grid. */ diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp new file mode 100644 index 0000000000..5657136d05 --- /dev/null +++ b/libopenage/pathfinding/integration_field.cpp @@ -0,0 +1,146 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "integration_field.h" + +#include +#include + +#include "error/error.h" + +#include "pathfinding/cost_field.h" +#include "pathfinding/definitions.h" + + +namespace openage::path { + +/** + * Update the open list for a neighbour index during integration field + * calculation (subroutine of integrate(..)). + * + * Checks whether: + * + * 1. the neighbour index has not already been found + * 2. the neighbour index is reachable + * + * If both conditions are met, the neighbour index is added to the open list. + * + * @param idx Index of the cell to update. + * @param integrate_cost Integrated cost of the cell. + * @param open_list Cells that still have to be visited. + * @param found Cells that have been found. + */ +void update_list(size_t idx, + integrate_t integrate_cost, + std::deque &open_list, + std::unordered_set &found) { + if (not found.contains(idx)) { + found.insert(idx); + + if (integrate_cost != INTEGRATE_UNREACHABLE) { + open_list.push_back(idx); + } + } +} + + +IntegrationField::IntegrationField(size_t size) : + size{size}, + cells(this->size * this->size, INTEGRATE_UNREACHABLE) { +} + +size_t IntegrationField::get_size() const { + return this->size; +} + +void IntegrationField::integrate(const std::shared_ptr &cost_field, + size_t target_x, + size_t target_y) { + ENSURE(cost_field->get_size() == this->get_size(), + "cost field size " + << cost_field->get_size() << "x" << cost_field->get_size() + << " does not match integration field size " + << this->get_size() << "x" << this->get_size()); + + // Reset the integration field + this->reset(); + + // Target cell index + auto target_idx = target_x + target_y * this->size; + + // Cells that have been found + std::unordered_set found; + found.reserve(this->size * this->size); + // Cells that still have to be visited + std::deque open_list; + + // Move outwards from the target cell, updating the integration field + this->cells[target_idx] = INTEGRATE_START; + open_list.push_back(target_idx); + while (!open_list.empty()) { + auto idx = open_list.front(); + open_list.pop_front(); + + // Get the x and y coordinates of the current cell + auto x = idx % this->size; + auto y = idx / this->size; + + auto integrated_current = this->cells[idx]; + + // Update the integration field of the 4 neighbouring cells + if (y > 0) { + auto up_idx = idx - this->size; + auto neighbor_cost = this->update_cell(up_idx, + cost_field->get_cost(up_idx), + integrated_current); + update_list(up_idx, neighbor_cost, open_list, found); + } + if (x > 0) { + auto left_idx = idx - 1; + auto neighbor_cost = this->update_cell(left_idx, + cost_field->get_cost(left_idx), + integrated_current); + update_list(left_idx, neighbor_cost, open_list, found); + } + if (y < this->size - 1) { + auto down_idx = idx + this->size; + auto neighbor_cost = this->update_cell(down_idx, + cost_field->get_cost(down_idx), + integrated_current); + update_list(down_idx, neighbor_cost, open_list, found); + } + if (x < this->size - 1) { + auto right_idx = idx + 1; + auto neighbor_cost = this->update_cell(right_idx, + cost_field->get_cost(right_idx), + integrated_current); + update_list(right_idx, neighbor_cost, open_list, found); + } + } +} + +void IntegrationField::reset() { + for (auto &cell : this->cells) { + cell = INTEGRATE_UNREACHABLE; + } +} + +integrate_t IntegrationField::update_cell(size_t idx, + cost_t cell_cost, + cost_t integrate_cost) { + ENSURE(cell_cost > COST_INIT, "cost field cell value must be non-zero"); + + // Check if the cell is impassable. + if (cell_cost == COST_IMPASSABLE) { + return INTEGRATE_UNREACHABLE; + } + + // Update the integration field value of the cell. + auto integrated = integrate_cost + cell_cost; + if (integrated < this->cells.at(idx)) { + this->cells[idx] = integrated; + } + + return integrated; +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h new file mode 100644 index 0000000000..ecb21f5e0b --- /dev/null +++ b/libopenage/pathfinding/integration_field.h @@ -0,0 +1,75 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include + +#include "pathfinding/types.h" + + +namespace openage::path { +class CostField; + +/** + * Integration field in the flow-field pathfinding algorithm. + */ +class IntegrationField { +public: + /** + * Create a square integration field with a specified size. + * + * @param size Length of one side of the square field. + */ + IntegrationField(size_t size); + + /** + * Get the size of the integration field. + * + * @return Size of the integration field. + */ + size_t get_size() const; + + /** + * Calculate the integration field for a target cell. + * + * @param cost_field Cost field to integrate. + * @param target_x X coordinate of the target cell. + * @param target_y Y coordinate of the target cell. + */ + void integrate(const std::shared_ptr &cost_field, + size_t target_x, + size_t target_y); + +private: + /** + * Reset the integration field for a new integration. + */ + void reset(); + + /** + * Update a cell in the integration field. + * + * @param idx Index of the cell that is updated. + * @param cell_cost Cost of the cell from the cost field. + * @param integrate_cost Integrated cost of the updating cell in the integration field. + * + * @return New integration value of the cell. + */ + integrate_t update_cell(size_t idx, + cost_t cell_cost, + cost_t integrate_cost); + + /** + * Length of one side of the square integration field. + */ + size_t size; + + /** + * Integration field values. + */ + std::vector cells; +}; + +} // namespace openage::path From 7d14a6adeef71fb82ef45cac3a3469df8986d464 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 9 Feb 2024 23:09:49 +0100 Subject: [PATCH 298/771] path: Flow field value types. --- libopenage/pathfinding/definitions.h | 32 ++++++++++++++++++++++++++++ libopenage/pathfinding/types.h | 8 +++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h index f90e4c9799..ea8936d809 100644 --- a/libopenage/pathfinding/definitions.h +++ b/libopenage/pathfinding/definitions.h @@ -42,4 +42,36 @@ const integrate_t INTEGRATE_START = 0; */ constexpr integrate_t INTEGRATE_UNREACHABLE = std::numeric_limits::max(); + +/** + * Flow field direction types. + * + * Encoded into the flow_t values. + */ +enum class flow_dir_t : uint8_t { + NORTH = 0x00, + NORTH_EAST = 0x01, + EAST = 0x02, + SOUTH_EAST = 0x03, + SOUTH = 0x04, + SOUTH_WEST = 0x05, + WEST = 0x06, + NORTH_WEST = 0x07, +}; + +/** + * Mask for the flow direction bits in a flow_t value. + */ +constexpr flow_t FLOW_DIR_MASK = 0x0F; + +/** + * Mask for the pathable flag in a flow_t value. + */ +constexpr flow_t FLOW_PATHABLE = 0x10; + +/** + * Mask for the line of sight flag in a flow_t value. + */ +constexpr flow_t FLOW_LOS = 0x20; + } // namespace openage::path diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index cfc23941ef..d68f8b39ca 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -22,8 +22,12 @@ using cost_t = uint8_t; using integrate_t = uint16_t; /** - * Flow field direction value. + * Flow field cell value. + * + * Bit 2: Line of sight flag. + * Bit 3: Pathable flag. + * Bits 4-7: flow direction. */ -using flow_dir_t = uint8_t; +using flow_t = uint8_t; } // namespace openage::path From c1b6890545a4c445d0defa39645f6d0015cf83ba Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 9 Feb 2024 23:10:21 +0100 Subject: [PATCH 299/771] path: Flow field computation. --- libopenage/pathfinding/CMakeLists.txt | 1 + libopenage/pathfinding/cost_field.h | 6 +- libopenage/pathfinding/flow_field.cpp | 115 +++++++++++++++++++ libopenage/pathfinding/flow_field.h | 58 ++++++++++ libopenage/pathfinding/integration_field.cpp | 4 + libopenage/pathfinding/integration_field.h | 11 +- 6 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 libopenage/pathfinding/flow_field.cpp create mode 100644 libopenage/pathfinding/flow_field.h diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index b40027bdaa..6f943971e3 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -2,6 +2,7 @@ add_sources(libopenage a_star.cpp cost_field.cpp definitions.cpp + flow_field.cpp heuristics.cpp integration_field.cpp path.cpp diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index e9afee27ac..f256af91ca 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -18,7 +18,7 @@ class CostField { /** * Create a square cost field with a specified size. * - * @param size Length of one side of the square field. + * @param size Side length of the field. */ CostField(size_t size); @@ -71,8 +71,8 @@ class CostField { private: /** - * Length of one side of the square cost field. - */ + * Side length of the field. + */ size_t size; /** diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp new file mode 100644 index 0000000000..fe67640e2f --- /dev/null +++ b/libopenage/pathfinding/flow_field.cpp @@ -0,0 +1,115 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "flow_field.h" + +#include "error/error.h" + +#include "pathfinding/integration_field.h" + + +namespace openage::path { + +FlowField::FlowField(size_t size) : + size{size}, + cells(this->size * this->size, 0) { +} + +FlowField::FlowField(const std::shared_ptr &integrate_field) : + size{integrate_field->get_size()}, + cells(this->size * this->size, 0) { + this->build(integrate_field); +} + +size_t FlowField::get_size() const { + return this->size; +} + +void FlowField::build(const std::shared_ptr &integrate_field) { + ENSURE(integrate_field->get_size() == this->get_size(), + "integration field size " + << integrate_field->get_size() << "x" << integrate_field->get_size() + << " does not match flow field size " + << this->get_size() << "x" << this->get_size()); + + auto &integrate_cells = integrate_field->get_cells(); + auto &flow_cells = this->cells; + + for (size_t y = 0; y < this->size; ++y) { + for (size_t x = 0; x < this->size; ++x) { + size_t idx = y * this->size + x; + + if (integrate_cells[idx] == INTEGRATE_UNREACHABLE) { + // Cell cannot be used as path + continue; + } + + // Find the neighbor with the smallest cost. + flow_dir_t direction; + integrate_t smallest_cost = INTEGRATE_UNREACHABLE; + if (y > 0) { + integrate_t cost = integrate_cells[idx - this->size]; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::NORTH; + } + } + if (x < this->size - 1 && y > 0) { + integrate_t cost = integrate_cells[idx - this->size + 1]; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::NORTH_EAST; + } + } + if (x < this->size - 1) { + integrate_t cost = integrate_cells[idx + 1]; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::EAST; + } + } + if (x < this->size - 1 && y < this->size - 1) { + integrate_t cost = integrate_cells[idx + this->size + 1]; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::SOUTH_EAST; + } + } + if (y < this->size - 1) { + integrate_t cost = integrate_cells[idx + this->size]; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::SOUTH; + } + } + if (x > 0 && y < this->size - 1) { + integrate_t cost = integrate_cells[idx + this->size - 1]; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::SOUTH_WEST; + } + } + if (x > 0) { + integrate_t cost = integrate_cells[idx - 1]; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::WEST; + } + } + if (x > 0 && y > 0) { + integrate_t cost = integrate_cells[idx - this->size - 1]; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::NORTH_WEST; + } + } + + // Set the flow field cell to pathable. + flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE; + + // Set the flow field cell to the direction of the smallest cost. + flow_cells[idx] = flow_cells[idx] | static_cast(direction); + } + } +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h new file mode 100644 index 0000000000..d8a1cacc78 --- /dev/null +++ b/libopenage/pathfinding/flow_field.h @@ -0,0 +1,58 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include + +#include "pathfinding/definitions.h" +#include "pathfinding/types.h" + + +namespace openage::path { +class IntegrationField; + +class FlowField { +public: + /** + * Create a square flow field with a specified size. + * + * @param size Side length of the field. + */ + FlowField(size_t size); + + /** + * Create a flow field from an existing integration field. + * + * @param integrate_field Integration field. + */ + FlowField(const std::shared_ptr &integrate_field); + + /** + * Get the size of the flow field. + * + * @return Size of the flow field. + */ + size_t get_size() const; + + /** + * Build the flow field. + * + * @param integrate_field Integration field. + */ + void build(const std::shared_ptr &integrate_field); + +private: + /** + * Side length of the field. + */ + size_t size; + + /** + * Flow field cells. + */ + std::vector cells; +}; + +} // namespace openage::path diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 5657136d05..4e6d59b50d 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -118,6 +118,10 @@ void IntegrationField::integrate(const std::shared_ptr &cost_field, } } +const std::vector &IntegrationField::get_cells() const { + return this->cells; +} + void IntegrationField::reset() { for (auto &cell : this->cells) { cell = INTEGRATE_UNREACHABLE; diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index ecb21f5e0b..333b47f528 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -20,7 +20,7 @@ class IntegrationField { /** * Create a square integration field with a specified size. * - * @param size Length of one side of the square field. + * @param size Side length of the field. */ IntegrationField(size_t size); @@ -42,6 +42,13 @@ class IntegrationField { size_t target_x, size_t target_y); + /** + * Get the integration field values. + * + * @return Integration field values. + */ + const std::vector &get_cells() const; + private: /** * Reset the integration field for a new integration. @@ -62,7 +69,7 @@ class IntegrationField { cost_t integrate_cost); /** - * Length of one side of the square integration field. + * Side length of the field. */ size_t size; From a58b631d4bf81b121f1e3d7d5f38c771c4f1f075 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 9 Feb 2024 23:35:24 +0100 Subject: [PATCH 300/771] path: Integrator implementation. --- libopenage/pathfinding/CMakeLists.txt | 1 + libopenage/pathfinding/integrator.cpp | 26 ++++++++++++++++ libopenage/pathfinding/integrator.h | 45 +++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 libopenage/pathfinding/integrator.cpp create mode 100644 libopenage/pathfinding/integrator.h diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index 6f943971e3..0a080e694e 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -5,6 +5,7 @@ add_sources(libopenage flow_field.cpp heuristics.cpp integration_field.cpp + integrator.cpp path.cpp tests.cpp types.cpp diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp new file mode 100644 index 0000000000..19cd0c408f --- /dev/null +++ b/libopenage/pathfinding/integrator.cpp @@ -0,0 +1,26 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "integrator.h" + +#include "pathfinding/cost_field.h" +#include "pathfinding/flow_field.h" +#include "pathfinding/integration_field.h" + + +namespace openage::path { + +void Integrator::set_cost_field(const std::shared_ptr &cost_field) { + this->cost_field = cost_field; +} + +std::shared_ptr Integrator::build(size_t target_x, size_t target_y) { + auto flow_field = std::make_shared(this->cost_field->get_size()); + auto integrate_field = std::make_shared(this->cost_field->get_size()); + + integrate_field->integrate(this->cost_field, target_x, target_y); + flow_field->build(integrate_field); + + return flow_field; +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h new file mode 100644 index 0000000000..2d2338982d --- /dev/null +++ b/libopenage/pathfinding/integrator.h @@ -0,0 +1,45 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + + +namespace openage::path { +class CostField; +class FlowField; + +/** + * Integrator for the flow field pathfinding algorithm. + */ +class Integrator { +public: + Integrator() = default; + ~Integrator() = default; + + /** + * Set the cost field. + * + * @param cost_field Cost field. + */ + void set_cost_field(const std::shared_ptr &cost_field); + + /** + * Build the flow field for a target cell. + * + * @param target_x X coordinate of the target cell. + * @param target_y Y coordinate of the target cell. + * + * @return Flow field. + */ + std::shared_ptr build(size_t target_x, size_t target_y); + +private: + /** + * Cost field. + */ + std::shared_ptr cost_field; +}; + +} // namespace openage::path From 5d97ce850b13945898f1b3a8746a6a4562a94580 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 9 Feb 2024 23:44:20 +0100 Subject: [PATCH 301/771] path: Rename old A* cost type to avoid conflicts. --- libopenage/pathfinding/a_star.cpp | 10 +++++----- libopenage/pathfinding/a_star.h | 4 ++-- libopenage/pathfinding/heuristics.cpp | 22 +++++++++++----------- libopenage/pathfinding/heuristics.h | 14 +++++++------- libopenage/pathfinding/path.cpp | 8 ++++---- libopenage/pathfinding/path.h | 16 ++++++++-------- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/libopenage/pathfinding/a_star.cpp b/libopenage/pathfinding/a_star.cpp index f80879eb91..6fc4a53c8c 100644 --- a/libopenage/pathfinding/a_star.cpp +++ b/libopenage/pathfinding/a_star.cpp @@ -1,4 +1,4 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. /** @file * @@ -31,7 +31,7 @@ Path to_point(coord::phys3 start, auto valid_end = [&](const coord::phys3 &point) -> bool { return euclidean_squared_cost(point, end) < path_grid_size.to_float(); }; - auto heuristic = [&](const coord::phys3 &point) -> cost_t { + auto heuristic = [&](const coord::phys3 &point) -> cost_old_t { return euclidean_cost(point, end); }; return a_star(start, valid_end, heuristic, passable); @@ -41,13 +41,13 @@ Path find_nearest(coord::phys3 start, std::function valid_end, std::function passable) { // Use Dijkstra (heuristic = 0) - auto zero = [](const coord::phys3 &) -> cost_t { return .0f; }; + auto zero = [](const coord::phys3 &) -> cost_old_t { return .0f; }; return a_star(start, valid_end, zero, passable); } Path a_star(coord::phys3 start, std::function valid_end, - std::function heuristic, + std::function heuristic, std::function passable) { // path node storage, always provides cheapest next node. heap_t node_candidates; @@ -94,7 +94,7 @@ Path a_star(coord::phys3 start, } bool not_visited = (visited_tiles.count(neighbor->position) == 0); - cost_t new_past_cost = best_candidate->past_cost + best_candidate->cost_to(*neighbor); + cost_old_t new_past_cost = best_candidate->past_cost + best_candidate->cost_to(*neighbor); // if new cost is better than the previous path if (not_visited or new_past_cost < neighbor->past_cost) { diff --git a/libopenage/pathfinding/a_star.h b/libopenage/pathfinding/a_star.h index 23d3e76747..24e09fee2e 100644 --- a/libopenage/pathfinding/a_star.h +++ b/libopenage/pathfinding/a_star.h @@ -1,4 +1,4 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -35,7 +35,7 @@ Path find_nearest(coord::phys3 start, */ Path a_star(coord::phys3 start, std::function valid_end, - std::function heuristic, + std::function heuristic, std::function passable); } // namespace path diff --git a/libopenage/pathfinding/heuristics.cpp b/libopenage/pathfinding/heuristics.cpp index 9a1a1213dc..51a24618a0 100644 --- a/libopenage/pathfinding/heuristics.cpp +++ b/libopenage/pathfinding/heuristics.cpp @@ -1,4 +1,4 @@ -// Copyright 2014-2018 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #include "heuristics.h" @@ -9,25 +9,25 @@ namespace openage { namespace path { -cost_t manhattan_cost(const coord::phys3 &start, const coord::phys3 &end) { - cost_t dx = std::abs(start.ne - end.ne).to_float(); - cost_t dy = std::abs(start.se - end.se).to_float(); +cost_old_t manhattan_cost(const coord::phys3 &start, const coord::phys3 &end) { + cost_old_t dx = std::abs(start.ne - end.ne).to_float(); + cost_old_t dy = std::abs(start.se - end.se).to_float(); return dx + dy; } -cost_t chebyshev_cost(const coord::phys3 &start, const coord::phys3 &end) { - cost_t dx = std::abs(start.ne - end.ne).to_float(); - cost_t dy = std::abs(start.se - end.se).to_float(); +cost_old_t chebyshev_cost(const coord::phys3 &start, const coord::phys3 &end) { + cost_old_t dx = std::abs(start.ne - end.ne).to_float(); + cost_old_t dy = std::abs(start.se - end.se).to_float(); return std::max(dx, dy); } -cost_t euclidean_cost(const coord::phys3 &start, const coord::phys3 &end) { +cost_old_t euclidean_cost(const coord::phys3 &start, const coord::phys3 &end) { return (end - start).length(); } -cost_t euclidean_squared_cost(const coord::phys3 &start, const coord::phys3 &end) { - cost_t dx = (start.ne - end.ne).to_float(); - cost_t dy = (start.se - end.se).to_float(); +cost_old_t euclidean_squared_cost(const coord::phys3 &start, const coord::phys3 &end) { + cost_old_t dx = (start.ne - end.ne).to_float(); + cost_old_t dy = (start.se - end.se).to_float(); return dx * dx + dy * dy; } diff --git a/libopenage/pathfinding/heuristics.h b/libopenage/pathfinding/heuristics.h index 7755f1288a..9d84560f4c 100644 --- a/libopenage/pathfinding/heuristics.h +++ b/libopenage/pathfinding/heuristics.h @@ -1,4 +1,4 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -10,36 +10,36 @@ namespace path { /** * function pointer type for distance estimation functions. */ -using heuristic_t = cost_t (*)(const coord::phys3 &start, const coord::phys3 &end); +using heuristic_t = cost_old_t (*)(const coord::phys3 &start, const coord::phys3 &end); /** * Manhattan distance cost estimation. * @returns the sum of the x and y difference. */ -cost_t manhattan_cost(const coord::phys3 &start, const coord::phys3 &end); +cost_old_t manhattan_cost(const coord::phys3 &start, const coord::phys3 &end); /** * Chebyshev distance cost estimation. * @returns y or x difference, whichever is higher. */ -cost_t chebyshev_cost(const coord::phys3 &start, const coord::phys3 &end); +cost_old_t chebyshev_cost(const coord::phys3 &start, const coord::phys3 &end); /** * Euclidean distance cost estimation. * @returns the hypotenuse length of the rectangular triangle formed. */ -cost_t euclidean_cost(const coord::phys3 &start, const coord::phys3 &end); +cost_old_t euclidean_cost(const coord::phys3 &start, const coord::phys3 &end); /** * Squared euclidean distance cost estimation. * @returns the square of the hypotenuse length of the rectangular triangle formed. */ -cost_t euclidean_squared_cost(const coord::phys3 &start, const coord::phys3 &end); +cost_old_t euclidean_squared_cost(const coord::phys3 &start, const coord::phys3 &end); /** * Calculate euclidean distance from a already calculated squared euclidean distance */ -cost_t euclidean_squared_to_euclidean_cost(const cost_t euclidean_squared_value); +cost_old_t euclidean_squared_to_euclidean_cost(const cost_old_t euclidean_squared_value); } // namespace path } // namespace openage diff --git a/libopenage/pathfinding/path.cpp b/libopenage/pathfinding/path.cpp index f1da51af78..a709ab0648 100644 --- a/libopenage/pathfinding/path.cpp +++ b/libopenage/pathfinding/path.cpp @@ -1,4 +1,4 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #include @@ -26,13 +26,13 @@ Node::Node(const coord::phys3 &pos, node_pt prev) : this->direction = (this->position - prev->position).normalize(); // TODO: add dot product to coord - cost_t similarity = ((this->direction.ne.to_float() * prev->direction.ne.to_float()) + (this->direction.se.to_float() * prev->direction.se.to_float())); + cost_old_t similarity = ((this->direction.ne.to_float() * prev->direction.ne.to_float()) + (this->direction.se.to_float() * prev->direction.se.to_float())); this->factor += (1 - similarity); } } -Node::Node(const coord::phys3 &pos, node_pt prev, cost_t past, cost_t heuristic) : +Node::Node(const coord::phys3 &pos, node_pt prev, cost_old_t past, cost_old_t heuristic) : Node{pos, prev} { this->past_cost = past; this->heuristic_cost = heuristic; @@ -50,7 +50,7 @@ bool Node::operator==(const Node &other) const { } -cost_t Node::cost_to(const Node &other) const { +cost_old_t Node::cost_to(const Node &other) const { // ignore the up-position, thus convert to phys2 return ((this->position - other.position).to_phys2().length() * other.factor * this->factor); diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/path.h index e41ddbcc28..bbe7aa0f62 100644 --- a/libopenage/pathfinding/path.h +++ b/libopenage/pathfinding/path.h @@ -1,4 +1,4 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -24,7 +24,7 @@ class Path; /** * The data type for movement cost */ -using cost_t = float; +using cost_old_t = float; /** * Type for storing navigation nodes. @@ -80,7 +80,7 @@ bool passable_line(node_pt start, node_pt end, std::function { public: Node(const coord::phys3 &pos, node_pt prev); - Node(const coord::phys3 &pos, node_pt prev, cost_t past, cost_t heuristic); + Node(const coord::phys3 &pos, node_pt prev, cost_old_t past, cost_old_t heuristic); /** * Orders nodes according to their future cost value. @@ -96,7 +96,7 @@ class Node : public std::enable_shared_from_this { /** * Calculates the actual movement cose to another node. */ - cost_t cost_to(const Node &other) const; + cost_old_t cost_to(const Node &other) const; /** * Create a backtrace path beginning at this node. @@ -119,20 +119,20 @@ class Node : public std::enable_shared_from_this { /** * Future cost estimation value for this node. */ - cost_t future_cost; + cost_old_t future_cost; /** * Evaluated past cost value for the node. * This stores the actual cost from start to this node. */ - cost_t past_cost; + cost_old_t past_cost; /** * Heuristic cost cache. * Calculated once, is the heuristic distance from this node * to the goal. */ - cost_t heuristic_cost; + cost_old_t heuristic_cost; /** * Can this node be passed? @@ -155,7 +155,7 @@ class Node : public std::enable_shared_from_this { * Factor to adjust movement cost. * default: 1 */ - cost_t factor; + cost_old_t factor; /** * Node where this one was reached by least cost. From 7ddd25d992cf38d210773dc3b00c01cf3b5e36c1 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 10 Feb 2024 00:17:02 +0100 Subject: [PATCH 302/771] path: Test flow field calculations. --- libopenage/pathfinding/flow_field.cpp | 4 +++ libopenage/pathfinding/flow_field.h | 7 ++++ libopenage/pathfinding/tests.cpp | 51 ++++++++++++++++++++++++--- openage/testing/testlist.py | 3 +- 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index fe67640e2f..bd16ec924e 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -112,4 +112,8 @@ void FlowField::build(const std::shared_ptr &integrate_field) } } +const std::vector &FlowField::get_cells() const { + return this->cells; +} + } // namespace openage::path diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index d8a1cacc78..03d2806d37 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -43,6 +43,13 @@ class FlowField { */ void build(const std::shared_ptr &integrate_field); + /** + * Get the flow field values. + * + * @return Flow field values. + */ + const std::vector &get_cells() const; + private: /** * Side length of the field. diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index 35f29295e8..00304237ec 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -1,9 +1,12 @@ -// Copyright 2015-2018 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "../log/log.h" #include "../testing/testing.h" +#include "cost_field.h" +#include "flow_field.h" #include "heuristics.h" +#include "integrator.h" #include "path.h" namespace openage { @@ -110,7 +113,7 @@ void node_cost_to_0() { TESTEQUALS(n2->cost_to(*n0), 5); // Testing cost_to with both se and ne: - coord::phys3 p3{3, 4, 0}; // -> sqrt(3*3 + 4*4) == 5 + coord::phys3 p3{3, 4, 0}; // -> sqrt(3*3 + 4*4) == 5 node_pt n3 = std::make_unique(p3, nullptr); TESTEQUALS(n0->cost_to(*n3), 5); @@ -274,7 +277,8 @@ bool not_passable(const coord::phys3 &) { bool sometimes_passable(const coord::phys3 &pos) { if (pos.ne == 20) { return false; - } else { + } + else { return true; } } @@ -311,6 +315,45 @@ void path_node() { node_passable_line_0(); } +void flow_field() { + // Create initial cost grid + auto cost_field = std::make_shared(3); + + // | 1 | 1 | 1 | + // | 1 | X | 1 | + // | 1 | 1 | 1 | + cost_field->set_costs({1, 1, 1, 1, 255, 1, 1, 1, 1}); + + // Integrator for managing the flow field + auto integrator = std::make_shared(); + integrator->set_cost_field(cost_field); + + // Build the flow field + auto flow_field = integrator->build(2, 2); + auto cells = flow_field->get_cells(); + + // The directions in the flow field should look like: + // | E | SE | S | + // | SE | X | S | + // | E | E | N | + auto expected = std::vector{ + FLOW_PATHABLE | static_cast(flow_dir_t::EAST), + FLOW_PATHABLE | static_cast(flow_dir_t::SOUTH_EAST), + FLOW_PATHABLE | static_cast(flow_dir_t::SOUTH), + FLOW_PATHABLE | static_cast(flow_dir_t::SOUTH_EAST), + 0, + FLOW_PATHABLE | static_cast(flow_dir_t::SOUTH), + FLOW_PATHABLE | static_cast(flow_dir_t::EAST), + FLOW_PATHABLE | static_cast(flow_dir_t::EAST), + FLOW_PATHABLE | static_cast(flow_dir_t::NORTH), + }; + + for (size_t i = 0; i < cells.size(); i++) { + TESTEQUALS(cells[i], expected[i]); + } +} + + } // namespace tests -} // namespace pathfinding +} // namespace path } // namespace openage diff --git a/openage/testing/testlist.py b/openage/testing/testlist.py index 75a684e85e..2a6a4780fd 100644 --- a/openage/testing/testlist.py +++ b/openage/testing/testlist.py @@ -1,4 +1,4 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. """ Lists of all possible tests; enter your tests here. """ @@ -85,6 +85,7 @@ def tests_cpp(): yield "openage::datastructure::tests::pairing_heap" yield "openage::job::tests::test_job_manager" yield "openage::path::tests::path_node", "pathfinding" + yield "openage::path::tests::flow_field", "pathfinding" yield "openage::pyinterface::tests::pyobject" yield "openage::pyinterface::tests::err_py_to_cpp" yield "openage::renderer::tests::font" From a11200993ebb1ff41ac5c8486dbafab1ebb14ec5 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 10 Feb 2024 16:38:43 +0100 Subject: [PATCH 303/771] path: Test individual field types. --- libopenage/pathfinding/tests.cpp | 69 ++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index 00304237ec..6e060c4301 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -4,10 +4,13 @@ #include "../testing/testing.h" #include "cost_field.h" +#include "definitions.h" #include "flow_field.h" #include "heuristics.h" +#include "integration_field.h" #include "integrator.h" #include "path.h" +#include "types.h" namespace openage { namespace path { @@ -324,19 +327,11 @@ void flow_field() { // | 1 | 1 | 1 | cost_field->set_costs({1, 1, 1, 1, 255, 1, 1, 1, 1}); - // Integrator for managing the flow field - auto integrator = std::make_shared(); - integrator->set_cost_field(cost_field); - - // Build the flow field - auto flow_field = integrator->build(2, 2); - auto cells = flow_field->get_cells(); - - // The directions in the flow field should look like: + // The flow field for targeting (2, 2) hould look like this: // | E | SE | S | // | SE | X | S | // | E | E | N | - auto expected = std::vector{ + auto ff_expected = std::vector{ FLOW_PATHABLE | static_cast(flow_dir_t::EAST), FLOW_PATHABLE | static_cast(flow_dir_t::SOUTH_EAST), FLOW_PATHABLE | static_cast(flow_dir_t::SOUTH), @@ -348,8 +343,58 @@ void flow_field() { FLOW_PATHABLE | static_cast(flow_dir_t::NORTH), }; - for (size_t i = 0; i < cells.size(); i++) { - TESTEQUALS(cells[i], expected[i]); + // Test the different field types + { + auto integration_field = std::make_shared(3); + integration_field->integrate(cost_field, 2, 2); + auto int_cells = integration_field->get_cells(); + + // The integration field should look like: + // | 4 | 3 | 2 | + // | 3 | X | 1 | + // | 2 | 1 | 0 | + auto int_expected = std::vector{ + 4, + 3, + 2, + 3, + 65535, + 1, + 2, + 1, + 0, + }; + + // Compare the integration field cells with the expected values + for (size_t i = 0; i < int_cells.size(); i++) { + TESTEQUALS(int_cells[i], int_expected[i]); + } + + // Build the flow field + auto flow_field = std::make_shared(3); + flow_field->build(integration_field); + auto ff_cells = flow_field->get_cells(); + + // Compare the flow field cells with the expected values + for (size_t i = 0; i < ff_cells.size(); i++) { + TESTEQUALS(ff_cells[i], ff_expected[i]); + } + } + + // Integrator test + { + // Integrator for managing the flow field + auto integrator = std::make_shared(); + integrator->set_cost_field(cost_field); + + // Build the flow field + auto flow_field = integrator->build(2, 2); + auto ff_cells = flow_field->get_cells(); + + // Compare the flow field cells with the expected values + for (size_t i = 0; i < ff_cells.size(); i++) { + TESTEQUALS(ff_cells[i], ff_expected[i]); + } } } From a1449a4fef150dd8507f40972d3ac134b6cebce6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 10 Feb 2024 18:02:37 +0100 Subject: [PATCH 304/771] path: Add demo skeleton for flowfield pathfinder. --- libopenage/pathfinding/CMakeLists.txt | 2 + libopenage/pathfinding/demo/CMakeLists.txt | 8 ++++ libopenage/pathfinding/demo/demo_0.cpp | 27 ++++++++++++ libopenage/pathfinding/demo/demo_0.h | 23 ++++++++++ libopenage/pathfinding/demo/tests.cpp | 24 +++++++++++ libopenage/pathfinding/demo/tests.h | 20 +++++++++ openage/CMakeLists.txt | 1 + openage/pathfinding/CMakeLists.txt | 7 ++++ openage/pathfinding/__init__.py | 5 +++ openage/pathfinding/tests.pyx | 49 ++++++++++++++++++++++ openage/testing/testlist.py | 2 + 11 files changed, 168 insertions(+) create mode 100644 libopenage/pathfinding/demo/CMakeLists.txt create mode 100644 libopenage/pathfinding/demo/demo_0.cpp create mode 100644 libopenage/pathfinding/demo/demo_0.h create mode 100644 libopenage/pathfinding/demo/tests.cpp create mode 100644 libopenage/pathfinding/demo/tests.h create mode 100644 openage/pathfinding/CMakeLists.txt create mode 100644 openage/pathfinding/__init__.py create mode 100644 openage/pathfinding/tests.pyx diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index 0a080e694e..917cae429a 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -10,3 +10,5 @@ add_sources(libopenage tests.cpp types.cpp ) + +add_subdirectory("demo") diff --git a/libopenage/pathfinding/demo/CMakeLists.txt b/libopenage/pathfinding/demo/CMakeLists.txt new file mode 100644 index 0000000000..27ad003577 --- /dev/null +++ b/libopenage/pathfinding/demo/CMakeLists.txt @@ -0,0 +1,8 @@ +add_sources(libopenage + demo_0.cpp + tests.cpp +) + +pxdgen( + tests.h +) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp new file mode 100644 index 0000000000..9cd18cf5a1 --- /dev/null +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -0,0 +1,27 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "demo_0.h" + +#include "renderer/gui/integration/public/gui_application_with_logger.h" +#include "renderer/opengl/window.h" + + +namespace openage::path::tests { + +void path_demo_0(const util::Path &path) { + auto qtapp = std::make_shared(); + + renderer::opengl::GlWindow window("openage pathfinding test", 1024, 768, true); + auto renderer = window.make_renderer(); + + while (not window.should_close()) { + qtapp->process_events(); + + window.update(); + + renderer->check_error(); + } + window.close(); +} + +} // namespace openage::path::tests diff --git a/libopenage/pathfinding/demo/demo_0.h b/libopenage/pathfinding/demo/demo_0.h new file mode 100644 index 0000000000..5758b9c33e --- /dev/null +++ b/libopenage/pathfinding/demo/demo_0.h @@ -0,0 +1,23 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include "util/path.h" + + +namespace openage::path::tests { + +/** + * Show the pathfinding functionality of the path module: + * - Cost field + * - Integration field + * - Flow field + * + * Visualizes the pathfinding results using our rendering backend. + * + * @param path Path to the project rootdir. + */ +void path_demo_0(const util::Path &path); + + +} // namespace openage::path::tests diff --git a/libopenage/pathfinding/demo/tests.cpp b/libopenage/pathfinding/demo/tests.cpp new file mode 100644 index 0000000000..da6e59479f --- /dev/null +++ b/libopenage/pathfinding/demo/tests.cpp @@ -0,0 +1,24 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "tests.h" + +#include "log/log.h" + +#include "pathfinding/demo/demo_0.h" + + +namespace openage::path::tests { + +void path_demo(int demo_id, const util::Path &path) { + switch (demo_id) { + case 0: + path_demo_0(path); + break; + + default: + log::log(MSG(err) << "Unknown pathfinding demo requested: " << demo_id << "."); + break; + } +} + +} // namespace openage::path::tests diff --git a/libopenage/pathfinding/demo/tests.h b/libopenage/pathfinding/demo/tests.h new file mode 100644 index 0000000000..2f411f6bf1 --- /dev/null +++ b/libopenage/pathfinding/demo/tests.h @@ -0,0 +1,20 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include "../../util/compiler.h" +// pxd: from libopenage.util.path cimport Path + + +namespace openage { +namespace util { +class Path; +} // namespace util + +namespace path::tests { + +// pxd: void path_demo(int demo_id, Path path) except + +OAAPI void path_demo(int demo_id, const util::Path &path); + +} // namespace path::tests +} // namespace openage diff --git a/openage/CMakeLists.txt b/openage/CMakeLists.txt index a9233357dd..1b31e7110f 100644 --- a/openage/CMakeLists.txt +++ b/openage/CMakeLists.txt @@ -27,6 +27,7 @@ add_subdirectory(gamestate) add_subdirectory(log) add_subdirectory(main) add_subdirectory(nyan) +add_subdirectory(pathfinding) add_subdirectory(renderer) add_subdirectory(testing) add_subdirectory(util) diff --git a/openage/pathfinding/CMakeLists.txt b/openage/pathfinding/CMakeLists.txt new file mode 100644 index 0000000000..03ab1f8e49 --- /dev/null +++ b/openage/pathfinding/CMakeLists.txt @@ -0,0 +1,7 @@ +add_cython_modules( + tests.pyx +) + +add_py_modules( + __init__.py +) diff --git a/openage/pathfinding/__init__.py b/openage/pathfinding/__init__.py new file mode 100644 index 0000000000..8c497dfafb --- /dev/null +++ b/openage/pathfinding/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2024-2024 the openage authors. See copying.md for legal info. + +""" +openage pathfinding system +""" diff --git a/openage/pathfinding/tests.pyx b/openage/pathfinding/tests.pyx new file mode 100644 index 0000000000..f180cb786d --- /dev/null +++ b/openage/pathfinding/tests.pyx @@ -0,0 +1,49 @@ +# Copyright 2024-2024 the openage authors. See copying.md for legal info. + +""" +tests for the pathfinding system. +""" + +import argparse + +from libopenage.util.path cimport Path as Path_cpp +from libopenage.pyinterface.pyobject cimport PyObj +from cpython.ref cimport PyObject +from libopenage.pathfinding.demo.tests cimport path_demo as path_demo_c + +def path_demo(list argv): + """ + invokes the available pathfinding demos. + """ + + cmd = argparse.ArgumentParser( + prog='... path_demo', + description='Demo of the pathfinding system') + cmd.add_argument("test_id", type=int, help="id of the demo to run.") + cmd.add_argument("--asset-dir", + help="Use this as an additional asset directory.") + cmd.add_argument("--cfg-dir", + help="Use this as an additional config directory.") + + args = cmd.parse_args(argv) + + from ..cvar.location import get_config_path + from ..assets import get_asset_path + from ..util.fslike.union import Union + + # create virtual file system for data paths + root = Union().root + + # mount the assets folder union at "assets/" + root["assets"].mount(get_asset_path(args.asset_dir)) + + # mount the config folder at "cfg/" + root["cfg"].mount(get_config_path(args.cfg_dir)) + + cdef int demo_id = args.test_id + + cdef Path_cpp root_cpp = Path_cpp(PyObj(root.fsobj), + root.parts) + + with nogil: + path_demo_c(demo_id, root_cpp) diff --git a/openage/testing/testlist.py b/openage/testing/testlist.py index 2a6a4780fd..37f323e8a8 100644 --- a/openage/testing/testlist.py +++ b/openage/testing/testlist.py @@ -52,6 +52,8 @@ def demos_py(): "play pong on steroids through future prediction") yield ("openage.gamestate.tests.simulation_demo", "showcases the game simulation") + yield ("openage.pathfinding.tests.path_demo", + "showcases the pathfinding system") yield ("openage.renderer.tests.renderer_demo", "showcases the renderer") yield ("openage.renderer.tests.renderer_stresstest", From fc2e9b6a86ffb16383bc4e9f85c7535a9ecebb5c Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 11 Feb 2024 00:24:47 +0100 Subject: [PATCH 305/771] path: Render background in flow field demo. --- .../test/shaders/pathfinding/CMakeLists.txt | 4 + .../pathfinding/demo_0_cost_field.frag.glsl | 1 + .../pathfinding/demo_0_cost_field.vert.glsl | 1 + .../pathfinding/demo_0_display.frag.glsl | 10 ++ .../pathfinding/demo_0_display.vert.glsl | 10 ++ .../pathfinding/demo_0_flow_field.frag.glsl | 1 + .../pathfinding/demo_0_flow_field.vert.glsl | 1 + .../demo_0_integration_field.frag.glsl | 1 + .../demo_0_integration_field.vert.glsl | 1 + .../shaders/pathfinding/demo_0_obj.frag.glsl | 9 ++ .../shaders/pathfinding/demo_0_obj.vert.glsl | 7 + libopenage/pathfinding/demo/demo_0.cpp | 122 ++++++++++++++++++ 12 files changed, 168 insertions(+) create mode 100644 assets/test/shaders/pathfinding/CMakeLists.txt create mode 100644 assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl create mode 100644 assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl create mode 100644 assets/test/shaders/pathfinding/demo_0_display.frag.glsl create mode 100644 assets/test/shaders/pathfinding/demo_0_display.vert.glsl create mode 100644 assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl create mode 100644 assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl create mode 100644 assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl create mode 100644 assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl create mode 100644 assets/test/shaders/pathfinding/demo_0_obj.frag.glsl create mode 100644 assets/test/shaders/pathfinding/demo_0_obj.vert.glsl diff --git a/assets/test/shaders/pathfinding/CMakeLists.txt b/assets/test/shaders/pathfinding/CMakeLists.txt new file mode 100644 index 0000000000..2d30f59426 --- /dev/null +++ b/assets/test/shaders/pathfinding/CMakeLists.txt @@ -0,0 +1,4 @@ +install(DIRECTORY "." + DESTINATION "${ASSET_DIR}/test/shaders/pathfinding" + FILES_MATCHING PATTERN "*.glsl" +) diff --git a/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl new file mode 100644 index 0000000000..813598b38b --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl @@ -0,0 +1 @@ +#version 330 diff --git a/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl new file mode 100644 index 0000000000..813598b38b --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl @@ -0,0 +1 @@ +#version 330 diff --git a/assets/test/shaders/pathfinding/demo_0_display.frag.glsl b/assets/test/shaders/pathfinding/demo_0_display.frag.glsl new file mode 100644 index 0000000000..a6732d0c7f --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_display.frag.glsl @@ -0,0 +1,10 @@ +#version 330 + +uniform sampler2D color_texture; + +in vec2 v_uv; +out vec4 col; + +void main() { + col = texture(color_texture, v_uv); +} diff --git a/assets/test/shaders/pathfinding/demo_0_display.vert.glsl b/assets/test/shaders/pathfinding/demo_0_display.vert.glsl new file mode 100644 index 0000000000..6112530242 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_display.vert.glsl @@ -0,0 +1,10 @@ +#version 330 + +layout(location=0) in vec2 position; +layout(location=1) in vec2 uv; +out vec2 v_uv; + +void main() { + gl_Position = vec4(position, 0.0, 1.0); + v_uv = uv; +} diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl new file mode 100644 index 0000000000..813598b38b --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl @@ -0,0 +1 @@ +#version 330 diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl new file mode 100644 index 0000000000..813598b38b --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl @@ -0,0 +1 @@ +#version 330 diff --git a/assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl new file mode 100644 index 0000000000..813598b38b --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl @@ -0,0 +1 @@ +#version 330 diff --git a/assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl new file mode 100644 index 0000000000..813598b38b --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl @@ -0,0 +1 @@ +#version 330 diff --git a/assets/test/shaders/pathfinding/demo_0_obj.frag.glsl b/assets/test/shaders/pathfinding/demo_0_obj.frag.glsl new file mode 100644 index 0000000000..ebbb10bc8f --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_obj.frag.glsl @@ -0,0 +1,9 @@ +#version 330 + +uniform vec4 color; + +out vec4 outcol; + +void main() { + outcol = color; +} diff --git a/assets/test/shaders/pathfinding/demo_0_obj.vert.glsl b/assets/test/shaders/pathfinding/demo_0_obj.vert.glsl new file mode 100644 index 0000000000..527e0bb652 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_obj.vert.glsl @@ -0,0 +1,7 @@ +#version 330 + +layout(location=0) in vec2 position; + +void main() { + gl_Position = vec4(position, 0.0, 1.0); +} diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 9cd18cf5a1..e0877ff66c 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -4,6 +4,11 @@ #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" +#include "renderer/renderer.h" +#include "renderer/resources/mesh_data.h" +#include "renderer/resources/shader_source.h" +#include "renderer/resources/texture_info.h" +#include "renderer/shader_program.h" namespace openage::path::tests { @@ -14,9 +19,126 @@ void path_demo_0(const util::Path &path) { renderer::opengl::GlWindow window("openage pathfinding test", 1024, 768, true); auto renderer = window.make_renderer(); + auto shaderdir = path / "assets" / "test" / "shaders" / "pathfinding"; + + /* Shader for rendering the cost field */ + auto cf_vshader_file = shaderdir / "demo_0_cost_field.vert.glsl"; + auto cf_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + cf_vshader_file); + + auto cf_fshader_file = shaderdir / "demo_0_cost_field.frag.glsl"; + auto cf_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + cf_fshader_file); + + /* Shader for rendering the integration field */ + auto if_vshader_file = shaderdir / "demo_0_integration_field.vert.glsl"; + auto if_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + if_vshader_file); + + auto if_fshader_file = shaderdir / "demo_0_integration_field.frag.glsl"; + auto if_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + if_fshader_file); + + /* Shader for rendering the flow field */ + auto ff_vshader_file = shaderdir / "demo_0_flow_field.vert.glsl"; + auto ff_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + ff_vshader_file); + + auto ff_fshader_file = shaderdir / "demo_0_flow_field.frag.glsl"; + auto ff_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + ff_fshader_file); + + /* Shader for monocolored objects. */ + auto obj_vshader_file = shaderdir / "demo_0_obj.vert.glsl"; + auto obj_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + obj_vshader_file); + + auto obj_fshader_file = shaderdir / "demo_0_obj.frag.glsl"; + auto obj_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + obj_fshader_file); + + /* Shader for rendering to the display target */ + auto display_vshader_file = shaderdir / "demo_0_display.vert.glsl"; + auto display_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + display_vshader_file); + + auto display_fshader_file = shaderdir / "demo_0_display.frag.glsl"; + auto display_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + display_fshader_file); + + // Create the shaders + auto cf_shader = renderer->add_shader({cf_vshader_src, cf_fshader_src}); + auto if_shader = renderer->add_shader({if_vshader_src, if_fshader_src}); + auto ff_shader = renderer->add_shader({ff_vshader_src, ff_fshader_src}); + auto obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src}); + auto display_shader = renderer->add_shader({display_vshader_src, display_fshader_src}); + + /* Make a framebuffer for the field render pass to draw into. */ + auto size = window.get_size(); + auto color_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto fbo = renderer->create_texture_target({color_texture, depth_texture}); + + auto obj_pass = renderer->add_render_pass({}, fbo); + + /* Make an object encompassing the entire screen for the display render pass */ + auto color_texture_unif = display_shader->new_uniform_input("color_texture", color_texture); + auto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); + renderer::Renderable display_obj{ + color_texture_unif, + quad, + false, + false, + }; + + auto display_pass = renderer->add_render_pass({display_obj}, renderer->get_display_target()); + + + auto background_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{64.0 / 256, 128.0 / 256, 196.0 / 256, 1.0}); + auto background = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); + renderer::Renderable background_obj{ + background_unifs, + background, + false, + false, + }; + obj_pass->add_renderables(background_obj); + + while (not window.should_close()) { qtapp->process_events(); + renderer->render(obj_pass); + renderer->render(display_pass); + window.update(); renderer->check_error(); From e17d0529a35ab69287ce7666d8c016f02a087014 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 13 Feb 2024 18:08:24 +0100 Subject: [PATCH 306/771] path: Convert indent in multi-line comments to tabs. --- libopenage/pathfinding/cost_field.h | 70 +++++++++++----------- libopenage/pathfinding/flow_field.h | 44 +++++++------- libopenage/pathfinding/integration_field.h | 8 +-- libopenage/pathfinding/integrator.h | 26 ++++---- 4 files changed, 74 insertions(+), 74 deletions(-) diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index f256af91ca..b4f5cfbb00 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -16,57 +16,57 @@ namespace openage::path { class CostField { public: /** - * Create a square cost field with a specified size. - * - * @param size Side length of the field. - */ + * Create a square cost field with a specified size. + * + * @param size Side length of the field. + */ CostField(size_t size); /** - * Get the size of the cost field. - * - * @return Size of the cost field. - */ + * Get the size of the cost field. + * + * @return Size of the cost field. + */ size_t get_size() const; /** - * Get the cost at a specified position. - * - * @param x X coordinate. - * @param y Y coordinate. - * @return Cost at the specified position. - */ + * Get the cost at a specified position. + * + * @param x X coordinate. + * @param y Y coordinate. + * @return Cost at the specified position. + */ cost_t get_cost(size_t x, size_t y) const; /** - * Get the cost at a specified position. - * - * @param idx Index of the cell. - * @return Cost at the specified position. - */ + * Get the cost at a specified position. + * + * @param idx Index of the cell. + * @return Cost at the specified position. + */ cost_t get_cost(size_t idx) const; /** - * Set the cost at a specified position. - * - * @param x X coordinate. - * @param y Y coordinate. - * @param cost Cost to set. - */ + * Set the cost at a specified position. + * + * @param x X coordinate. + * @param y Y coordinate. + * @param cost Cost to set. + */ void set_cost(size_t x, size_t y, cost_t cost); /** - * Get the cost field values. - * - * @return Cost field values. - */ + * Get the cost field values. + * + * @return Cost field values. + */ const std::vector &get_costs() const; /** - * Set the cost field values. - * - * @param cells Cost field values. - */ + * Set the cost field values. + * + * @param cells Cost field values. + */ void set_costs(std::vector &&cells); private: @@ -76,8 +76,8 @@ class CostField { size_t size; /** - * Cost field values. - */ + * Cost field values. + */ std::vector cells; }; diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index 03d2806d37..8b09e06017 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -16,38 +16,38 @@ class IntegrationField; class FlowField { public: /** - * Create a square flow field with a specified size. - * - * @param size Side length of the field. - */ + * Create a square flow field with a specified size. + * + * @param size Side length of the field. + */ FlowField(size_t size); /** - * Create a flow field from an existing integration field. - * - * @param integrate_field Integration field. - */ + * Create a flow field from an existing integration field. + * + * @param integrate_field Integration field. + */ FlowField(const std::shared_ptr &integrate_field); /** - * Get the size of the flow field. - * - * @return Size of the flow field. - */ + * Get the size of the flow field. + * + * @return Size of the flow field. + */ size_t get_size() const; /** - * Build the flow field. - * - * @param integrate_field Integration field. - */ + * Build the flow field. + * + * @param integrate_field Integration field. + */ void build(const std::shared_ptr &integrate_field); /** - * Get the flow field values. - * - * @return Flow field values. - */ + * Get the flow field values. + * + * @return Flow field values. + */ const std::vector &get_cells() const; private: @@ -57,8 +57,8 @@ class FlowField { size_t size; /** - * Flow field cells. - */ + * Flow field cells. + */ std::vector cells; }; diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 333b47f528..ad116c942e 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -43,10 +43,10 @@ class IntegrationField { size_t target_y); /** - * Get the integration field values. - * - * @return Integration field values. - */ + * Get the integration field values. + * + * @return Integration field values. + */ const std::vector &get_cells() const; private: diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index 2d2338982d..a00eaa2700 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -19,26 +19,26 @@ class Integrator { ~Integrator() = default; /** - * Set the cost field. - * - * @param cost_field Cost field. - */ + * Set the cost field. + * + * @param cost_field Cost field. + */ void set_cost_field(const std::shared_ptr &cost_field); /** - * Build the flow field for a target cell. - * - * @param target_x X coordinate of the target cell. - * @param target_y Y coordinate of the target cell. - * - * @return Flow field. - */ + * Build the flow field for a target cell. + * + * @param target_x X coordinate of the target cell. + * @param target_y Y coordinate of the target cell. + * + * @return Flow field. + */ std::shared_ptr build(size_t target_x, size_t target_y); private: /** - * Cost field. - */ + * Cost field. + */ std::shared_ptr cost_field; }; From 8f0242a00b09df2be5dd4275b66f83252a8b1720 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 13 Feb 2024 23:25:04 +0100 Subject: [PATCH 307/771] path: Draw cost field in demo. --- .../pathfinding/demo_0_cost_field.frag.glsl | 12 ++ .../pathfinding/demo_0_cost_field.vert.glsl | 14 ++ libopenage/pathfinding/demo/demo_0.cpp | 159 +++++++++++++++--- 3 files changed, 163 insertions(+), 22 deletions(-) diff --git a/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl index 813598b38b..26fe7ef44e 100644 --- a/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl +++ b/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl @@ -1 +1,13 @@ #version 330 + +in float v_cost; + +out vec4 out_col; + +void main() +{ + float cost = v_cost * 2.0; + float red = clamp(cost, 0.0, 1.0); + float green = clamp(2.0 - cost, 0.0, 1.0); + out_col = vec4(red, green, 0.0, 1.0); +} diff --git a/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl index 813598b38b..cf76408fe5 100644 --- a/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl +++ b/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl @@ -1 +1,15 @@ #version 330 + +layout (location = 0) in vec3 position; +layout (location = 1) in float cost; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 proj; + +out float v_cost; + +void main() { + gl_Position = proj * view * model * vec4(position, 1.0); + v_cost = cost / 256.0; +} diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index e0877ff66c..240b3c559a 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -2,6 +2,10 @@ #include "demo_0.h" +#include "pathfinding/cost_field.h" +#include "pathfinding/flow_field.h" +#include "pathfinding/integration_field.h" +#include "renderer/camera/camera.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" #include "renderer/renderer.h" @@ -9,19 +13,87 @@ #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" #include "renderer/shader_program.h" +#include "util/vector.h" namespace openage::path::tests { +renderer::resources::MeshData get_cost_field_mesh(const std::shared_ptr &field) { + // increase by 1 in every dimension because to get the vertex length + // of each dimension + util::Vector2s size{field->get_size() + 1, field->get_size() + 1}; + + // add vertices for the cells of the grid + std::vector verts{}; + auto vert_count = size[0] * size[1]; + verts.reserve(vert_count * 3); + for (int i = 0; i < (int)size[0]; ++i) { + for (int j = 0; j < (int)size[1]; ++j) { + coord::scene3 v{ + static_cast(i), + static_cast(j), + 0, + }; + auto world_v = v.to_world_space(); + verts.push_back(world_v[0]); + verts.push_back(world_v[1]); + verts.push_back(world_v[2]); + verts.push_back(1.0); // TODO: push back actual cost + } + } + + // split the grid into triangles using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we split each tile into two triangles + // with counter-clockwise vertex order + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + } + } + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLES, + renderer::resources::index_t::U16}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); + + return {std::move(vert_data), std::move(idx_data), info}; +} + void path_demo_0(const util::Path &path) { auto qtapp = std::make_shared(); renderer::opengl::GlWindow window("openage pathfinding test", 1024, 768, true); auto renderer = window.make_renderer(); + // Camera for correct projection of terrain + auto camera = std::make_shared(renderer, window.get_size()); + window.add_resize_callback([&](size_t w, size_t h, double /*scale*/) { + camera->resize(w, h); + }); + + // Shader sources auto shaderdir = path / "assets" / "test" / "shaders" / "pathfinding"; - /* Shader for rendering the cost field */ + // Shader for rendering the cost field auto cf_vshader_file = shaderdir / "demo_0_cost_field.vert.glsl"; auto cf_vshader_src = renderer::resources::ShaderSource( renderer::resources::shader_lang_t::glsl, @@ -34,7 +106,7 @@ void path_demo_0(const util::Path &path) { renderer::resources::shader_stage_t::fragment, cf_fshader_file); - /* Shader for rendering the integration field */ + // Shader for rendering the integration field auto if_vshader_file = shaderdir / "demo_0_integration_field.vert.glsl"; auto if_vshader_src = renderer::resources::ShaderSource( renderer::resources::shader_lang_t::glsl, @@ -47,7 +119,7 @@ void path_demo_0(const util::Path &path) { renderer::resources::shader_stage_t::fragment, if_fshader_file); - /* Shader for rendering the flow field */ + // Shader for rendering the flow field auto ff_vshader_file = shaderdir / "demo_0_flow_field.vert.glsl"; auto ff_vshader_src = renderer::resources::ShaderSource( renderer::resources::shader_lang_t::glsl, @@ -60,7 +132,7 @@ void path_demo_0(const util::Path &path) { renderer::resources::shader_stage_t::fragment, ff_fshader_file); - /* Shader for monocolored objects. */ + // Shader for monocolored objects auto obj_vshader_file = shaderdir / "demo_0_obj.vert.glsl"; auto obj_vshader_src = renderer::resources::ShaderSource( renderer::resources::shader_lang_t::glsl, @@ -73,7 +145,7 @@ void path_demo_0(const util::Path &path) { renderer::resources::shader_stage_t::fragment, obj_fshader_file); - /* Shader for rendering to the display target */ + // Shader for rendering to the display target auto display_vshader_file = shaderdir / "demo_0_display.vert.glsl"; auto display_vshader_src = renderer::resources::ShaderSource( renderer::resources::shader_lang_t::glsl, @@ -93,9 +165,9 @@ void path_demo_0(const util::Path &path) { auto obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src}); auto display_shader = renderer->add_shader({display_vshader_src, display_fshader_src}); - /* Make a framebuffer for the field render pass to draw into. */ + // Make a framebuffer for the background render pass to draw into auto size = window.get_size(); - auto color_texture = renderer->add_texture( + auto background_texture = renderer->add_texture( renderer::resources::Texture2dInfo(size[0], size[1], renderer::resources::pixel_format::rgba8)); @@ -103,23 +175,41 @@ void path_demo_0(const util::Path &path) { renderer::resources::Texture2dInfo(size[0], size[1], renderer::resources::pixel_format::depth24)); - auto fbo = renderer->create_texture_target({color_texture, depth_texture}); + auto fbo = renderer->create_texture_target({background_texture, depth_texture}); + auto background_pass = renderer->add_render_pass({}, fbo); - auto obj_pass = renderer->add_render_pass({}, fbo); + // Make a framebuffer for the field render passes to draw into + auto field_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture_2 = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto field_fbo = renderer->create_texture_target({field_texture, depth_texture_2}); + auto field_pass = renderer->add_render_pass({}, field_fbo); - /* Make an object encompassing the entire screen for the display render pass */ - auto color_texture_unif = display_shader->new_uniform_input("color_texture", color_texture); + // Make two objects that draw the results of the previous passes onto the screen + // in the display render pass + auto bg_texture_unif = display_shader->new_uniform_input("color_texture", background_texture); auto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); - renderer::Renderable display_obj{ - color_texture_unif, + renderer::Renderable bg_pass_obj{ + bg_texture_unif, quad, - false, - false, + true, + true, }; + auto field_texture_unif = display_shader->new_uniform_input("color_texture", field_texture); + renderer::Renderable field_pass_obj{ + field_texture_unif, + quad, + true, + true, + }; + auto display_pass = renderer->add_render_pass({bg_pass_obj, field_pass_obj}, renderer->get_display_target()); - auto display_pass = renderer->add_render_pass({display_obj}, renderer->get_display_target()); - - + // Background object for contrast between field and display auto background_unifs = obj_shader->new_uniform_input( "color", Eigen::Vector4f{64.0 / 256, 128.0 / 256, 196.0 / 256, 1.0}); @@ -127,16 +217,41 @@ void path_demo_0(const util::Path &path) { renderer::Renderable background_obj{ background_unifs, background, - false, - false, + true, + true, }; - obj_pass->add_renderables(background_obj); + background_pass->add_renderables(background_obj); + // Create the pathfinding fields + auto cost_field = std::make_shared(10); + auto integration_field = std::make_shared(10); + auto flow_field = std::make_shared(10); + + // Create object for the cost field + Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); + auto cost_field_unifs = cf_shader->new_uniform_input( + "model", + model, + "view", + camera->get_view_matrix(), + "proj", + camera->get_projection_matrix()); + auto cost_field_mesh = get_cost_field_mesh(cost_field); + auto cost_field_geometry = renderer->add_mesh_geometry(cost_field_mesh); + // auto cost_field_geometry = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); + renderer::Renderable cost_field_renderable{ + cost_field_unifs, + cost_field_geometry, + true, + true, + }; + field_pass->add_renderables(cost_field_renderable); while (not window.should_close()) { qtapp->process_events(); - renderer->render(obj_pass); + renderer->render(background_pass); + renderer->render(field_pass); renderer->render(display_pass); window.update(); From 51f09aea22179193deef3c26659a8c1da052e741 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 14 Feb 2024 00:33:03 +0100 Subject: [PATCH 308/771] path: Move camera in dem so that whole grid is visible. --- libopenage/pathfinding/demo/demo_0.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 240b3c559a..ea6436c1a7 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -81,11 +81,12 @@ renderer::resources::MeshData get_cost_field_mesh(const std::shared_ptr(); - renderer::opengl::GlWindow window("openage pathfinding test", 1024, 768, true); + renderer::opengl::GlWindow window("openage pathfinding test", 1440, 720, true); auto renderer = window.make_renderer(); // Camera for correct projection of terrain auto camera = std::make_shared(renderer, window.get_size()); + camera->look_at_coord({5, 5, 0}); window.add_resize_callback([&](size_t w, size_t h, double /*scale*/) { camera->resize(w, h); }); From a6840b44548641297593d2bb78a6dcdb6a05fe9d Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 15 Feb 2024 20:12:03 +0100 Subject: [PATCH 309/771] path: Draw grid lines on top of field. --- .../shaders/pathfinding/demo_0_grid.frag.glsl | 8 ++ .../shaders/pathfinding/demo_0_grid.vert.glsl | 11 ++ libopenage/pathfinding/demo/demo_0.cpp | 118 +++++++++++++++++- 3 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 assets/test/shaders/pathfinding/demo_0_grid.frag.glsl create mode 100644 assets/test/shaders/pathfinding/demo_0_grid.vert.glsl diff --git a/assets/test/shaders/pathfinding/demo_0_grid.frag.glsl b/assets/test/shaders/pathfinding/demo_0_grid.frag.glsl new file mode 100644 index 0000000000..19d2e459c3 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_grid.frag.glsl @@ -0,0 +1,8 @@ +#version 330 + +out vec4 out_col; + +void main() +{ + out_col = vec4(0.0, 0.0, 0.0, 0.3); +} diff --git a/assets/test/shaders/pathfinding/demo_0_grid.vert.glsl b/assets/test/shaders/pathfinding/demo_0_grid.vert.glsl new file mode 100644 index 0000000000..2f5eaeaa15 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_grid.vert.glsl @@ -0,0 +1,11 @@ +#version 330 + +layout (location = 0) in vec3 position; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 proj; + +void main() { + gl_Position = proj * view * model * vec4(position, 1.0); +} diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index ea6436c1a7..f70ad0439e 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -26,7 +26,7 @@ renderer::resources::MeshData get_cost_field_mesh(const std::shared_ptr verts{}; auto vert_count = size[0] * size[1]; - verts.reserve(vert_count * 3); + verts.reserve(vert_count * 4); for (int i = 0; i < (int)size[0]; ++i) { for (int j = 0; j < (int)size[1]; ++j) { coord::scene3 v{ @@ -78,6 +78,66 @@ renderer::resources::MeshData get_cost_field_mesh(const std::shared_ptr verts{}; + auto vert_count = size[0] * size[1]; + verts.reserve(vert_count * 3); + for (int i = 0; i < (int)size[0]; ++i) { + for (int j = 0; j < (int)size[1]; ++j) { + coord::scene3 v{ + static_cast(i), + static_cast(j), + 0, + }; + auto world_v = v.to_world_space(); + verts.push_back(world_v[0]); + verts.push_back(world_v[1]); + verts.push_back(world_v[2]); + } + } + + // split the grid into lines using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 8); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we just draw a square using the 4 vertices + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + i * size[1]); // bottom left + } + } + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::LINES, + renderer::resources::index_t::U16}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); + + return {std::move(vert_data), std::move(idx_data), info}; +} + void path_demo_0(const util::Path &path) { auto qtapp = std::make_shared(); @@ -133,6 +193,19 @@ void path_demo_0(const util::Path &path) { renderer::resources::shader_stage_t::fragment, ff_fshader_file); + // Shader for rendering the grid + auto grid_vshader_file = shaderdir / "demo_0_grid.vert.glsl"; + auto grid_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + grid_vshader_file); + + auto grid_fshader_file = shaderdir / "demo_0_grid.frag.glsl"; + auto grid_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + grid_fshader_file); + // Shader for monocolored objects auto obj_vshader_file = shaderdir / "demo_0_obj.vert.glsl"; auto obj_vshader_src = renderer::resources::ShaderSource( @@ -163,6 +236,7 @@ void path_demo_0(const util::Path &path) { auto cf_shader = renderer->add_shader({cf_vshader_src, cf_fshader_src}); auto if_shader = renderer->add_shader({if_vshader_src, if_fshader_src}); auto ff_shader = renderer->add_shader({ff_vshader_src, ff_fshader_src}); + auto grid_shader = renderer->add_shader({grid_vshader_src, grid_fshader_src}); auto obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src}); auto display_shader = renderer->add_shader({display_vshader_src, display_fshader_src}); @@ -191,6 +265,18 @@ void path_demo_0(const util::Path &path) { auto field_fbo = renderer->create_texture_target({field_texture, depth_texture_2}); auto field_pass = renderer->add_render_pass({}, field_fbo); + // Make a framebuffer for the grid render passes to draw into + auto grid_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture_3 = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto grid_fbo = renderer->create_texture_target({grid_texture, depth_texture_3}); + auto grid_pass = renderer->add_render_pass({}, grid_fbo); + // Make two objects that draw the results of the previous passes onto the screen // in the display render pass auto bg_texture_unif = display_shader->new_uniform_input("color_texture", background_texture); @@ -208,7 +294,16 @@ void path_demo_0(const util::Path &path) { true, true, }; - auto display_pass = renderer->add_render_pass({bg_pass_obj, field_pass_obj}, renderer->get_display_target()); + auto grid_texture_unif = display_shader->new_uniform_input("color_texture", grid_texture); + renderer::Renderable grid_pass_obj{ + grid_texture_unif, + quad, + true, + true, + }; + auto display_pass = renderer->add_render_pass( + {bg_pass_obj, field_pass_obj, grid_pass_obj}, + renderer->get_display_target()); // Background object for contrast between field and display auto background_unifs = obj_shader->new_uniform_input( @@ -248,11 +343,30 @@ void path_demo_0(const util::Path &path) { }; field_pass->add_renderables(cost_field_renderable); + // Create object for the grid + auto grid_unifs = grid_shader->new_uniform_input( + "model", + model, + "view", + camera->get_view_matrix(), + "proj", + camera->get_projection_matrix()); + auto grid_mesh = get_grid_mesh(cost_field->get_size()); + auto grid_geometry = renderer->add_mesh_geometry(grid_mesh); + renderer::Renderable grid_renderable{ + grid_unifs, + grid_geometry, + true, + true, + }; + grid_pass->add_renderables(grid_renderable); + while (not window.should_close()) { qtapp->process_events(); renderer->render(background_pass); renderer->render(field_pass); + renderer->render(grid_pass); renderer->render(display_pass); window.update(); From 29e49ffb1b7e42d2799059cc511c250f5338bf45 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 15 Feb 2024 20:47:46 +0100 Subject: [PATCH 310/771] renderer: Fix unindexed mesh being drawn with wrong primitive. --- libopenage/renderer/opengl/geometry.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/renderer/opengl/geometry.cpp b/libopenage/renderer/opengl/geometry.cpp index a01b4183af..0e2c930a2a 100644 --- a/libopenage/renderer/opengl/geometry.cpp +++ b/libopenage/renderer/opengl/geometry.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "geometry.h" @@ -68,7 +68,7 @@ void GlGeometry::draw() const { glDrawElements(mesh.primitive, mesh.vert_count, *mesh.index_type, nullptr); } else { - glDrawArrays(GL_TRIANGLE_STRIP, 0, mesh.vert_count); + glDrawArrays(mesh.primitive, 0, mesh.vert_count); } break; From 02e666b44b2a3ab414527bec767f8f9cbacc5d67 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 15 Feb 2024 20:56:10 +0100 Subject: [PATCH 311/771] path: Show cost as tile colours on grid. --- .../pathfinding/demo_0_cost_field.frag.glsl | 6 +- .../pathfinding/demo_0_cost_field.vert.glsl | 2 +- libopenage/pathfinding/demo/demo_0.cpp | 55 ++++++++++++++++--- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl index 26fe7ef44e..7ccb86dcf4 100644 --- a/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl +++ b/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl @@ -6,7 +6,11 @@ out vec4 out_col; void main() { - float cost = v_cost * 2.0; + if (v_cost == 255.0) { + out_col = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + float cost = (v_cost / 256) * 2.0; float red = clamp(cost, 0.0, 1.0); float green = clamp(2.0 - cost, 0.0, 1.0); out_col = vec4(red, green, 0.0, 1.0); diff --git a/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl index cf76408fe5..d12f0db1d9 100644 --- a/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl +++ b/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl @@ -11,5 +11,5 @@ out float v_cost; void main() { gl_Position = proj * view * model * vec4(position, 1.0); - v_cost = cost / 256.0; + v_cost = cost; } diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index f70ad0439e..54493c77b6 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -18,27 +18,62 @@ namespace openage::path::tests { -renderer::resources::MeshData get_cost_field_mesh(const std::shared_ptr &field) { +/** + * Create a mesh for the cost field. + * + * @param field Cost field to visualize. + * @param resolution Determines the number of subdivisions per grid cell. The number of + * quads per cell is resolution^2. (default = 2) + * + * @return Mesh data for the cost field. + */ +renderer::resources::MeshData get_cost_field_mesh(const std::shared_ptr &field, + size_t resolution = 2) { // increase by 1 in every dimension because to get the vertex length // of each dimension - util::Vector2s size{field->get_size() + 1, field->get_size() + 1}; + util::Vector2s size{ + field->get_size() * resolution + 1, + field->get_size() * resolution + 1, + }; + auto vert_distance = 1.0f / resolution; // add vertices for the cells of the grid std::vector verts{}; auto vert_count = size[0] * size[1]; verts.reserve(vert_count * 4); - for (int i = 0; i < (int)size[0]; ++i) { - for (int j = 0; j < (int)size[1]; ++j) { + for (int i = 0; i < static_cast(size[0]); ++i) { + for (int j = 0; j < static_cast(size[1]); ++j) { + // for each vertex, compare the surrounding tiles + std::vector surround{}; + if (j - 1 >= 0 and i - 1 >= 0) { + auto cost = field->get_cost((i - 1) / resolution, (j - 1) / resolution); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i - 1 >= 0) { + auto cost = field->get_cost((i - 1) / resolution, j / resolution); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { + auto cost = field->get_cost(i / resolution, j / resolution); + surround.push_back(cost); + } + if (j - 1 >= 0 and i < static_cast(field->get_size())) { + auto cost = field->get_cost(i / resolution, (j - 1) / resolution); + surround.push_back(cost); + } + // use the cost of the most expensive surrounding tile + auto max_cost = *std::max_element(surround.begin(), surround.end()); + coord::scene3 v{ - static_cast(i), - static_cast(j), + static_cast(i * vert_distance), + static_cast(j * vert_distance), 0, }; auto world_v = v.to_world_space(); verts.push_back(world_v[0]); verts.push_back(world_v[1]); verts.push_back(world_v[2]); - verts.push_back(1.0); // TODO: push back actual cost + verts.push_back(max_cost); // TODO: push back actual cost } } @@ -320,6 +355,10 @@ void path_demo_0(const util::Path &path) { // Create the pathfinding fields auto cost_field = std::make_shared(10); + cost_field->set_cost(0, 0, 255); + cost_field->set_cost(1, 0, 254); + cost_field->set_cost(4, 3, 128); + auto integration_field = std::make_shared(10); auto flow_field = std::make_shared(10); @@ -351,7 +390,7 @@ void path_demo_0(const util::Path &path) { camera->get_view_matrix(), "proj", camera->get_projection_matrix()); - auto grid_mesh = get_grid_mesh(cost_field->get_size()); + auto grid_mesh = get_grid_mesh(10); auto grid_geometry = renderer->add_mesh_geometry(grid_mesh); renderer::Renderable grid_renderable{ grid_unifs, From 3cb498398a4326a2e4ee3ab61ebec29f7b53455b Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 16 Feb 2024 01:15:33 +0100 Subject: [PATCH 312/771] path: Draw integration field in demo. --- .../demo_0_integration_field.frag.glsl | 15 +++ .../demo_0_integration_field.vert.glsl | 14 +++ libopenage/pathfinding/demo/demo_0.cpp | 114 +++++++++++++++++- libopenage/pathfinding/integration_field.cpp | 8 ++ libopenage/pathfinding/integration_field.h | 17 +++ 5 files changed, 167 insertions(+), 1 deletion(-) diff --git a/assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl index 813598b38b..d40715a338 100644 --- a/assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl +++ b/assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl @@ -1 +1,16 @@ #version 330 + +in float v_cost; + +out vec4 out_col; + +void main() +{ + if (v_cost > 512.0) { + out_col = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + float cost = 0.05 * v_cost; + float green = clamp(1.0 - cost, 0.0, 1.0); + out_col = vec4(0.75, green, 0.5, 1.0); +} diff --git a/assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl index 813598b38b..d12f0db1d9 100644 --- a/assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl +++ b/assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl @@ -1 +1,15 @@ #version 330 + +layout (location = 0) in vec3 position; +layout (location = 1) in float cost; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 proj; + +out float v_cost; + +void main() { + gl_Position = proj * view * model * vec4(position, 1.0); + v_cost = cost; +} diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 54493c77b6..2f3ccd8243 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -73,7 +73,7 @@ renderer::resources::MeshData get_cost_field_mesh(const std::shared_ptr &field, + size_t resolution = 2) { + // increase by 1 in every dimension because to get the vertex length + // of each dimension + util::Vector2s size{ + field->get_size() * resolution + 1, + field->get_size() * resolution + 1, + }; + auto vert_distance = 1.0f / resolution; + + // add vertices for the cells of the grid + std::vector verts{}; + auto vert_count = size[0] * size[1]; + verts.reserve(vert_count * 4); + for (int i = 0; i < static_cast(size[0]); ++i) { + for (int j = 0; j < static_cast(size[1]); ++j) { + // for each vertex, compare the surrounding tiles + std::vector surround{}; + if (j - 1 >= 0 and i - 1 >= 0) { + auto cost = field->get_cell((i - 1) / resolution, (j - 1) / resolution); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i - 1 >= 0) { + auto cost = field->get_cell((i - 1) / resolution, j / resolution); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { + auto cost = field->get_cell(i / resolution, j / resolution); + surround.push_back(cost); + } + if (j - 1 >= 0 and i < static_cast(field->get_size())) { + auto cost = field->get_cell(i / resolution, (j - 1) / resolution); + surround.push_back(cost); + } + // use the cost of the most expensive surrounding tile + auto max_cost = *std::max_element(surround.begin(), surround.end()); + + coord::scene3 v{ + static_cast(i * vert_distance), + static_cast(j * vert_distance), + 0, + }; + auto world_v = v.to_world_space(); + verts.push_back(world_v[0]); + verts.push_back(world_v[1]); + verts.push_back(world_v[2]); + verts.push_back(max_cost); + } + } + + // split the grid into triangles using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we split each tile into two triangles + // with counter-clockwise vertex order + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + } + } + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLES, + renderer::resources::index_t::U16}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); + + return {std::move(vert_data), std::move(idx_data), info}; +} + +/** + * Create a mesh for the grid. + * + * @param side_length Length of the grid's side. + * + * @return Mesh data for the grid. + */ renderer::resources::MeshData get_grid_mesh(size_t side_length) { // increase by 1 in every dimension because to get the vertex length // of each dimension @@ -360,6 +453,7 @@ void path_demo_0(const util::Path &path) { cost_field->set_cost(4, 3, 128); auto integration_field = std::make_shared(10); + integration_field->integrate(cost_field, 7, 7); auto flow_field = std::make_shared(10); // Create object for the cost field @@ -382,6 +476,24 @@ void path_demo_0(const util::Path &path) { }; field_pass->add_renderables(cost_field_renderable); + // Create object for the integration field + auto integration_field_unifs = if_shader->new_uniform_input( + "model", + model, + "view", + camera->get_view_matrix(), + "proj", + camera->get_projection_matrix()); + auto integration_field_mesh = get_integration_field_mesh(integration_field); + auto integration_field_geometry = renderer->add_mesh_geometry(integration_field_mesh); + renderer::Renderable integration_field_renderable{ + integration_field_unifs, + integration_field_geometry, + true, + true, + }; + field_pass->add_renderables(integration_field_renderable); + // Create object for the grid auto grid_unifs = grid_shader->new_uniform_input( "model", diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 4e6d59b50d..695e8340ce 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -52,6 +52,14 @@ size_t IntegrationField::get_size() const { return this->size; } +integrate_t IntegrationField::get_cell(size_t x, size_t y) const { + return this->cells.at(x + y * this->size); +} + +integrate_t IntegrationField::get_cell(size_t idy) const { + return this->cells.at(idy); +} + void IntegrationField::integrate(const std::shared_ptr &cost_field, size_t target_x, size_t target_y) { diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index ad116c942e..75356daa7c 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -31,6 +31,23 @@ class IntegrationField { */ size_t get_size() const; + /** + * Get the integration value at a specified position. + * + * @param x X coordinate. + * @param y Y coordinate. + * @return Integration value at the specified position. + */ + integrate_t get_cell(size_t x, size_t y) const; + + /** + * Get the integration value at a specified position. + * + * @param idx Index of the cell. + * @return Integration value at the specified position. + */ + integrate_t get_cell(size_t idy) const; + /** * Calculate the integration field for a target cell. * From 2dfde71eb62dd6a2e782bff014c630d18e2ea03e Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 17 Feb 2024 23:48:52 +0100 Subject: [PATCH 313/771] path: Fix method argument name. --- libopenage/pathfinding/integration_field.cpp | 4 ++-- libopenage/pathfinding/integration_field.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 695e8340ce..da4014092a 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -56,8 +56,8 @@ integrate_t IntegrationField::get_cell(size_t x, size_t y) const { return this->cells.at(x + y * this->size); } -integrate_t IntegrationField::get_cell(size_t idy) const { - return this->cells.at(idy); +integrate_t IntegrationField::get_cell(size_t idx) const { + return this->cells.at(idx); } void IntegrationField::integrate(const std::shared_ptr &cost_field, diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 75356daa7c..0f0ad16661 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -46,7 +46,7 @@ class IntegrationField { * @param idx Index of the cell. * @return Integration value at the specified position. */ - integrate_t get_cell(size_t idy) const; + integrate_t get_cell(size_t idx) const; /** * Calculate the integration field for a target cell. From c885c6e0bd03645ee359545c628a5f3459873420 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 18 Feb 2024 01:21:36 +0100 Subject: [PATCH 314/771] path: Fix integration update overflow. --- libopenage/pathfinding/integration_field.cpp | 2 +- libopenage/pathfinding/integration_field.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index da4014092a..ee0a91d96f 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -138,7 +138,7 @@ void IntegrationField::reset() { integrate_t IntegrationField::update_cell(size_t idx, cost_t cell_cost, - cost_t integrate_cost) { + integrate_t integrate_cost) { ENSURE(cell_cost > COST_INIT, "cost field cell value must be non-zero"); // Check if the cell is impassable. diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 0f0ad16661..fc83683a30 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -83,7 +83,7 @@ class IntegrationField { */ integrate_t update_cell(size_t idx, cost_t cell_cost, - cost_t integrate_cost); + integrate_t integrate_cost); /** * Side length of the field. From 520c9cf9f72bb97830db84241e7a150263a848d0 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 18 Feb 2024 01:22:04 +0100 Subject: [PATCH 315/771] patg: Get individual flow field cell values. --- libopenage/pathfinding/flow_field.cpp | 8 ++++++++ libopenage/pathfinding/flow_field.h | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index bd16ec924e..27e6e14560 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -24,6 +24,14 @@ size_t FlowField::get_size() const { return this->size; } +flow_t FlowField::get_cell(size_t x, size_t y) const { + return this->cells.at(x + y * this->size); +} + +flow_dir_t FlowField::get_dir(size_t x, size_t y) const { + return static_cast(this->get_cell(x, y) & FLOW_DIR_MASK); +} + void FlowField::build(const std::shared_ptr &integrate_field) { ENSURE(integrate_field->get_size() == this->get_size(), "integration field size " diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index 8b09e06017..7fc3f00470 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -36,6 +36,26 @@ class FlowField { */ size_t get_size() const; + /** + * Get the flow field value at a specified position. + * + * @param x X coordinate. + * @param y Y coordinate. + * + * @return Flowfield value at the specified position. + */ + flow_t get_cell(size_t x, size_t y) const; + + /** + * Get the flow field direction at a specified position. + * + * @param x X coordinate. + * @param y Y coordinate. + * + * @return Flowfield direction at the specified position. + */ + flow_dir_t get_dir(size_t x, size_t y) const; + /** * Build the flow field. * From 01bff57955b6a0519cf24ce0f4509fd82495f9de Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 18 Feb 2024 01:25:28 +0100 Subject: [PATCH 316/771] path: Draw flow field in demo. --- .../pathfinding/demo_0_flow_field.frag.glsl | 8 + .../pathfinding/demo_0_flow_field.vert.glsl | 10 + libopenage/pathfinding/demo/demo_0.cpp | 207 +++++++++++++++++- 3 files changed, 222 insertions(+), 3 deletions(-) diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl index 813598b38b..ebbb10bc8f 100644 --- a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl +++ b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl @@ -1 +1,9 @@ #version 330 + +uniform vec4 color; + +out vec4 outcol; + +void main() { + outcol = color; +} diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl index 813598b38b..8b6b015408 100644 --- a/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl +++ b/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl @@ -1 +1,11 @@ #version 330 + +layout(location=0) in vec3 position; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 proj; + +void main() { + gl_Position = proj * view * model * vec4(position, 1.0); +} diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 2f3ccd8243..2e03a508b3 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -13,6 +13,7 @@ #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" #include "renderer/shader_program.h" +#include "util/math_constants.h" #include "util/vector.h" @@ -113,6 +114,16 @@ renderer::resources::MeshData get_cost_field_mesh(const std::shared_ptr &field, size_t resolution = 2) { // increase by 1 in every dimension because to get the vertex length @@ -199,6 +210,127 @@ renderer::resources::MeshData get_integration_field_mesh(const std::shared_ptr &field, + size_t resolution = 2) { + // increase by 1 in every dimension because to get the vertex length + // of each dimension + util::Vector2s size{ + field->get_size() * resolution + 1, + field->get_size() * resolution + 1, + }; + auto vert_distance = 1.0f / resolution; + + // add vertices for the cells of the grid + std::vector verts{}; + auto vert_count = size[0] * size[1]; + verts.reserve(vert_count * 4); + for (int i = 0; i < static_cast(size[0]); ++i) { + for (int j = 0; j < static_cast(size[1]); ++j) { + // for each vertex, compare the surrounding tiles + std::vector surround{}; + if (j - 1 >= 0 and i - 1 >= 0) { + auto cost = field->get_cell((i - 1) / resolution, (j - 1) / resolution); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i - 1 >= 0) { + auto cost = field->get_cell((i - 1) / resolution, j / resolution); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { + auto cost = field->get_cell(i / resolution, j / resolution); + surround.push_back(cost); + } + if (j - 1 >= 0 and i < static_cast(field->get_size())) { + auto cost = field->get_cell(i / resolution, (j - 1) / resolution); + surround.push_back(cost); + } + // use the cost of the most expensive surrounding tile + auto max_cost = *std::max_element(surround.begin(), surround.end()); + + coord::scene3 v{ + static_cast(i * vert_distance), + static_cast(j * vert_distance), + 0, + }; + auto world_v = v.to_world_space(); + verts.push_back(world_v[0]); + verts.push_back(world_v[1]); + verts.push_back(world_v[2]); + verts.push_back(max_cost); + } + } + + // split the grid into triangles using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we split each tile into two triangles + // with counter-clockwise vertex order + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + } + } + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLES, + renderer::resources::index_t::U16}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); + + return {std::move(vert_data), std::move(idx_data), info}; +} + +/** + * Create a mesh for an arrow. + * + * @return Mesh data for an arrow. + */ +renderer::resources::MeshData get_arrow_mesh() { + // vertices for the arrow + // x, y, z + std::vector verts{ + // clang-format off + 0.2f, 0.01f, -0.05f, + 0.2f, 0.01f, 0.05f, + -0.2f, 0.01f, -0.05f, + -0.2f, 0.01f, 0.05f, + // clang-format on + }; + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLE_STRIP}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + return {std::move(vert_data), info}; +} + + /** * Create a mesh for the grid. * @@ -451,10 +583,16 @@ void path_demo_0(const util::Path &path) { cost_field->set_cost(0, 0, 255); cost_field->set_cost(1, 0, 254); cost_field->set_cost(4, 3, 128); + cost_field->set_cost(5, 3, 128); + cost_field->set_cost(6, 3, 128); + cost_field->set_cost(4, 4, 128); + cost_field->set_cost(5, 4, 128); + cost_field->set_cost(6, 4, 128); auto integration_field = std::make_shared(10); integration_field->integrate(cost_field, 7, 7); auto flow_field = std::make_shared(10); + flow_field->build(integration_field); // Create object for the cost field Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); @@ -467,14 +605,13 @@ void path_demo_0(const util::Path &path) { camera->get_projection_matrix()); auto cost_field_mesh = get_cost_field_mesh(cost_field); auto cost_field_geometry = renderer->add_mesh_geometry(cost_field_mesh); - // auto cost_field_geometry = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); renderer::Renderable cost_field_renderable{ cost_field_unifs, cost_field_geometry, true, true, }; - field_pass->add_renderables(cost_field_renderable); + // field_pass->add_renderables(cost_field_renderable); // Create object for the integration field auto integration_field_unifs = if_shader->new_uniform_input( @@ -484,7 +621,7 @@ void path_demo_0(const util::Path &path) { camera->get_view_matrix(), "proj", camera->get_projection_matrix()); - auto integration_field_mesh = get_integration_field_mesh(integration_field); + auto integration_field_mesh = get_integration_field_mesh(integration_field, 4); auto integration_field_geometry = renderer->add_mesh_geometry(integration_field_mesh); renderer::Renderable integration_field_renderable{ integration_field_unifs, @@ -494,6 +631,70 @@ void path_demo_0(const util::Path &path) { }; field_pass->add_renderables(integration_field_renderable); + // Create object for the flow field + auto flow_field_unifs = ff_shader->new_uniform_input( + "model", + model, + "view", + camera->get_view_matrix(), + "proj", + camera->get_projection_matrix(), + "color", + Eigen::Vector4f{192.0 / 256, 255.0 / 256, 64.0 / 256, 1.0}); + auto flow_field_mesh = get_flow_field_mesh(flow_field); + auto flow_field_geometry = renderer->add_mesh_geometry(flow_field_mesh); + renderer::Renderable flow_field_renderable{ + flow_field_unifs, + flow_field_geometry, + true, + true, + }; + // field_pass->add_renderables(flow_field_renderable); + + std::unordered_map offset_dir{ + {flow_dir_t::NORTH, {-0.25f, 0.0f, 0.0f}}, + {flow_dir_t::NORTH_EAST, {-0.25f, 0.0f, -0.25f}}, + {flow_dir_t::EAST, {0.0f, 0.0f, -0.25f}}, + {flow_dir_t::SOUTH_EAST, {0.25f, 0.0f, -0.25f}}, + {flow_dir_t::SOUTH, {0.25f, 0.0f, 0.0f}}, + {flow_dir_t::SOUTH_WEST, {0.25f, 0.0f, 0.25f}}, + {flow_dir_t::WEST, {0.0f, 0.0f, 0.25f}}, + {flow_dir_t::NORTH_WEST, {-0.25f, 0.0f, 0.25f}}, + }; + + for (size_t y = 0; y < flow_field->get_size(); ++y) { + for (size_t x = 0; x < flow_field->get_size(); ++x) { + auto cell = flow_field->get_cell(x, y); + if (cell & FLOW_PATHABLE) { + Eigen::Affine3f arrow_model = Eigen::Affine3f::Identity(); + arrow_model.translate(Eigen::Vector3f(y + 0.5, 0, -1.0f * x - 0.5)); + auto dir = static_cast(cell & FLOW_DIR_MASK); + arrow_model.translate(offset_dir[dir]); + + auto rotation_rad = (cell & FLOW_DIR_MASK) * -45 * math::DEGSPERRAD; + arrow_model.rotate(Eigen::AngleAxisf(rotation_rad, Eigen::Vector3f::UnitY())); + auto arrow_unifs = ff_shader->new_uniform_input( + "model", + arrow_model.matrix(), + "view", + camera->get_view_matrix(), + "proj", + camera->get_projection_matrix(), + "color", + Eigen::Vector4f{0.0f, 0.0f, 0.0f, 1.0f}); + auto arrow_mesh = get_arrow_mesh(); + auto arrow_geometry = renderer->add_mesh_geometry(arrow_mesh); + renderer::Renderable arrow_renderable{ + arrow_unifs, + arrow_geometry, + true, + true, + }; + field_pass->add_renderables(arrow_renderable); + } + } + } + // Create object for the grid auto grid_unifs = grid_shader->new_uniform_input( "model", From 791fa602060c5f9d5f881a80c40ba84829a32b44 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 24 Feb 2024 00:52:18 +0100 Subject: [PATCH 317/771] path: Change field that is rendered with F1-F4 keys. --- libopenage/pathfinding/demo/demo_0.cpp | 175 +++++++++++++++---------- 1 file changed, 103 insertions(+), 72 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 2e03a508b3..20ab91345f 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -2,6 +2,8 @@ #include "demo_0.h" +#include + #include "pathfinding/cost_field.h" #include "pathfinding/flow_field.h" #include "pathfinding/integration_field.h" @@ -595,6 +597,7 @@ void path_demo_0(const util::Path &path) { flow_field->build(integration_field); // Create object for the cost field + // this will be shown at the start Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); auto cost_field_unifs = cf_shader->new_uniform_input( "model", @@ -611,89 +614,117 @@ void path_demo_0(const util::Path &path) { true, true, }; - // field_pass->add_renderables(cost_field_renderable); - - // Create object for the integration field - auto integration_field_unifs = if_shader->new_uniform_input( - "model", - model, - "view", - camera->get_view_matrix(), - "proj", - camera->get_projection_matrix()); - auto integration_field_mesh = get_integration_field_mesh(integration_field, 4); - auto integration_field_geometry = renderer->add_mesh_geometry(integration_field_mesh); - renderer::Renderable integration_field_renderable{ - integration_field_unifs, - integration_field_geometry, - true, - true, - }; - field_pass->add_renderables(integration_field_renderable); - - // Create object for the flow field - auto flow_field_unifs = ff_shader->new_uniform_input( - "model", - model, - "view", - camera->get_view_matrix(), - "proj", - camera->get_projection_matrix(), - "color", - Eigen::Vector4f{192.0 / 256, 255.0 / 256, 64.0 / 256, 1.0}); - auto flow_field_mesh = get_flow_field_mesh(flow_field); - auto flow_field_geometry = renderer->add_mesh_geometry(flow_field_mesh); - renderer::Renderable flow_field_renderable{ - flow_field_unifs, - flow_field_geometry, - true, - true, - }; - // field_pass->add_renderables(flow_field_renderable); - - std::unordered_map offset_dir{ - {flow_dir_t::NORTH, {-0.25f, 0.0f, 0.0f}}, - {flow_dir_t::NORTH_EAST, {-0.25f, 0.0f, -0.25f}}, - {flow_dir_t::EAST, {0.0f, 0.0f, -0.25f}}, - {flow_dir_t::SOUTH_EAST, {0.25f, 0.0f, -0.25f}}, - {flow_dir_t::SOUTH, {0.25f, 0.0f, 0.0f}}, - {flow_dir_t::SOUTH_WEST, {0.25f, 0.0f, 0.25f}}, - {flow_dir_t::WEST, {0.0f, 0.0f, 0.25f}}, - {flow_dir_t::NORTH_WEST, {-0.25f, 0.0f, 0.25f}}, - }; - - for (size_t y = 0; y < flow_field->get_size(); ++y) { - for (size_t x = 0; x < flow_field->get_size(); ++x) { - auto cell = flow_field->get_cell(x, y); - if (cell & FLOW_PATHABLE) { - Eigen::Affine3f arrow_model = Eigen::Affine3f::Identity(); - arrow_model.translate(Eigen::Vector3f(y + 0.5, 0, -1.0f * x - 0.5)); - auto dir = static_cast(cell & FLOW_DIR_MASK); - arrow_model.translate(offset_dir[dir]); - - auto rotation_rad = (cell & FLOW_DIR_MASK) * -45 * math::DEGSPERRAD; - arrow_model.rotate(Eigen::AngleAxisf(rotation_rad, Eigen::Vector3f::UnitY())); - auto arrow_unifs = ff_shader->new_uniform_input( + field_pass->add_renderables(cost_field_renderable); + + window.add_key_callback([&](const QKeyEvent &ev) { + if (ev.type() == QEvent::KeyRelease) { + if (ev.key() == Qt::Key_F1) { // Show cost field + // Recreate object for the cost field + Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); + auto cost_field_unifs = cf_shader->new_uniform_input( + "model", + model, + "view", + camera->get_view_matrix(), + "proj", + camera->get_projection_matrix()); + auto cost_field_mesh = get_cost_field_mesh(cost_field); + auto cost_field_geometry = renderer->add_mesh_geometry(cost_field_mesh); + renderer::Renderable cost_field_renderable{ + cost_field_unifs, + cost_field_geometry, + true, + true, + }; + field_pass->set_renderables({cost_field_renderable}); + } + else if (ev.key() == Qt::Key_F2) { // Show integration field + // Create object for the integration field + auto integration_field_unifs = if_shader->new_uniform_input( + "model", + model, + "view", + camera->get_view_matrix(), + "proj", + camera->get_projection_matrix()); + auto integration_field_mesh = get_integration_field_mesh(integration_field, 4); + auto integration_field_geometry = renderer->add_mesh_geometry(integration_field_mesh); + renderer::Renderable integration_field_renderable{ + integration_field_unifs, + integration_field_geometry, + true, + true, + }; + field_pass->set_renderables({integration_field_renderable}); + } + else if (ev.key() == Qt::Key_F3) { // Show flow field + // Create object for the flow field + auto flow_field_unifs = ff_shader->new_uniform_input( "model", - arrow_model.matrix(), + model, "view", camera->get_view_matrix(), "proj", camera->get_projection_matrix(), "color", - Eigen::Vector4f{0.0f, 0.0f, 0.0f, 1.0f}); - auto arrow_mesh = get_arrow_mesh(); - auto arrow_geometry = renderer->add_mesh_geometry(arrow_mesh); - renderer::Renderable arrow_renderable{ - arrow_unifs, - arrow_geometry, + Eigen::Vector4f{192.0 / 256, 255.0 / 256, 64.0 / 256, 1.0}); + auto flow_field_mesh = get_flow_field_mesh(flow_field); + auto flow_field_geometry = renderer->add_mesh_geometry(flow_field_mesh); + renderer::Renderable flow_field_renderable{ + flow_field_unifs, + flow_field_geometry, true, true, }; - field_pass->add_renderables(arrow_renderable); + field_pass->set_renderables({flow_field_renderable}); + } + else if (ev.key() == Qt::Key_F4) { // Show steering vectors + static const std::unordered_map offset_dir{ + {flow_dir_t::NORTH, {-0.25f, 0.0f, 0.0f}}, + {flow_dir_t::NORTH_EAST, {-0.25f, 0.0f, -0.25f}}, + {flow_dir_t::EAST, {0.0f, 0.0f, -0.25f}}, + {flow_dir_t::SOUTH_EAST, {0.25f, 0.0f, -0.25f}}, + {flow_dir_t::SOUTH, {0.25f, 0.0f, 0.0f}}, + {flow_dir_t::SOUTH_WEST, {0.25f, 0.0f, 0.25f}}, + {flow_dir_t::WEST, {0.0f, 0.0f, 0.25f}}, + {flow_dir_t::NORTH_WEST, {-0.25f, 0.0f, 0.25f}}, + }; + + for (size_t y = 0; y < flow_field->get_size(); ++y) { + for (size_t x = 0; x < flow_field->get_size(); ++x) { + auto cell = flow_field->get_cell(x, y); + if (cell & FLOW_PATHABLE) { + Eigen::Affine3f arrow_model = Eigen::Affine3f::Identity(); + arrow_model.translate(Eigen::Vector3f(y + 0.5, 0, -1.0f * x - 0.5)); + auto dir = static_cast(cell & FLOW_DIR_MASK); + arrow_model.translate(offset_dir.at(dir)); + + auto rotation_rad = (cell & FLOW_DIR_MASK) * -45 * math::DEGSPERRAD; + arrow_model.rotate(Eigen::AngleAxisf(rotation_rad, Eigen::Vector3f::UnitY())); + auto arrow_unifs = ff_shader->new_uniform_input( + "model", + arrow_model.matrix(), + "view", + camera->get_view_matrix(), + "proj", + camera->get_projection_matrix(), + "color", + Eigen::Vector4f{0.0f, 0.0f, 0.0f, 1.0f}); + auto arrow_mesh = get_arrow_mesh(); + auto arrow_geometry = renderer->add_mesh_geometry(arrow_mesh); + renderer::Renderable arrow_renderable{ + arrow_unifs, + arrow_geometry, + true, + true, + }; + field_pass->add_renderables(arrow_renderable); + } + } + } } } - } + }); // Create object for the grid auto grid_unifs = grid_shader->new_uniform_input( From d24bc06e88a6e2bc7eb9878f3099e34498c1cc3c Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 24 Feb 2024 01:36:00 +0100 Subject: [PATCH 318/771] path: Reset flow field values on build. --- libopenage/pathfinding/flow_field.cpp | 9 +++++++++ libopenage/pathfinding/flow_field.h | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 27e6e14560..63d7bca860 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -39,6 +39,9 @@ void FlowField::build(const std::shared_ptr &integrate_field) << " does not match flow field size " << this->get_size() << "x" << this->get_size()); + // Reset the flow field. + this->reset(); + auto &integrate_cells = integrate_field->get_cells(); auto &flow_cells = this->cells; @@ -124,4 +127,10 @@ const std::vector &FlowField::get_cells() const { return this->cells; } +void FlowField::reset() { + for (auto &cell : this->cells) { + cell = 0; + } +} + } // namespace openage::path diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index 7fc3f00470..2fefd251ba 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -71,6 +71,11 @@ class FlowField { const std::vector &get_cells() const; private: + /** + * Reset the flow field values for rebuilding the field. + */ + void reset(); + /** * Side length of the field. */ From 0b0774ed2bfab9938b94d852e5fadf1c28cd2649 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 24 Feb 2024 01:36:49 +0100 Subject: [PATCH 319/771] path: Change goal with right mouse button. --- libopenage/pathfinding/demo/demo_0.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 20ab91345f..0b101a7266 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -3,6 +3,7 @@ #include "demo_0.h" #include +#include #include "pathfinding/cost_field.h" #include "pathfinding/flow_field.h" @@ -596,6 +597,27 @@ void path_demo_0(const util::Path &path) { auto flow_field = std::make_shared(10); flow_field->build(integration_field); + window.add_mouse_button_callback([&](const QMouseEvent &ev) { + if (ev.type() == QEvent::MouseButtonRelease) { + if (ev.button() == Qt::RightButton) { + auto grid_plane_normal = Eigen::Vector3f{0, 1, 0}; + auto grid_plane_point = Eigen::Vector3f{0, 0, 0}; + auto camera_direction = renderer::camera::cam_direction; + auto camera_position = camera->get_input_pos( + coord::input{ev.position().x(), ev.position().y()}); + + Eigen::Vector3f intersect = camera_position + camera_direction * (grid_plane_point - camera_position).dot(grid_plane_normal) / camera_direction.dot(grid_plane_normal); + auto grid_x = static_cast(-1 * intersect[2]); + auto grid_y = static_cast(intersect[0]); + + if (grid_x >= 0 && grid_x < 10 && grid_y >= 0 && grid_y < 10) { + integration_field->integrate(cost_field, grid_x, grid_y); + flow_field->build(integration_field); + } + } + } + }); + // Create object for the cost field // this will be shown at the start Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); From 89b210d5d7831c549cfd4865d18149d9de9fcfee Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 25 Feb 2024 00:44:47 +0100 Subject: [PATCH 320/771] path: Create RenderManager in demo for all graphics code. --- libopenage/pathfinding/demo/demo_0.cpp | 600 +++++++++++++++++++++++++ libopenage/pathfinding/demo/demo_0.h | 120 ++++- 2 files changed, 718 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 0b101a7266..2649cb8dd1 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -781,4 +781,604 @@ void path_demo_0(const util::Path &path) { window.close(); } + +RenderManager::RenderManager(const std::shared_ptr &app, + const std::shared_ptr &window, + const util::Path &path) : + path{path}, + app{app}, + window{window}, + renderer{window->make_renderer()}, + camera{std::make_shared(renderer, window->get_size())} { + // Position camera to look at center of the grid + camera->look_at_coord({5, 5, 0}); + window->add_resize_callback([&](size_t w, size_t h, double /*scale*/) { + camera->resize(w, h); + }); + + this->init_shaders(); + this->init_passes(); +} + + +void RenderManager::run() { + while (not this->window->should_close()) { + this->app->process_events(); + + this->renderer->render(this->background_pass); + this->renderer->render(this->field_pass); + this->renderer->render(this->grid_pass); + this->renderer->render(this->display_pass); + + this->window->update(); + + this->renderer->check_error(); + } + this->window->close(); +} + +void RenderManager::show_cost_field(const std::shared_ptr &field) { + Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); + auto unifs = this->cost_shader->new_uniform_input( + "model", + model, + "view", + this->camera->get_view_matrix(), + "proj", + this->camera->get_projection_matrix()); + auto mesh = RenderManager::get_cost_field_mesh(field); + auto geometry = this->renderer->add_mesh_geometry(mesh); + renderer::Renderable renderable{ + unifs, + geometry, + true, + true, + }; + this->field_pass->set_renderables({renderable}); +} + +void RenderManager::show_integration_field(const std::shared_ptr &field) { + Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); + auto unifs = this->integration_shader->new_uniform_input( + "model", + model, + "view", + this->camera->get_view_matrix(), + "proj", + this->camera->get_projection_matrix()); + auto mesh = get_integration_field_mesh(field, 4); + auto geometry = this->renderer->add_mesh_geometry(mesh); + renderer::Renderable renderable{ + unifs, + geometry, + true, + true, + }; + this->field_pass->set_renderables({renderable}); +} + +void RenderManager::show_flow_field(const std::shared_ptr &field) { + Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); + auto unifs = this->flow_shader->new_uniform_input( + "model", + model, + "view", + this->camera->get_view_matrix(), + "proj", + this->camera->get_projection_matrix(), + "color", + Eigen::Vector4f{192.0 / 256, 255.0 / 256, 64.0 / 256, 1.0}); + auto mesh = get_flow_field_mesh(field); + auto geometry = this->renderer->add_mesh_geometry(mesh); + renderer::Renderable renderable{ + unifs, + geometry, + true, + true, + }; + this->field_pass->set_renderables({renderable}); +} + +void RenderManager::init_shaders() { + // Shader sources + auto shaderdir = this->path / "assets" / "test" / "shaders" / "pathfinding"; + + // Shader for rendering the cost field + auto cf_vshader_file = shaderdir / "demo_0_cost_field.vert.glsl"; + auto cf_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + cf_vshader_file); + + auto cf_fshader_file = shaderdir / "demo_0_cost_field.frag.glsl"; + auto cf_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + cf_fshader_file); + + // Shader for rendering the integration field + auto if_vshader_file = shaderdir / "demo_0_integration_field.vert.glsl"; + auto if_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + if_vshader_file); + + auto if_fshader_file = shaderdir / "demo_0_integration_field.frag.glsl"; + auto if_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + if_fshader_file); + + // Shader for rendering the flow field + auto ff_vshader_file = shaderdir / "demo_0_flow_field.vert.glsl"; + auto ff_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + ff_vshader_file); + + auto ff_fshader_file = shaderdir / "demo_0_flow_field.frag.glsl"; + auto ff_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + ff_fshader_file); + + // Shader for rendering the grid + auto grid_vshader_file = shaderdir / "demo_0_grid.vert.glsl"; + auto grid_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + grid_vshader_file); + + auto grid_fshader_file = shaderdir / "demo_0_grid.frag.glsl"; + auto grid_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + grid_fshader_file); + + // Shader for monocolored objects + auto obj_vshader_file = shaderdir / "demo_0_obj.vert.glsl"; + auto obj_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + obj_vshader_file); + + auto obj_fshader_file = shaderdir / "demo_0_obj.frag.glsl"; + auto obj_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + obj_fshader_file); + + // Shader for rendering to the display target + auto display_vshader_file = shaderdir / "demo_0_display.vert.glsl"; + auto display_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + display_vshader_file); + + auto display_fshader_file = shaderdir / "demo_0_display.frag.glsl"; + auto display_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + display_fshader_file); + + // Create the shaders + this->cost_shader = renderer->add_shader({cf_vshader_src, cf_fshader_src}); + this->integration_shader = renderer->add_shader({if_vshader_src, if_fshader_src}); + this->flow_shader = renderer->add_shader({ff_vshader_src, ff_fshader_src}); + this->grid_shader = renderer->add_shader({grid_vshader_src, grid_fshader_src}); + this->obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src}); + this->display_shader = renderer->add_shader({display_vshader_src, display_fshader_src}); +} + +void RenderManager::init_passes() { + auto size = this->window->get_size(); + + // Make a framebuffer for the background render pass to draw into + auto background_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto fbo = renderer->create_texture_target({background_texture, depth_texture}); + this->background_pass = renderer->add_render_pass({}, fbo); + + // Make a framebuffer for the field render passes to draw into + auto field_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture_2 = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto field_fbo = renderer->create_texture_target({field_texture, depth_texture_2}); + this->field_pass = renderer->add_render_pass({}, field_fbo); + + // Make a framebuffer for the grid render passes to draw into + auto grid_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture_3 = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto grid_fbo = renderer->create_texture_target({grid_texture, depth_texture_3}); + this->grid_pass = renderer->add_render_pass({}, grid_fbo); + + // Make two objects that draw the results of the previous passes onto the screen + // in the display render pass + auto bg_texture_unif = display_shader->new_uniform_input("color_texture", background_texture); + auto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); + renderer::Renderable bg_pass_obj{ + bg_texture_unif, + quad, + true, + true, + }; + auto field_texture_unif = display_shader->new_uniform_input("color_texture", field_texture); + renderer::Renderable field_pass_obj{ + field_texture_unif, + quad, + true, + true, + }; + auto grid_texture_unif = display_shader->new_uniform_input("color_texture", grid_texture); + renderer::Renderable grid_pass_obj{ + grid_texture_unif, + quad, + true, + true, + }; + this->display_pass = renderer->add_render_pass( + {bg_pass_obj, field_pass_obj, grid_pass_obj}, + renderer->get_display_target()); +} + +renderer::resources::MeshData RenderManager::get_cost_field_mesh(const std::shared_ptr &field, + size_t resolution) { + // increase by 1 in every dimension because to get the vertex length + // of each dimension + util::Vector2s size{ + field->get_size() * resolution + 1, + field->get_size() * resolution + 1, + }; + auto vert_distance = 1.0f / resolution; + + // add vertices for the cells of the grid + std::vector verts{}; + auto vert_count = size[0] * size[1]; + verts.reserve(vert_count * 4); + for (int i = 0; i < static_cast(size[0]); ++i) { + for (int j = 0; j < static_cast(size[1]); ++j) { + // for each vertex, compare the surrounding tiles + std::vector surround{}; + if (j - 1 >= 0 and i - 1 >= 0) { + auto cost = field->get_cost((i - 1) / resolution, (j - 1) / resolution); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i - 1 >= 0) { + auto cost = field->get_cost((i - 1) / resolution, j / resolution); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { + auto cost = field->get_cost(i / resolution, j / resolution); + surround.push_back(cost); + } + if (j - 1 >= 0 and i < static_cast(field->get_size())) { + auto cost = field->get_cost(i / resolution, (j - 1) / resolution); + surround.push_back(cost); + } + // use the cost of the most expensive surrounding tile + auto max_cost = *std::max_element(surround.begin(), surround.end()); + + coord::scene3 v{ + static_cast(i * vert_distance), + static_cast(j * vert_distance), + 0, + }; + auto world_v = v.to_world_space(); + verts.push_back(world_v[0]); + verts.push_back(world_v[1]); + verts.push_back(world_v[2]); + verts.push_back(max_cost); + } + } + + // split the grid into triangles using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we split each tile into two triangles + // with counter-clockwise vertex order + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + } + } + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLES, + renderer::resources::index_t::U16}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); + + return {std::move(vert_data), std::move(idx_data), info}; +} + +renderer::resources::MeshData RenderManager::get_integration_field_mesh(const std::shared_ptr &field, + size_t resolution) { + // increase by 1 in every dimension because to get the vertex length + // of each dimension + util::Vector2s size{ + field->get_size() * resolution + 1, + field->get_size() * resolution + 1, + }; + auto vert_distance = 1.0f / resolution; + + // add vertices for the cells of the grid + std::vector verts{}; + auto vert_count = size[0] * size[1]; + verts.reserve(vert_count * 4); + for (int i = 0; i < static_cast(size[0]); ++i) { + for (int j = 0; j < static_cast(size[1]); ++j) { + // for each vertex, compare the surrounding tiles + std::vector surround{}; + if (j - 1 >= 0 and i - 1 >= 0) { + auto cost = field->get_cell((i - 1) / resolution, (j - 1) / resolution); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i - 1 >= 0) { + auto cost = field->get_cell((i - 1) / resolution, j / resolution); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { + auto cost = field->get_cell(i / resolution, j / resolution); + surround.push_back(cost); + } + if (j - 1 >= 0 and i < static_cast(field->get_size())) { + auto cost = field->get_cell(i / resolution, (j - 1) / resolution); + surround.push_back(cost); + } + // use the cost of the most expensive surrounding tile + auto max_cost = *std::max_element(surround.begin(), surround.end()); + + coord::scene3 v{ + static_cast(i * vert_distance), + static_cast(j * vert_distance), + 0, + }; + auto world_v = v.to_world_space(); + verts.push_back(world_v[0]); + verts.push_back(world_v[1]); + verts.push_back(world_v[2]); + verts.push_back(max_cost); + } + } + + // split the grid into triangles using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we split each tile into two triangles + // with counter-clockwise vertex order + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + } + } + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLES, + renderer::resources::index_t::U16}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); + + return {std::move(vert_data), std::move(idx_data), info}; +} + +renderer::resources::MeshData RenderManager::get_flow_field_mesh(const std::shared_ptr &field, + size_t resolution) { + // increase by 1 in every dimension because to get the vertex length + // of each dimension + util::Vector2s size{ + field->get_size() * resolution + 1, + field->get_size() * resolution + 1, + }; + auto vert_distance = 1.0f / resolution; + + // add vertices for the cells of the grid + std::vector verts{}; + auto vert_count = size[0] * size[1]; + verts.reserve(vert_count * 4); + for (int i = 0; i < static_cast(size[0]); ++i) { + for (int j = 0; j < static_cast(size[1]); ++j) { + // for each vertex, compare the surrounding tiles + std::vector surround{}; + if (j - 1 >= 0 and i - 1 >= 0) { + auto cost = field->get_cell((i - 1) / resolution, (j - 1) / resolution); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i - 1 >= 0) { + auto cost = field->get_cell((i - 1) / resolution, j / resolution); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { + auto cost = field->get_cell(i / resolution, j / resolution); + surround.push_back(cost); + } + if (j - 1 >= 0 and i < static_cast(field->get_size())) { + auto cost = field->get_cell(i / resolution, (j - 1) / resolution); + surround.push_back(cost); + } + // use the cost of the most expensive surrounding tile + auto max_cost = *std::max_element(surround.begin(), surround.end()); + + coord::scene3 v{ + static_cast(i * vert_distance), + static_cast(j * vert_distance), + 0, + }; + auto world_v = v.to_world_space(); + verts.push_back(world_v[0]); + verts.push_back(world_v[1]); + verts.push_back(world_v[2]); + verts.push_back(max_cost); + } + } + + // split the grid into triangles using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we split each tile into two triangles + // with counter-clockwise vertex order + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + } + } + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLES, + renderer::resources::index_t::U16}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); + + return {std::move(vert_data), std::move(idx_data), info}; +} + +renderer::resources::MeshData RenderManager::get_arrow_mesh() { + // vertices for the arrow + // x, y, z + std::vector verts{ + // clang-format off + 0.2f, 0.01f, -0.05f, + 0.2f, 0.01f, 0.05f, + -0.2f, 0.01f, -0.05f, + -0.2f, 0.01f, 0.05f, + // clang-format on + }; + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLE_STRIP}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + return {std::move(vert_data), info}; +} + +renderer::resources::MeshData RenderManager::get_grid_mesh(size_t side_length) { + // increase by 1 in every dimension because to get the vertex length + // of each dimension + util::Vector2s size{side_length + 1, side_length + 1}; + + // add vertices for the cells of the grid + std::vector verts{}; + auto vert_count = size[0] * size[1]; + verts.reserve(vert_count * 3); + for (int i = 0; i < (int)size[0]; ++i) { + for (int j = 0; j < (int)size[1]; ++j) { + coord::scene3 v{ + static_cast(i), + static_cast(j), + 0, + }; + auto world_v = v.to_world_space(); + verts.push_back(world_v[0]); + verts.push_back(world_v[1]); + verts.push_back(world_v[2]); + } + } + + // split the grid into lines using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 8); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we just draw a square using the 4 vertices + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + i * size[1]); // bottom left + } + } + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::LINES, + renderer::resources::index_t::U16}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); + + return {std::move(vert_data), std::move(idx_data), info}; +} + + } // namespace openage::path::tests diff --git a/libopenage/pathfinding/demo/demo_0.h b/libopenage/pathfinding/demo/demo_0.h index 5758b9c33e..4ab4a8d327 100644 --- a/libopenage/pathfinding/demo/demo_0.h +++ b/libopenage/pathfinding/demo/demo_0.h @@ -2,10 +2,37 @@ #pragma once +#include + #include "util/path.h" -namespace openage::path::tests { +namespace openage { +namespace renderer { +class RenderPass; +class Renderer; +class ShaderProgram; +class Window; + +namespace camera { +class Camera; +} // namespace camera + +namespace gui { +class GuiApplicationWithLogger; +} // namespace gui + +namespace resources { +class MeshData; +} // namespace resources +} // namespace renderer + +namespace path { +class CostField; +class IntegrationField; +class FlowField; + +namespace tests { /** * Show the pathfinding functionality of the path module: @@ -20,4 +47,93 @@ namespace openage::path::tests { void path_demo_0(const util::Path &path); -} // namespace openage::path::tests +class RenderManager { +public: + RenderManager(const std::shared_ptr &app, + const std::shared_ptr &window, + const util::Path &path); + ~RenderManager() = default; + + void run(); + + void show_cost_field(const std::shared_ptr &field); + void show_integration_field(const std::shared_ptr &field); + void show_flow_field(const std::shared_ptr &field); + +private: + void init_shaders(); + void init_passes(); + + + /** + * Create a mesh for the cost field. + * + * @param field Cost field to visualize. + * @param resolution Determines the number of subdivisions per grid cell. The number of + * quads per cell is resolution^2. (default = 2) + * + * @return Mesh data for the cost field. + */ + static renderer::resources::MeshData get_cost_field_mesh(const std::shared_ptr &field, + size_t resolution = 2); + + /** + * Create a mesh for the integration field. + * + * @param field Integration field to visualize. + * @param resolution Determines the number of subdivisions per grid cell. The number of + * quads per cell is resolution^2. (default = 2) + * + * @return Mesh data for the integration field. + */ + static renderer::resources::MeshData get_integration_field_mesh(const std::shared_ptr &field, + size_t resolution = 2); + + /** + * Create a mesh for the flow field. + * + * @param field Flow field to visualize. + */ + static renderer::resources::MeshData get_flow_field_mesh(const std::shared_ptr &field, + size_t resolution = 2); + + /** + * Create a mesh for an arrow. + * + * @return Mesh data for an arrow. + */ + static renderer::resources::MeshData get_arrow_mesh(); + + /** + * Create a mesh for the grid. + * + * @param side_length Length of the grid's side. + * + * @return Mesh data for the grid. + */ + static renderer::resources::MeshData get_grid_mesh(size_t side_length); + + + const util::Path &path; + + std::shared_ptr app; + std::shared_ptr window; + std::shared_ptr renderer; + std::shared_ptr camera; + + std::shared_ptr cost_shader; + std::shared_ptr integration_shader; + std::shared_ptr flow_shader; + std::shared_ptr grid_shader; + std::shared_ptr obj_shader; + std::shared_ptr display_shader; + + std::shared_ptr background_pass; + std::shared_ptr field_pass; + std::shared_ptr grid_pass; + std::shared_ptr display_pass; +}; + +} // namespace tests +} // namespace path +} // namespace openage From 53bee9dfe4d3d953a7ccef38603158654b04c1e8 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 25 Feb 2024 01:22:47 +0100 Subject: [PATCH 321/771] path: Remove old renderer code from demo. --- libopenage/pathfinding/demo/demo_0.cpp | 852 ++++--------------------- libopenage/pathfinding/demo/demo_0.h | 119 +++- 2 files changed, 245 insertions(+), 726 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 2649cb8dd1..796c1cc97e 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -22,567 +22,13 @@ namespace openage::path::tests { -/** - * Create a mesh for the cost field. - * - * @param field Cost field to visualize. - * @param resolution Determines the number of subdivisions per grid cell. The number of - * quads per cell is resolution^2. (default = 2) - * - * @return Mesh data for the cost field. - */ -renderer::resources::MeshData get_cost_field_mesh(const std::shared_ptr &field, - size_t resolution = 2) { - // increase by 1 in every dimension because to get the vertex length - // of each dimension - util::Vector2s size{ - field->get_size() * resolution + 1, - field->get_size() * resolution + 1, - }; - auto vert_distance = 1.0f / resolution; - - // add vertices for the cells of the grid - std::vector verts{}; - auto vert_count = size[0] * size[1]; - verts.reserve(vert_count * 4); - for (int i = 0; i < static_cast(size[0]); ++i) { - for (int j = 0; j < static_cast(size[1]); ++j) { - // for each vertex, compare the surrounding tiles - std::vector surround{}; - if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cost((i - 1) / resolution, (j - 1) / resolution); - surround.push_back(cost); - } - if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cost((i - 1) / resolution, j / resolution); - surround.push_back(cost); - } - if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cost(i / resolution, j / resolution); - surround.push_back(cost); - } - if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cost(i / resolution, (j - 1) / resolution); - surround.push_back(cost); - } - // use the cost of the most expensive surrounding tile - auto max_cost = *std::max_element(surround.begin(), surround.end()); - - coord::scene3 v{ - static_cast(i * vert_distance), - static_cast(j * vert_distance), - 0, - }; - auto world_v = v.to_world_space(); - verts.push_back(world_v[0]); - verts.push_back(world_v[1]); - verts.push_back(world_v[2]); - verts.push_back(max_cost); - } - } - - // split the grid into triangles using an index array - std::vector idxs; - idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); - // iterate over all tiles in the grid by columns, i.e. starting - // from the left corner to the bottom corner if you imagine it from - // the camera's point of view - for (size_t i = 0; i < size[0] - 1; ++i) { - for (size_t j = 0; j < size[1] - 1; ++j) { - // since we are working on tiles, we split each tile into two triangles - // with counter-clockwise vertex order - idxs.push_back(j + i * size[1]); // bottom left - idxs.push_back(j + 1 + i * size[1]); // bottom right - idxs.push_back(j + size[1] + i * size[1]); // top left - idxs.push_back(j + 1 + i * size[1]); // bottom right - idxs.push_back(j + size[1] + 1 + i * size[1]); // top right - idxs.push_back(j + size[1] + i * size[1]); // top left - } - } - - renderer::resources::VertexInputInfo info{ - {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32}, - renderer::resources::vertex_layout_t::AOS, - renderer::resources::vertex_primitive_t::TRIANGLES, - renderer::resources::index_t::U16}; - - auto const vert_data_size = verts.size() * sizeof(float); - std::vector vert_data(vert_data_size); - std::memcpy(vert_data.data(), verts.data(), vert_data_size); - - auto const idx_data_size = idxs.size() * sizeof(uint16_t); - std::vector idx_data(idx_data_size); - std::memcpy(idx_data.data(), idxs.data(), idx_data_size); - - return {std::move(vert_data), std::move(idx_data), info}; -} - - -/** - * Create a mesh for the integration field. - * - * @param field Integration field to visualize. - * @param resolution Determines the number of subdivisions per grid cell. The number of - * quads per cell is resolution^2. (default = 2) - * - * @return Mesh data for the integration field. - */ -renderer::resources::MeshData get_integration_field_mesh(const std::shared_ptr &field, - size_t resolution = 2) { - // increase by 1 in every dimension because to get the vertex length - // of each dimension - util::Vector2s size{ - field->get_size() * resolution + 1, - field->get_size() * resolution + 1, - }; - auto vert_distance = 1.0f / resolution; - - // add vertices for the cells of the grid - std::vector verts{}; - auto vert_count = size[0] * size[1]; - verts.reserve(vert_count * 4); - for (int i = 0; i < static_cast(size[0]); ++i) { - for (int j = 0; j < static_cast(size[1]); ++j) { - // for each vertex, compare the surrounding tiles - std::vector surround{}; - if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cell((i - 1) / resolution, (j - 1) / resolution); - surround.push_back(cost); - } - if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cell((i - 1) / resolution, j / resolution); - surround.push_back(cost); - } - if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cell(i / resolution, j / resolution); - surround.push_back(cost); - } - if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cell(i / resolution, (j - 1) / resolution); - surround.push_back(cost); - } - // use the cost of the most expensive surrounding tile - auto max_cost = *std::max_element(surround.begin(), surround.end()); - - coord::scene3 v{ - static_cast(i * vert_distance), - static_cast(j * vert_distance), - 0, - }; - auto world_v = v.to_world_space(); - verts.push_back(world_v[0]); - verts.push_back(world_v[1]); - verts.push_back(world_v[2]); - verts.push_back(max_cost); - } - } - - // split the grid into triangles using an index array - std::vector idxs; - idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); - // iterate over all tiles in the grid by columns, i.e. starting - // from the left corner to the bottom corner if you imagine it from - // the camera's point of view - for (size_t i = 0; i < size[0] - 1; ++i) { - for (size_t j = 0; j < size[1] - 1; ++j) { - // since we are working on tiles, we split each tile into two triangles - // with counter-clockwise vertex order - idxs.push_back(j + i * size[1]); // bottom left - idxs.push_back(j + 1 + i * size[1]); // bottom right - idxs.push_back(j + size[1] + i * size[1]); // top left - idxs.push_back(j + 1 + i * size[1]); // bottom right - idxs.push_back(j + size[1] + 1 + i * size[1]); // top right - idxs.push_back(j + size[1] + i * size[1]); // top left - } - } - - renderer::resources::VertexInputInfo info{ - {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32}, - renderer::resources::vertex_layout_t::AOS, - renderer::resources::vertex_primitive_t::TRIANGLES, - renderer::resources::index_t::U16}; - - auto const vert_data_size = verts.size() * sizeof(float); - std::vector vert_data(vert_data_size); - std::memcpy(vert_data.data(), verts.data(), vert_data_size); - - auto const idx_data_size = idxs.size() * sizeof(uint16_t); - std::vector idx_data(idx_data_size); - std::memcpy(idx_data.data(), idxs.data(), idx_data_size); - - return {std::move(vert_data), std::move(idx_data), info}; -} - -/** - * Create a mesh for the flow field. - * - * @param field Flow field to visualize. - */ -renderer::resources::MeshData get_flow_field_mesh(const std::shared_ptr &field, - size_t resolution = 2) { - // increase by 1 in every dimension because to get the vertex length - // of each dimension - util::Vector2s size{ - field->get_size() * resolution + 1, - field->get_size() * resolution + 1, - }; - auto vert_distance = 1.0f / resolution; - - // add vertices for the cells of the grid - std::vector verts{}; - auto vert_count = size[0] * size[1]; - verts.reserve(vert_count * 4); - for (int i = 0; i < static_cast(size[0]); ++i) { - for (int j = 0; j < static_cast(size[1]); ++j) { - // for each vertex, compare the surrounding tiles - std::vector surround{}; - if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cell((i - 1) / resolution, (j - 1) / resolution); - surround.push_back(cost); - } - if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cell((i - 1) / resolution, j / resolution); - surround.push_back(cost); - } - if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cell(i / resolution, j / resolution); - surround.push_back(cost); - } - if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cell(i / resolution, (j - 1) / resolution); - surround.push_back(cost); - } - // use the cost of the most expensive surrounding tile - auto max_cost = *std::max_element(surround.begin(), surround.end()); - - coord::scene3 v{ - static_cast(i * vert_distance), - static_cast(j * vert_distance), - 0, - }; - auto world_v = v.to_world_space(); - verts.push_back(world_v[0]); - verts.push_back(world_v[1]); - verts.push_back(world_v[2]); - verts.push_back(max_cost); - } - } - - // split the grid into triangles using an index array - std::vector idxs; - idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); - // iterate over all tiles in the grid by columns, i.e. starting - // from the left corner to the bottom corner if you imagine it from - // the camera's point of view - for (size_t i = 0; i < size[0] - 1; ++i) { - for (size_t j = 0; j < size[1] - 1; ++j) { - // since we are working on tiles, we split each tile into two triangles - // with counter-clockwise vertex order - idxs.push_back(j + i * size[1]); // bottom left - idxs.push_back(j + 1 + i * size[1]); // bottom right - idxs.push_back(j + size[1] + i * size[1]); // top left - idxs.push_back(j + 1 + i * size[1]); // bottom right - idxs.push_back(j + size[1] + 1 + i * size[1]); // top right - idxs.push_back(j + size[1] + i * size[1]); // top left - } - } - - renderer::resources::VertexInputInfo info{ - {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32}, - renderer::resources::vertex_layout_t::AOS, - renderer::resources::vertex_primitive_t::TRIANGLES, - renderer::resources::index_t::U16}; - - auto const vert_data_size = verts.size() * sizeof(float); - std::vector vert_data(vert_data_size); - std::memcpy(vert_data.data(), verts.data(), vert_data_size); - - auto const idx_data_size = idxs.size() * sizeof(uint16_t); - std::vector idx_data(idx_data_size); - std::memcpy(idx_data.data(), idxs.data(), idx_data_size); - - return {std::move(vert_data), std::move(idx_data), info}; -} - -/** - * Create a mesh for an arrow. - * - * @return Mesh data for an arrow. - */ -renderer::resources::MeshData get_arrow_mesh() { - // vertices for the arrow - // x, y, z - std::vector verts{ - // clang-format off - 0.2f, 0.01f, -0.05f, - 0.2f, 0.01f, 0.05f, - -0.2f, 0.01f, -0.05f, - -0.2f, 0.01f, 0.05f, - // clang-format on - }; - - renderer::resources::VertexInputInfo info{ - {renderer::resources::vertex_input_t::V3F32}, - renderer::resources::vertex_layout_t::AOS, - renderer::resources::vertex_primitive_t::TRIANGLE_STRIP}; - - auto const vert_data_size = verts.size() * sizeof(float); - std::vector vert_data(vert_data_size); - std::memcpy(vert_data.data(), verts.data(), vert_data_size); - - return {std::move(vert_data), info}; -} - - -/** - * Create a mesh for the grid. - * - * @param side_length Length of the grid's side. - * - * @return Mesh data for the grid. - */ -renderer::resources::MeshData get_grid_mesh(size_t side_length) { - // increase by 1 in every dimension because to get the vertex length - // of each dimension - util::Vector2s size{side_length + 1, side_length + 1}; - - // add vertices for the cells of the grid - std::vector verts{}; - auto vert_count = size[0] * size[1]; - verts.reserve(vert_count * 3); - for (int i = 0; i < (int)size[0]; ++i) { - for (int j = 0; j < (int)size[1]; ++j) { - coord::scene3 v{ - static_cast(i), - static_cast(j), - 0, - }; - auto world_v = v.to_world_space(); - verts.push_back(world_v[0]); - verts.push_back(world_v[1]); - verts.push_back(world_v[2]); - } - } - - // split the grid into lines using an index array - std::vector idxs; - idxs.reserve((size[0] - 1) * (size[1] - 1) * 8); - // iterate over all tiles in the grid by columns, i.e. starting - // from the left corner to the bottom corner if you imagine it from - // the camera's point of view - for (size_t i = 0; i < size[0] - 1; ++i) { - for (size_t j = 0; j < size[1] - 1; ++j) { - // since we are working on tiles, we just draw a square using the 4 vertices - idxs.push_back(j + i * size[1]); // bottom left - idxs.push_back(j + 1 + i * size[1]); // bottom right - idxs.push_back(j + 1 + i * size[1]); // bottom right - idxs.push_back(j + size[1] + 1 + i * size[1]); // top right - idxs.push_back(j + size[1] + 1 + i * size[1]); // top right - idxs.push_back(j + size[1] + i * size[1]); // top left - idxs.push_back(j + size[1] + i * size[1]); // top left - idxs.push_back(j + i * size[1]); // bottom left - } - } - - renderer::resources::VertexInputInfo info{ - {renderer::resources::vertex_input_t::V3F32}, - renderer::resources::vertex_layout_t::AOS, - renderer::resources::vertex_primitive_t::LINES, - renderer::resources::index_t::U16}; - - auto const vert_data_size = verts.size() * sizeof(float); - std::vector vert_data(vert_data_size); - std::memcpy(vert_data.data(), verts.data(), vert_data_size); - - auto const idx_data_size = idxs.size() * sizeof(uint16_t); - std::vector idx_data(idx_data_size); - std::memcpy(idx_data.data(), idxs.data(), idx_data_size); - - return {std::move(vert_data), std::move(idx_data), info}; -} - -void path_demo_0(const util::Path &path) { - auto qtapp = std::make_shared(); - - renderer::opengl::GlWindow window("openage pathfinding test", 1440, 720, true); - auto renderer = window.make_renderer(); - - // Camera for correct projection of terrain - auto camera = std::make_shared(renderer, window.get_size()); - camera->look_at_coord({5, 5, 0}); - window.add_resize_callback([&](size_t w, size_t h, double /*scale*/) { - camera->resize(w, h); - }); - - // Shader sources - auto shaderdir = path / "assets" / "test" / "shaders" / "pathfinding"; - - // Shader for rendering the cost field - auto cf_vshader_file = shaderdir / "demo_0_cost_field.vert.glsl"; - auto cf_vshader_src = renderer::resources::ShaderSource( - renderer::resources::shader_lang_t::glsl, - renderer::resources::shader_stage_t::vertex, - cf_vshader_file); - - auto cf_fshader_file = shaderdir / "demo_0_cost_field.frag.glsl"; - auto cf_fshader_src = renderer::resources::ShaderSource( - renderer::resources::shader_lang_t::glsl, - renderer::resources::shader_stage_t::fragment, - cf_fshader_file); - - // Shader for rendering the integration field - auto if_vshader_file = shaderdir / "demo_0_integration_field.vert.glsl"; - auto if_vshader_src = renderer::resources::ShaderSource( - renderer::resources::shader_lang_t::glsl, - renderer::resources::shader_stage_t::vertex, - if_vshader_file); - - auto if_fshader_file = shaderdir / "demo_0_integration_field.frag.glsl"; - auto if_fshader_src = renderer::resources::ShaderSource( - renderer::resources::shader_lang_t::glsl, - renderer::resources::shader_stage_t::fragment, - if_fshader_file); - - // Shader for rendering the flow field - auto ff_vshader_file = shaderdir / "demo_0_flow_field.vert.glsl"; - auto ff_vshader_src = renderer::resources::ShaderSource( - renderer::resources::shader_lang_t::glsl, - renderer::resources::shader_stage_t::vertex, - ff_vshader_file); - - auto ff_fshader_file = shaderdir / "demo_0_flow_field.frag.glsl"; - auto ff_fshader_src = renderer::resources::ShaderSource( - renderer::resources::shader_lang_t::glsl, - renderer::resources::shader_stage_t::fragment, - ff_fshader_file); - - // Shader for rendering the grid - auto grid_vshader_file = shaderdir / "demo_0_grid.vert.glsl"; - auto grid_vshader_src = renderer::resources::ShaderSource( - renderer::resources::shader_lang_t::glsl, - renderer::resources::shader_stage_t::vertex, - grid_vshader_file); - - auto grid_fshader_file = shaderdir / "demo_0_grid.frag.glsl"; - auto grid_fshader_src = renderer::resources::ShaderSource( - renderer::resources::shader_lang_t::glsl, - renderer::resources::shader_stage_t::fragment, - grid_fshader_file); - - // Shader for monocolored objects - auto obj_vshader_file = shaderdir / "demo_0_obj.vert.glsl"; - auto obj_vshader_src = renderer::resources::ShaderSource( - renderer::resources::shader_lang_t::glsl, - renderer::resources::shader_stage_t::vertex, - obj_vshader_file); - - auto obj_fshader_file = shaderdir / "demo_0_obj.frag.glsl"; - auto obj_fshader_src = renderer::resources::ShaderSource( - renderer::resources::shader_lang_t::glsl, - renderer::resources::shader_stage_t::fragment, - obj_fshader_file); - - // Shader for rendering to the display target - auto display_vshader_file = shaderdir / "demo_0_display.vert.glsl"; - auto display_vshader_src = renderer::resources::ShaderSource( - renderer::resources::shader_lang_t::glsl, - renderer::resources::shader_stage_t::vertex, - display_vshader_file); - - auto display_fshader_file = shaderdir / "demo_0_display.frag.glsl"; - auto display_fshader_src = renderer::resources::ShaderSource( - renderer::resources::shader_lang_t::glsl, - renderer::resources::shader_stage_t::fragment, - display_fshader_file); - - // Create the shaders - auto cf_shader = renderer->add_shader({cf_vshader_src, cf_fshader_src}); - auto if_shader = renderer->add_shader({if_vshader_src, if_fshader_src}); - auto ff_shader = renderer->add_shader({ff_vshader_src, ff_fshader_src}); - auto grid_shader = renderer->add_shader({grid_vshader_src, grid_fshader_src}); - auto obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src}); - auto display_shader = renderer->add_shader({display_vshader_src, display_fshader_src}); - - // Make a framebuffer for the background render pass to draw into - auto size = window.get_size(); - auto background_texture = renderer->add_texture( - renderer::resources::Texture2dInfo(size[0], - size[1], - renderer::resources::pixel_format::rgba8)); - auto depth_texture = renderer->add_texture( - renderer::resources::Texture2dInfo(size[0], - size[1], - renderer::resources::pixel_format::depth24)); - auto fbo = renderer->create_texture_target({background_texture, depth_texture}); - auto background_pass = renderer->add_render_pass({}, fbo); - - // Make a framebuffer for the field render passes to draw into - auto field_texture = renderer->add_texture( - renderer::resources::Texture2dInfo(size[0], - size[1], - renderer::resources::pixel_format::rgba8)); - auto depth_texture_2 = renderer->add_texture( - renderer::resources::Texture2dInfo(size[0], - size[1], - renderer::resources::pixel_format::depth24)); - auto field_fbo = renderer->create_texture_target({field_texture, depth_texture_2}); - auto field_pass = renderer->add_render_pass({}, field_fbo); - - // Make a framebuffer for the grid render passes to draw into - auto grid_texture = renderer->add_texture( - renderer::resources::Texture2dInfo(size[0], - size[1], - renderer::resources::pixel_format::rgba8)); - auto depth_texture_3 = renderer->add_texture( - renderer::resources::Texture2dInfo(size[0], - size[1], - renderer::resources::pixel_format::depth24)); - auto grid_fbo = renderer->create_texture_target({grid_texture, depth_texture_3}); - auto grid_pass = renderer->add_render_pass({}, grid_fbo); - - // Make two objects that draw the results of the previous passes onto the screen - // in the display render pass - auto bg_texture_unif = display_shader->new_uniform_input("color_texture", background_texture); - auto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); - renderer::Renderable bg_pass_obj{ - bg_texture_unif, - quad, - true, - true, - }; - auto field_texture_unif = display_shader->new_uniform_input("color_texture", field_texture); - renderer::Renderable field_pass_obj{ - field_texture_unif, - quad, - true, - true, - }; - auto grid_texture_unif = display_shader->new_uniform_input("color_texture", grid_texture); - renderer::Renderable grid_pass_obj{ - grid_texture_unif, - quad, - true, - true, - }; - auto display_pass = renderer->add_render_pass( - {bg_pass_obj, field_pass_obj, grid_pass_obj}, - renderer->get_display_target()); - - // Background object for contrast between field and display - auto background_unifs = obj_shader->new_uniform_input( - "color", - Eigen::Vector4f{64.0 / 256, 128.0 / 256, 196.0 / 256, 1.0}); - auto background = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); - renderer::Renderable background_obj{ - background_unifs, - background, - true, - true, - }; - background_pass->add_renderables(background_obj); +void path_demo_0(const util::Path &path) { + // Side length of the field + // Creates a 10x10 grid + auto field_length = 10; - // Create the pathfinding fields - auto cost_field = std::make_shared(10); + // Cost field with some obstacles + auto cost_field = std::make_shared(field_length); cost_field->set_cost(0, 0, 255); cost_field->set_cost(1, 0, 254); cost_field->set_cost(4, 3, 128); @@ -592,25 +38,30 @@ void path_demo_0(const util::Path &path) { cost_field->set_cost(5, 4, 128); cost_field->set_cost(6, 4, 128); - auto integration_field = std::make_shared(10); + // Create an integration field from the cost field + auto integration_field = std::make_shared(field_length); + + // Set cell (7, 7) to be the target cell integration_field->integrate(cost_field, 7, 7); - auto flow_field = std::make_shared(10); + + // Create a flow field from the integration field + auto flow_field = std::make_shared(field_length); flow_field->build(integration_field); - window.add_mouse_button_callback([&](const QMouseEvent &ev) { + // Render the grid and the pathfinding results + auto qtapp = std::make_shared(); + auto window = std::make_shared("openage pathfinding test", 1440, 720, true); + auto render_manager = std::make_shared(qtapp, window, path); + + // Enable mouse button callbacks + window->add_mouse_button_callback([&](const QMouseEvent &ev) { if (ev.type() == QEvent::MouseButtonRelease) { - if (ev.button() == Qt::RightButton) { - auto grid_plane_normal = Eigen::Vector3f{0, 1, 0}; - auto grid_plane_point = Eigen::Vector3f{0, 0, 0}; - auto camera_direction = renderer::camera::cam_direction; - auto camera_position = camera->get_input_pos( - coord::input{ev.position().x(), ev.position().y()}); - - Eigen::Vector3f intersect = camera_position + camera_direction * (grid_plane_point - camera_position).dot(grid_plane_normal) / camera_direction.dot(grid_plane_normal); - auto grid_x = static_cast(-1 * intersect[2]); - auto grid_y = static_cast(intersect[0]); - - if (grid_x >= 0 && grid_x < 10 && grid_y >= 0 && grid_y < 10) { + if (ev.button() == Qt::RightButton) { // Set target cell + auto tile_pos = render_manager->select_tile(ev.position().x(), ev.position().y()); + auto grid_x = tile_pos.first; + auto grid_y = tile_pos.second; + + if (grid_x >= 0 and grid_x < field_length and grid_y >= 0 and grid_y < field_length) { integration_field->integrate(cost_field, grid_x, grid_y); flow_field->build(integration_field); } @@ -618,167 +69,28 @@ void path_demo_0(const util::Path &path) { } }); - // Create object for the cost field - // this will be shown at the start - Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); - auto cost_field_unifs = cf_shader->new_uniform_input( - "model", - model, - "view", - camera->get_view_matrix(), - "proj", - camera->get_projection_matrix()); - auto cost_field_mesh = get_cost_field_mesh(cost_field); - auto cost_field_geometry = renderer->add_mesh_geometry(cost_field_mesh); - renderer::Renderable cost_field_renderable{ - cost_field_unifs, - cost_field_geometry, - true, - true, - }; - field_pass->add_renderables(cost_field_renderable); - - window.add_key_callback([&](const QKeyEvent &ev) { + window->add_key_callback([&](const QKeyEvent &ev) { if (ev.type() == QEvent::KeyRelease) { if (ev.key() == Qt::Key_F1) { // Show cost field - // Recreate object for the cost field - Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); - auto cost_field_unifs = cf_shader->new_uniform_input( - "model", - model, - "view", - camera->get_view_matrix(), - "proj", - camera->get_projection_matrix()); - auto cost_field_mesh = get_cost_field_mesh(cost_field); - auto cost_field_geometry = renderer->add_mesh_geometry(cost_field_mesh); - renderer::Renderable cost_field_renderable{ - cost_field_unifs, - cost_field_geometry, - true, - true, - }; - field_pass->set_renderables({cost_field_renderable}); + render_manager->show_cost_field(cost_field); } else if (ev.key() == Qt::Key_F2) { // Show integration field - // Create object for the integration field - auto integration_field_unifs = if_shader->new_uniform_input( - "model", - model, - "view", - camera->get_view_matrix(), - "proj", - camera->get_projection_matrix()); - auto integration_field_mesh = get_integration_field_mesh(integration_field, 4); - auto integration_field_geometry = renderer->add_mesh_geometry(integration_field_mesh); - renderer::Renderable integration_field_renderable{ - integration_field_unifs, - integration_field_geometry, - true, - true, - }; - field_pass->set_renderables({integration_field_renderable}); + render_manager->show_integration_field(integration_field); } else if (ev.key() == Qt::Key_F3) { // Show flow field - // Create object for the flow field - auto flow_field_unifs = ff_shader->new_uniform_input( - "model", - model, - "view", - camera->get_view_matrix(), - "proj", - camera->get_projection_matrix(), - "color", - Eigen::Vector4f{192.0 / 256, 255.0 / 256, 64.0 / 256, 1.0}); - auto flow_field_mesh = get_flow_field_mesh(flow_field); - auto flow_field_geometry = renderer->add_mesh_geometry(flow_field_mesh); - renderer::Renderable flow_field_renderable{ - flow_field_unifs, - flow_field_geometry, - true, - true, - }; - field_pass->set_renderables({flow_field_renderable}); + render_manager->show_flow_field(flow_field); } else if (ev.key() == Qt::Key_F4) { // Show steering vectors - static const std::unordered_map offset_dir{ - {flow_dir_t::NORTH, {-0.25f, 0.0f, 0.0f}}, - {flow_dir_t::NORTH_EAST, {-0.25f, 0.0f, -0.25f}}, - {flow_dir_t::EAST, {0.0f, 0.0f, -0.25f}}, - {flow_dir_t::SOUTH_EAST, {0.25f, 0.0f, -0.25f}}, - {flow_dir_t::SOUTH, {0.25f, 0.0f, 0.0f}}, - {flow_dir_t::SOUTH_WEST, {0.25f, 0.0f, 0.25f}}, - {flow_dir_t::WEST, {0.0f, 0.0f, 0.25f}}, - {flow_dir_t::NORTH_WEST, {-0.25f, 0.0f, 0.25f}}, - }; - - for (size_t y = 0; y < flow_field->get_size(); ++y) { - for (size_t x = 0; x < flow_field->get_size(); ++x) { - auto cell = flow_field->get_cell(x, y); - if (cell & FLOW_PATHABLE) { - Eigen::Affine3f arrow_model = Eigen::Affine3f::Identity(); - arrow_model.translate(Eigen::Vector3f(y + 0.5, 0, -1.0f * x - 0.5)); - auto dir = static_cast(cell & FLOW_DIR_MASK); - arrow_model.translate(offset_dir.at(dir)); - - auto rotation_rad = (cell & FLOW_DIR_MASK) * -45 * math::DEGSPERRAD; - arrow_model.rotate(Eigen::AngleAxisf(rotation_rad, Eigen::Vector3f::UnitY())); - auto arrow_unifs = ff_shader->new_uniform_input( - "model", - arrow_model.matrix(), - "view", - camera->get_view_matrix(), - "proj", - camera->get_projection_matrix(), - "color", - Eigen::Vector4f{0.0f, 0.0f, 0.0f, 1.0f}); - auto arrow_mesh = get_arrow_mesh(); - auto arrow_geometry = renderer->add_mesh_geometry(arrow_mesh); - renderer::Renderable arrow_renderable{ - arrow_unifs, - arrow_geometry, - true, - true, - }; - field_pass->add_renderables(arrow_renderable); - } - } - } + render_manager->show_vectors(flow_field); } } }); - // Create object for the grid - auto grid_unifs = grid_shader->new_uniform_input( - "model", - model, - "view", - camera->get_view_matrix(), - "proj", - camera->get_projection_matrix()); - auto grid_mesh = get_grid_mesh(10); - auto grid_geometry = renderer->add_mesh_geometry(grid_mesh); - renderer::Renderable grid_renderable{ - grid_unifs, - grid_geometry, - true, - true, - }; - grid_pass->add_renderables(grid_renderable); - - while (not window.should_close()) { - qtapp->process_events(); + // Show the cost field on startup + render_manager->show_cost_field(cost_field); - renderer->render(background_pass); - renderer->render(field_pass); - renderer->render(grid_pass); - renderer->render(display_pass); - - window.update(); - - renderer->check_error(); - } - window.close(); + // Run the render loop + render_manager->run(); } @@ -879,6 +191,66 @@ void RenderManager::show_flow_field(const std::shared_ptr &fiel this->field_pass->set_renderables({renderable}); } +void RenderManager::show_vectors(const std::shared_ptr &field) { + static const std::unordered_map offset_dir{ + {flow_dir_t::NORTH, {-0.25f, 0.0f, 0.0f}}, + {flow_dir_t::NORTH_EAST, {-0.25f, 0.0f, -0.25f}}, + {flow_dir_t::EAST, {0.0f, 0.0f, -0.25f}}, + {flow_dir_t::SOUTH_EAST, {0.25f, 0.0f, -0.25f}}, + {flow_dir_t::SOUTH, {0.25f, 0.0f, 0.0f}}, + {flow_dir_t::SOUTH_WEST, {0.25f, 0.0f, 0.25f}}, + {flow_dir_t::WEST, {0.0f, 0.0f, 0.25f}}, + {flow_dir_t::NORTH_WEST, {-0.25f, 0.0f, 0.25f}}, + }; + + for (size_t y = 0; y < field->get_size(); ++y) { + for (size_t x = 0; x < field->get_size(); ++x) { + auto cell = field->get_cell(x, y); + if (cell & FLOW_PATHABLE) { + Eigen::Affine3f arrow_model = Eigen::Affine3f::Identity(); + arrow_model.translate(Eigen::Vector3f(y + 0.5, 0, -1.0f * x - 0.5)); + auto dir = static_cast(cell & FLOW_DIR_MASK); + arrow_model.translate(offset_dir.at(dir)); + + auto rotation_rad = (cell & FLOW_DIR_MASK) * -45 * math::DEGSPERRAD; + arrow_model.rotate(Eigen::AngleAxisf(rotation_rad, Eigen::Vector3f::UnitY())); + auto arrow_unifs = this->flow_shader->new_uniform_input( + "model", + arrow_model.matrix(), + "view", + camera->get_view_matrix(), + "proj", + camera->get_projection_matrix(), + "color", + Eigen::Vector4f{0.0f, 0.0f, 0.0f, 1.0f}); + auto arrow_mesh = get_arrow_mesh(); + auto arrow_geometry = renderer->add_mesh_geometry(arrow_mesh); + renderer::Renderable arrow_renderable{ + arrow_unifs, + arrow_geometry, + true, + true, + }; + field_pass->add_renderables(arrow_renderable); + } + } + } +} + +std::pair RenderManager::select_tile(double x, double y) { + auto grid_plane_normal = Eigen::Vector3f{0, 1, 0}; + auto grid_plane_point = Eigen::Vector3f{0, 0, 0}; + auto camera_direction = renderer::camera::cam_direction; + auto camera_position = camera->get_input_pos( + coord::input{x, y}); + + Eigen::Vector3f intersect = camera_position + camera_direction * (grid_plane_point - camera_position).dot(grid_plane_normal) / camera_direction.dot(grid_plane_normal); + auto grid_x = static_cast(-1 * intersect[2]); + auto grid_y = static_cast(intersect[0]); + + return {grid_x, grid_y}; +} + void RenderManager::init_shaders() { // Shader sources auto shaderdir = this->path / "assets" / "test" / "shaders" / "pathfinding"; @@ -983,7 +355,19 @@ void RenderManager::init_passes() { size[1], renderer::resources::pixel_format::depth24)); auto fbo = renderer->create_texture_target({background_texture, depth_texture}); - this->background_pass = renderer->add_render_pass({}, fbo); + + // Background object for contrast between field and display + auto background_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{64.0 / 256, 128.0 / 256, 196.0 / 256, 1.0}); + auto background = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); + renderer::Renderable background_obj{ + background_unifs, + background, + true, + true, + }; + this->background_pass = renderer->add_render_pass({background_obj}, fbo); // Make a framebuffer for the field render passes to draw into auto field_texture = renderer->add_texture( @@ -1007,7 +391,25 @@ void RenderManager::init_passes() { size[1], renderer::resources::pixel_format::depth24)); auto grid_fbo = renderer->create_texture_target({grid_texture, depth_texture_3}); - this->grid_pass = renderer->add_render_pass({}, grid_fbo); + + // Create object for the grid + Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); + auto grid_unifs = grid_shader->new_uniform_input( + "model", + model, + "view", + camera->get_view_matrix(), + "proj", + camera->get_projection_matrix()); + auto grid_mesh = get_grid_mesh(10); + auto grid_geometry = renderer->add_mesh_geometry(grid_mesh); + renderer::Renderable grid_obj{ + grid_unifs, + grid_geometry, + true, + true, + }; + this->grid_pass = renderer->add_render_pass({grid_obj}, grid_fbo); // Make two objects that draw the results of the previous passes onto the screen // in the display render pass diff --git a/libopenage/pathfinding/demo/demo_0.h b/libopenage/pathfinding/demo/demo_0.h index 4ab4a8d327..2e31055f45 100644 --- a/libopenage/pathfinding/demo/demo_0.h +++ b/libopenage/pathfinding/demo/demo_0.h @@ -47,21 +47,77 @@ namespace tests { void path_demo_0(const util::Path &path); +/** + * Manages the graphical display of the pathfinding demo. + */ class RenderManager { public: + /** + * Create a new render manager. + * + * @param app GUI application. + * @param window Window to render to. + * @param path Path to the project rootdir. + */ RenderManager(const std::shared_ptr &app, const std::shared_ptr &window, const util::Path &path); ~RenderManager() = default; + /** + * Run the render loop. + */ void run(); + /** + * Draw a cost field to the screen. + * + * @param field Cost field. + */ void show_cost_field(const std::shared_ptr &field); + + /** + * Draw an integration field to the screen. + * + * @param field Integration field. + */ void show_integration_field(const std::shared_ptr &field); + + /** + * Draw a flow field to the screen. + * + * @param field Flow field. + */ void show_flow_field(const std::shared_ptr &field); + /** + * Draw the steering vectors of a flow field to the screen. + * + * @param field Flow field. + */ + void show_vectors(const std::shared_ptr &field); + + /** + * Get the cell coordinates at a given screen position. + * + * @param x X coordinate. + * @param y Y coordinate. + */ + std::pair select_tile(double x, double y); + private: + /** + * Load the shader sources for the demo and create the shader programs. + */ void init_shaders(); + + /** + * Create the following render passes for the demo: + * - Background pass: Mono-colored background object. + * - Field pass; Renders the cost, integration and flow fields. + * - Grid pass: Renders a grid on top of the fields. + * - Display pass: Draws the results of previous passes to the screen. + */ void init_passes(); @@ -113,24 +169,85 @@ class RenderManager { */ static renderer::resources::MeshData get_grid_mesh(size_t side_length); - + /** + * Path to the project rootdir. + */ const util::Path &path; + /* Renderer objects */ + + /** + * Qt GUI application. + */ std::shared_ptr app; + + /** + * openage window to render to. + */ std::shared_ptr window; + + /** + * openage renderer instance. + */ std::shared_ptr renderer; + + /** + * Camera to view the scene. + */ std::shared_ptr camera; + /* Shader programs */ + + /** + * Shader program for rendering a cost field. + */ std::shared_ptr cost_shader; + + /** + * Shader program for rendering a integration field. + */ std::shared_ptr integration_shader; + + /** + * Shader program for rendering a flow field. + */ std::shared_ptr flow_shader; + + /** + * Shader program for rendering a grid. + */ std::shared_ptr grid_shader; + + /** + * Shader program for rendering mono-colored objects. + */ std::shared_ptr obj_shader; + + /** + * Shader program for rendering the final display. + */ std::shared_ptr display_shader; + /* Render passes */ + + /** + * Background pass: Mono-colored background object. + */ std::shared_ptr background_pass; + + /** + * Field pass: Renders the cost, integration and flow fields. + */ std::shared_ptr field_pass; + + /** + * Grid pass: Renders a grid on top of the fields. + */ std::shared_ptr grid_pass; + + /** + * Display pass: Draws the results of previous passes to the screen. + */ std::shared_ptr display_pass; }; From e3005a8de83bec29a50bacbf51c49c45e3c0ea49 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 25 Feb 2024 01:32:41 +0100 Subject: [PATCH 322/771] path: Toggle vsibility of steering vectors. --- libopenage/pathfinding/demo/demo_0.cpp | 48 ++++++++++++++++++++++---- libopenage/pathfinding/demo/demo_0.h | 10 ++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 796c1cc97e..e31262101e 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -69,6 +69,10 @@ void path_demo_0(const util::Path &path) { } }); + // Make steering vector visibility toggleable + auto vectors_visible = false; + + // Enable key callbacks window->add_key_callback([&](const QKeyEvent &ev) { if (ev.type() == QEvent::KeyRelease) { if (ev.key() == Qt::Key_F1) { // Show cost field @@ -81,7 +85,14 @@ void path_demo_0(const util::Path &path) { render_manager->show_flow_field(flow_field); } else if (ev.key() == Qt::Key_F4) { // Show steering vectors - render_manager->show_vectors(flow_field); + if (vectors_visible) { + render_manager->hide_vectors(); + vectors_visible = false; + } + else { + render_manager->show_vectors(flow_field); + vectors_visible = true; + } } } }); @@ -119,6 +130,7 @@ void RenderManager::run() { this->renderer->render(this->background_pass); this->renderer->render(this->field_pass); + this->renderer->render(this->vector_pass); this->renderer->render(this->grid_pass); this->renderer->render(this->display_pass); @@ -203,6 +215,7 @@ void RenderManager::show_vectors(const std::shared_ptr &field) {flow_dir_t::NORTH_WEST, {-0.25f, 0.0f, 0.25f}}, }; + this->vector_pass->clear_renderables(); for (size_t y = 0; y < field->get_size(); ++y) { for (size_t x = 0; x < field->get_size(); ++x) { auto cell = field->get_cell(x, y); @@ -231,12 +244,16 @@ void RenderManager::show_vectors(const std::shared_ptr &field) true, true, }; - field_pass->add_renderables(arrow_renderable); + this->vector_pass->add_renderables(arrow_renderable); } } } } +void RenderManager::hide_vectors() { + this->vector_pass->clear_renderables(); +} + std::pair RenderManager::select_tile(double x, double y) { auto grid_plane_normal = Eigen::Vector3f{0, 1, 0}; auto grid_plane_point = Eigen::Vector3f{0, 0, 0}; @@ -369,7 +386,7 @@ void RenderManager::init_passes() { }; this->background_pass = renderer->add_render_pass({background_obj}, fbo); - // Make a framebuffer for the field render passes to draw into + // Make a framebuffer for the field render pass to draw into auto field_texture = renderer->add_texture( renderer::resources::Texture2dInfo(size[0], size[1], @@ -381,16 +398,28 @@ void RenderManager::init_passes() { auto field_fbo = renderer->create_texture_target({field_texture, depth_texture_2}); this->field_pass = renderer->add_render_pass({}, field_fbo); + // Make a framebuffer for the vector render passes to draw into + auto vector_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture_3 = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto vector_fbo = renderer->create_texture_target({vector_texture, depth_texture_3}); + this->vector_pass = renderer->add_render_pass({}, vector_fbo); + // Make a framebuffer for the grid render passes to draw into auto grid_texture = renderer->add_texture( renderer::resources::Texture2dInfo(size[0], size[1], renderer::resources::pixel_format::rgba8)); - auto depth_texture_3 = renderer->add_texture( + auto depth_texture_4 = renderer->add_texture( renderer::resources::Texture2dInfo(size[0], size[1], renderer::resources::pixel_format::depth24)); - auto grid_fbo = renderer->create_texture_target({grid_texture, depth_texture_3}); + auto grid_fbo = renderer->create_texture_target({grid_texture, depth_texture_4}); // Create object for the grid Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); @@ -428,6 +457,13 @@ void RenderManager::init_passes() { true, true, }; + auto vector_texture_unif = display_shader->new_uniform_input("color_texture", vector_texture); + renderer::Renderable vector_pass_obj{ + vector_texture_unif, + quad, + true, + true, + }; auto grid_texture_unif = display_shader->new_uniform_input("color_texture", grid_texture); renderer::Renderable grid_pass_obj{ grid_texture_unif, @@ -436,7 +472,7 @@ void RenderManager::init_passes() { true, }; this->display_pass = renderer->add_render_pass( - {bg_pass_obj, field_pass_obj, grid_pass_obj}, + {bg_pass_obj, field_pass_obj, vector_pass_obj, grid_pass_obj}, renderer->get_display_target()); } diff --git a/libopenage/pathfinding/demo/demo_0.h b/libopenage/pathfinding/demo/demo_0.h index 2e31055f45..19a0f9858d 100644 --- a/libopenage/pathfinding/demo/demo_0.h +++ b/libopenage/pathfinding/demo/demo_0.h @@ -97,6 +97,11 @@ class RenderManager { */ void show_vectors(const std::shared_ptr &field); + /** + * Hide drawn steering vectors. + */ + void hide_vectors(); + /** * Get the cell coordinates at a given screen position. * @@ -240,6 +245,11 @@ class RenderManager { */ std::shared_ptr field_pass; + /** + * Vector pass: Renders the steering vectors of a flow field. + */ + std::shared_ptr vector_pass; + /** * Grid pass: Renders a grid on top of the fields. */ From 94a423ee678dc930c5828eaf841fe26acb598f0c Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 25 Feb 2024 03:44:03 +0100 Subject: [PATCH 323/771] path: Change flow field tile colors depending on flags. --- .../pathfinding/demo_0_flow_field.frag.glsl | 17 ++++++++++++-- .../pathfinding/demo_0_flow_field.vert.glsl | 4 ++++ .../pathfinding/demo_0_vector.frag.glsl | 9 ++++++++ .../pathfinding/demo_0_vector.vert.glsl | 11 ++++++++++ libopenage/pathfinding/demo/demo_0.cpp | 22 ++++++++++++++----- libopenage/pathfinding/demo/demo_0.h | 7 +++++- 6 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 assets/test/shaders/pathfinding/demo_0_vector.frag.glsl create mode 100644 assets/test/shaders/pathfinding/demo_0_vector.vert.glsl diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl index ebbb10bc8f..51f4a66804 100644 --- a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl +++ b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl @@ -1,9 +1,22 @@ #version 330 -uniform vec4 color; +in float v_cost; out vec4 outcol; void main() { - outcol = color; + int cost = int(v_cost); + if (!bool(cost & 0x10)) { + // not pathable + outcol = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + + if (bool(cost & 0x20)) { + // line of sight + outcol = vec4(1.0, 1.0, 1.0, 1.0); + return; + } + + outcol = vec4(0.7, 0.7, 0.7, 1.0); } diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl index 8b6b015408..2c2190e57c 100644 --- a/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl +++ b/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl @@ -1,11 +1,15 @@ #version 330 layout(location=0) in vec3 position; +layout(location=1) in float cost; uniform mat4 model; uniform mat4 view; uniform mat4 proj; +out float v_cost; + void main() { gl_Position = proj * view * model * vec4(position, 1.0); + v_cost = cost; } diff --git a/assets/test/shaders/pathfinding/demo_0_vector.frag.glsl b/assets/test/shaders/pathfinding/demo_0_vector.frag.glsl new file mode 100644 index 0000000000..ebbb10bc8f --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_vector.frag.glsl @@ -0,0 +1,9 @@ +#version 330 + +uniform vec4 color; + +out vec4 outcol; + +void main() { + outcol = color; +} diff --git a/assets/test/shaders/pathfinding/demo_0_vector.vert.glsl b/assets/test/shaders/pathfinding/demo_0_vector.vert.glsl new file mode 100644 index 0000000000..8b6b015408 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_vector.vert.glsl @@ -0,0 +1,11 @@ +#version 330 + +layout(location=0) in vec3 position; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 proj; + +void main() { + gl_Position = proj * view * model * vec4(position, 1.0); +} diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index e31262101e..619246ddec 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -189,9 +189,7 @@ void RenderManager::show_flow_field(const std::shared_ptr &fiel "view", this->camera->get_view_matrix(), "proj", - this->camera->get_projection_matrix(), - "color", - Eigen::Vector4f{192.0 / 256, 255.0 / 256, 64.0 / 256, 1.0}); + this->camera->get_projection_matrix()); auto mesh = get_flow_field_mesh(field); auto geometry = this->renderer->add_mesh_geometry(mesh); renderer::Renderable renderable{ @@ -227,7 +225,7 @@ void RenderManager::show_vectors(const std::shared_ptr &field) auto rotation_rad = (cell & FLOW_DIR_MASK) * -45 * math::DEGSPERRAD; arrow_model.rotate(Eigen::AngleAxisf(rotation_rad, Eigen::Vector3f::UnitY())); - auto arrow_unifs = this->flow_shader->new_uniform_input( + auto arrow_unifs = this->vector_shader->new_uniform_input( "model", arrow_model.matrix(), "view", @@ -311,6 +309,19 @@ void RenderManager::init_shaders() { renderer::resources::shader_stage_t::fragment, ff_fshader_file); + // Shader for rendering steering vectors + auto vec_vshader_file = shaderdir / "demo_0_vector.vert.glsl"; + auto vec_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + vec_vshader_file); + + auto vec_fshader_file = shaderdir / "demo_0_vector.frag.glsl"; + auto vec_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + vec_fshader_file); + // Shader for rendering the grid auto grid_vshader_file = shaderdir / "demo_0_grid.vert.glsl"; auto grid_vshader_src = renderer::resources::ShaderSource( @@ -324,7 +335,7 @@ void RenderManager::init_shaders() { renderer::resources::shader_stage_t::fragment, grid_fshader_file); - // Shader for monocolored objects + // Shader for 2D monocolored objects auto obj_vshader_file = shaderdir / "demo_0_obj.vert.glsl"; auto obj_vshader_src = renderer::resources::ShaderSource( renderer::resources::shader_lang_t::glsl, @@ -354,6 +365,7 @@ void RenderManager::init_shaders() { this->cost_shader = renderer->add_shader({cf_vshader_src, cf_fshader_src}); this->integration_shader = renderer->add_shader({if_vshader_src, if_fshader_src}); this->flow_shader = renderer->add_shader({ff_vshader_src, ff_fshader_src}); + this->vector_shader = renderer->add_shader({vec_vshader_src, vec_fshader_src}); this->grid_shader = renderer->add_shader({grid_vshader_src, grid_fshader_src}); this->obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src}); this->display_shader = renderer->add_shader({display_vshader_src, display_fshader_src}); diff --git a/libopenage/pathfinding/demo/demo_0.h b/libopenage/pathfinding/demo/demo_0.h index 19a0f9858d..90874830d7 100644 --- a/libopenage/pathfinding/demo/demo_0.h +++ b/libopenage/pathfinding/demo/demo_0.h @@ -218,13 +218,18 @@ class RenderManager { */ std::shared_ptr flow_shader; + /** + * Shader program for rendering steering vectors. + */ + std::shared_ptr vector_shader; + /** * Shader program for rendering a grid. */ std::shared_ptr grid_shader; /** - * Shader program for rendering mono-colored objects. + * Shader program for rendering 2D mono-colored objects. */ std::shared_ptr obj_shader; From ea1a01da81ee170edec1655c5d420dbe2c51c15d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 25 Feb 2024 03:50:58 +0100 Subject: [PATCH 324/771] path: Update rendering when switching targets. --- libopenage/pathfinding/demo/demo_0.cpp | 34 +++++++++++++++++++++----- libopenage/pathfinding/demo/demo_0.h | 6 +++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 619246ddec..8c3a1769ff 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -53,6 +53,13 @@ void path_demo_0(const util::Path &path) { auto window = std::make_shared("openage pathfinding test", 1440, 720, true); auto render_manager = std::make_shared(qtapp, window, path); + // Show the cost field on startup + render_manager->show_cost_field(cost_field); + auto current_field = RenderManager::field_t::COST; + + // Make steering vector visibility toggleable + auto vectors_visible = false; + // Enable mouse button callbacks window->add_mouse_button_callback([&](const QMouseEvent &ev) { if (ev.type() == QEvent::MouseButtonRelease) { @@ -62,27 +69,45 @@ void path_demo_0(const util::Path &path) { auto grid_y = tile_pos.second; if (grid_x >= 0 and grid_x < field_length and grid_y >= 0 and grid_y < field_length) { + // Recalculate the integration field and the flow field integration_field->integrate(cost_field, grid_x, grid_y); flow_field->build(integration_field); + + // Show the new field values and vectors + switch (current_field) { + case RenderManager::field_t::COST: + render_manager->show_cost_field(cost_field); + break; + case RenderManager::field_t::INTEGRATION: + render_manager->show_integration_field(integration_field); + break; + case RenderManager::field_t::FLOW: + render_manager->show_flow_field(flow_field); + break; + } + + if (vectors_visible) { + render_manager->show_vectors(flow_field); + } } } } }); - // Make steering vector visibility toggleable - auto vectors_visible = false; - // Enable key callbacks window->add_key_callback([&](const QKeyEvent &ev) { if (ev.type() == QEvent::KeyRelease) { if (ev.key() == Qt::Key_F1) { // Show cost field render_manager->show_cost_field(cost_field); + current_field = RenderManager::field_t::COST; } else if (ev.key() == Qt::Key_F2) { // Show integration field render_manager->show_integration_field(integration_field); + current_field = RenderManager::field_t::INTEGRATION; } else if (ev.key() == Qt::Key_F3) { // Show flow field render_manager->show_flow_field(flow_field); + current_field = RenderManager::field_t::FLOW; } else if (ev.key() == Qt::Key_F4) { // Show steering vectors if (vectors_visible) { @@ -97,9 +122,6 @@ void path_demo_0(const util::Path &path) { } }); - // Show the cost field on startup - render_manager->show_cost_field(cost_field); - // Run the render loop render_manager->run(); } diff --git a/libopenage/pathfinding/demo/demo_0.h b/libopenage/pathfinding/demo/demo_0.h index 90874830d7..0dcd115c0e 100644 --- a/libopenage/pathfinding/demo/demo_0.h +++ b/libopenage/pathfinding/demo/demo_0.h @@ -52,6 +52,12 @@ void path_demo_0(const util::Path &path); */ class RenderManager { public: + enum class field_t { + COST, + INTEGRATION, + FLOW + }; + /** * Create a new render manager. * From 164ed608895e1616fa461490495b87663f34419c Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 25 Feb 2024 20:58:08 +0100 Subject: [PATCH 325/771] path: Fix wavefront algorithm. Cells can now be visited multiple times if their costs change again during integration steps. --- libopenage/pathfinding/integration_field.cpp | 108 ++++++++----------- libopenage/pathfinding/integration_field.h | 18 ++-- 2 files changed, 56 insertions(+), 70 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index ee0a91d96f..eb530374e4 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -2,9 +2,6 @@ #include "integration_field.h" -#include -#include - #include "error/error.h" #include "pathfinding/cost_field.h" @@ -13,36 +10,6 @@ namespace openage::path { -/** - * Update the open list for a neighbour index during integration field - * calculation (subroutine of integrate(..)). - * - * Checks whether: - * - * 1. the neighbour index has not already been found - * 2. the neighbour index is reachable - * - * If both conditions are met, the neighbour index is added to the open list. - * - * @param idx Index of the cell to update. - * @param integrate_cost Integrated cost of the cell. - * @param open_list Cells that still have to be visited. - * @param found Cells that have been found. - */ -void update_list(size_t idx, - integrate_t integrate_cost, - std::deque &open_list, - std::unordered_set &found) { - if (not found.contains(idx)) { - found.insert(idx); - - if (integrate_cost != INTEGRATE_UNREACHABLE) { - open_list.push_back(idx); - } - } -} - - IntegrationField::IntegrationField(size_t size) : size{size}, cells(this->size * this->size, INTEGRATE_UNREACHABLE) { @@ -75,12 +42,18 @@ void IntegrationField::integrate(const std::shared_ptr &cost_field, // Target cell index auto target_idx = target_x + target_y * this->size; - // Cells that have been found - std::unordered_set found; - found.reserve(this->size * this->size); + // Lookup table for cells that are in the open list + std::unordered_set in_list; + in_list.reserve(this->size * this->size); + // Cells that still have to be visited + // they may be visited multiple times std::deque open_list; + // Stores neighbors of the current cell + std::vector neighbors; + neighbors.reserve(4); + // Move outwards from the target cell, updating the integration field this->cells[target_idx] = INTEGRATE_START; open_list.push_back(target_idx); @@ -94,35 +67,34 @@ void IntegrationField::integrate(const std::shared_ptr &cost_field, auto integrated_current = this->cells[idx]; - // Update the integration field of the 4 neighbouring cells + // Get the neighbors of the current cell if (y > 0) { - auto up_idx = idx - this->size; - auto neighbor_cost = this->update_cell(up_idx, - cost_field->get_cost(up_idx), - integrated_current); - update_list(up_idx, neighbor_cost, open_list, found); + neighbors.push_back(idx - this->size); } if (x > 0) { - auto left_idx = idx - 1; - auto neighbor_cost = this->update_cell(left_idx, - cost_field->get_cost(left_idx), - integrated_current); - update_list(left_idx, neighbor_cost, open_list, found); + neighbors.push_back(idx - 1); } if (y < this->size - 1) { - auto down_idx = idx + this->size; - auto neighbor_cost = this->update_cell(down_idx, - cost_field->get_cost(down_idx), - integrated_current); - update_list(down_idx, neighbor_cost, open_list, found); + neighbors.push_back(idx + this->size); } if (x < this->size - 1) { - auto right_idx = idx + 1; - auto neighbor_cost = this->update_cell(right_idx, - cost_field->get_cost(right_idx), - integrated_current); - update_list(right_idx, neighbor_cost, open_list, found); + neighbors.push_back(idx + 1); } + + // Update the integration field of the neighboring cells + for (auto &neighbor_idx : neighbors) { + this->update_neighbor(neighbor_idx, + cost_field->get_cost(neighbor_idx), + integrated_current, + open_list, + in_list); + } + + // Clear the neighbors vector + neighbors.clear(); + + // Remove the current cell from the open list lookup table + in_list.erase(idx); } } @@ -136,23 +108,33 @@ void IntegrationField::reset() { } } -integrate_t IntegrationField::update_cell(size_t idx, - cost_t cell_cost, - integrate_t integrate_cost) { +integrate_t IntegrationField::update_neighbor(size_t idx, + cost_t cell_cost, + integrate_t integrated_cost, + std::deque &open_list, + std::unordered_set &in_list) { ENSURE(cell_cost > COST_INIT, "cost field cell value must be non-zero"); - // Check if the cell is impassable. + // Check if the cell is impassable + // then we don't need to update the integration field if (cell_cost == COST_IMPASSABLE) { return INTEGRATE_UNREACHABLE; } - // Update the integration field value of the cell. - auto integrated = integrate_cost + cell_cost; + auto integrated = integrated_cost + cell_cost; if (integrated < this->cells.at(idx)) { + // If the new integration value is smaller than the current one, + // update the cell and add it to the open list this->cells[idx] = integrated; + + if (not in_list.contains(idx)) { + in_list.insert(idx); + open_list.push_back(idx); + } } return integrated; } + } // namespace openage::path diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index fc83683a30..1860c6cfad 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -3,7 +3,9 @@ #pragma once #include +#include #include +#include #include #include "pathfinding/types.h" @@ -73,17 +75,19 @@ class IntegrationField { void reset(); /** - * Update a cell in the integration field. + * Update a neigbor cell during the integration process. * - * @param idx Index of the cell that is updated. - * @param cell_cost Cost of the cell from the cost field. - * @param integrate_cost Integrated cost of the updating cell in the integration field. + * @param idx Index of the neighbor cell that is updated. + * @param cell_cost Cost of the neighbor cell from the cost field. + * @param integrated_cost Current integrated cost of the updating cell in the integration field. * * @return New integration value of the cell. */ - integrate_t update_cell(size_t idx, - cost_t cell_cost, - integrate_t integrate_cost); + integrate_t update_neighbor(size_t idx, + cost_t cell_cost, + integrate_t integrated_cost, + std::deque &open_list, + std::unordered_set &in_list); /** * Side length of the field. From 2800ac8df2745277e72f81b17356b2777c7d9537 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 25 Feb 2024 22:23:02 +0100 Subject: [PATCH 326/771] path: Turn off OpenGL debugging for demo. --- libopenage/pathfinding/demo/demo_0.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 8c3a1769ff..04e70680ef 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -50,7 +50,7 @@ void path_demo_0(const util::Path &path) { // Render the grid and the pathfinding results auto qtapp = std::make_shared(); - auto window = std::make_shared("openage pathfinding test", 1440, 720, true); + auto window = std::make_shared("openage pathfinding test", 1440, 720); auto render_manager = std::make_shared(qtapp, window, path); // Show the cost field on startup From 1746590e488d0cf9899fed934f2b8d50c297531b Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 25 Feb 2024 22:31:09 +0100 Subject: [PATCH 327/771] path: Add helpful log messages to demo. --- libopenage/pathfinding/demo/demo_0.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 04e70680ef..b2c5476127 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -37,25 +37,33 @@ void path_demo_0(const util::Path &path) { cost_field->set_cost(4, 4, 128); cost_field->set_cost(5, 4, 128); cost_field->set_cost(6, 4, 128); + log::log(INFO << "Created cost field"); // Create an integration field from the cost field auto integration_field = std::make_shared(field_length); + log::log(INFO << "Created integration field"); // Set cell (7, 7) to be the target cell integration_field->integrate(cost_field, 7, 7); + log::log(INFO << "Calculated integration field for target cell (7, 7)"); // Create a flow field from the integration field auto flow_field = std::make_shared(field_length); + log::log(INFO << "Created flow field"); + flow_field->build(integration_field); + log::log(INFO << "Built flow field from integration field"); // Render the grid and the pathfinding results auto qtapp = std::make_shared(); auto window = std::make_shared("openage pathfinding test", 1440, 720); auto render_manager = std::make_shared(qtapp, window, path); + log::log(INFO << "Created RenderManager for pathfinding demo"); // Show the cost field on startup render_manager->show_cost_field(cost_field); auto current_field = RenderManager::field_t::COST; + log::log(INFO << "Showing cost field"); // Make steering vector visibility toggleable auto vectors_visible = false; @@ -69,9 +77,15 @@ void path_demo_0(const util::Path &path) { auto grid_y = tile_pos.second; if (grid_x >= 0 and grid_x < field_length and grid_y >= 0 and grid_y < field_length) { + log::log(INFO << "Selected new target cell (" << grid_x << ", " << grid_y << ")"); + // Recalculate the integration field and the flow field integration_field->integrate(cost_field, grid_x, grid_y); + log::log(INFO << "Calculated integration field for target cell (" + << grid_x << ", " << grid_y << ")"); + flow_field->build(integration_field); + log::log(INFO << "Built flow field from integration field"); // Show the new field values and vectors switch (current_field) { @@ -100,28 +114,37 @@ void path_demo_0(const util::Path &path) { if (ev.key() == Qt::Key_F1) { // Show cost field render_manager->show_cost_field(cost_field); current_field = RenderManager::field_t::COST; + log::log(INFO << "Showing cost field"); } else if (ev.key() == Qt::Key_F2) { // Show integration field render_manager->show_integration_field(integration_field); current_field = RenderManager::field_t::INTEGRATION; + log::log(INFO << "Showing integration field"); } else if (ev.key() == Qt::Key_F3) { // Show flow field render_manager->show_flow_field(flow_field); current_field = RenderManager::field_t::FLOW; + log::log(INFO << "Showing flow field"); } else if (ev.key() == Qt::Key_F4) { // Show steering vectors if (vectors_visible) { render_manager->hide_vectors(); vectors_visible = false; + log::log(INFO << "Hiding steering vectors"); } else { render_manager->show_vectors(flow_field); vectors_visible = true; + log::log(INFO << "Showing steering vectors"); } } } }); + log::log(INFO << "Instructions:"); + log::log(INFO << " 1. Press F1/F2/F3 to show cost/integration/flow field"); + log::log(INFO << " 2. Press F4 to toggle steering vectors"); + // Run the render loop render_manager->run(); } From 88fd1f11eff9049b049d5b29211768c7f0253ca1 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 25 Feb 2024 22:37:12 +0100 Subject: [PATCH 328/771] path: Add log messages for field types. --- libopenage/pathfinding/cost_field.cpp | 2 ++ libopenage/pathfinding/flow_field.cpp | 4 ++++ libopenage/pathfinding/integration_field.cpp | 3 +++ 3 files changed, 9 insertions(+) diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index d00f51decf..ad19f8b910 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -3,6 +3,7 @@ #include "cost_field.h" #include "error/error.h" +#include "log/log.h" #include "pathfinding/definitions.h" @@ -12,6 +13,7 @@ namespace openage::path { CostField::CostField(size_t size) : size{size}, cells(this->size * this->size, COST_MIN) { + log::log(DBG << "Created cost field with size " << this->size << "x" << this->size); } size_t CostField::get_size() const { diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 63d7bca860..36487d4613 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -3,6 +3,7 @@ #include "flow_field.h" #include "error/error.h" +#include "log/log.h" #include "pathfinding/integration_field.h" @@ -12,6 +13,7 @@ namespace openage::path { FlowField::FlowField(size_t size) : size{size}, cells(this->size * this->size, 0) { + log::log(DBG << "Created flow field with size " << this->size << "x" << this->size); } FlowField::FlowField(const std::shared_ptr &integrate_field) : @@ -131,6 +133,8 @@ void FlowField::reset() { for (auto &cell : this->cells) { cell = 0; } + + log::log(DBG << "Flow field has been reset"); } } // namespace openage::path diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index eb530374e4..aef6fc7594 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -3,6 +3,7 @@ #include "integration_field.h" #include "error/error.h" +#include "log/log.h" #include "pathfinding/cost_field.h" #include "pathfinding/definitions.h" @@ -13,6 +14,7 @@ namespace openage::path { IntegrationField::IntegrationField(size_t size) : size{size}, cells(this->size * this->size, INTEGRATE_UNREACHABLE) { + log::log(DBG << "Created integration field with size " << this->size << "x" << this->size); } size_t IntegrationField::get_size() const { @@ -106,6 +108,7 @@ void IntegrationField::reset() { for (auto &cell : this->cells) { cell = INTEGRATE_UNREACHABLE; } + log::log(DBG << "Integration field has been reset"); } integrate_t IntegrationField::update_neighbor(size_t idx, From 23d8298a197756fda9aef6882653623c3ad205e7 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 25 Feb 2024 22:39:34 +0100 Subject: [PATCH 329/771] path: Add constant for initial flow field cell value. --- libopenage/pathfinding/definitions.h | 9 +++++++-- libopenage/pathfinding/demo/demo_0.cpp | 2 +- libopenage/pathfinding/flow_field.cpp | 8 ++++---- libopenage/pathfinding/tests.cpp | 16 ++++++++-------- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h index ea8936d809..5756ac8c61 100644 --- a/libopenage/pathfinding/definitions.h +++ b/libopenage/pathfinding/definitions.h @@ -59,6 +59,11 @@ enum class flow_dir_t : uint8_t { NORTH_WEST = 0x07, }; +/** + * Initial value for a flow field cell. + */ +constexpr flow_t FLOW_INIT = 0; + /** * Mask for the flow direction bits in a flow_t value. */ @@ -67,11 +72,11 @@ constexpr flow_t FLOW_DIR_MASK = 0x0F; /** * Mask for the pathable flag in a flow_t value. */ -constexpr flow_t FLOW_PATHABLE = 0x10; +constexpr flow_t FLOW_PATHABLE_MASK = 0x10; /** * Mask for the line of sight flag in a flow_t value. */ -constexpr flow_t FLOW_LOS = 0x20; +constexpr flow_t FLOW_LOS_MASK = 0x20; } // namespace openage::path diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index b2c5476127..0fca97c5f9 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -262,7 +262,7 @@ void RenderManager::show_vectors(const std::shared_ptr &field) for (size_t y = 0; y < field->get_size(); ++y) { for (size_t x = 0; x < field->get_size(); ++x) { auto cell = field->get_cell(x, y); - if (cell & FLOW_PATHABLE) { + if (cell & FLOW_PATHABLE_MASK) { Eigen::Affine3f arrow_model = Eigen::Affine3f::Identity(); arrow_model.translate(Eigen::Vector3f(y + 0.5, 0, -1.0f * x - 0.5)); auto dir = static_cast(cell & FLOW_DIR_MASK); diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 36487d4613..4757a7cc95 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -12,13 +12,13 @@ namespace openage::path { FlowField::FlowField(size_t size) : size{size}, - cells(this->size * this->size, 0) { + cells(this->size * this->size, FLOW_INIT) { log::log(DBG << "Created flow field with size " << this->size << "x" << this->size); } FlowField::FlowField(const std::shared_ptr &integrate_field) : size{integrate_field->get_size()}, - cells(this->size * this->size, 0) { + cells(this->size * this->size, FLOW_INIT) { this->build(integrate_field); } @@ -117,7 +117,7 @@ void FlowField::build(const std::shared_ptr &integrate_field) } // Set the flow field cell to pathable. - flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE; + flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; // Set the flow field cell to the direction of the smallest cost. flow_cells[idx] = flow_cells[idx] | static_cast(direction); @@ -131,7 +131,7 @@ const std::vector &FlowField::get_cells() const { void FlowField::reset() { for (auto &cell : this->cells) { - cell = 0; + cell = FLOW_INIT; } log::log(DBG << "Flow field has been reset"); diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index 6e060c4301..f7b9cae197 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -332,15 +332,15 @@ void flow_field() { // | SE | X | S | // | E | E | N | auto ff_expected = std::vector{ - FLOW_PATHABLE | static_cast(flow_dir_t::EAST), - FLOW_PATHABLE | static_cast(flow_dir_t::SOUTH_EAST), - FLOW_PATHABLE | static_cast(flow_dir_t::SOUTH), - FLOW_PATHABLE | static_cast(flow_dir_t::SOUTH_EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), 0, - FLOW_PATHABLE | static_cast(flow_dir_t::SOUTH), - FLOW_PATHABLE | static_cast(flow_dir_t::EAST), - FLOW_PATHABLE | static_cast(flow_dir_t::EAST), - FLOW_PATHABLE | static_cast(flow_dir_t::NORTH), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::NORTH), }; // Test the different field types From 67d60675d1bf8d87a195d11976dcdb6e0971fed5 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 25 Feb 2024 23:24:14 +0100 Subject: [PATCH 330/771] path: Add flags to integration field cell type. --- libopenage/pathfinding/definitions.h | 23 +++++++++--- libopenage/pathfinding/demo/demo_0.cpp | 12 +++---- libopenage/pathfinding/flow_field.cpp | 20 +++++------ libopenage/pathfinding/integration_field.cpp | 38 ++++++++++---------- libopenage/pathfinding/integration_field.h | 20 +++++------ libopenage/pathfinding/integrator.cpp | 2 +- libopenage/pathfinding/tests.cpp | 6 ++-- libopenage/pathfinding/types.h | 27 ++++++++++++-- 8 files changed, 92 insertions(+), 56 deletions(-) diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h index 5756ac8c61..561f7b58a0 100644 --- a/libopenage/pathfinding/definitions.h +++ b/libopenage/pathfinding/definitions.h @@ -35,12 +35,27 @@ constexpr cost_t COST_IMPASSABLE = 255; /** * Start value for goal cells. */ -const integrate_t INTEGRATE_START = 0; +const integrated_cost_t INTEGRATED_COST_START = 0; /** * Unreachable value for a cells in the integration grid. */ -constexpr integrate_t INTEGRATE_UNREACHABLE = std::numeric_limits::max(); +constexpr integrated_cost_t INTEGRATED_COST_UNREACHABLE = std::numeric_limits::max(); + +/** + * Line of sight flag in an integrated_flags_t value. + */ +constexpr integrated_flags_t INTEGRATE_LOS_MASK = 0x01; + +/** + * Wavefront blocked flag in an integrated_flags_t value. + */ +constexpr integrated_flags_t INTEGRATE_WAVEFRONT_BLOCKED_MASK = 0x02; + +/** + * Initial value for a cell in the integration grid. + */ +constexpr integrate_t INTEGRATE_INIT = {INTEGRATED_COST_UNREACHABLE, 0}; /** @@ -70,12 +85,12 @@ constexpr flow_t FLOW_INIT = 0; constexpr flow_t FLOW_DIR_MASK = 0x0F; /** - * Mask for the pathable flag in a flow_t value. + * Pathable flag in a flow_t value. */ constexpr flow_t FLOW_PATHABLE_MASK = 0x10; /** - * Mask for the line of sight flag in a flow_t value. + * Line of sight flag in a flow_t value. */ constexpr flow_t FLOW_LOS_MASK = 0x20; diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 0fca97c5f9..a395fb5437 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -44,7 +44,7 @@ void path_demo_0(const util::Path &path) { log::log(INFO << "Created integration field"); // Set cell (7, 7) to be the target cell - integration_field->integrate(cost_field, 7, 7); + integration_field->integrate_cost(cost_field, 7, 7); log::log(INFO << "Calculated integration field for target cell (7, 7)"); // Create a flow field from the integration field @@ -80,7 +80,7 @@ void path_demo_0(const util::Path &path) { log::log(INFO << "Selected new target cell (" << grid_x << ", " << grid_y << ")"); // Recalculate the integration field and the flow field - integration_field->integrate(cost_field, grid_x, grid_y); + integration_field->integrate_cost(cost_field, grid_x, grid_y); log::log(INFO << "Calculated integration field for target cell (" << grid_x << ", " << grid_y << ")"); @@ -638,19 +638,19 @@ renderer::resources::MeshData RenderManager::get_integration_field_mesh(const st // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cell((i - 1) / resolution, (j - 1) / resolution); + auto cost = field->get_cell((i - 1) / resolution, (j - 1) / resolution).cost; surround.push_back(cost); } if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cell((i - 1) / resolution, j / resolution); + auto cost = field->get_cell((i - 1) / resolution, j / resolution).cost; surround.push_back(cost); } if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cell(i / resolution, j / resolution); + auto cost = field->get_cell(i / resolution, j / resolution).cost; surround.push_back(cost); } if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cell(i / resolution, (j - 1) / resolution); + auto cost = field->get_cell(i / resolution, (j - 1) / resolution).cost; surround.push_back(cost); } // use the cost of the most expensive surrounding tile diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 4757a7cc95..afb050fd82 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -51,65 +51,65 @@ void FlowField::build(const std::shared_ptr &integrate_field) for (size_t x = 0; x < this->size; ++x) { size_t idx = y * this->size + x; - if (integrate_cells[idx] == INTEGRATE_UNREACHABLE) { + if (integrate_cells[idx].cost == INTEGRATED_COST_UNREACHABLE) { // Cell cannot be used as path continue; } // Find the neighbor with the smallest cost. flow_dir_t direction; - integrate_t smallest_cost = INTEGRATE_UNREACHABLE; + auto smallest_cost = INTEGRATED_COST_UNREACHABLE; if (y > 0) { - integrate_t cost = integrate_cells[idx - this->size]; + auto cost = integrate_cells[idx - this->size].cost; if (cost < smallest_cost) { smallest_cost = cost; direction = flow_dir_t::NORTH; } } if (x < this->size - 1 && y > 0) { - integrate_t cost = integrate_cells[idx - this->size + 1]; + auto cost = integrate_cells[idx - this->size + 1].cost; if (cost < smallest_cost) { smallest_cost = cost; direction = flow_dir_t::NORTH_EAST; } } if (x < this->size - 1) { - integrate_t cost = integrate_cells[idx + 1]; + auto cost = integrate_cells[idx + 1].cost; if (cost < smallest_cost) { smallest_cost = cost; direction = flow_dir_t::EAST; } } if (x < this->size - 1 && y < this->size - 1) { - integrate_t cost = integrate_cells[idx + this->size + 1]; + auto cost = integrate_cells[idx + this->size + 1].cost; if (cost < smallest_cost) { smallest_cost = cost; direction = flow_dir_t::SOUTH_EAST; } } if (y < this->size - 1) { - integrate_t cost = integrate_cells[idx + this->size]; + auto cost = integrate_cells[idx + this->size].cost; if (cost < smallest_cost) { smallest_cost = cost; direction = flow_dir_t::SOUTH; } } if (x > 0 && y < this->size - 1) { - integrate_t cost = integrate_cells[idx + this->size - 1]; + auto cost = integrate_cells[idx + this->size - 1].cost; if (cost < smallest_cost) { smallest_cost = cost; direction = flow_dir_t::SOUTH_WEST; } } if (x > 0) { - integrate_t cost = integrate_cells[idx - 1]; + auto cost = integrate_cells[idx - 1].cost; if (cost < smallest_cost) { smallest_cost = cost; direction = flow_dir_t::WEST; } } if (x > 0 && y > 0) { - integrate_t cost = integrate_cells[idx - this->size - 1]; + auto cost = integrate_cells[idx - this->size - 1].cost; if (cost < smallest_cost) { smallest_cost = cost; direction = flow_dir_t::NORTH_WEST; diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index aef6fc7594..f682db2320 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -13,7 +13,7 @@ namespace openage::path { IntegrationField::IntegrationField(size_t size) : size{size}, - cells(this->size * this->size, INTEGRATE_UNREACHABLE) { + cells(this->size * this->size, INTEGRATE_INIT) { log::log(DBG << "Created integration field with size " << this->size << "x" << this->size); } @@ -21,17 +21,17 @@ size_t IntegrationField::get_size() const { return this->size; } -integrate_t IntegrationField::get_cell(size_t x, size_t y) const { +const integrate_t &IntegrationField::get_cell(size_t x, size_t y) const { return this->cells.at(x + y * this->size); } -integrate_t IntegrationField::get_cell(size_t idx) const { +const integrate_t &IntegrationField::get_cell(size_t idx) const { return this->cells.at(idx); } -void IntegrationField::integrate(const std::shared_ptr &cost_field, - size_t target_x, - size_t target_y) { +void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, + size_t target_x, + size_t target_y) { ENSURE(cost_field->get_size() == this->get_size(), "cost field size " << cost_field->get_size() << "x" << cost_field->get_size() @@ -57,7 +57,7 @@ void IntegrationField::integrate(const std::shared_ptr &cost_field, neighbors.reserve(4); // Move outwards from the target cell, updating the integration field - this->cells[target_idx] = INTEGRATE_START; + this->cells[target_idx].cost = INTEGRATED_COST_START; open_list.push_back(target_idx); while (!open_list.empty()) { auto idx = open_list.front(); @@ -67,7 +67,7 @@ void IntegrationField::integrate(const std::shared_ptr &cost_field, auto x = idx % this->size; auto y = idx / this->size; - auto integrated_current = this->cells[idx]; + auto integrated_current = this->cells.at(idx).cost; // Get the neighbors of the current cell if (y > 0) { @@ -106,37 +106,35 @@ const std::vector &IntegrationField::get_cells() const { void IntegrationField::reset() { for (auto &cell : this->cells) { - cell = INTEGRATE_UNREACHABLE; + cell = INTEGRATE_INIT; } log::log(DBG << "Integration field has been reset"); } -integrate_t IntegrationField::update_neighbor(size_t idx, - cost_t cell_cost, - integrate_t integrated_cost, - std::deque &open_list, - std::unordered_set &in_list) { +void IntegrationField::update_neighbor(size_t idx, + cost_t cell_cost, + integrated_cost_t integrated_cost, + std::deque &open_list, + std::unordered_set &in_list) { ENSURE(cell_cost > COST_INIT, "cost field cell value must be non-zero"); // Check if the cell is impassable // then we don't need to update the integration field if (cell_cost == COST_IMPASSABLE) { - return INTEGRATE_UNREACHABLE; + return; } - auto integrated = integrated_cost + cell_cost; - if (integrated < this->cells.at(idx)) { + auto cost = integrated_cost + cell_cost; + if (cost < this->cells.at(idx).cost) { // If the new integration value is smaller than the current one, // update the cell and add it to the open list - this->cells[idx] = integrated; + this->cells[idx].cost = cost; if (not in_list.contains(idx)) { in_list.insert(idx); open_list.push_back(idx); } } - - return integrated; } diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 1860c6cfad..81cb224111 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -40,7 +40,7 @@ class IntegrationField { * @param y Y coordinate. * @return Integration value at the specified position. */ - integrate_t get_cell(size_t x, size_t y) const; + const integrate_t &get_cell(size_t x, size_t y) const; /** * Get the integration value at a specified position. @@ -48,7 +48,7 @@ class IntegrationField { * @param idx Index of the cell. * @return Integration value at the specified position. */ - integrate_t get_cell(size_t idx) const; + const integrate_t &get_cell(size_t idx) const; /** * Calculate the integration field for a target cell. @@ -57,9 +57,9 @@ class IntegrationField { * @param target_x X coordinate of the target cell. * @param target_y Y coordinate of the target cell. */ - void integrate(const std::shared_ptr &cost_field, - size_t target_x, - size_t target_y); + void integrate_cost(const std::shared_ptr &cost_field, + size_t target_x, + size_t target_y); /** * Get the integration field values. @@ -83,11 +83,11 @@ class IntegrationField { * * @return New integration value of the cell. */ - integrate_t update_neighbor(size_t idx, - cost_t cell_cost, - integrate_t integrated_cost, - std::deque &open_list, - std::unordered_set &in_list); + void update_neighbor(size_t idx, + cost_t cell_cost, + integrated_cost_t integrated_cost, + std::deque &open_list, + std::unordered_set &in_list); /** * Side length of the field. diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 19cd0c408f..3f5d2af252 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -17,7 +17,7 @@ std::shared_ptr Integrator::build(size_t target_x, size_t target_y) { auto flow_field = std::make_shared(this->cost_field->get_size()); auto integrate_field = std::make_shared(this->cost_field->get_size()); - integrate_field->integrate(this->cost_field, target_x, target_y); + integrate_field->integrate_cost(this->cost_field, target_x, target_y); flow_field->build(integrate_field); return flow_field; diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index f7b9cae197..7b15a4406a 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -346,14 +346,14 @@ void flow_field() { // Test the different field types { auto integration_field = std::make_shared(3); - integration_field->integrate(cost_field, 2, 2); + integration_field->integrate_cost(cost_field, 2, 2); auto int_cells = integration_field->get_cells(); // The integration field should look like: // | 4 | 3 | 2 | // | 3 | X | 1 | // | 2 | 1 | 0 | - auto int_expected = std::vector{ + auto int_expected = std::vector{ 4, 3, 2, @@ -367,7 +367,7 @@ void flow_field() { // Compare the integration field cells with the expected values for (size_t i = 0; i < int_cells.size(); i++) { - TESTEQUALS(int_cells[i], int_expected[i]); + TESTEQUALS(int_cells[i].cost, int_expected[i]); } // Build the flow field diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index d68f8b39ca..55e8bb9da5 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -17,9 +17,32 @@ namespace openage::path { using cost_t = uint8_t; /** - * Total integrated cost in the integration field. + * Integrated cost in the integration field. */ -using integrate_t = uint16_t; +using integrated_cost_t = uint16_t; + +/** + * Integrated field cell flags. + */ +using integrated_flags_t = uint8_t; + +/** + * Integration field cell value. + */ +struct integrate_t { + /** + * Total integrated cost. + */ + integrated_cost_t cost; + + /** + * Flags. + * + * Bit 6: Wave front blocked flag. + * Bit 7: Line of sight flag. + */ + integrated_flags_t flags; +}; /** * Flow field cell value. From 824fcca0a2d49c45089dc052a308044f63fadf56 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 25 Feb 2024 23:28:00 +0100 Subject: [PATCH 331/771] path: Check for line of sight flag when building flow field. --- libopenage/pathfinding/flow_field.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index afb050fd82..e728199fd9 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -56,6 +56,12 @@ void FlowField::build(const std::shared_ptr &integrate_field) continue; } + if (integrate_cells[idx].flags & INTEGRATE_LOS_MASK) { + // Cell is in line of sight + this->cells[idx] = this->cells[idx] | FLOW_LOS_MASK; + // continue; + } + // Find the neighbor with the smallest cost. flow_dir_t direction; auto smallest_cost = INTEGRATED_COST_UNREACHABLE; From 35533ce7948d3784905590b9c972540a81e75a87 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 2 Mar 2024 23:01:51 +0100 Subject: [PATCH 332/771] path: LOS pass in integration field. --- libopenage/pathfinding/integration_field.cpp | 68 ++++++++++++++++++++ libopenage/pathfinding/integration_field.h | 11 ++++ 2 files changed, 79 insertions(+) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index f682db2320..fc4b0557cf 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -29,6 +29,74 @@ const integrate_t &IntegrationField::get_cell(size_t idx) const { return this->cells.at(idx); } +void IntegrationField::integrate_los(const std::shared_ptr &cost_field, + size_t target_x, + size_t target_y) { + ENSURE(cost_field->get_size() == this->get_size(), + "cost field size " + << cost_field->get_size() << "x" << cost_field->get_size() + << " does not match integration field size " + << this->get_size() << "x" << this->get_size()); + + // Reset the integration field + this->reset(); + + + // Target cell index + auto target_idx = target_x + target_y * this->size; + + // Lookup table for cells that have been found + std::unordered_set found; + found.reserve(this->size * this->size); + + // Cells that still have to be visited + std::deque open_list; + + // Move outwards from the target cell, updating the integration field + open_list.push_back(target_idx); + while (!open_list.empty()) { + auto idx = open_list.front(); + open_list.pop_front(); + + if (found.contains(idx)) { + // Skip cells that have already been found + continue; + } + + // Get the x and y coordinates of the current cell + auto x = idx % this->size; + auto y = idx / this->size; + + // Get the cost of the current cell + auto cell_cost = cost_field->get_cost(idx); + + if (cell_cost > COST_MIN) { + // TODO: stop the LOS search and check for LOS corners + continue; + } + + // Set the LOS flag of the current cell + this->cells[idx].flags |= INTEGRATE_LOS_MASK; + + // Search the neighbors of the current cell + if (y > 0) { + open_list.push_back(idx - this->size); + } + if (x > 0) { + open_list.push_back(idx - 1); + } + if (y < this->size - 1) { + open_list.push_back(idx + this->size); + } + if (x < this->size - 1) { + open_list.push_back(idx + 1); + } + + // Add the current cell to the found cells + found.insert(idx); + } +} + void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, size_t target_x, size_t target_y) { diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 81cb224111..c65e038cfa 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -50,6 +50,17 @@ class IntegrationField { */ const integrate_t &get_cell(size_t idx) const; + /** + * Calculate the line-of-sight cells for a target cell. + * + * @param cost_field Cost field to integrate. + * @param target_x X coordinate of the target cell. + * @param target_y Y coordinate of the target cell. + */ + void integrate_los(const std::shared_ptr &cost_field, + size_t target_x, + size_t target_y); + /** * Calculate the integration field for a target cell. * From a42fc33372d2b99549e0ab25035436e99e7eee09 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 2 Mar 2024 23:06:57 +0100 Subject: [PATCH 333/771] path: Manual resetting of integration/flow field. --- libopenage/pathfinding/demo/demo_0.cpp | 4 +++- libopenage/pathfinding/flow_field.cpp | 3 --- libopenage/pathfinding/flow_field.h | 2 +- libopenage/pathfinding/integration_field.cpp | 7 ------- libopenage/pathfinding/integration_field.h | 2 +- 5 files changed, 5 insertions(+), 13 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index a395fb5437..828d9acece 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -80,10 +80,12 @@ void path_demo_0(const util::Path &path) { log::log(INFO << "Selected new target cell (" << grid_x << ", " << grid_y << ")"); // Recalculate the integration field and the flow field + integration_field->reset(); integration_field->integrate_cost(cost_field, grid_x, grid_y); log::log(INFO << "Calculated integration field for target cell (" << grid_x << ", " << grid_y << ")"); + flow_field->reset(); flow_field->build(integration_field); log::log(INFO << "Built flow field from integration field"); @@ -302,7 +304,7 @@ std::pair RenderManager::select_tile(double x, double y) { auto grid_plane_point = Eigen::Vector3f{0, 0, 0}; auto camera_direction = renderer::camera::cam_direction; auto camera_position = camera->get_input_pos( - coord::input{x, y}); + coord::input(x, y)); Eigen::Vector3f intersect = camera_position + camera_direction * (grid_plane_point - camera_position).dot(grid_plane_normal) / camera_direction.dot(grid_plane_normal); auto grid_x = static_cast(-1 * intersect[2]); diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index e728199fd9..baa3c494e7 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -41,9 +41,6 @@ void FlowField::build(const std::shared_ptr &integrate_field) << " does not match flow field size " << this->get_size() << "x" << this->get_size()); - // Reset the flow field. - this->reset(); - auto &integrate_cells = integrate_field->get_cells(); auto &flow_cells = this->cells; diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index 2fefd251ba..5d8ab25580 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -70,12 +70,12 @@ class FlowField { */ const std::vector &get_cells() const; -private: /** * Reset the flow field values for rebuilding the field. */ void reset(); +private: /** * Side length of the field. */ diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index fc4b0557cf..2db57a4367 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -38,10 +38,6 @@ void IntegrationField::integrate_los(const std::shared_ptr &cost_fiel << " does not match integration field size " << this->get_size() << "x" << this->get_size()); - // Reset the integration field - this->reset(); - - // Target cell index auto target_idx = target_x + target_y * this->size; @@ -106,9 +102,6 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie << " does not match integration field size " << this->get_size() << "x" << this->get_size()); - // Reset the integration field - this->reset(); - // Target cell index auto target_idx = target_x + target_y * this->size; diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index c65e038cfa..707fc341b3 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -79,12 +79,12 @@ class IntegrationField { */ const std::vector &get_cells() const; -private: /** * Reset the integration field for a new integration. */ void reset(); +private: /** * Update a neigbor cell during the integration process. * From ce0210bc4d28f37b554f21b80f6bcc31f7712587 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 4 Mar 2024 02:42:24 +0100 Subject: [PATCH 334/771] path: Handle LOS corners. --- .../pathfinding/demo_0_flow_field.frag.glsl | 15 +- libopenage/pathfinding/definitions.h | 5 + libopenage/pathfinding/demo/demo_0.cpp | 4 +- libopenage/pathfinding/flow_field.cpp | 7 +- libopenage/pathfinding/integration_field.cpp | 245 +++++++++++++++++- libopenage/pathfinding/integration_field.h | 34 ++- libopenage/pathfinding/integrator.cpp | 1 + libopenage/pathfinding/types.h | 6 +- 8 files changed, 302 insertions(+), 15 deletions(-) diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl index 51f4a66804..b5ff4042a3 100644 --- a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl +++ b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl @@ -6,9 +6,9 @@ out vec4 outcol; void main() { int cost = int(v_cost); - if (!bool(cost & 0x10)) { - // not pathable - outcol = vec4(0.0, 0.0, 0.0, 1.0); + if (bool(cost & 0x40)) { + // wavefront blocked + outcol = vec4(0.9, 0.9, 0.9, 1.0); return; } @@ -18,5 +18,12 @@ void main() { return; } - outcol = vec4(0.7, 0.7, 0.7, 1.0); + if (bool(cost & 0x10)) { + // pathable + outcol = vec4(0.7, 0.7, 0.7, 1.0); + return; + } + + // not pathable + outcol = vec4(0.0, 0.0, 0.0, 1.0); } diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h index 561f7b58a0..3d8584616b 100644 --- a/libopenage/pathfinding/definitions.h +++ b/libopenage/pathfinding/definitions.h @@ -94,4 +94,9 @@ constexpr flow_t FLOW_PATHABLE_MASK = 0x10; */ constexpr flow_t FLOW_LOS_MASK = 0x20; +/** + * Wavefront blocked flag in a flow_t value. + */ +constexpr flow_t FLOW_WAVEFRONT_BLOCKED_MASK = 0x40; + } // namespace openage::path diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 828d9acece..66aec6417e 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -44,6 +44,7 @@ void path_demo_0(const util::Path &path) { log::log(INFO << "Created integration field"); // Set cell (7, 7) to be the target cell + integration_field->integrate_los(cost_field, 7, 7); integration_field->integrate_cost(cost_field, 7, 7); log::log(INFO << "Calculated integration field for target cell (7, 7)"); @@ -81,6 +82,7 @@ void path_demo_0(const util::Path &path) { // Recalculate the integration field and the flow field integration_field->reset(); + integration_field->integrate_los(cost_field, grid_x, grid_y); integration_field->integrate_cost(cost_field, grid_x, grid_y); log::log(INFO << "Calculated integration field for target cell (" << grid_x << ", " << grid_y << ")"); @@ -237,7 +239,7 @@ void RenderManager::show_flow_field(const std::shared_ptr &fiel this->camera->get_view_matrix(), "proj", this->camera->get_projection_matrix()); - auto mesh = get_flow_field_mesh(field); + auto mesh = get_flow_field_mesh(field, 4); auto geometry = this->renderer->add_mesh_geometry(mesh); renderer::Renderable renderable{ unifs, diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index baa3c494e7..64e7d82d23 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -56,7 +56,12 @@ void FlowField::build(const std::shared_ptr &integrate_field) if (integrate_cells[idx].flags & INTEGRATE_LOS_MASK) { // Cell is in line of sight this->cells[idx] = this->cells[idx] | FLOW_LOS_MASK; - // continue; + continue; + } + + if (integrate_cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { + // Cell is blocked by a line-of-sight corner + this->cells[idx] = this->cells[idx] | FLOW_WAVEFRONT_BLOCKED_MASK; } // Find the neighbor with the smallest cost. diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 2db57a4367..5e6459f873 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -2,6 +2,8 @@ #include "integration_field.h" +#include + #include "error/error.h" #include "log/log.h" @@ -58,6 +60,13 @@ void IntegrationField::integrate_los(const std::shared_ptr &cost_fiel // Skip cells that have already been found continue; } + else if (this->cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { + // Skip cells that are blocked by a LOS corner + continue; + } + + // Add the current cell to the found cells + found.insert(idx); // Get the x and y coordinates of the current cell auto x = idx % this->size; @@ -67,7 +76,16 @@ void IntegrationField::integrate_los(const std::shared_ptr &cost_fiel auto cell_cost = cost_field->get_cost(idx); if (cell_cost > COST_MIN) { - // TODO: stop the LOS search and check for LOS corners + // check each neighbor for a corner + auto corners = this->get_los_corners(cost_field, target_x, target_y, x, y); + + for (auto &corner : corners) { + auto blocked_cells = this->bresenhams_line(target_x, target_y, corner.first, corner.second); + for (auto &blocked_idx : blocked_cells) { + this->cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + } + } + continue; } @@ -87,9 +105,6 @@ void IntegrationField::integrate_los(const std::shared_ptr &cost_fiel if (x < this->size - 1) { open_list.push_back(idx + 1); } - - // Add the current cell to the found cells - found.insert(idx); } } @@ -198,5 +213,227 @@ void IntegrationField::update_neighbor(size_t idx, } } +std::vector> IntegrationField::get_los_corners(const std::shared_ptr &cost_field, + size_t target_x, + size_t target_y, + size_t blocker_x, + size_t blocker_y) { + std::vector> corners; + + // Get the cost of the blocking cell's neighbors + + // Set all costs to MAX at the beginning + auto top_cost = COST_MAX; + auto left_cost = COST_MAX; + auto bottom_cost = COST_MAX; + auto right_cost = COST_MAX; + + std::pair top_left{blocker_x, blocker_y}; + std::pair top_right{blocker_x + 1, blocker_y}; + std::pair bottom_left{blocker_x, blocker_y + 1}; + std::pair bottom_right{blocker_x + 1, blocker_y + 1}; + + // Get neighbor costs (if they exist) + if (blocker_y > 0) { + top_cost = cost_field->get_cost(blocker_x, blocker_y - 1); + } + if (blocker_x > 0) { + left_cost = cost_field->get_cost(blocker_x - 1, blocker_y); + } + if (blocker_y < this->size - 1) { + bottom_cost = cost_field->get_cost(blocker_x, blocker_y + 1); + } + if (blocker_x < this->size - 1) { + right_cost = cost_field->get_cost(blocker_x + 1, blocker_y); + } + + // Check which corners are blocking LOS + // TODO: Currently super complicated and could likely be optimized + if (blocker_x == target_x) { + // blocking cell is parallel to target on y-axis + if (blocker_y < target_y) { + if (left_cost == COST_MIN) { + // top + corners.push_back(bottom_left); + } + if (right_cost == COST_MIN) { + // top + corners.push_back(bottom_right); + } + } + else { + if (left_cost == COST_MIN) { + // bottom + corners.push_back(top_left); + } + if (right_cost == COST_MIN) { + // bottom + corners.push_back(top_right); + } + } + } + else if (blocker_y == target_y) { + // blocking cell is parallel to target on x-axis + if (blocker_x < target_x) { + if (top_cost == COST_MIN) { + // right + corners.push_back(top_right); + } + if (bottom_cost == COST_MIN) { + // right + corners.push_back(bottom_right); + } + } + else { + if (top_cost == COST_MIN) { + // left + corners.push_back(top_left); + } + if (bottom_cost == COST_MIN) { + // left + corners.push_back(bottom_left); + } + } + } + else { + // blocking cell is diagonal to target on + if (blocker_x < target_x) { + if (blocker_y < target_y) { + // top and right + if (top_cost == COST_MIN and right_cost == COST_MIN) { + // right + corners.push_back(top_right); + } + if (left_cost == COST_MIN and bottom_cost == COST_MIN) { + // bottom + corners.push_back(bottom_left); + } + } + else { + // bottom and right + if (bottom_cost == COST_MIN and right_cost == COST_MIN) { + // right + corners.push_back(bottom_right); + } + if (left_cost == COST_MIN and top_cost == COST_MIN) { + // top + corners.push_back(top_left); + } + } + } + else { + if (blocker_y < target_y) { + // top and left + if (top_cost == COST_MIN and left_cost == COST_MIN) { + // left + corners.push_back(top_left); + } + if (right_cost == COST_MIN and bottom_cost == COST_MIN) { + // bottom + corners.push_back(bottom_right); + } + } + else { + // bottom and left + if (bottom_cost == COST_MIN and left_cost == COST_MIN) { + // left + corners.push_back(bottom_left); + } + if (right_cost == COST_MIN and top_cost == COST_MIN) { + // top + corners.push_back(top_right); + } + } + } + } + + return corners; +} + +std::vector IntegrationField::bresenhams_line(int target_x, + int target_y, + int corner_x, + int corner_y) { + std::vector cells; + + auto x = corner_x; + auto y = corner_y; + double tx = target_x + 0.5; + double ty = target_y + 0.5; + double dx = std::abs(tx - corner_x); + double dy = std::abs(ty - corner_y); + auto m = dy / dx; + + auto error = 0.5; + + // Check which direction the line is going + if (corner_x < tx) { + if (corner_y < ty) { + // left and up + y -= 1; + while (x > 0 and y > 0) { + if (error > 1.0) { + y -= 1; + error -= 1.0; + } + else { + x -= 1; + error += m; + } + cells.push_back(x + y * this->size); + } + } + else { + // left and down + while (x > 0 and y < this->size - 1) { + if (error > 1.0) { + y += 1; + error -= 1.0; + } + else { + x -= 1; + error += m; + } + cells.push_back(x + y * this->size); + } + } + } + else { + if (corner_y < ty) { + // right and up + x -= 1; + y -= 1; + while (x < this->size - 1 and y > 0) { + if (error > 1.0) { + y -= 1; + error -= 1.0; + } + else { + x += 1; + error += m; + } + cells.push_back(x + y * this->size); + } + } + else { + // right and down + x -= 1; + while (x < this->size - 1 and y < this->size - 1) { + if (error > 1.0) { + y += 1; + error -= 1.0; + } + else { + x += 1; + error += m; + } + cells.push_back(x + y * this->size); + } + } + } + + return cells; +} + } // namespace openage::path diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 707fc341b3..263a948fc1 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -51,7 +51,7 @@ class IntegrationField { const integrate_t &get_cell(size_t idx) const; /** - * Calculate the line-of-sight cells for a target cell. + * Calculate the line-of-sight integration flags for a target cell. * * @param cost_field Cost field to integrate. * @param target_x X coordinate of the target cell. @@ -62,7 +62,7 @@ class IntegrationField { size_t target_y); /** - * Calculate the integration field for a target cell. + * Calculate the cost integration field for a target cell. * * @param cost_field Cost field to integrate. * @param target_x X coordinate of the target cell. @@ -86,7 +86,7 @@ class IntegrationField { private: /** - * Update a neigbor cell during the integration process. + * Update a neigbor cell during the cost integration process. * * @param idx Index of the neighbor cell that is updated. * @param cell_cost Cost of the neighbor cell from the cost field. @@ -100,6 +100,34 @@ class IntegrationField { std::deque &open_list, std::unordered_set &in_list); + /** + * Get the LOS corners around a cell. + * + * @param cost_field Cost field to integrate. + * @param target_x X coordinate of the target. + * @param target_y Y coordinate of the target. + * @param blocker_x X coordinate of the cell blocking LOS. + * @param blocker_y Y coordinate of the cell blocking LOS. + */ + std::vector> get_los_corners(const std::shared_ptr &cost_field, + size_t target_x, + size_t target_y, + size_t blocker_x, + size_t blocker_y); + + /** + * Get the cells in a bresenham's line between the corner cell and the field edge. + * + * @param target_x X coordinate of the target. + * @param target_y Y coordinate of the target. + * @param corner_x X coordinate edge of the LOS corner. + * @param corner_y Y coordinate edge of the LOS corner. + */ + std::vector bresenhams_line(int target_x, + int target_y, + int corner_x, + int corner_y); + /** * Side length of the field. */ diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 3f5d2af252..cbff7fd7cb 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -17,6 +17,7 @@ std::shared_ptr Integrator::build(size_t target_x, size_t target_y) { auto flow_field = std::make_shared(this->cost_field->get_size()); auto integrate_field = std::make_shared(this->cost_field->get_size()); + integrate_field->integrate_los(this->cost_field, target_x, target_y); integrate_field->integrate_cost(this->cost_field, target_x, target_y); flow_field->build(integrate_field); diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index 55e8bb9da5..8280dfcdf8 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -47,8 +47,10 @@ struct integrate_t { /** * Flow field cell value. * - * Bit 2: Line of sight flag. - * Bit 3: Pathable flag. + * Bit 0: Line of sight flag. + * Bit 1: Pathable flag. + * Bit 2: Wavefront blocked flag. + * Bit 3: Unused. * Bits 4-7: flow direction. */ using flow_t = uint8_t; From 1ee2baa5f6afab6583a4cdd4a3b25fdc5ec0ce95 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 4 Mar 2024 02:59:20 +0100 Subject: [PATCH 335/771] path: Add another impassible cell to demo. --- libopenage/pathfinding/demo/demo_0.cpp | 3 ++- libopenage/pathfinding/integration_field.cpp | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 66aec6417e..b8462a7f27 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -29,7 +29,7 @@ void path_demo_0(const util::Path &path) { // Cost field with some obstacles auto cost_field = std::make_shared(field_length); - cost_field->set_cost(0, 0, 255); + cost_field->set_cost(0, 0, COST_IMPASSABLE); cost_field->set_cost(1, 0, 254); cost_field->set_cost(4, 3, 128); cost_field->set_cost(5, 3, 128); @@ -37,6 +37,7 @@ void path_demo_0(const util::Path &path) { cost_field->set_cost(4, 4, 128); cost_field->set_cost(5, 4, 128); cost_field->set_cost(6, 4, 128); + cost_field->set_cost(1, 7, COST_IMPASSABLE); log::log(INFO << "Created cost field"); // Create an integration field from the cost field diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 5e6459f873..587806de44 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -222,11 +222,11 @@ std::vector> IntegrationField::get_los_corners(const s // Get the cost of the blocking cell's neighbors - // Set all costs to MAX at the beginning - auto top_cost = COST_MAX; - auto left_cost = COST_MAX; - auto bottom_cost = COST_MAX; - auto right_cost = COST_MAX; + // Set all costs to IMPASSABLE at the beginning + auto top_cost = COST_IMPASSABLE; + auto left_cost = COST_IMPASSABLE; + auto bottom_cost = COST_IMPASSABLE; + auto right_cost = COST_IMPASSABLE; std::pair top_left{blocker_x, blocker_y}; std::pair top_right{blocker_x + 1, blocker_y}; From a36931f7480124cc0ebea210b80bdfd7f35406ed Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 8 Mar 2024 00:13:48 +0100 Subject: [PATCH 336/771] path: Integrate cost withexisting wavefront. --- libopenage/pathfinding/integration_field.cpp | 29 ++++++++++++++++---- libopenage/pathfinding/integration_field.h | 23 +++++++++++++--- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 587806de44..d871192c36 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -31,15 +31,18 @@ const integrate_t &IntegrationField::get_cell(size_t idx) const { return this->cells.at(idx); } -void IntegrationField::integrate_los(const std::shared_ptr &cost_field, - size_t target_x, - size_t target_y) { +std::vector IntegrationField::integrate_los(const std::shared_ptr &cost_field, + size_t target_x, + size_t target_y) { ENSURE(cost_field->get_size() == this->get_size(), "cost field size " << cost_field->get_size() << "x" << cost_field->get_size() << " does not match integration field size " << this->get_size() << "x" << this->get_size()); + // Store the wavefront cells + std::vector wavefront; + // Target cell index auto target_idx = target_x + target_y * this->size; @@ -84,6 +87,7 @@ void IntegrationField::integrate_los(const std::shared_ptr &cost_fiel for (auto &blocked_idx : blocked_cells) { this->cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; } + wavefront.insert(wavefront.end(), blocked_cells.begin(), blocked_cells.end()); } continue; @@ -106,6 +110,8 @@ void IntegrationField::integrate_los(const std::shared_ptr &cost_fiel open_list.push_back(idx + 1); } } + + return wavefront; } void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, @@ -120,6 +126,13 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie // Target cell index auto target_idx = target_x + target_y * this->size; + // Move outwards from the target cell, updating the integration field + this->cells[target_idx].cost = INTEGRATED_COST_START; + this->integrate_cost(cost_field, {target_idx}); +} + +void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, + std::vector &&start_cells) { // Lookup table for cells that are in the open list std::unordered_set in_list; in_list.reserve(this->size * this->size); @@ -132,9 +145,8 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie std::vector neighbors; neighbors.reserve(4); - // Move outwards from the target cell, updating the integration field - this->cells[target_idx].cost = INTEGRATED_COST_START; - open_list.push_back(target_idx); + // Move outwards from the wavefront, updating the integration field + open_list.insert(open_list.end(), start_cells.begin(), start_cells.end()); while (!open_list.empty()) { auto idx = open_list.front(); open_list.pop_front(); @@ -200,6 +212,11 @@ void IntegrationField::update_neighbor(size_t idx, return; } + // if (this->cells.at(idx).flags & INTEGRATE_LOS_MASK) { + // // If the cell is part of the LOS, we don't need to update it + // return; + // } + auto cost = integrated_cost + cell_cost; if (cost < this->cells.at(idx).cost) { // If the new integration value is smaller than the current one, diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 263a948fc1..43621f1959 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -53,25 +53,40 @@ class IntegrationField { /** * Calculate the line-of-sight integration flags for a target cell. * + * Returns a list of cells that are flagged as "wavefront blocked". These cells + * can be used as a starting point for the cost integration. + * * @param cost_field Cost field to integrate. * @param target_x X coordinate of the target cell. * @param target_y Y coordinate of the target cell. + * + * @return Cells flagged as "wavefront blocked". */ - void integrate_los(const std::shared_ptr &cost_field, - size_t target_x, - size_t target_y); + std::vector integrate_los(const std::shared_ptr &cost_field, + size_t target_x, + size_t target_y); /** - * Calculate the cost integration field for a target cell. + * Calculate the cost integration field starting from a target cell. * * @param cost_field Cost field to integrate. * @param target_x X coordinate of the target cell. * @param target_y Y coordinate of the target cell. + * @param start_cells Cells flagged as "wavefront blocked" from a LOS pass. */ void integrate_cost(const std::shared_ptr &cost_field, size_t target_x, size_t target_y); + /** + * Calculate the cost integration field starting from a wavefront. + * + * @param cost_field Cost field to integrate. + * @param start_cells Cells flagged as "wavefront blocked" from a LOS pass. + */ + void integrate_cost(const std::shared_ptr &cost_field, + std::vector &&start_cells = {}); + /** * Get the integration field values. * From 1cfac16d47477f183ede60f995e800b8e73f1778 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 8 Mar 2024 01:07:45 +0100 Subject: [PATCH 337/771] path: Use wavefront blocked cells as start for cost integration in demo. --- libopenage/pathfinding/demo/demo_0.cpp | 10 +- libopenage/pathfinding/integration_field.cpp | 117 +++++++++++-------- 2 files changed, 72 insertions(+), 55 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index b8462a7f27..e655598979 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -44,9 +44,9 @@ void path_demo_0(const util::Path &path) { auto integration_field = std::make_shared(field_length); log::log(INFO << "Created integration field"); - // Set cell (7, 7) to be the target cell - integration_field->integrate_los(cost_field, 7, 7); - integration_field->integrate_cost(cost_field, 7, 7); + // Set cell (7, 7) to be the initial target cell + auto wavefront_blocked = integration_field->integrate_los(cost_field, 7, 7); + integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); log::log(INFO << "Calculated integration field for target cell (7, 7)"); // Create a flow field from the integration field @@ -83,8 +83,8 @@ void path_demo_0(const util::Path &path) { // Recalculate the integration field and the flow field integration_field->reset(); - integration_field->integrate_los(cost_field, grid_x, grid_y); - integration_field->integrate_cost(cost_field, grid_x, grid_y); + auto wavefront_blocked = integration_field->integrate_los(cost_field, grid_x, grid_y); + integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); log::log(INFO << "Calculated integration field for target cell (" << grid_x << ", " << grid_y << ")"); diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index d871192c36..f3c8901828 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -40,8 +40,8 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrget_size() << "x" << this->get_size()); - // Store the wavefront cells - std::vector wavefront; + // Store the wavefront_blocked cells + std::vector wavefront_blocked; // Target cell index auto target_idx = target_x + target_y * this->size; @@ -50,68 +50,85 @@ std::vector IntegrationField::integrate_los(const std::shared_ptr found; found.reserve(this->size * this->size); - // Cells that still have to be visited - std::deque open_list; + // Cells that still have to be visited by the current wave + std::deque current_wave; - // Move outwards from the target cell, updating the integration field - open_list.push_back(target_idx); - while (!open_list.empty()) { - auto idx = open_list.front(); - open_list.pop_front(); + // Cells that have to be visited in the next wave + std::deque next_wave; - if (found.contains(idx)) { - // Skip cells that have already been found - continue; - } - else if (this->cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { - // Skip cells that are blocked by a LOS corner - continue; - } + // Cost of the current wave + integrated_cost_t cost = INTEGRATED_COST_START; - // Add the current cell to the found cells - found.insert(idx); + // Move outwards from the target cell, updating the integration field + current_wave.push_back(target_idx); + do { + while (!current_wave.empty()) { + auto idx = current_wave.front(); + current_wave.pop_front(); + + if (found.contains(idx)) { + // Skip cells that have already been found + continue; + } + else if (this->cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { + // Skip cells that are blocked by a LOS corner + this->cells[idx].cost = cost - 1 + cost_field->get_cost(idx); + continue; + } - // Get the x and y coordinates of the current cell - auto x = idx % this->size; - auto y = idx / this->size; + // Add the current cell to the found cells + found.insert(idx); + + // Get the x and y coordinates of the current cell + auto x = idx % this->size; + auto y = idx / this->size; - // Get the cost of the current cell - auto cell_cost = cost_field->get_cost(idx); + // Get the cost of the current cell + auto cell_cost = cost_field->get_cost(idx); - if (cell_cost > COST_MIN) { - // check each neighbor for a corner - auto corners = this->get_los_corners(cost_field, target_x, target_y, x, y); + if (cell_cost > COST_MIN) { + // check each neighbor for a corner + auto corners = this->get_los_corners(cost_field, target_x, target_y, x, y); - for (auto &corner : corners) { - auto blocked_cells = this->bresenhams_line(target_x, target_y, corner.first, corner.second); - for (auto &blocked_idx : blocked_cells) { - this->cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + for (auto &corner : corners) { + auto blocked_cells = this->bresenhams_line(target_x, target_y, corner.first, corner.second); + for (auto &blocked_idx : blocked_cells) { + // TODO: stop if blocked_idx is impassable + this->cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + } + wavefront_blocked.insert(wavefront_blocked.end(), blocked_cells.begin(), blocked_cells.end()); } - wavefront.insert(wavefront.end(), blocked_cells.begin(), blocked_cells.end()); - } - continue; - } + continue; + } - // Set the LOS flag of the current cell - this->cells[idx].flags |= INTEGRATE_LOS_MASK; + // The cell is in the line of sight at min cost + // Set the LOS flag and cost + this->cells[idx].cost = cost; + this->cells[idx].flags |= INTEGRATE_LOS_MASK; - // Search the neighbors of the current cell - if (y > 0) { - open_list.push_back(idx - this->size); - } - if (x > 0) { - open_list.push_back(idx - 1); - } - if (y < this->size - 1) { - open_list.push_back(idx + this->size); - } - if (x < this->size - 1) { - open_list.push_back(idx + 1); + // Search the neighbors of the current cell + if (y > 0) { + next_wave.push_back(idx - this->size); + } + if (x > 0) { + next_wave.push_back(idx - 1); + } + if (y < this->size - 1) { + next_wave.push_back(idx + this->size); + } + if (x < this->size - 1) { + next_wave.push_back(idx + 1); + } } + + // increment the cost and advance the wavefront outwards + cost += 1; + current_wave.swap(next_wave); } + while (!current_wave.empty()); - return wavefront; + return wavefront_blocked; } void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, From f9e4007e1ff49585185555399f8fcbb3a7d1e0a1 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 9 Mar 2024 11:21:51 +0100 Subject: [PATCH 338/771] path: Fix initial wavefront hit not being considered for cost. --- libopenage/pathfinding/integration_field.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index f3c8901828..f412f73657 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -87,6 +87,13 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrget_cost(idx); if (cell_cost > COST_MIN) { + if (cell_cost != COST_IMPASSABLE) { + // Add the current cell to the blocked wavefront if it's not a wall + wavefront_blocked.push_back(idx); + this->cells[idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + this->cells[idx].cost = cost - 1 + cost_field->get_cost(idx); + } + // check each neighbor for a corner auto corners = this->get_los_corners(cost_field, target_x, target_y, x, y); @@ -98,7 +105,6 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells.at(idx).flags & INTEGRATE_LOS_MASK) { - // // If the cell is part of the LOS, we don't need to update it - // return; - // } - auto cost = integrated_cost + cell_cost; if (cost < this->cells.at(idx).cost) { // If the new integration value is smaller than the current one, From 3b631136ad9a41ac3531a78bc99eb7e909454eae Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 9 Mar 2024 12:49:32 +0100 Subject: [PATCH 339/771] path: Fix cost calculation for wavefront blocked cells. --- libopenage/pathfinding/integration_field.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index f412f73657..88778bd5e1 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -71,8 +71,9 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { - // Skip cells that are blocked by a LOS corner + // Stop at cells that are blocked by a LOS corner this->cells[idx].cost = cost - 1 + cost_field->get_cost(idx); + found.insert(idx); continue; } @@ -90,7 +91,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + // this->cells[idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; this->cells[idx].cost = cost - 1 + cost_field->get_cost(idx); } @@ -100,7 +101,10 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrbresenhams_line(target_x, target_y, corner.first, corner.second); for (auto &blocked_idx : blocked_cells) { - // TODO: stop if blocked_idx is impassable + if (cost_field->get_cost(blocked_idx) == COST_IMPASSABLE) { + // stop if blocked_idx is impassable + break; + } this->cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; } wavefront_blocked.insert(wavefront_blocked.end(), blocked_cells.begin(), blocked_cells.end()); From 88f42e5d90297c84ca74fd1e8f81239f8172ba4c Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 10 Mar 2024 18:46:41 +0100 Subject: [PATCH 340/771] path: Increase precision of wavefront blocked lines. --- libopenage/pathfinding/integration_field.cpp | 34 ++++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 88778bd5e1..303591acc5 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -88,24 +88,30 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrget_cost(idx); if (cell_cost > COST_MIN) { + // cell blocks line of sight + // and we have to check for corners if (cell_cost != COST_IMPASSABLE) { // Add the current cell to the blocked wavefront if it's not a wall wavefront_blocked.push_back(idx); - // this->cells[idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; this->cells[idx].cost = cost - 1 + cost_field->get_cost(idx); } // check each neighbor for a corner auto corners = this->get_los_corners(cost_field, target_x, target_y, x, y); - for (auto &corner : corners) { + // draw a line from the corner to the edge of the field + // to get the cells blocked by the corner auto blocked_cells = this->bresenhams_line(target_x, target_y, corner.first, corner.second); for (auto &blocked_idx : blocked_cells) { - if (cost_field->get_cost(blocked_idx) == COST_IMPASSABLE) { + if (cost_field->get_cost(blocked_idx) > COST_MIN) { // stop if blocked_idx is impassable break; } + // set the blocked flag for the cell this->cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + + // clear los flag if it was set + this->cells[blocked_idx].flags &= ~INTEGRATE_LOS_MASK; } wavefront_blocked.insert(wavefront_blocked.end(), blocked_cells.begin(), blocked_cells.end()); } @@ -403,14 +409,16 @@ std::vector IntegrationField::bresenhams_line(int target_x, double dy = std::abs(ty - corner_y); auto m = dy / dx; - auto error = 0.5; + auto error = m; // Check which direction the line is going if (corner_x < tx) { if (corner_y < ty) { // left and up y -= 1; - while (x > 0 and y > 0) { + x -= 1; + while (x >= 0 and y >= 0) { + cells.push_back(x + y * this->size); if (error > 1.0) { y -= 1; error -= 1.0; @@ -419,12 +427,13 @@ std::vector IntegrationField::bresenhams_line(int target_x, x -= 1; error += m; } - cells.push_back(x + y * this->size); } } else { // left and down - while (x > 0 and y < this->size - 1) { + x -= 1; + while (x >= 0 and y < this->size) { + cells.push_back(x + y * this->size); if (error > 1.0) { y += 1; error -= 1.0; @@ -433,16 +442,15 @@ std::vector IntegrationField::bresenhams_line(int target_x, x -= 1; error += m; } - cells.push_back(x + y * this->size); } } } else { if (corner_y < ty) { // right and up - x -= 1; y -= 1; - while (x < this->size - 1 and y > 0) { + while (x < this->size and y >= 0) { + cells.push_back(x + y * this->size); if (error > 1.0) { y -= 1; error -= 1.0; @@ -451,13 +459,12 @@ std::vector IntegrationField::bresenhams_line(int target_x, x += 1; error += m; } - cells.push_back(x + y * this->size); } } else { // right and down - x -= 1; - while (x < this->size - 1 and y < this->size - 1) { + while (x < this->size and y < this->size) { + cells.push_back(x + y * this->size); if (error > 1.0) { y += 1; error -= 1.0; @@ -466,7 +473,6 @@ std::vector IntegrationField::bresenhams_line(int target_x, x += 1; error += m; } - cells.push_back(x + y * this->size); } } } From e3e420d4930645f99e5dcffc12a1ef2d879ac573 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 10 Mar 2024 19:02:52 +0100 Subject: [PATCH 341/771] path: Document our bresenham's line algorithm implementation. --- libopenage/pathfinding/integration_field.cpp | 69 +++++++++++--------- libopenage/pathfinding/integration_field.h | 25 ++++--- 2 files changed, 56 insertions(+), 38 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 303591acc5..09cebb0f4d 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -401,76 +401,85 @@ std::vector IntegrationField::bresenhams_line(int target_x, int corner_y) { std::vector cells; - auto x = corner_x; - auto y = corner_y; + // cell coordinates + // these have to be offset depending on the line direction + auto cell_x = corner_x; + auto cell_y = corner_y; + + // field edge boundary + int boundary = this->size; + + // target coordinates + // offset by 0.5 to get the center of the cell double tx = target_x + 0.5; double ty = target_y + 0.5; + + // slope of the line double dx = std::abs(tx - corner_x); double dy = std::abs(ty - corner_y); auto m = dy / dx; + // error margin for the line + // if the error is greater than 1.0, we have to move in the y direction auto error = m; // Check which direction the line is going if (corner_x < tx) { - if (corner_y < ty) { - // left and up - y -= 1; - x -= 1; - while (x >= 0 and y >= 0) { - cells.push_back(x + y * this->size); + if (corner_y < ty) { // left and up + cell_y -= 1; + cell_x -= 1; + while (cell_x >= 0 and cell_y >= 0) { + cells.push_back(cell_x + cell_y * this->size); if (error > 1.0) { - y -= 1; + cell_y -= 1; error -= 1.0; } else { - x -= 1; + cell_x -= 1; error += m; } } } - else { - // left and down - x -= 1; - while (x >= 0 and y < this->size) { - cells.push_back(x + y * this->size); + + else { // left and down + cell_x -= 1; + while (cell_x >= 0 and cell_y < boundary) { + cells.push_back(cell_x + cell_y * this->size); if (error > 1.0) { - y += 1; + cell_y += 1; error -= 1.0; } else { - x -= 1; + cell_x -= 1; error += m; } } } } else { - if (corner_y < ty) { - // right and up - y -= 1; - while (x < this->size and y >= 0) { - cells.push_back(x + y * this->size); + if (corner_y < ty) { // right and up + cell_y -= 1; + while (cell_x < boundary and cell_y >= 0) { + cells.push_back(cell_x + cell_y * this->size); if (error > 1.0) { - y -= 1; + cell_y -= 1; error -= 1.0; } else { - x += 1; + cell_x += 1; error += m; } } } - else { - // right and down - while (x < this->size and y < this->size) { - cells.push_back(x + y * this->size); + else { // right and down + while (cell_x < boundary and cell_y < boundary) { + cells.push_back(cell_x + cell_y * this->size); if (error > 1.0) { - y += 1; + cell_y += 1; error -= 1.0; } else { - x += 1; + cell_x += 1; error += m; } } diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 43621f1959..dc5821c856 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -106,6 +106,8 @@ class IntegrationField { * @param idx Index of the neighbor cell that is updated. * @param cell_cost Cost of the neighbor cell from the cost field. * @param integrated_cost Current integrated cost of the updating cell in the integration field. + * @param open_list List of cells to be updated. + * @param in_list Set of cells that have been updated. * * @return New integration value of the cell. */ @@ -119,10 +121,12 @@ class IntegrationField { * Get the LOS corners around a cell. * * @param cost_field Cost field to integrate. - * @param target_x X coordinate of the target. - * @param target_y Y coordinate of the target. - * @param blocker_x X coordinate of the cell blocking LOS. - * @param blocker_y Y coordinate of the cell blocking LOS. + * @param target_x X cell coordinate of the target. + * @param target_y Y cell coordinate of the target. + * @param blocker_x X cell coordinate of the cell blocking LOS. + * @param blocker_y Y cell coordinate of the cell blocking LOS. + * + * @return Field coordinates of the LOS corners. */ std::vector> get_los_corners(const std::shared_ptr &cost_field, size_t target_x, @@ -133,10 +137,15 @@ class IntegrationField { /** * Get the cells in a bresenham's line between the corner cell and the field edge. * - * @param target_x X coordinate of the target. - * @param target_y Y coordinate of the target. - * @param corner_x X coordinate edge of the LOS corner. - * @param corner_y Y coordinate edge of the LOS corner. + * This function is a modified version of the bresenham's line algorithm that + * retrieves the cells between the corner point and the field's edge, rather than + * the cells between two arbitrary points. We do this because the intersection + * point with the field edge is unknown. + * + * @param target_x X cell coordinate of the target. + * @param target_y Y cell coordinate of the target. + * @param corner_x X field coordinate edge of the LOS corner. + * @param corner_y Y field coordinate edge of the LOS corner. */ std::vector bresenhams_line(int target_x, int target_y, From 897bfe108fe9a083758817c8407a3fcc3cf015eb Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 10 Mar 2024 19:27:12 +0100 Subject: [PATCH 342/771] path: Add LOS pass to integrator. --- libopenage/pathfinding/flow_field.cpp | 4 +++ libopenage/pathfinding/integrator.cpp | 4 +-- libopenage/pathfinding/tests.cpp | 48 ++++++++++++++++++--------- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 64e7d82d23..52f4d7738e 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -56,6 +56,10 @@ void FlowField::build(const std::shared_ptr &integrate_field) if (integrate_cells[idx].flags & INTEGRATE_LOS_MASK) { // Cell is in line of sight this->cells[idx] = this->cells[idx] | FLOW_LOS_MASK; + + // we can skip calculating the flow direction as we can + // move straight to the target from this cell + this->cells[idx] = this->cells[idx] | FLOW_PATHABLE_MASK; continue; } diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index cbff7fd7cb..2c083b5615 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -17,8 +17,8 @@ std::shared_ptr Integrator::build(size_t target_x, size_t target_y) { auto flow_field = std::make_shared(this->cost_field->get_size()); auto integrate_field = std::make_shared(this->cost_field->get_size()); - integrate_field->integrate_los(this->cost_field, target_x, target_y); - integrate_field->integrate_cost(this->cost_field, target_x, target_y); + auto wavefront_blocked = integrate_field->integrate_los(this->cost_field, target_x, target_y); + integrate_field->integrate_cost(this->cost_field, std::move(wavefront_blocked)); flow_field->build(integrate_field); return flow_field; diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index 7b15a4406a..4cd6d40363 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -327,22 +327,6 @@ void flow_field() { // | 1 | 1 | 1 | cost_field->set_costs({1, 1, 1, 1, 255, 1, 1, 1, 1}); - // The flow field for targeting (2, 2) hould look like this: - // | E | SE | S | - // | SE | X | S | - // | E | E | N | - auto ff_expected = std::vector{ - FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), - FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), - FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH), - FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), - 0, - FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH), - FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), - FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), - FLOW_PATHABLE_MASK | static_cast(flow_dir_t::NORTH), - }; - // Test the different field types { auto integration_field = std::make_shared(3); @@ -365,6 +349,22 @@ void flow_field() { 0, }; + // The flow field for targeting (2, 2) hould look like this: + // | E | SE | S | + // | SE | X | S | + // | E | E | N | + auto ff_expected = std::vector{ + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), + 0, + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::NORTH), + }; + // Compare the integration field cells with the expected values for (size_t i = 0; i < int_cells.size(); i++) { TESTEQUALS(int_cells[i].cost, int_expected[i]); @@ -391,6 +391,22 @@ void flow_field() { auto flow_field = integrator->build(2, 2); auto ff_cells = flow_field->get_cells(); + // The flow field for targeting (2, 2) hould look like this: + // | E | SE | S | + // | SE | X | S | + // | E | E | N | + auto ff_expected = std::vector{ + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_WAVEFRONT_BLOCKED_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), + FLOW_LOS_MASK | FLOW_PATHABLE_MASK, + FLOW_WAVEFRONT_BLOCKED_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), + 0, + FLOW_LOS_MASK | FLOW_PATHABLE_MASK, + FLOW_LOS_MASK | FLOW_PATHABLE_MASK, + FLOW_LOS_MASK | FLOW_PATHABLE_MASK, + FLOW_LOS_MASK | FLOW_PATHABLE_MASK, + }; + // Compare the flow field cells with the expected values for (size_t i = 0; i < ff_cells.size(); i++) { TESTEQUALS(ff_cells[i], ff_expected[i]); From b693ea1bc295e05ada51a406c32cc8be0ddd59cf Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 17 Mar 2024 01:33:46 +0100 Subject: [PATCH 343/771] path: Grid for multiple fields. --- libopenage/pathfinding/CMakeLists.txt | 1 + libopenage/pathfinding/grid.cpp | 20 +++++++++ libopenage/pathfinding/grid.h | 64 +++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 libopenage/pathfinding/grid.cpp create mode 100644 libopenage/pathfinding/grid.h diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index 917cae429a..8c7531dd16 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -3,6 +3,7 @@ add_sources(libopenage cost_field.cpp definitions.cpp flow_field.cpp + grid.cpp heuristics.cpp integration_field.cpp integrator.cpp diff --git a/libopenage/pathfinding/grid.cpp b/libopenage/pathfinding/grid.cpp new file mode 100644 index 0000000000..fc3424b333 --- /dev/null +++ b/libopenage/pathfinding/grid.cpp @@ -0,0 +1,20 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "grid.h" + + +namespace openage::path { + +CostGrid::CostGrid(size_t width, size_t height, size_t field_size) : + width{width}, height{height}, fields{width * height, CostField{field_size}} { +} + +std::pair CostGrid::get_size() const { + return {this->width, this->height}; +} + +CostField &CostGrid::get_field(size_t x, size_t y) { + return this->fields[y * this->width + x]; +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/grid.h b/libopenage/pathfinding/grid.h new file mode 100644 index 0000000000..19eb7d682c --- /dev/null +++ b/libopenage/pathfinding/grid.h @@ -0,0 +1,64 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include + +#include "pathfinding/cost_field.h" + + +namespace openage::path { + +/** + * Grid of cost fields for flow field pathfinding. + */ +class CostGrid { +public: + /** + * Create a grid with a specified size and field size. + * + * @param width Width of the grid. + * @param height Height of the grid. + * @param field_size Size of the cost fields. + */ + CostGrid(size_t width, + size_t height, + size_t field_size); + + /** + * Get the size of the grid. + * + * @return Size of the grid (width x height). + */ + std::pair get_size() const; + + /** + * Get the cost field at a specified position. + * + * @param x X coordinate. + * @param y Y coordinate. + * @return Cost field at the specified position. + */ + CostField &get_field(size_t x, size_t y); + +private: + /** + * Width of the grid. + */ + size_t width; + + /** + * Height of the grid. + */ + size_t height; + + /** + * Cost fields. + */ + std::vector fields; +}; + + +} // namespace openage::path From 4278de9eeae55499d1cc9d7d0fad84de9c6d9e01 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 17 Mar 2024 16:11:24 +0100 Subject: [PATCH 344/771] path: Portals between fields. --- libopenage/pathfinding/CMakeLists.txt | 1 + libopenage/pathfinding/portal.cpp | 104 ++++++++++++++ libopenage/pathfinding/portal.h | 196 ++++++++++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 libopenage/pathfinding/portal.cpp create mode 100644 libopenage/pathfinding/portal.h diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index 8c7531dd16..c5d1b7aaa4 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -8,6 +8,7 @@ add_sources(libopenage integration_field.cpp integrator.cpp path.cpp + portal.cpp tests.cpp types.cpp ) diff --git a/libopenage/pathfinding/portal.cpp b/libopenage/pathfinding/portal.cpp new file mode 100644 index 0000000000..b959cbdc6c --- /dev/null +++ b/libopenage/pathfinding/portal.cpp @@ -0,0 +1,104 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "portal.h" + +#include "error/error.h" + + +namespace openage::path { + +Portal::Portal(std::shared_ptr sector0, + std::shared_ptr sector1, + std::vector> sector0_exits, + std::vector> sector1_exits, + PortalDirection direction, + size_t cell_start_x, + size_t cell_start_y, + size_t cell_end_x, + size_t cell_end_y) : + sector0{sector0}, + sector1{sector1}, + sector0_exits{sector0_exits}, + sector1_exits{sector1_exits}, + direction{direction}, + cell_start_x{cell_start_x}, + cell_start_y{cell_start_y}, + cell_end_x{cell_end_x}, + cell_end_y{cell_end_y} { +} + +const std::vector> &Portal::get_exits(const std::shared_ptr &entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + return this->sector1_exits; + } + return this->sector0_exits; +} + +const std::shared_ptr &Portal::get_exit_sector(const std::shared_ptr &entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + return this->sector1; + } + return this->sector0; +} + +std::pair Portal::get_entry_start(const std::shared_ptr &entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + return this->get_sector0_start(); + } + + return this->get_sector1_start(); +} + +std::pair Portal::get_entry_end(const std::shared_ptr &entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + return this->get_sector0_end(); + } + + return this->get_sector1_end(); +} + +std::pair Portal::get_exit_start(const std::shared_ptr &entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + return this->get_sector1_start(); + } + + return this->get_sector0_start(); +} + +std::pair Portal::get_exit_end(const std::shared_ptr &entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + return this->get_sector1_end(); + } + + return this->get_sector0_end(); +} + +std::pair Portal::get_sector0_start() const { + return {this->cell_start_x, this->cell_start_y}; +} + +std::pair Portal::get_sector0_end() const { + return {this->cell_end_x, this->cell_end_y}; +} + +std::pair Portal::get_sector1_start() const { + return {this->cell_end_x, this->cell_end_y}; +} + +std::pair Portal::get_sector1_end() const { + return {this->cell_start_x, this->cell_start_y}; +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/portal.h b/libopenage/pathfinding/portal.h new file mode 100644 index 0000000000..3632e3cecd --- /dev/null +++ b/libopenage/pathfinding/portal.h @@ -0,0 +1,196 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + + +#include + +#include "pathfinding/cost_field.h" + + +namespace openage::path { +class CostField; + +/** + * Possible directions of a portal node. + */ +enum class PortalDirection { + NORTH_SOUTH, + EAST_WEST +}; + +/** + * Biderectional gateway for connecting two sectors in the flow field pathfinder. + * + * Portals are located at the border of two sectors (0 and 1), and allow units to move between them. + * For each of these sectors, the portal stores the start and end coordinates where the + * sectors overlap as well as the other portals that can be reached in the same + * sector. For simplicity, the portal is assumed to be a straight line of cells from the start + * to the end. + * + * The portal is bidirectional, meaning that it can be entered from either sector and + * exited into the other sector. The direction of the portal from one sector to the other + * is stored in the portal node. As a convention and to simplify computations, sector 0 must be + * the either the north or east sector on the grid in relation to sector 1. + */ +class Portal { +public: + /** + * Create a new portal between two sectors. + * + * As a convention, sector 0 must be the either the north or east sector + * on the grid in relation to sector 1. + * + * @param sector0 First sector connected by the portal. + * Must be north or east on the grid in relation to sector 1. + * @param sector1 Second sector connected by the portal. + * Must be south or west on the grid in relation to sector 0. + * @param sector0_exits Portals reachable from this portal in sector 0. + * @param sector1_exits Portals reachable from this portal in sector 1. + * @param direction Direction of the portal from sector 0 to sector 1. + * @param cell_start_x Start cell x coordinate in sector 0. + * @param cell_start_y Start cell y coordinate in sector 0. + * @param cell_end_x End cell x coordinate in sector 0. + * @param cell_end_y End cell y coordinate in sector 0. + */ + Portal(std::shared_ptr sector0, + std::shared_ptr sector1, + std::vector> sector0_exits, + std::vector> sector1_exits, + PortalDirection direction, + size_t cell_start_x, + size_t cell_start_y, + size_t cell_end_x, + size_t cell_end_y); + + ~Portal() = default; + + /** + * Get the exit portals reachable via the portal when entering from a specified sector. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Exit portals reachable from the portal. + */ + const std::vector> &get_exits(const std::shared_ptr &entry_sector) const; + + /** + * Get the cost field of the sector where the portal is exited. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Cost field of the sector where the portal is exited. + */ + const std::shared_ptr &get_exit_sector(const std::shared_ptr &entry_sector) const; + + /** + * Get the cost field of the sector from which the portal is entered. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Cost field of the sector from which the portal is entered. + */ + std::pair get_entry_start(const std::shared_ptr &entry_sector) const; + + /** + * Get the cell coordinates of the start of the portal in the entry sector. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Cell coordinates of the start of the portal in the entry sector. + */ + std::pair get_entry_end(const std::shared_ptr &entry_sector) const; + + /** + * Get the cell coordinates of the start of the portal in the exit sector. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Cell coordinates of the start of the portal in the exit sector. + */ + std::pair get_exit_start(const std::shared_ptr &entry_sector) const; + + /** + * Get the cell coordinates of the end of the portal in the exit sector. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Cell coordinates of the end of the portal in the exit sector. + */ + std::pair get_exit_end(const std::shared_ptr &entry_sector) const; + +private: + /** + * Get the start cell coordinates of the portal. + * + * @return Start cell coordinates of the portal. + */ + std::pair get_sector0_start() const; + + /** + * Get the end cell coordinates of the portal. + * + * @return End cell coordinates of the portal. + */ + std::pair get_sector0_end() const; + + /** + * Get the start cell coordinates of the portal. + * + * @return Start cell coordinates of the portal. + */ + std::pair get_sector1_start() const; + + /** + * Get the end cell coordinates of the portal. + * + * @return End cell coordinates of the portal. + */ + std::pair get_sector1_end() const; + + /** + * Sector 0 + */ + std::shared_ptr sector0; + + /** + * Sector 1 + */ + std::shared_ptr sector1; + + /** + * Exits from sector 0 + */ + std::vector> sector0_exits; + + /** + * Exits from sector 1 + */ + std::vector> sector1_exits; + + /** + * Direction of the portal + */ + PortalDirection direction; + + /** + * Start cell x coordinate + */ + size_t cell_start_x; + + /** + * Start cell y coordinate + */ + size_t cell_start_y; + + /** + * End cell x coordinate + */ + size_t cell_end_x; + + /** + * End cell y coordinate + */ + size_t cell_end_y; +}; +} // namespace openage::path From a92b93d5751979a19c1ce68fb443033473c53afb Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 17 Mar 2024 18:35:28 +0100 Subject: [PATCH 345/771] path: Separate grid into sectors. --- libopenage/pathfinding/CMakeLists.txt | 1 + libopenage/pathfinding/grid.cpp | 30 +++++-- libopenage/pathfinding/grid.h | 49 ++++++++--- libopenage/pathfinding/portal.cpp | 33 +++++--- libopenage/pathfinding/portal.h | 55 +++++++------ libopenage/pathfinding/sector.cpp | 113 ++++++++++++++++++++++++++ libopenage/pathfinding/sector.h | 108 ++++++++++++++++++++++++ libopenage/pathfinding/types.h | 6 ++ 8 files changed, 340 insertions(+), 55 deletions(-) create mode 100644 libopenage/pathfinding/sector.cpp create mode 100644 libopenage/pathfinding/sector.h diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index c5d1b7aaa4..55a3f19ee0 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -9,6 +9,7 @@ add_sources(libopenage integrator.cpp path.cpp portal.cpp + sector.cpp tests.cpp types.cpp ) diff --git a/libopenage/pathfinding/grid.cpp b/libopenage/pathfinding/grid.cpp index fc3424b333..86b71d8681 100644 --- a/libopenage/pathfinding/grid.cpp +++ b/libopenage/pathfinding/grid.cpp @@ -2,19 +2,39 @@ #include "grid.h" +#include "pathfinding/sector.h" + namespace openage::path { -CostGrid::CostGrid(size_t width, size_t height, size_t field_size) : - width{width}, height{height}, fields{width * height, CostField{field_size}} { +Grid::Grid(size_t width, size_t height, size_t sector_size) : + width{width}, + height{height} { + for (size_t y = 0; y < height; y++) { + for (size_t x = 0; x < width; x++) { + this->sectors.push_back(std::make_shared(x + y * width, sector_size)); + } + } +} + +Grid::Grid(size_t width, + size_t height, + std::vector> &§ors) : + width{width}, + height{height}, + sectors{std::move(sectors)} { } -std::pair CostGrid::get_size() const { +std::pair Grid::get_size() const { return {this->width, this->height}; } -CostField &CostGrid::get_field(size_t x, size_t y) { - return this->fields[y * this->width + x]; +const std::shared_ptr &Grid::get_sector(size_t x, size_t y) { + return this->sectors.at(x + y * this->width); +} + +const std::shared_ptr &Grid::get_sector(sector_id_t id) const { + return this->sectors.at(id); } } // namespace openage::path diff --git a/libopenage/pathfinding/grid.h b/libopenage/pathfinding/grid.h index 19eb7d682c..22c551066c 100644 --- a/libopenage/pathfinding/grid.h +++ b/libopenage/pathfinding/grid.h @@ -3,29 +3,42 @@ #pragma once #include +#include #include #include -#include "pathfinding/cost_field.h" +#include "pathfinding/types.h" namespace openage::path { +class Sector; /** - * Grid of cost fields for flow field pathfinding. + * Grid for flow field pathfinding. */ -class CostGrid { +class Grid { public: /** - * Create a grid with a specified size and field size. + * Create a new empty grid of width x height sectors with a specified size. * * @param width Width of the grid. * @param height Height of the grid. - * @param field_size Size of the cost fields. + * @param sector_size Side length of each sector. */ - CostGrid(size_t width, - size_t height, - size_t field_size); + Grid(size_t width, + size_t height, + size_t sector_size); + + /** + * Create a grid of width x height sectors from a list of existing sectors. + * + * @param width Width of the grid. + * @param height Height of the grid. + * @param sectors Existing sectors. + */ + Grid(size_t width, + size_t height, + std::vector> &§ors); /** * Get the size of the grid. @@ -35,13 +48,23 @@ class CostGrid { std::pair get_size() const; /** - * Get the cost field at a specified position. + * Get the sector at a specified position. * * @param x X coordinate. * @param y Y coordinate. - * @return Cost field at the specified position. + * + * @return Sector at the specified position. + */ + const std::shared_ptr &get_sector(size_t x, size_t y); + + /** + * Get the sector with a specified ID + * + * @param id ID of the sector. + * + * @return Sector with the specified ID. */ - CostField &get_field(size_t x, size_t y); + const std::shared_ptr &get_sector(sector_id_t id) const; private: /** @@ -55,9 +78,9 @@ class CostGrid { size_t height; /** - * Cost fields. + * Sectors of the grid. */ - std::vector fields; + std::vector> sectors; }; diff --git a/libopenage/pathfinding/portal.cpp b/libopenage/pathfinding/portal.cpp index b959cbdc6c..40111e0b98 100644 --- a/libopenage/pathfinding/portal.cpp +++ b/libopenage/pathfinding/portal.cpp @@ -7,10 +7,8 @@ namespace openage::path { -Portal::Portal(std::shared_ptr sector0, - std::shared_ptr sector1, - std::vector> sector0_exits, - std::vector> sector1_exits, +Portal::Portal(sector_id_t sector0, + sector_id_t sector1, PortalDirection direction, size_t cell_start_x, size_t cell_start_y, @@ -18,8 +16,8 @@ Portal::Portal(std::shared_ptr sector0, size_t cell_end_y) : sector0{sector0}, sector1{sector1}, - sector0_exits{sector0_exits}, - sector1_exits{sector1_exits}, + sector0_exits{}, + sector1_exits{}, direction{direction}, cell_start_x{cell_start_x}, cell_start_y{cell_start_y}, @@ -27,7 +25,7 @@ Portal::Portal(std::shared_ptr sector0, cell_end_y{cell_end_y} { } -const std::vector> &Portal::get_exits(const std::shared_ptr &entry_sector) const { +const std::vector> &Portal::get_exits(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -36,7 +34,18 @@ const std::vector> &Portal::get_exits(const std::shared_ return this->sector0_exits; } -const std::shared_ptr &Portal::get_exit_sector(const std::shared_ptr &entry_sector) const { +void Portal::set_exits(sector_id_t sector, const std::vector> &exits) { + ENSURE(sector == this->sector0 || sector == this->sector1, "Portal does not connect to sector"); + + if (sector == this->sector0) { + this->sector0_exits = exits; + } + else { + this->sector1_exits = exits; + } +} + +sector_id_t Portal::get_exit_sector(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -45,7 +54,7 @@ const std::shared_ptr &Portal::get_exit_sector(const std::shared_ptr< return this->sector0; } -std::pair Portal::get_entry_start(const std::shared_ptr &entry_sector) const { +std::pair Portal::get_entry_start(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -55,7 +64,7 @@ std::pair Portal::get_entry_start(const std::shared_ptrget_sector1_start(); } -std::pair Portal::get_entry_end(const std::shared_ptr &entry_sector) const { +std::pair Portal::get_entry_end(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -65,7 +74,7 @@ std::pair Portal::get_entry_end(const std::shared_ptr return this->get_sector1_end(); } -std::pair Portal::get_exit_start(const std::shared_ptr &entry_sector) const { +std::pair Portal::get_exit_start(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -75,7 +84,7 @@ std::pair Portal::get_exit_start(const std::shared_ptrget_sector0_start(); } -std::pair Portal::get_exit_end(const std::shared_ptr &entry_sector) const { +std::pair Portal::get_exit_end(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { diff --git a/libopenage/pathfinding/portal.h b/libopenage/pathfinding/portal.h index 3632e3cecd..dd6e474931 100644 --- a/libopenage/pathfinding/portal.h +++ b/libopenage/pathfinding/portal.h @@ -2,10 +2,11 @@ #pragma once - #include +#include +#include -#include "pathfinding/cost_field.h" +#include "pathfinding/types.h" namespace openage::path { @@ -45,18 +46,14 @@ class Portal { * Must be north or east on the grid in relation to sector 1. * @param sector1 Second sector connected by the portal. * Must be south or west on the grid in relation to sector 0. - * @param sector0_exits Portals reachable from this portal in sector 0. - * @param sector1_exits Portals reachable from this portal in sector 1. * @param direction Direction of the portal from sector 0 to sector 1. * @param cell_start_x Start cell x coordinate in sector 0. * @param cell_start_y Start cell y coordinate in sector 0. * @param cell_end_x End cell x coordinate in sector 0. * @param cell_end_y End cell y coordinate in sector 0. */ - Portal(std::shared_ptr sector0, - std::shared_ptr sector1, - std::vector> sector0_exits, - std::vector> sector1_exits, + Portal(sector_id_t sector0, + sector_id_t sector1, PortalDirection direction, size_t cell_start_x, size_t cell_start_y, @@ -72,7 +69,15 @@ class Portal { * * @return Exit portals reachable from the portal. */ - const std::vector> &get_exits(const std::shared_ptr &entry_sector) const; + const std::vector> &get_exits(sector_id_t entry_sector) const; + + /** + * Set the exit portals reachable for a specified sector. + * + * @param sector Sector for which the exit portals are set. + * @param exits Exit portals reachable from the portal. + */ + void set_exits(sector_id_t sector, const std::vector> &exits); /** * Get the cost field of the sector where the portal is exited. @@ -81,7 +86,7 @@ class Portal { * * @return Cost field of the sector where the portal is exited. */ - const std::shared_ptr &get_exit_sector(const std::shared_ptr &entry_sector) const; + sector_id_t get_exit_sector(sector_id_t entry_sector) const; /** * Get the cost field of the sector from which the portal is entered. @@ -90,7 +95,7 @@ class Portal { * * @return Cost field of the sector from which the portal is entered. */ - std::pair get_entry_start(const std::shared_ptr &entry_sector) const; + std::pair get_entry_start(sector_id_t entry_sector) const; /** * Get the cell coordinates of the start of the portal in the entry sector. @@ -99,7 +104,7 @@ class Portal { * * @return Cell coordinates of the start of the portal in the entry sector. */ - std::pair get_entry_end(const std::shared_ptr &entry_sector) const; + std::pair get_entry_end(sector_id_t entry_sector) const; /** * Get the cell coordinates of the start of the portal in the exit sector. @@ -108,7 +113,7 @@ class Portal { * * @return Cell coordinates of the start of the portal in the exit sector. */ - std::pair get_exit_start(const std::shared_ptr &entry_sector) const; + std::pair get_exit_start(sector_id_t entry_sector) const; /** * Get the cell coordinates of the end of the portal in the exit sector. @@ -117,7 +122,7 @@ class Portal { * * @return Cell coordinates of the end of the portal in the exit sector. */ - std::pair get_exit_end(const std::shared_ptr &entry_sector) const; + std::pair get_exit_end(sector_id_t entry_sector) const; private: /** @@ -149,47 +154,47 @@ class Portal { std::pair get_sector1_end() const; /** - * Sector 0 + * First sector connected by the portal. */ - std::shared_ptr sector0; + sector_id_t sector0; /** - * Sector 1 + * Second sector connected by the portal. */ - std::shared_ptr sector1; + sector_id_t sector1; /** - * Exits from sector 0 + * Exits in sector 0 reachable from the portal. */ std::vector> sector0_exits; /** - * Exits from sector 1 + * Exits in sector 1 reachable from the portal. */ std::vector> sector1_exits; /** - * Direction of the portal + * Direction of the portal from sector 0 to sector 1. */ PortalDirection direction; /** - * Start cell x coordinate + * Start cell x coordinate. */ size_t cell_start_x; /** - * Start cell y coordinate + * Start cell y coordinate. */ size_t cell_start_y; /** - * End cell x coordinate + * End cell x coordinate. */ size_t cell_end_x; /** - * End cell y coordinate + * End cell y coordinate. */ size_t cell_end_y; }; diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp new file mode 100644 index 0000000000..8c5221821d --- /dev/null +++ b/libopenage/pathfinding/sector.cpp @@ -0,0 +1,113 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "sector.h" + +#include "error/error.h" + +#include "pathfinding/cost_field.h" +#include "pathfinding/definitions.h" + + +namespace openage::path { + +Sector::Sector(sector_id_t id, size_t field_size) : + id{id}, + cost_field{std::make_shared(field_size)} { +} + +Sector::Sector(sector_id_t id, const std::shared_ptr &cost_field) : + id{id}, + cost_field{cost_field} { +} + +const sector_id_t &Sector::get_id() const { + return this->id; +} + +const std::shared_ptr &Sector::get_cost_field() const { + return this->cost_field; +} + +const std::vector> &Sector::get_portals() const { + return this->portals; +} + +void Sector::add_portal(const std::shared_ptr &portal) { + this->portals.push_back(portal); +} + +std::vector> Sector::find_portals(const std::shared_ptr &other, + PortalDirection direction) const { + ENSURE(this->cost_field->get_size() == other->get_cost_field()->get_size(), "Sector size mismatch"); + + std::vector> result; + + // cost field of the other sector + auto other_cost = other->get_cost_field(); + + // compare the edges of the sectors + if (direction == PortalDirection::NORTH_SOUTH) { + // search from left to right + size_t start = 0; + bool passable_edge = false; + for (size_t x = 0; x < this->cost_field->get_size(); x++) { + if (this->cost_field->get_cost(x, this->cost_field->get_size() - 1) != COST_IMPASSABLE + and other_cost->get_cost(x, 0) != COST_IMPASSABLE) { + if (not passable_edge) { + start = x; + passable_edge = true; + } + } + else { + if (passable_edge) { + result.push_back( + std::make_shared( + this->id, + other->get_id(), + direction, + start, + this->cost_field->get_size() - 1, + x - 1, + 0)); + passable_edge = false; + } + } + } + } + else if (direction == PortalDirection::EAST_WEST) { + // search from top to bottom + size_t start = 0; + bool passable_edge = false; + for (size_t y = 0; y < this->cost_field->get_size(); y++) { + if (this->cost_field->get_cost(this->cost_field->get_size() - 1, y) != COST_IMPASSABLE + and other_cost->get_cost(0, y) != COST_IMPASSABLE) { + if (not passable_edge) { + start = y; + passable_edge = true; + } + } + else { + if (passable_edge) { + result.push_back( + std::make_shared( + this->id, + other->get_id(), + direction, + this->cost_field->get_size() - 1, + start, + 0, + y - 1)); + passable_edge = false; + } + } + } + } + + return result; +} + +void Sector::connect_portals() { + // TODO: Flood fill to find connected sectors +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h new file mode 100644 index 0000000000..b0dbd2bc18 --- /dev/null +++ b/libopenage/pathfinding/sector.h @@ -0,0 +1,108 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include + +#include "pathfinding/portal.h" +#include "pathfinding/types.h" + + +namespace openage::path { +class CostField; +class Portal; + +/** + * Sector in a grid for flow field pathfinding. + * + * Sectors consist of a cost field and a list of portals connecting them to adjacent + * sectors. + */ +class Sector { +public: + /** + * Create a new sector with a specified ID and an uninitialized cost field. + * + * @param id ID of the sector. + * @param field_size Size of the cost field. + */ + Sector(sector_id_t id, + size_t field_size); + + /** + * Create a new sector with a specified ID and an existing cost field. + * + * @param id ID of the sector. + * @param cost_field Cost field of the sector. + */ + Sector(sector_id_t id, + const std::shared_ptr &cost_field); + + /** + * Get the ID of this sector. + * + * @return ID of the sector. + */ + const sector_id_t &get_id() const; + + /** + * Get the cost field of this sector. + * + * @return Cost field of this sector. + */ + const std::shared_ptr &get_cost_field() const; + + /** + * Get the portals connecting this sector to other sectors. + * + * @return Outgoing portals of this sector. + */ + const std::vector> &get_portals() const; + + /** + * Add a portal to another sector. + * + * @param portal Portal to another sector. + */ + void add_portal(const std::shared_ptr &portal); + + /** + * Find portals connecting this sector to another sector. + * + * @param other Sector to which the portals should connect. + * @param direction Direction from this sector to the other sector. + * + * @return Portals connecting this sector to the other sector. + */ + std::vector> find_portals(const std::shared_ptr &other, + PortalDirection direction) const; + + /** + * Connect all portals that are mutually reachable. + * + * This method should be called after all sectors and portals have + * been created and initialized. + */ + void connect_portals(); + +private: + /** + * ID of the sector. + */ + sector_id_t id; + + /** + * Cost field of the sector. + */ + std::shared_ptr cost_field; + + /** + * Portals of the sector. + */ + std::vector> portals; +}; + + +} // namespace openage::path diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index 8280dfcdf8..118c8c116d 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -2,6 +2,7 @@ #pragma once +#include #include @@ -55,4 +56,9 @@ struct integrate_t { */ using flow_t = uint8_t; +/** + * Sector identifier on a grid. + */ +using sector_id_t = size_t; + } // namespace openage::path From 1f48794edf47fc0f303010abb2f799e54aa817da Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 17 Mar 2024 19:23:38 +0100 Subject: [PATCH 346/771] path: Move old path implementation to 'legacy' folder. --- libopenage/pathfinding/CMakeLists.txt | 4 +- libopenage/pathfinding/legacy/CMakeLists.txt | 6 + .../pathfinding/{ => legacy}/a_star.cpp | 10 +- libopenage/pathfinding/{ => legacy}/a_star.h | 0 .../pathfinding/{ => legacy}/heuristics.cpp | 0 .../pathfinding/{ => legacy}/heuristics.h | 2 +- libopenage/pathfinding/{ => legacy}/path.cpp | 2 +- libopenage/pathfinding/{ => legacy}/path.h | 10 +- .../pathfinding/{ => legacy}/path_utils.h | 0 libopenage/pathfinding/legacy/tests.cpp | 317 +++++++++++++++++ libopenage/pathfinding/tests.cpp | 320 +----------------- 11 files changed, 344 insertions(+), 327 deletions(-) create mode 100644 libopenage/pathfinding/legacy/CMakeLists.txt rename libopenage/pathfinding/{ => legacy}/a_star.cpp (96%) rename libopenage/pathfinding/{ => legacy}/a_star.h (100%) rename libopenage/pathfinding/{ => legacy}/heuristics.cpp (100%) rename libopenage/pathfinding/{ => legacy}/heuristics.h (97%) rename libopenage/pathfinding/{ => legacy}/path.cpp (98%) rename libopenage/pathfinding/{ => legacy}/path.h (96%) rename libopenage/pathfinding/{ => legacy}/path_utils.h (100%) create mode 100644 libopenage/pathfinding/legacy/tests.cpp diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index 55a3f19ee0..84e7374203 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -1,13 +1,10 @@ add_sources(libopenage - a_star.cpp cost_field.cpp definitions.cpp flow_field.cpp grid.cpp - heuristics.cpp integration_field.cpp integrator.cpp - path.cpp portal.cpp sector.cpp tests.cpp @@ -15,3 +12,4 @@ add_sources(libopenage ) add_subdirectory("demo") +add_subdirectory("legacy") diff --git a/libopenage/pathfinding/legacy/CMakeLists.txt b/libopenage/pathfinding/legacy/CMakeLists.txt new file mode 100644 index 0000000000..ad828995c9 --- /dev/null +++ b/libopenage/pathfinding/legacy/CMakeLists.txt @@ -0,0 +1,6 @@ +add_sources(libopenage + a_star.cpp + heuristics.cpp + path.cpp + tests.cpp +) diff --git a/libopenage/pathfinding/a_star.cpp b/libopenage/pathfinding/legacy/a_star.cpp similarity index 96% rename from libopenage/pathfinding/a_star.cpp rename to libopenage/pathfinding/legacy/a_star.cpp index 6fc4a53c8c..640a20bb32 100644 --- a/libopenage/pathfinding/a_star.cpp +++ b/libopenage/pathfinding/legacy/a_star.cpp @@ -14,11 +14,11 @@ #include -#include "../datastructure/pairing_heap.h" -#include "../log/log.h" -#include "../util/strings.h" -#include "heuristics.h" -#include "path.h" +#include "datastructure/pairing_heap.h" +#include "log/log.h" +#include "pathfinding/legacy/heuristics.h" +#include "pathfinding/legacy/path.h" +#include "util/strings.h" namespace openage { diff --git a/libopenage/pathfinding/a_star.h b/libopenage/pathfinding/legacy/a_star.h similarity index 100% rename from libopenage/pathfinding/a_star.h rename to libopenage/pathfinding/legacy/a_star.h diff --git a/libopenage/pathfinding/heuristics.cpp b/libopenage/pathfinding/legacy/heuristics.cpp similarity index 100% rename from libopenage/pathfinding/heuristics.cpp rename to libopenage/pathfinding/legacy/heuristics.cpp diff --git a/libopenage/pathfinding/heuristics.h b/libopenage/pathfinding/legacy/heuristics.h similarity index 97% rename from libopenage/pathfinding/heuristics.h rename to libopenage/pathfinding/legacy/heuristics.h index 9d84560f4c..e9e9a03709 100644 --- a/libopenage/pathfinding/heuristics.h +++ b/libopenage/pathfinding/legacy/heuristics.h @@ -2,7 +2,7 @@ #pragma once -#include "path.h" +#include "pathfinding/legacy/path.h" namespace openage { namespace path { diff --git a/libopenage/pathfinding/path.cpp b/libopenage/pathfinding/legacy/path.cpp similarity index 98% rename from libopenage/pathfinding/path.cpp rename to libopenage/pathfinding/legacy/path.cpp index a709ab0648..6db3c528f9 100644 --- a/libopenage/pathfinding/path.cpp +++ b/libopenage/pathfinding/legacy/path.cpp @@ -2,7 +2,7 @@ #include -#include "path.h" +#include "pathfinding/legacy/path.h" namespace openage::path { diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/legacy/path.h similarity index 96% rename from libopenage/pathfinding/path.h rename to libopenage/pathfinding/legacy/path.h index bbe7aa0f62..a6d7aa80ae 100644 --- a/libopenage/pathfinding/path.h +++ b/libopenage/pathfinding/legacy/path.h @@ -7,11 +7,11 @@ #include #include -#include "../coord/phys.h" -#include "../coord/tile.h" -#include "../datastructure/pairing_heap.h" -#include "../util/hash.h" -#include "../util/misc.h" +#include "coord/phys.h" +#include "coord/tile.h" +#include "datastructure/pairing_heap.h" +#include "util/hash.h" +#include "util/misc.h" namespace openage { diff --git a/libopenage/pathfinding/path_utils.h b/libopenage/pathfinding/legacy/path_utils.h similarity index 100% rename from libopenage/pathfinding/path_utils.h rename to libopenage/pathfinding/legacy/path_utils.h diff --git a/libopenage/pathfinding/legacy/tests.cpp b/libopenage/pathfinding/legacy/tests.cpp new file mode 100644 index 0000000000..f68e512660 --- /dev/null +++ b/libopenage/pathfinding/legacy/tests.cpp @@ -0,0 +1,317 @@ +// Copyright 2015-2024 the openage authors. See copying.md for legal info. + +#include "log/log.h" +#include "testing/testing.h" + +#include "pathfinding/legacy/heuristics.h" +#include "pathfinding/legacy/path.h" + +namespace openage { +namespace path { +namespace tests { + +/** + * This function tests setting up basic nodes that point to a previous node. + * Tests that direction is set correctly and that factor is set correctly. + */ +void node_0() { + coord::phys3 p0{0, 0, 0}; + coord::phys3 p1{1, 0, 0}; + coord::phys3 p2{1, 1, 0}; + coord::phys3 p3{1, -1, 0}; + coord::phys3 p4{2, 0, 0}; + coord::phys3 p5{2, 2, 0}; + coord::phys3 p6{2, -2, 0}; + + node_pt n0 = std::make_unique(p0, nullptr); + node_pt n1 = std::make_unique(p1, n0); + node_pt n2 = std::make_unique(p2, n1); + node_pt n3 = std::make_unique(p3, n1); + node_pt n4 = std::make_unique(p0, n1); + + // Testing how the factor is effected from the change in + // direction from one node to another + TESTEQUALS(n1->direction.ne, 1); + TESTEQUALS(n1->direction.se, 0); + + // Expect this to be 2 since the similarity between nodes is zero + TESTEQUALS(n1->factor, 2); + + TESTEQUALS(n2->direction.ne, 0); + TESTEQUALS(n2->direction.se, 1); + + // Expect this to be 2 since it takes a 90 degree turn from n1 + TESTEQUALS(n2->factor, 2); + + TESTEQUALS(n3->direction.ne, 0); + TESTEQUALS(n3->direction.se, -1); + + // Expect this to be 2 since it takes a 90 degree turn from n1 + TESTEQUALS(n3->factor, 2); + + TESTEQUALS(n4->direction.ne, -1); + TESTEQUALS(n4->direction.se, 0); + + // Expect this to be 3 since it takes a 180 degree turn from n1 + TESTEQUALS(n4->factor, 3); + + // Testing that the distance from the previous node noes not + // effect the factor, only change in direction + + n1 = std::make_unique(p4, n0); + n2 = std::make_unique(p5, n1); + n3 = std::make_unique(p6, n1); + n4 = std::make_unique(p0, n1); + + TESTEQUALS(n1->direction.ne, 1); + TESTEQUALS(n1->direction.se, 0); + + // Expect this to be 2 since the similarity between nodes is zero + TESTEQUALS(n1->factor, 2); + TESTEQUALS(n2->direction.ne, 0); + TESTEQUALS(n2->direction.se, 1); + + // Expect this to be 2 since it takes a 90 degree turn from n1 + TESTEQUALS(n2->factor, 2); + TESTEQUALS(n3->direction.ne, 0); + TESTEQUALS(n3->direction.se, -1); + + // Expect this to be 2 since it takes a 90 degree turn from n1 + TESTEQUALS(n3->factor, 2); + TESTEQUALS(n4->direction.ne, -1); + TESTEQUALS(n4->direction.se, 0); + + // Expect this to be 3 since it takes a 180 degree turn from n1 + TESTEQUALS(n4->factor, 3); +} + +/** + * This function tests Node->cost_to. The testing is done on 2 unrelated + * nodes (They have no previous node) to test the basic cost without adding + * the cost from node->factor. + */ +void node_cost_to_0() { + // Testing basic cost_to with ne only + coord::phys3 p0{0, 0, 0}; + coord::phys3 p1{10, 0, 0}; + + node_pt n0 = std::make_unique(p0, nullptr); + node_pt n1 = std::make_unique(p1, nullptr); + + TESTEQUALS(n0->cost_to(*n1), 10); + TESTEQUALS(n1->cost_to(*n0), 10); + + // Testing basic cost_to with se only + coord::phys3 p2{0, 5, 0}; + + node_pt n2 = std::make_unique(p2, nullptr); + + TESTEQUALS(n0->cost_to(*n2), 5); + TESTEQUALS(n2->cost_to(*n0), 5); + + // Testing cost_to with both se and ne: + coord::phys3 p3{3, 4, 0}; // -> sqrt(3*3 + 4*4) == 5 + + node_pt n3 = std::make_unique(p3, nullptr); + TESTEQUALS(n0->cost_to(*n3), 5); + TESTEQUALS(n3->cost_to(*n0), 5); + + // Test cost_to and check that `up` has no effect + coord::phys3 p4{3, 4, 8}; + + node_pt n4 = std::make_unique(p4, nullptr); + + TESTEQUALS(n0->cost_to(*n4), 5); + TESTEQUALS(n4->cost_to(*n0), 5); +} + +/** + * This function tests Node->cost_to. The testing is done on the neighbor + * nodes to test how the directional factor effects the cost. + */ +void node_cost_to_1() { + // Set up coords so that n1 will have a direction of ne = 1 + // but n0 with not be in n1s neighbors + coord::phys3 p0{-0.125, 0, 0}; + coord::phys3 p1{0.125, 0, 0}; + + node_pt n0 = std::make_unique(p0, nullptr); + node_pt n1 = std::make_unique(p1, n0); + + // We expect twice the normal cost since n0 had not direction + // thus we get a factor of 2 on n1 + TESTEQUALS_FLOAT(n0->cost_to(*n1), 0.5, 0.001); + TESTEQUALS_FLOAT(n1->cost_to(*n0), 0.5, 0.001); + + nodemap_t visited_tiles; + visited_tiles[n0->position] = n0; + + // Collect the costs to go to all the neighbors of n1 + std::vector costs; + for (node_pt neighbor : n1->get_neighbors(visited_tiles, 1)) { + costs.push_back(n1->cost_to(*neighbor)); + } + + TESTEQUALS_FLOAT(costs[0], 0.45711, 0.001); + TESTEQUALS_FLOAT(costs[1], 0.25, 0.001); + TESTEQUALS_FLOAT(costs[2], 0.45711, 0.001); + TESTEQUALS_FLOAT(costs[3], 0.5, 0.001); + TESTEQUALS_FLOAT(costs[4], 0.95709, 0.001); + TESTEQUALS_FLOAT(costs[5], 0.75, 0.001); + TESTEQUALS_FLOAT(costs[6], 0.95709, 0.001); + TESTEQUALS_FLOAT(costs[7], 0.5, 0.001); +} + +/** + * This function does a basic test of generating a backtrace from the + * last node in a path. + */ +void node_generate_backtrace_0() { + coord::phys3 p0{0, 0, 0}; + coord::phys3 p1{10, 0, 0}; + coord::phys3 p2{20, 0, 0}; + coord::phys3 p3{30, 0, 0}; + + node_pt n0 = std::make_unique(p0, nullptr); + node_pt n1 = std::make_unique(p1, n0); + node_pt n2 = std::make_unique(p2, n1); + node_pt n3 = std::make_unique(p3, n2); + + Path path = n3->generate_backtrace(); + + (path.waypoints[0] == *n3) or TESTFAIL; + (path.waypoints[1] == *n2) or TESTFAIL; + (path.waypoints[2] == *n1) or TESTFAIL; +} + +/** + * This function tests Node->get_neighbors and how the scale effects + * the neighbors given. + */ +void node_get_neighbors_0() { + coord::phys3 p0{0, 0, 0}; + + node_pt n0 = std::make_unique(p0, nullptr); + nodemap_t map; + + // Testing get_neighbors returning all surounding tiles with + // a factor of 1 + + std::vector neighbors = n0->get_neighbors(map, 1); + TESTEQUALS(neighbors.size(), 8); + + TESTEQUALS_FLOAT(neighbors[0]->position.ne.to_double(), 0.125, 0.001); + TESTEQUALS_FLOAT(neighbors[0]->position.se.to_double(), -0.125, 0.001); + + TESTEQUALS_FLOAT(neighbors[1]->position.ne.to_double(), 0.125, 0.001); + TESTEQUALS_FLOAT(neighbors[1]->position.se.to_double(), 0, 0.001); + + TESTEQUALS_FLOAT(neighbors[2]->position.ne.to_double(), 0.125, 0.001); + TESTEQUALS_FLOAT(neighbors[2]->position.se.to_double(), 0.125, 0.001); + + TESTEQUALS_FLOAT(neighbors[3]->position.ne.to_double(), 0, 0.001); + TESTEQUALS_FLOAT(neighbors[3]->position.se.to_double(), 0.125, 0.001); + + TESTEQUALS_FLOAT(neighbors[4]->position.ne.to_double(), -0.125, 0.001); + TESTEQUALS_FLOAT(neighbors[4]->position.se.to_double(), 0.125, 0.001); + + TESTEQUALS_FLOAT(neighbors[5]->position.ne.to_double(), -0.125, 0.001); + TESTEQUALS_FLOAT(neighbors[5]->position.se.to_double(), 0, 0.001); + + TESTEQUALS_FLOAT(neighbors[6]->position.ne.to_double(), -0.125, 0.001); + TESTEQUALS_FLOAT(neighbors[6]->position.se.to_double(), -0.125, 0.001); + + TESTEQUALS_FLOAT(neighbors[7]->position.ne.to_double(), 0, 0.001); + TESTEQUALS_FLOAT(neighbors[7]->position.se.to_double(), -0.125, 0.001); + + // Testing how a larger scale changes the neighbors generated + neighbors = n0->get_neighbors(map, 2); + TESTEQUALS(neighbors.size(), 8); + + TESTEQUALS_FLOAT(neighbors[0]->position.ne.to_double(), 0.25, 0.001); + TESTEQUALS_FLOAT(neighbors[0]->position.se.to_double(), -0.25, 0.001); + + TESTEQUALS_FLOAT(neighbors[1]->position.ne.to_double(), 0.25, 0.001); + TESTEQUALS_FLOAT(neighbors[1]->position.se.to_double(), 0, 0.001); + + TESTEQUALS_FLOAT(neighbors[2]->position.ne.to_double(), 0.25, 0.001); + TESTEQUALS_FLOAT(neighbors[2]->position.se.to_double(), 0.25, 0.001); + + TESTEQUALS_FLOAT(neighbors[3]->position.ne.to_double(), 0, 0.001); + TESTEQUALS_FLOAT(neighbors[3]->position.se.to_double(), 0.25, 0.001); + + TESTEQUALS_FLOAT(neighbors[4]->position.ne.to_double(), -0.25, 0.001); + TESTEQUALS_FLOAT(neighbors[4]->position.se.to_double(), 0.25, 0.001); + + TESTEQUALS_FLOAT(neighbors[5]->position.ne.to_double(), -0.25, 0.001); + TESTEQUALS_FLOAT(neighbors[5]->position.se.to_double(), 0, 0.001); + + TESTEQUALS_FLOAT(neighbors[6]->position.ne.to_double(), -0.25, 0.001); + TESTEQUALS_FLOAT(neighbors[6]->position.se.to_double(), -0.25, 0.001); + + TESTEQUALS_FLOAT(neighbors[7]->position.ne.to_double(), 0, 0.001); + TESTEQUALS_FLOAT(neighbors[7]->position.se.to_double(), -0.25, 0.001); +} + +/** + * This is a helper passable function that alwalys returns true. + */ +bool always_passable(const coord::phys3 &) { + return true; +} + +/** + * This is a helper passable function that always returns false. + */ +bool not_passable(const coord::phys3 &) { + return false; +} + +/** + * This is a helper passable function that only returns true when + * pos.ne == 20. + */ +bool sometimes_passable(const coord::phys3 &pos) { + if (pos.ne == 20) { + return false; + } + else { + return true; + } +} + +/** + * This function tests passable_line. Tests with always false, always true, + * and position dependant functions being passed in as args. + */ +void node_passable_line_0() { + coord::phys3 p0{0, 0, 0}; + coord::phys3 p1{1000, 0, 0}; + + node_pt n0 = std::make_unique(p0, nullptr); + node_pt n1 = std::make_unique(p1, n0); + + TESTEQUALS(path::passable_line(n0, n1, path::tests::always_passable), true); + TESTEQUALS(path::passable_line(n0, n1, path::tests::not_passable), false); + + // The next 2 cases show that a different sample can change the results + // for the same path + TESTEQUALS(path::passable_line(n0, n1, path::tests::sometimes_passable, 10), true); + TESTEQUALS(path::passable_line(n0, n1, path::tests::sometimes_passable, 50), false); +} + +/** + * Top level node test. + */ +void path_node() { + node_0(); + node_cost_to_0(); + node_cost_to_1(); + node_generate_backtrace_0(); + node_get_neighbors_0(); + node_passable_line_0(); +} + +} // namespace tests +} // namespace path +} // namespace openage diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index 4cd6d40363..e31e87498e 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -1,323 +1,19 @@ // Copyright 2015-2024 the openage authors. See copying.md for legal info. -#include "../log/log.h" -#include "../testing/testing.h" +#include "log/log.h" +#include "testing/testing.h" -#include "cost_field.h" -#include "definitions.h" -#include "flow_field.h" -#include "heuristics.h" -#include "integration_field.h" -#include "integrator.h" -#include "path.h" -#include "types.h" +#include "pathfinding/cost_field.h" +#include "pathfinding/definitions.h" +#include "pathfinding/flow_field.h" +#include "pathfinding/integration_field.h" +#include "pathfinding/integrator.h" +#include "pathfinding/types.h" namespace openage { namespace path { namespace tests { -/** - * This function tests setting up basic nodes that point to a previous node. - * Tests that direction is set correctly and that factor is set correctly. - */ -void node_0() { - coord::phys3 p0{0, 0, 0}; - coord::phys3 p1{1, 0, 0}; - coord::phys3 p2{1, 1, 0}; - coord::phys3 p3{1, -1, 0}; - coord::phys3 p4{2, 0, 0}; - coord::phys3 p5{2, 2, 0}; - coord::phys3 p6{2, -2, 0}; - - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, n0); - node_pt n2 = std::make_unique(p2, n1); - node_pt n3 = std::make_unique(p3, n1); - node_pt n4 = std::make_unique(p0, n1); - - // Testing how the factor is effected from the change in - // direction from one node to another - TESTEQUALS(n1->direction.ne, 1); - TESTEQUALS(n1->direction.se, 0); - - // Expect this to be 2 since the similarity between nodes is zero - TESTEQUALS(n1->factor, 2); - - TESTEQUALS(n2->direction.ne, 0); - TESTEQUALS(n2->direction.se, 1); - - // Expect this to be 2 since it takes a 90 degree turn from n1 - TESTEQUALS(n2->factor, 2); - - TESTEQUALS(n3->direction.ne, 0); - TESTEQUALS(n3->direction.se, -1); - - // Expect this to be 2 since it takes a 90 degree turn from n1 - TESTEQUALS(n3->factor, 2); - - TESTEQUALS(n4->direction.ne, -1); - TESTEQUALS(n4->direction.se, 0); - - // Expect this to be 3 since it takes a 180 degree turn from n1 - TESTEQUALS(n4->factor, 3); - - // Testing that the distance from the previous node noes not - // effect the factor, only change in direction - - n1 = std::make_unique(p4, n0); - n2 = std::make_unique(p5, n1); - n3 = std::make_unique(p6, n1); - n4 = std::make_unique(p0, n1); - - TESTEQUALS(n1->direction.ne, 1); - TESTEQUALS(n1->direction.se, 0); - - // Expect this to be 2 since the similarity between nodes is zero - TESTEQUALS(n1->factor, 2); - TESTEQUALS(n2->direction.ne, 0); - TESTEQUALS(n2->direction.se, 1); - - // Expect this to be 2 since it takes a 90 degree turn from n1 - TESTEQUALS(n2->factor, 2); - TESTEQUALS(n3->direction.ne, 0); - TESTEQUALS(n3->direction.se, -1); - - // Expect this to be 2 since it takes a 90 degree turn from n1 - TESTEQUALS(n3->factor, 2); - TESTEQUALS(n4->direction.ne, -1); - TESTEQUALS(n4->direction.se, 0); - - // Expect this to be 3 since it takes a 180 degree turn from n1 - TESTEQUALS(n4->factor, 3); -} - -/** - * This function tests Node->cost_to. The testing is done on 2 unrelated - * nodes (They have no previous node) to test the basic cost without adding - * the cost from node->factor. - */ -void node_cost_to_0() { - // Testing basic cost_to with ne only - coord::phys3 p0{0, 0, 0}; - coord::phys3 p1{10, 0, 0}; - - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, nullptr); - - TESTEQUALS(n0->cost_to(*n1), 10); - TESTEQUALS(n1->cost_to(*n0), 10); - - // Testing basic cost_to with se only - coord::phys3 p2{0, 5, 0}; - - node_pt n2 = std::make_unique(p2, nullptr); - - TESTEQUALS(n0->cost_to(*n2), 5); - TESTEQUALS(n2->cost_to(*n0), 5); - - // Testing cost_to with both se and ne: - coord::phys3 p3{3, 4, 0}; // -> sqrt(3*3 + 4*4) == 5 - - node_pt n3 = std::make_unique(p3, nullptr); - TESTEQUALS(n0->cost_to(*n3), 5); - TESTEQUALS(n3->cost_to(*n0), 5); - - // Test cost_to and check that `up` has no effect - coord::phys3 p4{3, 4, 8}; - - node_pt n4 = std::make_unique(p4, nullptr); - - TESTEQUALS(n0->cost_to(*n4), 5); - TESTEQUALS(n4->cost_to(*n0), 5); -} - -/** - * This function tests Node->cost_to. The testing is done on the neighbor - * nodes to test how the directional factor effects the cost. - */ -void node_cost_to_1() { - // Set up coords so that n1 will have a direction of ne = 1 - // but n0 with not be in n1s neighbors - coord::phys3 p0{-0.125, 0, 0}; - coord::phys3 p1{0.125, 0, 0}; - - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, n0); - - // We expect twice the normal cost since n0 had not direction - // thus we get a factor of 2 on n1 - TESTEQUALS_FLOAT(n0->cost_to(*n1), 0.5, 0.001); - TESTEQUALS_FLOAT(n1->cost_to(*n0), 0.5, 0.001); - - nodemap_t visited_tiles; - visited_tiles[n0->position] = n0; - - // Collect the costs to go to all the neighbors of n1 - std::vector costs; - for (node_pt neighbor : n1->get_neighbors(visited_tiles, 1)) { - costs.push_back(n1->cost_to(*neighbor)); - } - - TESTEQUALS_FLOAT(costs[0], 0.45711, 0.001); - TESTEQUALS_FLOAT(costs[1], 0.25, 0.001); - TESTEQUALS_FLOAT(costs[2], 0.45711, 0.001); - TESTEQUALS_FLOAT(costs[3], 0.5, 0.001); - TESTEQUALS_FLOAT(costs[4], 0.95709, 0.001); - TESTEQUALS_FLOAT(costs[5], 0.75, 0.001); - TESTEQUALS_FLOAT(costs[6], 0.95709, 0.001); - TESTEQUALS_FLOAT(costs[7], 0.5, 0.001); -} - -/** - * This function does a basic test of generating a backtrace from the - * last node in a path. - */ -void node_generate_backtrace_0() { - coord::phys3 p0{0, 0, 0}; - coord::phys3 p1{10, 0, 0}; - coord::phys3 p2{20, 0, 0}; - coord::phys3 p3{30, 0, 0}; - - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, n0); - node_pt n2 = std::make_unique(p2, n1); - node_pt n3 = std::make_unique(p3, n2); - - Path path = n3->generate_backtrace(); - - (path.waypoints[0] == *n3) or TESTFAIL; - (path.waypoints[1] == *n2) or TESTFAIL; - (path.waypoints[2] == *n1) or TESTFAIL; -} - -/** - * This function tests Node->get_neighbors and how the scale effects - * the neighbors given. - */ -void node_get_neighbors_0() { - coord::phys3 p0{0, 0, 0}; - - node_pt n0 = std::make_unique(p0, nullptr); - nodemap_t map; - - // Testing get_neighbors returning all surounding tiles with - // a factor of 1 - - std::vector neighbors = n0->get_neighbors(map, 1); - TESTEQUALS(neighbors.size(), 8); - - TESTEQUALS_FLOAT(neighbors[0]->position.ne.to_double(), 0.125, 0.001); - TESTEQUALS_FLOAT(neighbors[0]->position.se.to_double(), -0.125, 0.001); - - TESTEQUALS_FLOAT(neighbors[1]->position.ne.to_double(), 0.125, 0.001); - TESTEQUALS_FLOAT(neighbors[1]->position.se.to_double(), 0, 0.001); - - TESTEQUALS_FLOAT(neighbors[2]->position.ne.to_double(), 0.125, 0.001); - TESTEQUALS_FLOAT(neighbors[2]->position.se.to_double(), 0.125, 0.001); - - TESTEQUALS_FLOAT(neighbors[3]->position.ne.to_double(), 0, 0.001); - TESTEQUALS_FLOAT(neighbors[3]->position.se.to_double(), 0.125, 0.001); - - TESTEQUALS_FLOAT(neighbors[4]->position.ne.to_double(), -0.125, 0.001); - TESTEQUALS_FLOAT(neighbors[4]->position.se.to_double(), 0.125, 0.001); - - TESTEQUALS_FLOAT(neighbors[5]->position.ne.to_double(), -0.125, 0.001); - TESTEQUALS_FLOAT(neighbors[5]->position.se.to_double(), 0, 0.001); - - TESTEQUALS_FLOAT(neighbors[6]->position.ne.to_double(), -0.125, 0.001); - TESTEQUALS_FLOAT(neighbors[6]->position.se.to_double(), -0.125, 0.001); - - TESTEQUALS_FLOAT(neighbors[7]->position.ne.to_double(), 0, 0.001); - TESTEQUALS_FLOAT(neighbors[7]->position.se.to_double(), -0.125, 0.001); - - // Testing how a larger scale changes the neighbors generated - neighbors = n0->get_neighbors(map, 2); - TESTEQUALS(neighbors.size(), 8); - - TESTEQUALS_FLOAT(neighbors[0]->position.ne.to_double(), 0.25, 0.001); - TESTEQUALS_FLOAT(neighbors[0]->position.se.to_double(), -0.25, 0.001); - - TESTEQUALS_FLOAT(neighbors[1]->position.ne.to_double(), 0.25, 0.001); - TESTEQUALS_FLOAT(neighbors[1]->position.se.to_double(), 0, 0.001); - - TESTEQUALS_FLOAT(neighbors[2]->position.ne.to_double(), 0.25, 0.001); - TESTEQUALS_FLOAT(neighbors[2]->position.se.to_double(), 0.25, 0.001); - - TESTEQUALS_FLOAT(neighbors[3]->position.ne.to_double(), 0, 0.001); - TESTEQUALS_FLOAT(neighbors[3]->position.se.to_double(), 0.25, 0.001); - - TESTEQUALS_FLOAT(neighbors[4]->position.ne.to_double(), -0.25, 0.001); - TESTEQUALS_FLOAT(neighbors[4]->position.se.to_double(), 0.25, 0.001); - - TESTEQUALS_FLOAT(neighbors[5]->position.ne.to_double(), -0.25, 0.001); - TESTEQUALS_FLOAT(neighbors[5]->position.se.to_double(), 0, 0.001); - - TESTEQUALS_FLOAT(neighbors[6]->position.ne.to_double(), -0.25, 0.001); - TESTEQUALS_FLOAT(neighbors[6]->position.se.to_double(), -0.25, 0.001); - - TESTEQUALS_FLOAT(neighbors[7]->position.ne.to_double(), 0, 0.001); - TESTEQUALS_FLOAT(neighbors[7]->position.se.to_double(), -0.25, 0.001); -} - -/** - * This is a helper passable function that alwalys returns true. - */ -bool always_passable(const coord::phys3 &) { - return true; -} - -/** - * This is a helper passable function that always returns false. - */ -bool not_passable(const coord::phys3 &) { - return false; -} - -/** - * This is a helper passable function that only returns true when - * pos.ne == 20. - */ -bool sometimes_passable(const coord::phys3 &pos) { - if (pos.ne == 20) { - return false; - } - else { - return true; - } -} - -/** - * This function tests passable_line. Tests with always false, always true, - * and position dependant functions being passed in as args. - */ -void node_passable_line_0() { - coord::phys3 p0{0, 0, 0}; - coord::phys3 p1{1000, 0, 0}; - - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, n0); - - TESTEQUALS(path::passable_line(n0, n1, path::tests::always_passable), true); - TESTEQUALS(path::passable_line(n0, n1, path::tests::not_passable), false); - - // The next 2 cases show that a different sample can change the results - // for the same path - TESTEQUALS(path::passable_line(n0, n1, path::tests::sometimes_passable, 10), true); - TESTEQUALS(path::passable_line(n0, n1, path::tests::sometimes_passable, 50), false); -} - -/** - * Top level node test. - */ -void path_node() { - node_0(); - node_cost_to_0(); - node_cost_to_1(); - node_generate_backtrace_0(); - node_get_neighbors_0(); - node_passable_line_0(); -} - void flow_field() { // Create initial cost grid auto cost_field = std::make_shared(3); From d7424b38bba2352c377712d5d52077f92bef5f4c Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 17 Mar 2024 21:09:05 +0100 Subject: [PATCH 347/771] path: Pathfinder object. --- libopenage/pathfinding/CMakeLists.txt | 1 + libopenage/pathfinding/pathfinder.cpp | 19 +++++++++ libopenage/pathfinding/pathfinder.h | 55 +++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 libopenage/pathfinding/pathfinder.cpp create mode 100644 libopenage/pathfinding/pathfinder.h diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index 84e7374203..ed55ffc463 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -5,6 +5,7 @@ add_sources(libopenage grid.cpp integration_field.cpp integrator.cpp + pathfinder.cpp portal.cpp sector.cpp tests.cpp diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp new file mode 100644 index 0000000000..7b3378f91e --- /dev/null +++ b/libopenage/pathfinding/pathfinder.cpp @@ -0,0 +1,19 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "pathfinder.h" + +#include "pathfinding/integrator.h" + + +namespace openage::path { + +Pathfinder::Pathfinder() : + grids{}, + integrator{std::make_shared()} { +} + +const std::shared_ptr &Pathfinder::get_grid(size_t idx) const { + return this->grids.at(idx); +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/pathfinder.h b/libopenage/pathfinding/pathfinder.h new file mode 100644 index 0000000000..e93b5fccf1 --- /dev/null +++ b/libopenage/pathfinding/pathfinder.h @@ -0,0 +1,55 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + + +namespace openage::path { +class Grid; +class Integrator; + +/** + * Pathfinder for flow field pathfinding. + * + * The pathfinder manages the grids defining the pathable ingame areas and + * provides an interface for making pathfinding requests. + * + * Pathfinding consists of a multi-step process: First, there is a high-level + * search using A* to identify the sectors of the grid that should be traversed. + * Afterwards, flow fields are calculated from the target sector to the start + * sector, which are then used to guide the actual unit movement. + */ +class Pathfinder { +public: + /** + * Create a new pathfinder. + */ + Pathfinder(); + ~Pathfinder() = default; + + /** + * Get the grid at a specified index. + * + * @param idx Index of the grid. + * + * @return Pathfinding grid. + */ + const std::shared_ptr &get_grid(size_t idx) const; + +private: + /** + * Grids managed by this pathfinder. + * + * Each grid can have separate pathing. + */ + std::unordered_map> grids; + + /** + * Integrator for flow field calculations. + */ + std::shared_ptr integrator; +}; + +} // namespace openage::path From b46c9fad20eaa9b72e5bca098fd2bc57e90029c7 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 18 Mar 2024 00:29:18 +0100 Subject: [PATCH 348/771] path: Path request class. --- libopenage/pathfinding/CMakeLists.txt | 1 + libopenage/pathfinding/path.cpp | 10 ++++++++ libopenage/pathfinding/path.h | 34 +++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 libopenage/pathfinding/path.cpp create mode 100644 libopenage/pathfinding/path.h diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index ed55ffc463..7f85264d01 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -5,6 +5,7 @@ add_sources(libopenage grid.cpp integration_field.cpp integrator.cpp + path.cpp pathfinder.cpp portal.cpp sector.cpp diff --git a/libopenage/pathfinding/path.cpp b/libopenage/pathfinding/path.cpp new file mode 100644 index 0000000000..fe13989e77 --- /dev/null +++ b/libopenage/pathfinding/path.cpp @@ -0,0 +1,10 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "path.h" + + +namespace openage::path { + +// this file is intentionally empty + +} // namespace openage::path diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/path.h new file mode 100644 index 0000000000..cb08e09991 --- /dev/null +++ b/libopenage/pathfinding/path.h @@ -0,0 +1,34 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include "coord/tile.h" + + +namespace openage::path { + +/** + * Path request for the pathfinder. + */ +struct PathRequest { + // ID of the grid to use for pathfinding. + size_t grid_id; + // Start position of the path. + coord::tile start; + // Target position of the path. + coord::tile target; +}; + +/** + * Path found by the pathfinder. + */ +struct Path { + // ID of the grid to used for pathfinding. + size_t grid_id; + // Waypoints of the path. + std::vector waypoints; +}; + +} // namespace openage::path From 2f589870dee957a8b35f6eb96b26f357dca4ae03 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Mar 2024 12:05:16 +0100 Subject: [PATCH 349/771] path: Do not render vectors for LOS cells in demo. --- libopenage/pathfinding/demo/demo_0.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index e655598979..0de6d13fc3 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -267,7 +267,7 @@ void RenderManager::show_vectors(const std::shared_ptr &field) for (size_t y = 0; y < field->get_size(); ++y) { for (size_t x = 0; x < field->get_size(); ++x) { auto cell = field->get_cell(x, y); - if (cell & FLOW_PATHABLE_MASK) { + if (cell & FLOW_PATHABLE_MASK and not(cell & FLOW_LOS_MASK)) { Eigen::Affine3f arrow_model = Eigen::Affine3f::Identity(); arrow_model.translate(Eigen::Vector3f(y + 0.5, 0, -1.0f * x - 0.5)); auto dir = static_cast(cell & FLOW_DIR_MASK); From 4170ff1d5a13ba72b62f2d2a6620cb804d505a83 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Mar 2024 12:44:31 +0100 Subject: [PATCH 350/771] path: Assign IDs to grids and portals. --- libopenage/pathfinding/grid.cpp | 16 ++++++++++++++-- libopenage/pathfinding/grid.h | 20 ++++++++++++++++++-- libopenage/pathfinding/pathfinder.cpp | 4 ++-- libopenage/pathfinding/pathfinder.h | 4 ++-- libopenage/pathfinding/portal.cpp | 12 +++++++++++- libopenage/pathfinding/portal.h | 25 ++++++++++++++++++++++++- libopenage/pathfinding/sector.cpp | 7 ++++++- libopenage/pathfinding/sector.h | 14 +++++++++----- libopenage/pathfinding/types.h | 12 +++++++++++- 9 files changed, 97 insertions(+), 17 deletions(-) diff --git a/libopenage/pathfinding/grid.cpp b/libopenage/pathfinding/grid.cpp index 86b71d8681..6ad53178f0 100644 --- a/libopenage/pathfinding/grid.cpp +++ b/libopenage/pathfinding/grid.cpp @@ -2,12 +2,15 @@ #include "grid.h" +#include "error/error.h" + #include "pathfinding/sector.h" namespace openage::path { -Grid::Grid(size_t width, size_t height, size_t sector_size) : +Grid::Grid(grid_id_t id, size_t width, size_t height, size_t sector_size) : + id{id}, width{width}, height{height} { for (size_t y = 0; y < height; y++) { @@ -17,12 +20,21 @@ Grid::Grid(size_t width, size_t height, size_t sector_size) : } } -Grid::Grid(size_t width, +Grid::Grid(grid_id_t id, + size_t width, size_t height, std::vector> &§ors) : + id{id}, width{width}, height{height}, sectors{std::move(sectors)} { + ENSURE(this->sectors.size() == width * height, + "Grid has size " << width << "x" << height << " (" << width * height << " sectors), " + << "but only " << this->sectors.size() << " sectors were provided"); +} + +grid_id_t Grid::get_id() const { + return this->id; } std::pair Grid::get_size() const { diff --git a/libopenage/pathfinding/grid.h b/libopenage/pathfinding/grid.h index 22c551066c..adf030cf53 100644 --- a/libopenage/pathfinding/grid.h +++ b/libopenage/pathfinding/grid.h @@ -21,25 +21,36 @@ class Grid { /** * Create a new empty grid of width x height sectors with a specified size. * + * @param id ID of the grid. * @param width Width of the grid. * @param height Height of the grid. * @param sector_size Side length of each sector. */ - Grid(size_t width, + Grid(grid_id_t id, + size_t width, size_t height, size_t sector_size); /** * Create a grid of width x height sectors from a list of existing sectors. * + * @param id ID of the grid. * @param width Width of the grid. * @param height Height of the grid. * @param sectors Existing sectors. */ - Grid(size_t width, + Grid(grid_id_t id, + size_t width, size_t height, std::vector> &§ors); + /** + * Get the ID of the grid. + * + * @return ID of the grid. + */ + grid_id_t get_id() const; + /** * Get the size of the grid. * @@ -67,6 +78,11 @@ class Grid { const std::shared_ptr &get_sector(sector_id_t id) const; private: + /** + * ID of the grid. + */ + grid_id_t id; + /** * Width of the grid. */ diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 7b3378f91e..44be92ccd1 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -12,8 +12,8 @@ Pathfinder::Pathfinder() : integrator{std::make_shared()} { } -const std::shared_ptr &Pathfinder::get_grid(size_t idx) const { - return this->grids.at(idx); +const std::shared_ptr &Pathfinder::get_grid(size_t id) const { + return this->grids.at(id); } } // namespace openage::path diff --git a/libopenage/pathfinding/pathfinder.h b/libopenage/pathfinding/pathfinder.h index e93b5fccf1..d423272ff1 100644 --- a/libopenage/pathfinding/pathfinder.h +++ b/libopenage/pathfinding/pathfinder.h @@ -32,11 +32,11 @@ class Pathfinder { /** * Get the grid at a specified index. * - * @param idx Index of the grid. + * @param id ID of the grid. * * @return Pathfinding grid. */ - const std::shared_ptr &get_grid(size_t idx) const; + const std::shared_ptr &get_grid(size_t id) const; private: /** diff --git a/libopenage/pathfinding/portal.cpp b/libopenage/pathfinding/portal.cpp index 40111e0b98..0b3b472667 100644 --- a/libopenage/pathfinding/portal.cpp +++ b/libopenage/pathfinding/portal.cpp @@ -7,13 +7,15 @@ namespace openage::path { -Portal::Portal(sector_id_t sector0, +Portal::Portal(portal_id_t id, + sector_id_t sector0, sector_id_t sector1, PortalDirection direction, size_t cell_start_x, size_t cell_start_y, size_t cell_end_x, size_t cell_end_y) : + id{id}, sector0{sector0}, sector1{sector1}, sector0_exits{}, @@ -25,6 +27,10 @@ Portal::Portal(sector_id_t sector0, cell_end_y{cell_end_y} { } +portal_id_t Portal::get_id() const { + return this->id; +} + const std::vector> &Portal::get_exits(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); @@ -94,6 +100,10 @@ std::pair Portal::get_exit_end(sector_id_t entry_sector) const { return this->get_sector0_end(); } +PortalDirection Portal::get_direction() const { + return this->direction; +} + std::pair Portal::get_sector0_start() const { return {this->cell_start_x, this->cell_start_y}; } diff --git a/libopenage/pathfinding/portal.h b/libopenage/pathfinding/portal.h index dd6e474931..170fd463ca 100644 --- a/libopenage/pathfinding/portal.h +++ b/libopenage/pathfinding/portal.h @@ -42,6 +42,7 @@ class Portal { * As a convention, sector 0 must be the either the north or east sector * on the grid in relation to sector 1. * + * @param id ID of the portal. Should be unique per grid. * @param sector0 First sector connected by the portal. * Must be north or east on the grid in relation to sector 1. * @param sector1 Second sector connected by the portal. @@ -52,7 +53,8 @@ class Portal { * @param cell_end_x End cell x coordinate in sector 0. * @param cell_end_y End cell y coordinate in sector 0. */ - Portal(sector_id_t sector0, + Portal(portal_id_t id, + sector_id_t sector0, sector_id_t sector1, PortalDirection direction, size_t cell_start_x, @@ -62,6 +64,15 @@ class Portal { ~Portal() = default; + /** + * Get the ID of the portal. + * + * IDs are unique per grid. + * + * @return ID of the portal. + */ + portal_id_t get_id() const; + /** * Get the exit portals reachable via the portal when entering from a specified sector. * @@ -124,6 +135,13 @@ class Portal { */ std::pair get_exit_end(sector_id_t entry_sector) const; + /** + * Get the direction of the portal from sector 0 to sector 1. + * + * @return Direction of the portal. + */ + PortalDirection get_direction() const; + private: /** * Get the start cell coordinates of the portal. @@ -153,6 +171,11 @@ class Portal { */ std::pair get_sector1_end() const; + /** + * ID of the portal. + */ + portal_id_t id; + /** * First sector connected by the portal. */ diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index 8c5221821d..554a5518b1 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -37,7 +37,8 @@ void Sector::add_portal(const std::shared_ptr &portal) { } std::vector> Sector::find_portals(const std::shared_ptr &other, - PortalDirection direction) const { + PortalDirection direction, + portal_id_t next_id) const { ENSURE(this->cost_field->get_size() == other->get_cost_field()->get_size(), "Sector size mismatch"); std::vector> result; @@ -62,6 +63,7 @@ std::vector> Sector::find_portals(const std::shared_ptr< if (passable_edge) { result.push_back( std::make_shared( + next_id, this->id, other->get_id(), direction, @@ -70,6 +72,7 @@ std::vector> Sector::find_portals(const std::shared_ptr< x - 1, 0)); passable_edge = false; + next_id += 1; } } } @@ -90,6 +93,7 @@ std::vector> Sector::find_portals(const std::shared_ptr< if (passable_edge) { result.push_back( std::make_shared( + next_id, this->id, other->get_id(), direction, @@ -98,6 +102,7 @@ std::vector> Sector::find_portals(const std::shared_ptr< 0, y - 1)); passable_edge = false; + next_id += 1; } } } diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h index b0dbd2bc18..66c2090ec9 100644 --- a/libopenage/pathfinding/sector.h +++ b/libopenage/pathfinding/sector.h @@ -25,7 +25,7 @@ class Sector { /** * Create a new sector with a specified ID and an uninitialized cost field. * - * @param id ID of the sector. + * @param id ID of the sector. Should be unique per grid. * @param field_size Size of the cost field. */ Sector(sector_id_t id, @@ -34,7 +34,7 @@ class Sector { /** * Create a new sector with a specified ID and an existing cost field. * - * @param id ID of the sector. + * @param id ID of the sector. Should be unique per grid. * @param cost_field Cost field of the sector. */ Sector(sector_id_t id, @@ -43,6 +43,8 @@ class Sector { /** * Get the ID of this sector. * + * IDs are unique per grid. + * * @return ID of the sector. */ const sector_id_t &get_id() const; @@ -72,12 +74,14 @@ class Sector { * Find portals connecting this sector to another sector. * * @param other Sector to which the portals should connect. - * @param direction Direction from this sector to the other sector. + * @param direction Direction from this sector to \p other sector. + * @param next_id ID of the next portal to be created. Should be unique per grid. * - * @return Portals connecting this sector to the other sector. + * @return Portals connecting this sector to \p other sector. */ std::vector> find_portals(const std::shared_ptr &other, - PortalDirection direction) const; + PortalDirection direction, + portal_id_t next_id) const; /** * Connect all portals that are mutually reachable. diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index 118c8c116d..6ed735e896 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -57,8 +57,18 @@ struct integrate_t { using flow_t = uint8_t; /** - * Sector identifier on a grid. + * Grid identifier. + */ +using grid_id_t = size_t; + +/** + * Sector identifier (unique per grid). */ using sector_id_t = size_t; +/** + * Portal identifier (unique per grid). + */ +using portal_id_t = size_t; + } // namespace openage::path From 73d80628003cdabf8b6a7f76a2c6d13434941ee9 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Mar 2024 13:45:10 +0100 Subject: [PATCH 351/771] path: Port flowfield pathfinder to coordinate system. --- libopenage/coord/tile.h | 3 - libopenage/pathfinding/cost_field.cpp | 13 ++-- libopenage/pathfinding/cost_field.h | 28 +++++--- libopenage/pathfinding/demo/demo_0.cpp | 49 ++++++------- libopenage/pathfinding/flow_field.cpp | 10 +-- libopenage/pathfinding/flow_field.h | 20 +++--- libopenage/pathfinding/integration_field.cpp | 74 +++++++++----------- libopenage/pathfinding/integration_field.h | 49 ++++++------- libopenage/pathfinding/integrator.cpp | 4 +- libopenage/pathfinding/integrator.h | 15 ++-- libopenage/pathfinding/portal.cpp | 42 +++++------ libopenage/pathfinding/portal.h | 47 +++++-------- libopenage/pathfinding/sector.cpp | 21 +++--- libopenage/pathfinding/tests.cpp | 6 +- 14 files changed, 194 insertions(+), 187 deletions(-) diff --git a/libopenage/coord/tile.h b/libopenage/coord/tile.h index 5cb3f2475d..cf020823a7 100644 --- a/libopenage/coord/tile.h +++ b/libopenage/coord/tile.h @@ -9,9 +9,6 @@ #include "declarations.h" namespace openage { - -class Terrain; - namespace coord { /* diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index ad19f8b910..248a260d8e 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -5,6 +5,7 @@ #include "error/error.h" #include "log/log.h" +#include "coord/tile.h" #include "pathfinding/definitions.h" @@ -20,16 +21,20 @@ size_t CostField::get_size() const { return this->size; } -cost_t CostField::get_cost(size_t x, size_t y) const { - return this->cells[x + y * this->size]; +cost_t CostField::get_cost(const coord::tile &pos) const { + return this->cells.at(pos.ne + pos.se * this->size); } cost_t CostField::get_cost(size_t idx) const { return this->cells.at(idx); } -void CostField::set_cost(size_t x, size_t y, cost_t cost) { - this->cells[x + y * this->size] = cost; +void CostField::set_cost(const coord::tile &pos, cost_t cost) { + this->cells[pos.ne + pos.se * this->size] = cost; +} + +void CostField::set_cost(size_t idx, cost_t cost) { + this->cells[idx] = cost; } const std::vector &CostField::get_costs() const { diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index b4f5cfbb00..ffb27bdec8 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -8,7 +8,12 @@ #include "pathfinding/types.h" -namespace openage::path { +namespace openage { +namespace coord { +struct tile; +} // namespace coord + +namespace path { /** * Cost field in the flow-field pathfinding algorithm. @@ -32,11 +37,10 @@ class CostField { /** * Get the cost at a specified position. * - * @param x X coordinate. - * @param y Y coordinate. + * @param pos Coordinates of the cell. * @return Cost at the specified position. */ - cost_t get_cost(size_t x, size_t y) const; + cost_t get_cost(const coord::tile &pos) const; /** * Get the cost at a specified position. @@ -49,11 +53,18 @@ class CostField { /** * Set the cost at a specified position. * - * @param x X coordinate. - * @param y Y coordinate. + * @param pos Coordinates of the cell. + * @param cost Cost to set. + */ + void set_cost(const coord::tile &pos, cost_t cost); + + /** + * Set the cost at a specified position. + * + * @param idx Index of the cell. * @param cost Cost to set. */ - void set_cost(size_t x, size_t y, cost_t cost); + void set_cost(size_t idx, cost_t cost); /** * Get the cost field values. @@ -81,4 +92,5 @@ class CostField { std::vector cells; }; -} // namespace openage::path +} // namespace path +} // namespace openage diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 0de6d13fc3..badbfef0d6 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -5,6 +5,7 @@ #include #include +#include "coord/tile.h" #include "pathfinding/cost_field.h" #include "pathfinding/flow_field.h" #include "pathfinding/integration_field.h" @@ -29,15 +30,15 @@ void path_demo_0(const util::Path &path) { // Cost field with some obstacles auto cost_field = std::make_shared(field_length); - cost_field->set_cost(0, 0, COST_IMPASSABLE); - cost_field->set_cost(1, 0, 254); - cost_field->set_cost(4, 3, 128); - cost_field->set_cost(5, 3, 128); - cost_field->set_cost(6, 3, 128); - cost_field->set_cost(4, 4, 128); - cost_field->set_cost(5, 4, 128); - cost_field->set_cost(6, 4, 128); - cost_field->set_cost(1, 7, COST_IMPASSABLE); + cost_field->set_cost(coord::tile{0, 0}, COST_IMPASSABLE); + cost_field->set_cost(coord::tile{1, 0}, 254); + cost_field->set_cost(coord::tile{4, 3}, 128); + cost_field->set_cost(coord::tile{5, 3}, 128); + cost_field->set_cost(coord::tile{6, 3}, 128); + cost_field->set_cost(coord::tile{4, 4}, 128); + cost_field->set_cost(coord::tile{5, 4}, 128); + cost_field->set_cost(coord::tile{6, 4}, 128); + cost_field->set_cost(coord::tile{1, 7}, COST_IMPASSABLE); log::log(INFO << "Created cost field"); // Create an integration field from the cost field @@ -45,7 +46,7 @@ void path_demo_0(const util::Path &path) { log::log(INFO << "Created integration field"); // Set cell (7, 7) to be the initial target cell - auto wavefront_blocked = integration_field->integrate_los(cost_field, 7, 7); + auto wavefront_blocked = integration_field->integrate_los(cost_field, coord::tile{7, 7}); integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); log::log(INFO << "Calculated integration field for target cell (7, 7)"); @@ -83,7 +84,7 @@ void path_demo_0(const util::Path &path) { // Recalculate the integration field and the flow field integration_field->reset(); - auto wavefront_blocked = integration_field->integrate_los(cost_field, grid_x, grid_y); + auto wavefront_blocked = integration_field->integrate_los(cost_field, coord::tile{grid_x, grid_y}); integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); log::log(INFO << "Calculated integration field for target cell (" << grid_x << ", " << grid_y << ")"); @@ -266,7 +267,7 @@ void RenderManager::show_vectors(const std::shared_ptr &field) this->vector_pass->clear_renderables(); for (size_t y = 0; y < field->get_size(); ++y) { for (size_t x = 0; x < field->get_size(); ++x) { - auto cell = field->get_cell(x, y); + auto cell = field->get_cell(coord::tile{x, y}); if (cell & FLOW_PATHABLE_MASK and not(cell & FLOW_LOS_MASK)) { Eigen::Affine3f arrow_model = Eigen::Affine3f::Identity(); arrow_model.translate(Eigen::Vector3f(y + 0.5, 0, -1.0f * x - 0.5)); @@ -557,19 +558,19 @@ renderer::resources::MeshData RenderManager::get_cost_field_mesh(const std::shar // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cost((i - 1) / resolution, (j - 1) / resolution); + auto cost = field->get_cost(coord::tile{(i - 1) / resolution, (j - 1) / resolution}); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cost((i - 1) / resolution, j / resolution); + auto cost = field->get_cost(coord::tile{(i - 1) / resolution, j / resolution}); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cost(i / resolution, j / resolution); + auto cost = field->get_cost(coord::tile{i / resolution, j / resolution}); surround.push_back(cost); } if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cost(i / resolution, (j - 1) / resolution); + auto cost = field->get_cost(coord::tile{i / resolution, (j - 1) / resolution}); surround.push_back(cost); } // use the cost of the most expensive surrounding tile @@ -643,19 +644,19 @@ renderer::resources::MeshData RenderManager::get_integration_field_mesh(const st // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cell((i - 1) / resolution, (j - 1) / resolution).cost; + auto cost = field->get_cell(coord::tile{(i - 1) / resolution, (j - 1) / resolution}).cost; surround.push_back(cost); } if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cell((i - 1) / resolution, j / resolution).cost; + auto cost = field->get_cell(coord::tile{(i - 1) / resolution, j / resolution}).cost; surround.push_back(cost); } if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cell(i / resolution, j / resolution).cost; + auto cost = field->get_cell(coord::tile{i / resolution, j / resolution}).cost; surround.push_back(cost); } if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cell(i / resolution, (j - 1) / resolution).cost; + auto cost = field->get_cell(coord::tile{i / resolution, (j - 1) / resolution}).cost; surround.push_back(cost); } // use the cost of the most expensive surrounding tile @@ -729,19 +730,19 @@ renderer::resources::MeshData RenderManager::get_flow_field_mesh(const std::shar // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cell((i - 1) / resolution, (j - 1) / resolution); + auto cost = field->get_cell(coord::tile{(i - 1) / resolution, (j - 1) / resolution}); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cell((i - 1) / resolution, j / resolution); + auto cost = field->get_cell(coord::tile{(i - 1) / resolution, j / resolution}); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cell(i / resolution, j / resolution); + auto cost = field->get_cell(coord::tile{i / resolution, j / resolution}); surround.push_back(cost); } if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cell(i / resolution, (j - 1) / resolution); + auto cost = field->get_cell(coord::tile{i / resolution, (j - 1) / resolution}); surround.push_back(cost); } // use the cost of the most expensive surrounding tile diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 52f4d7738e..96c1a0cd56 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -5,6 +5,8 @@ #include "error/error.h" #include "log/log.h" +#include "coord/tile.h" +#include "pathfinding/definitions.h" #include "pathfinding/integration_field.h" @@ -26,12 +28,12 @@ size_t FlowField::get_size() const { return this->size; } -flow_t FlowField::get_cell(size_t x, size_t y) const { - return this->cells.at(x + y * this->size); +flow_t FlowField::get_cell(const coord::tile &pos) const { + return this->cells.at(pos.ne + pos.se * this->size); } -flow_dir_t FlowField::get_dir(size_t x, size_t y) const { - return static_cast(this->get_cell(x, y) & FLOW_DIR_MASK); +flow_dir_t FlowField::get_dir(const coord::tile &pos) const { + return static_cast(this->get_cell(pos) & FLOW_DIR_MASK); } void FlowField::build(const std::shared_ptr &integrate_field) { diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index 5d8ab25580..8389b2a0f7 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -10,7 +10,12 @@ #include "pathfinding/types.h" -namespace openage::path { +namespace openage { +namespace coord { +struct tile; +} // namespace coord + +namespace path { class IntegrationField; class FlowField { @@ -39,22 +44,20 @@ class FlowField { /** * Get the flow field value at a specified position. * - * @param x X coordinate. - * @param y Y coordinate. + * @param pos Coordinates of the cell. * * @return Flowfield value at the specified position. */ - flow_t get_cell(size_t x, size_t y) const; + flow_t get_cell(const coord::tile &pos) const; /** * Get the flow field direction at a specified position. * - * @param x X coordinate. - * @param y Y coordinate. + * @param pos Coordinates of the cell. * * @return Flowfield direction at the specified position. */ - flow_dir_t get_dir(size_t x, size_t y) const; + flow_dir_t get_dir(const coord::tile &pos) const; /** * Build the flow field. @@ -87,4 +90,5 @@ class FlowField { std::vector cells; }; -} // namespace openage::path +} // namespace path +} // namespace openage diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 09cebb0f4d..9881ec0f3c 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -7,6 +7,7 @@ #include "error/error.h" #include "log/log.h" +#include "coord/tile.h" #include "pathfinding/cost_field.h" #include "pathfinding/definitions.h" @@ -23,8 +24,8 @@ size_t IntegrationField::get_size() const { return this->size; } -const integrate_t &IntegrationField::get_cell(size_t x, size_t y) const { - return this->cells.at(x + y * this->size); +const integrate_t &IntegrationField::get_cell(const coord::tile &pos) const { + return this->cells.at(pos.ne + pos.se * this->size); } const integrate_t &IntegrationField::get_cell(size_t idx) const { @@ -32,8 +33,7 @@ const integrate_t &IntegrationField::get_cell(size_t idx) const { } std::vector IntegrationField::integrate_los(const std::shared_ptr &cost_field, - size_t target_x, - size_t target_y) { + const coord::tile &target) { ENSURE(cost_field->get_size() == this->get_size(), "cost field size " << cost_field->get_size() << "x" << cost_field->get_size() @@ -44,7 +44,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptr wavefront_blocked; // Target cell index - auto target_idx = target_x + target_y * this->size; + auto target_idx = target.ne + target.se * this->size; // Lookup table for cells that have been found std::unordered_set found; @@ -97,11 +97,11 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrget_los_corners(cost_field, target_x, target_y, x, y); + auto corners = this->get_los_corners(cost_field, target, coord::tile{x, y}); for (auto &corner : corners) { // draw a line from the corner to the edge of the field // to get the cells blocked by the corner - auto blocked_cells = this->bresenhams_line(target_x, target_y, corner.first, corner.second); + auto blocked_cells = this->bresenhams_line(target, corner.first, corner.second); for (auto &blocked_idx : blocked_cells) { if (cost_field->get_cost(blocked_idx) > COST_MIN) { // stop if blocked_idx is impassable @@ -148,8 +148,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptr &cost_field, - size_t target_x, - size_t target_y) { + const coord::tile &target) { ENSURE(cost_field->get_size() == this->get_size(), "cost field size " << cost_field->get_size() << "x" << cost_field->get_size() @@ -157,7 +156,7 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie << this->get_size() << "x" << this->get_size()); // Target cell index - auto target_idx = target_x + target_y * this->size; + auto target_idx = target.ne + target.se * this->size; // Move outwards from the target cell, updating the integration field this->cells[target_idx].cost = INTEGRATED_COST_START; @@ -258,12 +257,10 @@ void IntegrationField::update_neighbor(size_t idx, } } -std::vector> IntegrationField::get_los_corners(const std::shared_ptr &cost_field, - size_t target_x, - size_t target_y, - size_t blocker_x, - size_t blocker_y) { - std::vector> corners; +std::vector> IntegrationField::get_los_corners(const std::shared_ptr &cost_field, + const coord::tile &target, + const coord::tile &blocker) { + std::vector> corners; // Get the cost of the blocking cell's neighbors @@ -273,30 +270,30 @@ std::vector> IntegrationField::get_los_corners(const s auto bottom_cost = COST_IMPASSABLE; auto right_cost = COST_IMPASSABLE; - std::pair top_left{blocker_x, blocker_y}; - std::pair top_right{blocker_x + 1, blocker_y}; - std::pair bottom_left{blocker_x, blocker_y + 1}; - std::pair bottom_right{blocker_x + 1, blocker_y + 1}; + std::pair top_left{blocker.ne, blocker.se}; + std::pair top_right{blocker.ne + 1, blocker.se}; + std::pair bottom_left{blocker.ne, blocker.se + 1}; + std::pair bottom_right{blocker.ne + 1, blocker.se + 1}; // Get neighbor costs (if they exist) - if (blocker_y > 0) { - top_cost = cost_field->get_cost(blocker_x, blocker_y - 1); + if (blocker.se > 0) { + top_cost = cost_field->get_cost(coord::tile{blocker.ne, blocker.se - 1}); } - if (blocker_x > 0) { - left_cost = cost_field->get_cost(blocker_x - 1, blocker_y); + if (blocker.ne > 0) { + left_cost = cost_field->get_cost(coord::tile{blocker.ne - 1, blocker.se}); } - if (blocker_y < this->size - 1) { - bottom_cost = cost_field->get_cost(blocker_x, blocker_y + 1); + if (blocker.se < this->size - 1) { + bottom_cost = cost_field->get_cost(coord::tile{blocker.ne, blocker.se + 1}); } - if (blocker_x < this->size - 1) { - right_cost = cost_field->get_cost(blocker_x + 1, blocker_y); + if (blocker.ne < this->size - 1) { + right_cost = cost_field->get_cost(coord::tile{blocker.ne + 1, blocker.se}); } // Check which corners are blocking LOS // TODO: Currently super complicated and could likely be optimized - if (blocker_x == target_x) { + if (blocker.ne == target.ne) { // blocking cell is parallel to target on y-axis - if (blocker_y < target_y) { + if (blocker.se < target.se) { if (left_cost == COST_MIN) { // top corners.push_back(bottom_left); @@ -317,9 +314,9 @@ std::vector> IntegrationField::get_los_corners(const s } } } - else if (blocker_y == target_y) { + else if (blocker.se == target.se) { // blocking cell is parallel to target on x-axis - if (blocker_x < target_x) { + if (blocker.ne < target.ne) { if (top_cost == COST_MIN) { // right corners.push_back(top_right); @@ -342,8 +339,8 @@ std::vector> IntegrationField::get_los_corners(const s } else { // blocking cell is diagonal to target on - if (blocker_x < target_x) { - if (blocker_y < target_y) { + if (blocker.ne < target.ne) { + if (blocker.se < target.se) { // top and right if (top_cost == COST_MIN and right_cost == COST_MIN) { // right @@ -367,7 +364,7 @@ std::vector> IntegrationField::get_los_corners(const s } } else { - if (blocker_y < target_y) { + if (blocker.se < target.se) { // top and left if (top_cost == COST_MIN and left_cost == COST_MIN) { // left @@ -395,8 +392,7 @@ std::vector> IntegrationField::get_los_corners(const s return corners; } -std::vector IntegrationField::bresenhams_line(int target_x, - int target_y, +std::vector IntegrationField::bresenhams_line(const coord::tile &target, int corner_x, int corner_y) { std::vector cells; @@ -411,8 +407,8 @@ std::vector IntegrationField::bresenhams_line(int target_x, // target coordinates // offset by 0.5 to get the center of the cell - double tx = target_x + 0.5; - double ty = target_y + 0.5; + double tx = target.ne + 0.5; + double ty = target.se + 0.5; // slope of the line double dx = std::abs(tx - corner_x); diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index dc5821c856..3e01275d9d 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -11,7 +11,12 @@ #include "pathfinding/types.h" -namespace openage::path { +namespace openage { +namespace coord { +struct tile; +} // namespace coord + +namespace path { class CostField; /** @@ -36,11 +41,10 @@ class IntegrationField { /** * Get the integration value at a specified position. * - * @param x X coordinate. - * @param y Y coordinate. + * @param pos Coordinates of the cell. * @return Integration value at the specified position. */ - const integrate_t &get_cell(size_t x, size_t y) const; + const integrate_t &get_cell(const coord::tile &pos) const; /** * Get the integration value at a specified position. @@ -57,26 +61,22 @@ class IntegrationField { * can be used as a starting point for the cost integration. * * @param cost_field Cost field to integrate. - * @param target_x X coordinate of the target cell. - * @param target_y Y coordinate of the target cell. + * @param target Coordinates of the target cell. * * @return Cells flagged as "wavefront blocked". */ std::vector integrate_los(const std::shared_ptr &cost_field, - size_t target_x, - size_t target_y); + const coord::tile &target); /** * Calculate the cost integration field starting from a target cell. * * @param cost_field Cost field to integrate. - * @param target_x X coordinate of the target cell. - * @param target_y Y coordinate of the target cell. + * @param target Coordinates of the target cell. * @param start_cells Cells flagged as "wavefront blocked" from a LOS pass. */ void integrate_cost(const std::shared_ptr &cost_field, - size_t target_x, - size_t target_y); + const coord::tile &target); /** * Calculate the cost integration field starting from a wavefront. @@ -121,18 +121,14 @@ class IntegrationField { * Get the LOS corners around a cell. * * @param cost_field Cost field to integrate. - * @param target_x X cell coordinate of the target. - * @param target_y Y cell coordinate of the target. - * @param blocker_x X cell coordinate of the cell blocking LOS. - * @param blocker_y Y cell coordinate of the cell blocking LOS. + * @param target Cell coordinates of the target. + * @param blocker Cell coordinates of the cell blocking LOS. * * @return Field coordinates of the LOS corners. */ - std::vector> get_los_corners(const std::shared_ptr &cost_field, - size_t target_x, - size_t target_y, - size_t blocker_x, - size_t blocker_y); + std::vector> get_los_corners(const std::shared_ptr &cost_field, + const coord::tile &target, + const coord::tile &blocker); /** * Get the cells in a bresenham's line between the corner cell and the field edge. @@ -142,13 +138,13 @@ class IntegrationField { * the cells between two arbitrary points. We do this because the intersection * point with the field edge is unknown. * - * @param target_x X cell coordinate of the target. - * @param target_y Y cell coordinate of the target. + * @param target Cell coordinates of the target. * @param corner_x X field coordinate edge of the LOS corner. * @param corner_y Y field coordinate edge of the LOS corner. + * + * @return Cell indices of the LOS line. */ - std::vector bresenhams_line(int target_x, - int target_y, + std::vector bresenhams_line(const coord::tile &target, int corner_x, int corner_y); @@ -163,4 +159,5 @@ class IntegrationField { std::vector cells; }; -} // namespace openage::path +} // namespace path +} // namespace openage diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 2c083b5615..3e9ecbe902 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -13,11 +13,11 @@ void Integrator::set_cost_field(const std::shared_ptr &cost_field) { this->cost_field = cost_field; } -std::shared_ptr Integrator::build(size_t target_x, size_t target_y) { +std::shared_ptr Integrator::build(const coord::tile &target) { auto flow_field = std::make_shared(this->cost_field->get_size()); auto integrate_field = std::make_shared(this->cost_field->get_size()); - auto wavefront_blocked = integrate_field->integrate_los(this->cost_field, target_x, target_y); + auto wavefront_blocked = integrate_field->integrate_los(this->cost_field, target); integrate_field->integrate_cost(this->cost_field, std::move(wavefront_blocked)); flow_field->build(integrate_field); diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index a00eaa2700..fc0d433c1d 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -6,7 +6,12 @@ #include -namespace openage::path { +namespace openage { +namespace coord { +struct tile; +} // namespace coord + +namespace path { class CostField; class FlowField; @@ -28,12 +33,11 @@ class Integrator { /** * Build the flow field for a target cell. * - * @param target_x X coordinate of the target cell. - * @param target_y Y coordinate of the target cell. + * @param target Coordinates of the target cell. * * @return Flow field. */ - std::shared_ptr build(size_t target_x, size_t target_y); + std::shared_ptr build(const coord::tile &target); private: /** @@ -42,4 +46,5 @@ class Integrator { std::shared_ptr cost_field; }; -} // namespace openage::path +} // namespace path +} // namespace openage diff --git a/libopenage/pathfinding/portal.cpp b/libopenage/pathfinding/portal.cpp index 0b3b472667..72d4a0ee55 100644 --- a/libopenage/pathfinding/portal.cpp +++ b/libopenage/pathfinding/portal.cpp @@ -11,20 +11,16 @@ Portal::Portal(portal_id_t id, sector_id_t sector0, sector_id_t sector1, PortalDirection direction, - size_t cell_start_x, - size_t cell_start_y, - size_t cell_end_x, - size_t cell_end_y) : + const coord::tile &cell_start, + const coord::tile &cell_end) : id{id}, sector0{sector0}, sector1{sector1}, sector0_exits{}, sector1_exits{}, direction{direction}, - cell_start_x{cell_start_x}, - cell_start_y{cell_start_y}, - cell_end_x{cell_end_x}, - cell_end_y{cell_end_y} { + cell_start{cell_start}, + cell_end{cell_end} { } portal_id_t Portal::get_id() const { @@ -60,7 +56,7 @@ sector_id_t Portal::get_exit_sector(sector_id_t entry_sector) const { return this->sector0; } -std::pair Portal::get_entry_start(sector_id_t entry_sector) const { +const coord::tile Portal::get_entry_start(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -70,7 +66,7 @@ std::pair Portal::get_entry_start(sector_id_t entry_sector) cons return this->get_sector1_start(); } -std::pair Portal::get_entry_end(sector_id_t entry_sector) const { +const coord::tile Portal::get_entry_end(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -80,7 +76,7 @@ std::pair Portal::get_entry_end(sector_id_t entry_sector) const return this->get_sector1_end(); } -std::pair Portal::get_exit_start(sector_id_t entry_sector) const { +const coord::tile Portal::get_exit_start(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -90,7 +86,7 @@ std::pair Portal::get_exit_start(sector_id_t entry_sector) const return this->get_sector0_start(); } -std::pair Portal::get_exit_end(sector_id_t entry_sector) const { +const coord::tile Portal::get_exit_end(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -104,20 +100,26 @@ PortalDirection Portal::get_direction() const { return this->direction; } -std::pair Portal::get_sector0_start() const { - return {this->cell_start_x, this->cell_start_y}; +const coord::tile &Portal::get_sector0_start() const { + return this->cell_start; } -std::pair Portal::get_sector0_end() const { - return {this->cell_end_x, this->cell_end_y}; +const coord::tile &Portal::get_sector0_end() const { + return this->cell_end; } -std::pair Portal::get_sector1_start() const { - return {this->cell_end_x, this->cell_end_y}; +const coord::tile Portal::get_sector1_start() const { + if (this->direction == PortalDirection::NORTH_SOUTH) { + return {this->cell_start.ne, 0}; + } + return {0, this->cell_start.se}; } -std::pair Portal::get_sector1_end() const { - return {this->cell_start_x, this->cell_start_y}; +const coord::tile Portal::get_sector1_end() const { + if (this->direction == PortalDirection::NORTH_SOUTH) { + return {this->cell_end.ne, 0}; + } + return {0, this->cell_end.se}; } } // namespace openage::path diff --git a/libopenage/pathfinding/portal.h b/libopenage/pathfinding/portal.h index 170fd463ca..4855d0eb89 100644 --- a/libopenage/pathfinding/portal.h +++ b/libopenage/pathfinding/portal.h @@ -6,6 +6,7 @@ #include #include +#include "coord/tile.h" #include "pathfinding/types.h" @@ -48,19 +49,15 @@ class Portal { * @param sector1 Second sector connected by the portal. * Must be south or west on the grid in relation to sector 0. * @param direction Direction of the portal from sector 0 to sector 1. - * @param cell_start_x Start cell x coordinate in sector 0. - * @param cell_start_y Start cell y coordinate in sector 0. - * @param cell_end_x End cell x coordinate in sector 0. - * @param cell_end_y End cell y coordinate in sector 0. + * @param cell_start Start cell coordinate in sector 0. + * @param cell_end End cell coordinate in sector 0. */ Portal(portal_id_t id, sector_id_t sector0, sector_id_t sector1, PortalDirection direction, - size_t cell_start_x, - size_t cell_start_y, - size_t cell_end_x, - size_t cell_end_y); + const coord::tile &cell_start, + const coord::tile &cell_end); ~Portal() = default; @@ -106,7 +103,7 @@ class Portal { * * @return Cost field of the sector from which the portal is entered. */ - std::pair get_entry_start(sector_id_t entry_sector) const; + const coord::tile get_entry_start(sector_id_t entry_sector) const; /** * Get the cell coordinates of the start of the portal in the entry sector. @@ -115,7 +112,7 @@ class Portal { * * @return Cell coordinates of the start of the portal in the entry sector. */ - std::pair get_entry_end(sector_id_t entry_sector) const; + const coord::tile get_entry_end(sector_id_t entry_sector) const; /** * Get the cell coordinates of the start of the portal in the exit sector. @@ -124,7 +121,7 @@ class Portal { * * @return Cell coordinates of the start of the portal in the exit sector. */ - std::pair get_exit_start(sector_id_t entry_sector) const; + const coord::tile get_exit_start(sector_id_t entry_sector) const; /** * Get the cell coordinates of the end of the portal in the exit sector. @@ -133,7 +130,7 @@ class Portal { * * @return Cell coordinates of the end of the portal in the exit sector. */ - std::pair get_exit_end(sector_id_t entry_sector) const; + const coord::tile get_exit_end(sector_id_t entry_sector) const; /** * Get the direction of the portal from sector 0 to sector 1. @@ -148,28 +145,28 @@ class Portal { * * @return Start cell coordinates of the portal. */ - std::pair get_sector0_start() const; + const coord::tile &get_sector0_start() const; /** * Get the end cell coordinates of the portal. * * @return End cell coordinates of the portal. */ - std::pair get_sector0_end() const; + const coord::tile &get_sector0_end() const; /** * Get the start cell coordinates of the portal. * * @return Start cell coordinates of the portal. */ - std::pair get_sector1_start() const; + const coord::tile get_sector1_start() const; /** * Get the end cell coordinates of the portal. * * @return End cell coordinates of the portal. */ - std::pair get_sector1_end() const; + const coord::tile get_sector1_end() const; /** * ID of the portal. @@ -202,23 +199,13 @@ class Portal { PortalDirection direction; /** - * Start cell x coordinate. + * Start cell coordinate. */ - size_t cell_start_x; + coord::tile cell_start; /** - * Start cell y coordinate. + * End cell coordinate. */ - size_t cell_start_y; - - /** - * End cell x coordinate. - */ - size_t cell_end_x; - - /** - * End cell y coordinate. - */ - size_t cell_end_y; + coord::tile cell_end; }; } // namespace openage::path diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index 554a5518b1..05e8c1460f 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -4,6 +4,7 @@ #include "error/error.h" +#include "coord/tile.h" #include "pathfinding/cost_field.h" #include "pathfinding/definitions.h" @@ -52,8 +53,8 @@ std::vector> Sector::find_portals(const std::shared_ptr< size_t start = 0; bool passable_edge = false; for (size_t x = 0; x < this->cost_field->get_size(); x++) { - if (this->cost_field->get_cost(x, this->cost_field->get_size() - 1) != COST_IMPASSABLE - and other_cost->get_cost(x, 0) != COST_IMPASSABLE) { + if (this->cost_field->get_cost(coord::tile{x, this->cost_field->get_size() - 1}) != COST_IMPASSABLE + and other_cost->get_cost(coord::tile{x, 0}) != COST_IMPASSABLE) { if (not passable_edge) { start = x; passable_edge = true; @@ -67,10 +68,8 @@ std::vector> Sector::find_portals(const std::shared_ptr< this->id, other->get_id(), direction, - start, - this->cost_field->get_size() - 1, - x - 1, - 0)); + coord::tile{start, this->cost_field->get_size() - 1}, + coord::tile{x - 1, 0})); passable_edge = false; next_id += 1; } @@ -82,8 +81,8 @@ std::vector> Sector::find_portals(const std::shared_ptr< size_t start = 0; bool passable_edge = false; for (size_t y = 0; y < this->cost_field->get_size(); y++) { - if (this->cost_field->get_cost(this->cost_field->get_size() - 1, y) != COST_IMPASSABLE - and other_cost->get_cost(0, y) != COST_IMPASSABLE) { + if (this->cost_field->get_cost(coord::tile{this->cost_field->get_size() - 1, y}) != COST_IMPASSABLE + and other_cost->get_cost(coord::tile{0, y}) != COST_IMPASSABLE) { if (not passable_edge) { start = y; passable_edge = true; @@ -97,10 +96,8 @@ std::vector> Sector::find_portals(const std::shared_ptr< this->id, other->get_id(), direction, - this->cost_field->get_size() - 1, - start, - 0, - y - 1)); + coord::tile{this->cost_field->get_size() - 1, start}, + coord::tile{0, y - 1})); passable_edge = false; next_id += 1; } diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index e31e87498e..ab62d2ff2f 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -3,6 +3,7 @@ #include "log/log.h" #include "testing/testing.h" +#include "coord/tile.h" #include "pathfinding/cost_field.h" #include "pathfinding/definitions.h" #include "pathfinding/flow_field.h" @@ -10,6 +11,7 @@ #include "pathfinding/integrator.h" #include "pathfinding/types.h" + namespace openage { namespace path { namespace tests { @@ -26,7 +28,7 @@ void flow_field() { // Test the different field types { auto integration_field = std::make_shared(3); - integration_field->integrate_cost(cost_field, 2, 2); + integration_field->integrate_cost(cost_field, coord::tile{2, 2}); auto int_cells = integration_field->get_cells(); // The integration field should look like: @@ -84,7 +86,7 @@ void flow_field() { integrator->set_cost_field(cost_field); // Build the flow field - auto flow_field = integrator->build(2, 2); + auto flow_field = integrator->build(coord::tile{2, 2}); auto ff_cells = flow_field->get_cells(); // The flow field for targeting (2, 2) hould look like this: From ca05032d4af69bd6a65374c9ce8a2f424d4f0de3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Mar 2024 17:06:52 +0100 Subject: [PATCH 352/771] path: Store grid size in vector. --- libopenage/pathfinding/grid.cpp | 29 ++++++++++++++--------------- libopenage/pathfinding/grid.h | 24 ++++++++---------------- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/libopenage/pathfinding/grid.cpp b/libopenage/pathfinding/grid.cpp index 6ad53178f0..8fa452630f 100644 --- a/libopenage/pathfinding/grid.cpp +++ b/libopenage/pathfinding/grid.cpp @@ -9,27 +9,26 @@ namespace openage::path { -Grid::Grid(grid_id_t id, size_t width, size_t height, size_t sector_size) : +Grid::Grid(grid_id_t id, + const util::Vector2s &size, + size_t sector_size) : id{id}, - width{width}, - height{height} { - for (size_t y = 0; y < height; y++) { - for (size_t x = 0; x < width; x++) { - this->sectors.push_back(std::make_shared(x + y * width, sector_size)); + size{size} { + for (size_t y = 0; y < size[1]; y++) { + for (size_t x = 0; x < size[0]; x++) { + this->sectors.push_back(std::make_shared(x + y * this->size[0], sector_size)); } } } Grid::Grid(grid_id_t id, - size_t width, - size_t height, + const util::Vector2s &size, std::vector> &§ors) : id{id}, - width{width}, - height{height}, + size{size}, sectors{std::move(sectors)} { - ENSURE(this->sectors.size() == width * height, - "Grid has size " << width << "x" << height << " (" << width * height << " sectors), " + ENSURE(this->sectors.size() == size[0] * size[1], + "Grid has size " << size[0] << "x" << size[1] << " (" << size[0] * size[1] << " sectors), " << "but only " << this->sectors.size() << " sectors were provided"); } @@ -37,12 +36,12 @@ grid_id_t Grid::get_id() const { return this->id; } -std::pair Grid::get_size() const { - return {this->width, this->height}; +const util::Vector2s &Grid::get_size() const { + return this->size; } const std::shared_ptr &Grid::get_sector(size_t x, size_t y) { - return this->sectors.at(x + y * this->width); + return this->sectors.at(x + y * this->size[0]); } const std::shared_ptr &Grid::get_sector(sector_id_t id) const { diff --git a/libopenage/pathfinding/grid.h b/libopenage/pathfinding/grid.h index adf030cf53..2daa0eab12 100644 --- a/libopenage/pathfinding/grid.h +++ b/libopenage/pathfinding/grid.h @@ -8,6 +8,7 @@ #include #include "pathfinding/types.h" +#include "util/vector.h" namespace openage::path { @@ -22,26 +23,22 @@ class Grid { * Create a new empty grid of width x height sectors with a specified size. * * @param id ID of the grid. - * @param width Width of the grid. - * @param height Height of the grid. + * @param size Size of the grid (width x height). * @param sector_size Side length of each sector. */ Grid(grid_id_t id, - size_t width, - size_t height, + const util::Vector2s &size, size_t sector_size); /** * Create a grid of width x height sectors from a list of existing sectors. * * @param id ID of the grid. - * @param width Width of the grid. - * @param height Height of the grid. + * @param size Size of the grid (width x height). * @param sectors Existing sectors. */ Grid(grid_id_t id, - size_t width, - size_t height, + const util::Vector2s &size, std::vector> &§ors); /** @@ -56,7 +53,7 @@ class Grid { * * @return Size of the grid (width x height). */ - std::pair get_size() const; + const util::Vector2s &get_size() const; /** * Get the sector at a specified position. @@ -84,14 +81,9 @@ class Grid { grid_id_t id; /** - * Width of the grid. + * Size of the grid (width x height). */ - size_t width; - - /** - * Height of the grid. - */ - size_t height; + util::Vector2s size; /** * Sectors of the grid. From f943d86ec9983681b57dd537df62ec39acaecbb5 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Mar 2024 23:20:49 +0100 Subject: [PATCH 353/771] path: Demo for pathfinding grid. --- libopenage/pathfinding/demo/CMakeLists.txt | 1 + libopenage/pathfinding/demo/demo_0.h | 2 +- libopenage/pathfinding/demo/demo_1.cpp | 62 +++++++ libopenage/pathfinding/demo/demo_1.h | 197 +++++++++++++++++++++ libopenage/pathfinding/demo/tests.cpp | 5 + libopenage/pathfinding/grid.cpp | 4 + libopenage/pathfinding/grid.h | 7 + 7 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 libopenage/pathfinding/demo/demo_1.cpp create mode 100644 libopenage/pathfinding/demo/demo_1.h diff --git a/libopenage/pathfinding/demo/CMakeLists.txt b/libopenage/pathfinding/demo/CMakeLists.txt index 27ad003577..938d6ee3f0 100644 --- a/libopenage/pathfinding/demo/CMakeLists.txt +++ b/libopenage/pathfinding/demo/CMakeLists.txt @@ -1,5 +1,6 @@ add_sources(libopenage demo_0.cpp + demo_1.cpp tests.cpp ) diff --git a/libopenage/pathfinding/demo/demo_0.h b/libopenage/pathfinding/demo/demo_0.h index 0dcd115c0e..f84472ee54 100644 --- a/libopenage/pathfinding/demo/demo_0.h +++ b/libopenage/pathfinding/demo/demo_0.h @@ -35,7 +35,7 @@ class FlowField; namespace tests { /** - * Show the pathfinding functionality of the path module: + * Show the functionality of the different flowfield types: * - Cost field * - Integration field * - Flow field diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp new file mode 100644 index 0000000000..171eeb6f47 --- /dev/null +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -0,0 +1,62 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "demo_1.h" + +#include "pathfinding/cost_field.h" +#include "pathfinding/grid.h" +#include "pathfinding/portal.h" +#include "pathfinding/sector.h" + + +namespace openage::path::tests { + +void path_demo_1(const util::Path &path) { + auto grid = std::make_shared(0, util::Vector2s{4, 3}, 10); + + // Initialize the cost field for each sector. + for (auto sector : grid->get_sectors()) { + auto cost_field = sector->get_cost_field(); + auto sector_cost = sectors_cost.at(sector->get_id()); + cost_field->set_costs(std::move(sector_cost)); + } + + // Initialize portals between sectors. + auto grid_size = grid->get_size(); + auto portal_id = 0; + for (size_t y = 0; y < grid_size[1]; y++) { + for (size_t x = 0; x < grid_size[0]; x++) { + auto sector = grid->get_sector(x, y); + + if (x < grid_size[0] - 1) { + auto neighbor = grid->get_sector(x + 1, y); + auto portals = sector->find_portals(neighbor, PortalDirection::EAST_WEST, portal_id); + for (auto portal : portals) { + sector->add_portal(portal); + neighbor->add_portal(portal); + } + portal_id += portals.size(); + } + if (y < grid_size[1] - 1) { + auto neighbor = grid->get_sector(x, y + 1); + auto portals = sector->find_portals(neighbor, PortalDirection::NORTH_SOUTH, portal_id); + for (auto portal : portals) { + sector->add_portal(portal); + neighbor->add_portal(portal); + } + portal_id += portals.size(); + } + } + } + + for (auto sector : grid->get_sectors()) { + log::log(MSG(info) << "Sector " << sector->get_id() << " has " << sector->get_portals().size() << " portals."); + for (auto portal : sector->get_portals()) { + log::log(MSG(info) << " Portal " << portal->get_id() << " connects sectors " + << sector->get_id() << " and " + << portal->get_exit_sector(sector->get_id()) << " at tiles " + << portal->get_entry_start(sector->get_id()) << " and " + << portal->get_entry_end(sector->get_id())); + } + } +} +} // namespace openage::path::tests diff --git a/libopenage/pathfinding/demo/demo_1.h b/libopenage/pathfinding/demo/demo_1.h new file mode 100644 index 0000000000..e74f79e487 --- /dev/null +++ b/libopenage/pathfinding/demo/demo_1.h @@ -0,0 +1,197 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include "pathfinding/definitions.h" +#include "util/path.h" + + +namespace openage::path::tests { + + +/** + * Show the functionality of the high-level pathfinder: + * - Grids + * - Sectors + * - Portals + * + * Visualizes the pathfinding results using our rendering backend. + * + * @param path Path to the project rootdir. + */ +void path_demo_1(const util::Path &path); + + +// Cost for the sectors in the grid +// taken from Figure 23.1 in "Crowd Pathfinding and Steering Using Flow Field Tiles" +const std::vector> sectors_cost = { + { + // clang-format off + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 255, 255, 255, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 255, 255, 255, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 255, 1, 1, + 1, 1, 255, 255, 255, 255, 255, 1, 1, 1, + 1, 1, 255, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 255, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 1, 1, 255, 255, 255, 255, 255, 255, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 1, 1, 255, 1, 1, 1, + 1, 1, 1, 255, 1, 255, 255, 1, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 255, 255, 255, + 1, 1, 1, 1, 255, 255, 255, 1, 1, 1, + 1, 1, 1, 1, 255, 255, 1, 1, 1, 1, + 1, 1, 1, 255, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 255, 255, 1, 1, 1, 1, 1, 255, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 255, 255, 1, 1, 1, + 1, 1, 1, 1, 1, 255, 255, 255, 255, 255, + 1, 1, 1, 1, 255, 255, 255, 255, 255, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 1, 1, 1, 1, 1, 1, + 1, 1, 255, 255, 1, 1, 1, 1, 1, 1, + 255, 255, 255, 255, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 1, 1, 255, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 255, 1, 1, 1, 1, 1, 255, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 255, 1, 1, 1, 1, 1, + 1, 1, 255, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 255, 255, 255, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 255, 255, 255, + // clang-format on + }, + { + // clang-format off + 1, 1, 1, 255, 1, 1, 1, 1, 1, 255, + 1, 1, 1, 255, 1, 255, 255, 255, 255, 255, + 1, 1, 1, 255, 1, 1, 1, 1, 255, 255, + 1, 1, 1, 255, 1, 1, 1, 1, 255, 255, + 1, 255, 255, 1, 1, 1, 1, 1, 255, 255, + 1, 1, 255, 1, 1, 255, 1, 1, 1, 1, + 1, 1, 255, 1, 1, 255, 1, 1, 1, 1, + 255, 255, 255, 255, 255, 255, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 255, 255, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 255, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 255, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 255, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 255, 255, 255, 255, + 255, 1, 1, 1, 255, 255, 255, 255, 255, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 255, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 255, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 255, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 1, 1, 255, 1, 1, 1, 1, 255, 255, 255, + 1, 1, 255, 1, 1, 1, 255, 255, 1, 1, + 1, 1, 1, 1, 1, 255, 255, 255, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 255, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 1, 1, 1, 1, 1, 1, + 1, 1, 255, 255, 255, 255, 255, 255, 1, 1, + 1, 255, 255, 255, 1, 1, 1, 1, 1, 1, + 255, 255, 255, 255, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 255, 1, 1, + 1, 1, 255, 255, 1, 1, 1, 255, 1, 1, + 1, 255, 255, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 255, 255, 255, 1, 1, + 1, 1, 1, 1, 1, 1, 255, 1, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }}; + +} // namespace openage::path::tests diff --git a/libopenage/pathfinding/demo/tests.cpp b/libopenage/pathfinding/demo/tests.cpp index da6e59479f..633dddb621 100644 --- a/libopenage/pathfinding/demo/tests.cpp +++ b/libopenage/pathfinding/demo/tests.cpp @@ -5,6 +5,7 @@ #include "log/log.h" #include "pathfinding/demo/demo_0.h" +#include "pathfinding/demo/demo_1.h" namespace openage::path::tests { @@ -15,6 +16,10 @@ void path_demo(int demo_id, const util::Path &path) { path_demo_0(path); break; + case 1: + path_demo_1(path); + break; + default: log::log(MSG(err) << "Unknown pathfinding demo requested: " << demo_id << "."); break; diff --git a/libopenage/pathfinding/grid.cpp b/libopenage/pathfinding/grid.cpp index 8fa452630f..d0ee691774 100644 --- a/libopenage/pathfinding/grid.cpp +++ b/libopenage/pathfinding/grid.cpp @@ -48,4 +48,8 @@ const std::shared_ptr &Grid::get_sector(sector_id_t id) const { return this->sectors.at(id); } +const std::vector> &Grid::get_sectors() const { + return this->sectors; +} + } // namespace openage::path diff --git a/libopenage/pathfinding/grid.h b/libopenage/pathfinding/grid.h index 2daa0eab12..4cd2a97b7b 100644 --- a/libopenage/pathfinding/grid.h +++ b/libopenage/pathfinding/grid.h @@ -74,6 +74,13 @@ class Grid { */ const std::shared_ptr &get_sector(sector_id_t id) const; + /** + * Get the sectors of the grid. + * + * @return Sectors of the grid. + */ + const std::vector> &get_sectors() const; + private: /** * ID of the grid. From b3b620ba99293342ecab913006654ee92300f9da Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 23 Mar 2024 23:52:39 +0100 Subject: [PATCH 354/771] path: Fix portals not being created if they end in a corner. --- libopenage/pathfinding/demo/demo_1.cpp | 2 +- libopenage/pathfinding/sector.cpp | 117 ++++++++++++++----------- 2 files changed, 69 insertions(+), 50 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 171eeb6f47..e4dfd3aa6f 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -51,7 +51,7 @@ void path_demo_1(const util::Path &path) { for (auto sector : grid->get_sectors()) { log::log(MSG(info) << "Sector " << sector->get_id() << " has " << sector->get_portals().size() << " portals."); for (auto portal : sector->get_portals()) { - log::log(MSG(info) << " Portal " << portal->get_id() << " connects sectors " + log::log(MSG(info) << "\tPortal " << portal->get_id() << " connects sectors " << sector->get_id() << " and " << portal->get_exit_sector(sector->get_id()) << " at tiles " << portal->get_entry_start(sector->get_id()) << " and " diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index 05e8c1460f..0b826ce16c 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -48,63 +48,82 @@ std::vector> Sector::find_portals(const std::shared_ptr< auto other_cost = other->get_cost_field(); // compare the edges of the sectors - if (direction == PortalDirection::NORTH_SOUTH) { - // search from left to right - size_t start = 0; - bool passable_edge = false; - for (size_t x = 0; x < this->cost_field->get_size(); x++) { - if (this->cost_field->get_cost(coord::tile{x, this->cost_field->get_size() - 1}) != COST_IMPASSABLE - and other_cost->get_cost(coord::tile{x, 0}) != COST_IMPASSABLE) { - if (not passable_edge) { - start = x; - passable_edge = true; - } - } - else { - if (passable_edge) { - result.push_back( - std::make_shared( - next_id, - this->id, - other->get_id(), - direction, - coord::tile{start, this->cost_field->get_size() - 1}, - coord::tile{x - 1, 0})); - passable_edge = false; - next_id += 1; - } + size_t start; + bool passable_edge; + for (size_t i = 0; i < this->cost_field->get_size(); ++i) { + auto coord_this = coord::tile{0, 0}; + auto coord_other = coord::tile{0, 0}; + if (direction == PortalDirection::NORTH_SOUTH) { + // right edge; top to bottom + coord_this = coord::tile{i, this->cost_field->get_size() - 1}; + coord_other = coord::tile{i, 0}; + } + else if (direction == PortalDirection::EAST_WEST) { + // bottom edge; east to west + coord_this = coord::tile{this->cost_field->get_size() - 1, i}; + coord_other = coord::tile{0, i}; + } + + if (this->cost_field->get_cost(coord_this) != COST_IMPASSABLE + and other_cost->get_cost(coord_other) != COST_IMPASSABLE) { + if (not passable_edge) { + start = i; + passable_edge = true; } } - } - else if (direction == PortalDirection::EAST_WEST) { - // search from top to bottom - size_t start = 0; - bool passable_edge = false; - for (size_t y = 0; y < this->cost_field->get_size(); y++) { - if (this->cost_field->get_cost(coord::tile{this->cost_field->get_size() - 1, y}) != COST_IMPASSABLE - and other_cost->get_cost(coord::tile{0, y}) != COST_IMPASSABLE) { - if (not passable_edge) { - start = y; - passable_edge = true; + else { + if (passable_edge) { + auto coord_start = coord::tile{0, 0}; + auto coord_end = coord::tile{0, 0}; + if (direction == PortalDirection::NORTH_SOUTH) { + // right edge; top to bottom + coord_start = coord::tile{start, this->cost_field->get_size() - 1}; + coord_end = coord::tile{i - 1, this->cost_field->get_size() - 1}; } - } - else { - if (passable_edge) { - result.push_back( - std::make_shared( - next_id, - this->id, - other->get_id(), - direction, - coord::tile{this->cost_field->get_size() - 1, start}, - coord::tile{0, y - 1})); - passable_edge = false; - next_id += 1; + else if (direction == PortalDirection::EAST_WEST) { + // bottom edge; east to west + coord_start = coord::tile{this->cost_field->get_size() - 1, start}; + coord_end = coord::tile{this->cost_field->get_size() - 1, i - 1}; } + + result.push_back( + std::make_shared( + next_id, + this->id, + other->get_id(), + direction, + coord_start, + coord_end)); + passable_edge = false; + next_id += 1; } } } + // recheck for the last tile on the edge + // because it may be the end of a portal + if (passable_edge) { + auto coord_start = coord::tile{0, 0}; + auto coord_end = coord::tile{0, 0}; + if (direction == PortalDirection::NORTH_SOUTH) { + coord_start = coord::tile{start, this->cost_field->get_size() - 1}; + coord_end = coord::tile{this->cost_field->get_size() - 1, this->cost_field->get_size() - 1}; + } + else if (direction == PortalDirection::EAST_WEST) { + coord_start = coord::tile{this->cost_field->get_size() - 1, start}; + coord_end = coord::tile{this->cost_field->get_size() - 1, this->cost_field->get_size() - 1}; + } + + result.push_back( + std::make_shared( + next_id, + this->id, + other->get_id(), + direction, + coord_start, + coord_end)); + } + return result; } From b382abb8fcc05886338e0fa4dce03f107de7c038 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Mar 2024 03:18:38 +0100 Subject: [PATCH 355/771] path: Connect portals between sectors. --- libopenage/pathfinding/demo/demo_1.cpp | 15 ++- libopenage/pathfinding/portal.cpp | 9 ++ libopenage/pathfinding/portal.h | 9 ++ libopenage/pathfinding/sector.cpp | 128 ++++++++++++++++++++++++- libopenage/pathfinding/sector.h | 2 +- 5 files changed, 155 insertions(+), 8 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index e4dfd3aa6f..3954ba80b8 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -48,14 +48,19 @@ void path_demo_1(const util::Path &path) { } } + for (auto sector : grid->get_sectors()) { + sector->connect_exits(); + } + for (auto sector : grid->get_sectors()) { log::log(MSG(info) << "Sector " << sector->get_id() << " has " << sector->get_portals().size() << " portals."); for (auto portal : sector->get_portals()) { - log::log(MSG(info) << "\tPortal " << portal->get_id() << " connects sectors " - << sector->get_id() << " and " - << portal->get_exit_sector(sector->get_id()) << " at tiles " - << portal->get_entry_start(sector->get_id()) << " and " - << portal->get_entry_end(sector->get_id())); + log::log(MSG(info) << " Portal " << portal->get_id() << ":"); + log::log(MSG(info) << " Connects sectors: " << sector->get_id() << " to " + << portal->get_exit_sector(sector->get_id())); + log::log(MSG(info) << " Entry start: " << portal->get_entry_start(sector->get_id())); + log::log(MSG(info) << " Entry end: " << portal->get_entry_end(sector->get_id())); + log::log(MSG(info) << " Connected portals: " << portal->get_connected(sector->get_id()).size()); } } } diff --git a/libopenage/pathfinding/portal.cpp b/libopenage/pathfinding/portal.cpp index 72d4a0ee55..142780843b 100644 --- a/libopenage/pathfinding/portal.cpp +++ b/libopenage/pathfinding/portal.cpp @@ -27,6 +27,15 @@ portal_id_t Portal::get_id() const { return this->id; } +const std::vector> &Portal::get_connected(sector_id_t sector) const { + ENSURE(sector == this->sector0 || sector == this->sector1, "Portal does not connect to sector"); + + if (sector == this->sector0) { + return this->sector0_exits; + } + return this->sector1_exits; +} + const std::vector> &Portal::get_exits(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); diff --git a/libopenage/pathfinding/portal.h b/libopenage/pathfinding/portal.h index 4855d0eb89..509ef0b318 100644 --- a/libopenage/pathfinding/portal.h +++ b/libopenage/pathfinding/portal.h @@ -70,6 +70,15 @@ class Portal { */ portal_id_t get_id() const; + /** + * Get the connected portals in the specified sector. + * + * @param sector Sector ID. + * + * @return Connected portals in the sector. + */ + const std::vector> &get_connected(sector_id_t sector) const; + /** * Get the exit portals reachable via the portal when entering from a specified sector. * diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index 0b826ce16c..14c0ddeb36 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -2,6 +2,9 @@ #include "sector.h" +#include +#include + #include "error/error.h" #include "coord/tile.h" @@ -127,8 +130,129 @@ std::vector> Sector::find_portals(const std::shared_ptr< return result; } -void Sector::connect_portals() { - // TODO: Flood fill to find connected sectors +void Sector::connect_exits() { + if (this->portals.empty()) { + return; + } + + std::unordered_set portal_ids; + for (const auto &portal : this->portals) { + portal_ids.insert(portal->get_id()); + } + + // check all portals in the sector + std::vector> search_portals = this->portals; + while (not portal_ids.empty()) { + auto portal = search_portals.back(); + search_portals.pop_back(); + portal_ids.erase(portal->get_id()); + + auto start = portal->get_entry_start(this->id); + auto end = portal->get_entry_end(this->id); + + std::unordered_set visited; + std::deque open_list; + std::vector neighbors; + neighbors.reserve(4); + + if (portal->get_direction() == PortalDirection::NORTH_SOUTH) { + // right edge; top to bottom + for (size_t i = start.se; i <= end.se; ++i) { + open_list.push_back(start.ne + i * this->cost_field->get_size()); + } + } + else if (portal->get_direction() == PortalDirection::EAST_WEST) { + // bottom edge; east to west + for (size_t i = start.ne; i <= end.ne; ++i) { + open_list.push_back(i + start.se * this->cost_field->get_size()); + } + } + + // flood fill the grid to find connected portals + while (not open_list.empty()) { + auto current = open_list.front(); + open_list.pop_front(); + + if (visited.contains(current)) { + continue; + } + + // Get the x and y coordinates of the current cell + auto x = current % this->cost_field->get_size(); + auto y = current / this->cost_field->get_size(); + + // check the neighbors + if (y > 0) { + neighbors.push_back(current - this->cost_field->get_size()); + } + if (x > 0) { + neighbors.push_back(current - 1); + } + if (y < this->cost_field->get_size() - 1) { + neighbors.push_back(current + this->cost_field->get_size()); + } + if (x < this->cost_field->get_size() - 1) { + neighbors.push_back(current + 1); + } + + // add the neighbors to the open list + for (const auto &neighbor : neighbors) { + if (this->cost_field->get_cost(neighbor) != COST_IMPASSABLE) { + open_list.push_back(neighbor); + } + } + neighbors.clear(); + + // mark the current cell as visited + visited.insert(current); + } + + // check if the visited cells are connected to another portal + std::vector> connected_portals; + for (auto &exit : this->portals) { + if (exit->get_id() == portal->get_id()) { + // skip the current portal + continue; + } + + // get the start cell of the exit portal + // we only have to check one cell since the flood fill + // should reach any exit cell + auto exit_start = exit->get_entry_start(this->id); + auto exit_cell = exit_start.ne + exit_start.se * this->cost_field->get_size(); + + // check if the exit cell is connected to the visited cells + if (visited.contains(exit_cell)) { + connected_portals.push_back(exit); + } + } + + // set the exits for the current portal + portal->set_exits(this->id, connected_portals); + + // All connected portals share the same exits + // so we can connect them here + for (auto &connected : connected_portals) { + // make a new vector with all connected portals except the current one + std::vector> other_connected; + for (auto &other : connected_portals) { + if (other->get_id() != connected->get_id()) { + other_connected.push_back(other); + } + } + + // add the original portal as it is not in the connected portals vector + other_connected.push_back(portal); + + // set the exits for the connected portal + connected->set_exits(this->id, other_connected); + + // we don't need to food fill for this portal since we have + // found all exits, so we can remove it from the portals that + // should be searched + portal_ids.erase(connected->get_id()); + } + } } } // namespace openage::path diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h index 66c2090ec9..84f4bea0c4 100644 --- a/libopenage/pathfinding/sector.h +++ b/libopenage/pathfinding/sector.h @@ -89,7 +89,7 @@ class Sector { * This method should be called after all sectors and portals have * been created and initialized. */ - void connect_portals(); + void connect_exits(); private: /** From 9a1cc15969f032756863944e031a039d8e346fd0 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Mar 2024 22:42:37 +0100 Subject: [PATCH 356/771] path: Refactor demo 1 code for readability. --- libopenage/pathfinding/demo/demo_1.cpp | 3 +- libopenage/pathfinding/demo/demo_1.h | 240 ++++++++++++------------- 2 files changed, 121 insertions(+), 122 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 3954ba80b8..44cbfd906a 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -48,11 +48,10 @@ void path_demo_1(const util::Path &path) { } } + // Connect portals inside sectors. for (auto sector : grid->get_sectors()) { sector->connect_exits(); - } - for (auto sector : grid->get_sectors()) { log::log(MSG(info) << "Sector " << sector->get_id() << " has " << sector->get_portals().size() << " portals."); for (auto portal : sector->get_portals()) { log::log(MSG(info) << " Portal " << portal->get_id() << ":"); diff --git a/libopenage/pathfinding/demo/demo_1.h b/libopenage/pathfinding/demo/demo_1.h index e74f79e487..933d12e7c3 100644 --- a/libopenage/pathfinding/demo/demo_1.h +++ b/libopenage/pathfinding/demo/demo_1.h @@ -27,170 +27,170 @@ void path_demo_1(const util::Path &path); const std::vector> sectors_cost = { { // clang-format off - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 255, 255, 255, 255, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 255, 255, 255, 255, 1, 1, - 1, 1, 1, 255, 255, 255, 255, 255, 1, 1, - 1, 1, 255, 255, 255, 255, 255, 1, 1, 1, - 1, 1, 255, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 255, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 255, 255, 255, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 255, 255, 255, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 255, 1, 1, + 1, 1, 255, 255, 255, 255, 255, 1, 1, 1, + 1, 1, 255, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 255, 1, 1, 1, 1, 1, 1, 1, // clang-format on }, { // clang-format off - 1, 1, 255, 255, 255, 255, 255, 255, 255, 255, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 255, 1, 1, 255, 1, 1, 1, - 1, 1, 1, 255, 1, 255, 255, 1, 1, 1, - 1, 1, 1, 255, 255, 255, 255, 255, 255, 255, - 1, 1, 1, 1, 255, 255, 255, 1, 1, 1, - 1, 1, 1, 1, 255, 255, 1, 1, 1, 1, - 1, 1, 1, 255, 1, 1, 1, 1, 1, 1, + 1, 1, 255, 255, 255, 255, 255, 255, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 1, 1, 255, 1, 1, 1, + 1, 1, 1, 255, 1, 255, 255, 1, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 255, 255, 255, + 1, 1, 1, 1, 255, 255, 255, 1, 1, 1, + 1, 1, 1, 1, 255, 255, 1, 1, 1, 1, + 1, 1, 1, 255, 1, 1, 1, 1, 1, 1, // clang-format on }, { // clang-format off - 255, 255, 1, 1, 1, 1, 1, 255, 255, 255, - 1, 1, 1, 1, 1, 1, 1, 1, 255, 255, - 1, 1, 1, 1, 1, 1, 1, 1, 255, 255, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 255, 255, 255, 1, 1, 1, - 1, 1, 1, 1, 1, 255, 255, 255, 255, 255, - 1, 1, 1, 1, 255, 255, 255, 255, 255, 1, - 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 255, 1, 1, 1, 1, 255, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 1, 1, 255, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 255, 255, 1, 1, 1, + 1, 1, 1, 1, 1, 255, 255, 255, 255, 255, + 1, 1, 1, 1, 255, 255, 255, 255, 255, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // clang-format on }, { // clang-format off - 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, - 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, - 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 255, 1, 1, 1, 1, 1, 1, - 1, 1, 255, 255, 1, 1, 1, 1, 1, 1, - 255, 255, 255, 255, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 1, 1, 1, 1, 1, 1, + 1, 1, 255, 255, 1, 1, 1, 1, 1, 1, + 255, 255, 255, 255, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // clang-format on }, { // clang-format off - 1, 1, 255, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 255, 1, 1, 1, 1, 1, 255, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, - 1, 1, 1, 255, 255, 1, 1, 1, 1, 1, - 1, 1, 255, 1, 255, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 255, 255, 255, 255, 255, 255, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 255, 255, 255, + 1, 1, 255, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 255, 1, 1, 1, 1, 1, 255, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 255, 1, 1, 1, 1, 1, + 1, 1, 255, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 255, 255, 255, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 255, 255, 255, // clang-format on }, { // clang-format off - 1, 1, 1, 255, 1, 1, 1, 1, 1, 255, - 1, 1, 1, 255, 1, 255, 255, 255, 255, 255, - 1, 1, 1, 255, 1, 1, 1, 1, 255, 255, - 1, 1, 1, 255, 1, 1, 1, 1, 255, 255, - 1, 255, 255, 1, 1, 1, 1, 1, 255, 255, - 1, 1, 255, 1, 1, 255, 1, 1, 1, 1, - 1, 1, 255, 1, 1, 255, 1, 1, 1, 1, - 255, 255, 255, 255, 255, 255, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 1, 1, 1, 1, 1, 255, + 1, 1, 1, 255, 1, 255, 255, 255, 255, 255, + 1, 1, 1, 255, 1, 1, 1, 1, 255, 255, + 1, 1, 1, 255, 1, 1, 1, 1, 255, 255, + 1, 255, 255, 1, 1, 1, 1, 1, 255, 255, + 1, 1, 255, 1, 1, 255, 1, 1, 1, 1, + 1, 1, 255, 1, 1, 255, 1, 1, 1, 1, + 255, 255, 255, 255, 255, 255, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, // clang-format on }, { // clang-format off - 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 255, 1, 1, 1, 1, 1, 1, 1, 255, 255, - 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 255, 1, 1, 1, 1, 1, 255, 1, 1, 1, - 255, 1, 1, 1, 1, 1, 255, 1, 1, 1, - 255, 1, 1, 1, 1, 1, 255, 1, 1, 1, - 255, 1, 1, 1, 1, 1, 255, 255, 255, 255, - 255, 1, 1, 1, 255, 255, 255, 255, 255, 1, - 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 255, 255, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 255, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 255, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 255, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 255, 255, 255, 255, + 255, 1, 1, 1, 255, 255, 255, 255, 255, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, // clang-format on }, { // clang-format off - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 255, 255, 255, 255, 255, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 255, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 255, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 255, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 255, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 255, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // clang-format on }, { // clang-format off - 1, 1, 255, 1, 1, 1, 1, 255, 255, 255, - 1, 1, 255, 1, 1, 1, 255, 255, 1, 1, - 1, 1, 1, 1, 1, 255, 255, 255, 1, 1, - 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 255, - 1, 1, 1, 1, 1, 1, 1, 1, 255, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 255, 1, 1, 1, 1, 255, 255, 255, + 1, 1, 255, 1, 1, 1, 255, 255, 1, 1, + 1, 1, 1, 1, 1, 255, 255, 255, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 255, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // clang-format on }, { // clang-format off - 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 255, 1, 1, 1, 1, 1, 1, - 1, 1, 255, 255, 255, 255, 255, 255, 1, 1, - 1, 255, 255, 255, 1, 1, 1, 1, 1, 1, - 255, 255, 255, 255, 1, 1, 1, 1, 1, 1, - 255, 1, 1, 1, 1, 1, 1, 1, 1, 255, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 1, 1, 1, 1, 1, 1, + 1, 1, 255, 255, 255, 255, 255, 255, 1, 1, + 1, 255, 255, 255, 1, 1, 1, 1, 1, 1, + 255, 255, 255, 255, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // clang-format on }, { // clang-format off - 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 255, 255, 255, 255, 255, 1, 1, - 1, 1, 255, 255, 1, 1, 1, 255, 1, 1, - 1, 255, 255, 1, 1, 1, 1, 1, 1, 1, - 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, - 255, 255, 1, 1, 1, 255, 255, 255, 1, 1, - 1, 1, 1, 1, 1, 1, 255, 1, 1, 1, - 1, 1, 1, 255, 255, 255, 255, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 255, 1, 1, + 1, 1, 255, 255, 1, 1, 1, 255, 1, 1, + 1, 255, 255, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 255, 255, 255, 1, 1, + 1, 1, 1, 1, 1, 1, 255, 1, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // clang-format on }, { // clang-format off - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // clang-format on }}; From 2dd0c3aca4d985eb89647c7b282770d49f83b451 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Mar 2024 22:43:39 +0100 Subject: [PATCH 357/771] fix copyright years --- libopenage/coord/tile.h | 2 +- libopenage/pathfinding/legacy/path_utils.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/coord/tile.h b/libopenage/coord/tile.h index cf020823a7..e8e61a09c2 100644 --- a/libopenage/coord/tile.h +++ b/libopenage/coord/tile.h @@ -1,4 +1,4 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. +// Copyright 2016-2024 the openage authors. See copying.md for legal info. #pragma once diff --git a/libopenage/pathfinding/legacy/path_utils.h b/libopenage/pathfinding/legacy/path_utils.h index c4f4b6fcca..de1aa99da8 100644 --- a/libopenage/pathfinding/legacy/path_utils.h +++ b/libopenage/pathfinding/legacy/path_utils.h @@ -1,4 +1,4 @@ -// Copyright 2014-2016 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once From bf6136bbef22251a964d2a6ad8e8029c26234cc3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Mar 2024 03:01:13 +0100 Subject: [PATCH 358/771] path: Get sector size of grid. --- libopenage/pathfinding/grid.cpp | 10 +++++++++- libopenage/pathfinding/grid.h | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/libopenage/pathfinding/grid.cpp b/libopenage/pathfinding/grid.cpp index d0ee691774..182bd71fc6 100644 --- a/libopenage/pathfinding/grid.cpp +++ b/libopenage/pathfinding/grid.cpp @@ -4,6 +4,7 @@ #include "error/error.h" +#include "pathfinding/cost_field.h" #include "pathfinding/sector.h" @@ -13,7 +14,8 @@ Grid::Grid(grid_id_t id, const util::Vector2s &size, size_t sector_size) : id{id}, - size{size} { + size{size}, + sector_size{sector_size} { for (size_t y = 0; y < size[1]; y++) { for (size_t x = 0; x < size[0]; x++) { this->sectors.push_back(std::make_shared(x + y * this->size[0], sector_size)); @@ -30,6 +32,8 @@ Grid::Grid(grid_id_t id, ENSURE(this->sectors.size() == size[0] * size[1], "Grid has size " << size[0] << "x" << size[1] << " (" << size[0] * size[1] << " sectors), " << "but only " << this->sectors.size() << " sectors were provided"); + + this->sector_size = sectors.at(0)->get_cost_field()->get_size(); } grid_id_t Grid::get_id() const { @@ -40,6 +44,10 @@ const util::Vector2s &Grid::get_size() const { return this->size; } +size_t Grid::get_sector_size() const { + return this->sector_size; +} + const std::shared_ptr &Grid::get_sector(size_t x, size_t y) { return this->sectors.at(x + y * this->size[0]); } diff --git a/libopenage/pathfinding/grid.h b/libopenage/pathfinding/grid.h index 4cd2a97b7b..c5e783ddfa 100644 --- a/libopenage/pathfinding/grid.h +++ b/libopenage/pathfinding/grid.h @@ -55,6 +55,13 @@ class Grid { */ const util::Vector2s &get_size() const; + /** + * Get the side length of the sectors on the grid. + * + * @return Sector side length. + */ + size_t get_sector_size() const; + /** * Get the sector at a specified position. * @@ -92,6 +99,11 @@ class Grid { */ util::Vector2s size; + /** + * Side length of the grid sectors. + */ + size_t sector_size; + /** * Sectors of the grid. */ From 87bf80529b0a2c01636aadb063de62cde257fc3b Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Mar 2024 03:02:22 +0100 Subject: [PATCH 359/771] path: Append demo ID to RenderManager class name. --- libopenage/pathfinding/demo/demo_0.cpp | 60 +++++++++++++------------- libopenage/pathfinding/demo/demo_0.h | 11 ++--- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index badbfef0d6..23a5f12d46 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -60,12 +60,12 @@ void path_demo_0(const util::Path &path) { // Render the grid and the pathfinding results auto qtapp = std::make_shared(); auto window = std::make_shared("openage pathfinding test", 1440, 720); - auto render_manager = std::make_shared(qtapp, window, path); - log::log(INFO << "Created RenderManager for pathfinding demo"); + auto render_manager = std::make_shared(qtapp, window, path); + log::log(INFO << "Created render manager for pathfinding demo"); // Show the cost field on startup render_manager->show_cost_field(cost_field); - auto current_field = RenderManager::field_t::COST; + auto current_field = RenderManager0::field_t::COST; log::log(INFO << "Showing cost field"); // Make steering vector visibility toggleable @@ -95,13 +95,13 @@ void path_demo_0(const util::Path &path) { // Show the new field values and vectors switch (current_field) { - case RenderManager::field_t::COST: + case RenderManager0::field_t::COST: render_manager->show_cost_field(cost_field); break; - case RenderManager::field_t::INTEGRATION: + case RenderManager0::field_t::INTEGRATION: render_manager->show_integration_field(integration_field); break; - case RenderManager::field_t::FLOW: + case RenderManager0::field_t::FLOW: render_manager->show_flow_field(flow_field); break; } @@ -119,17 +119,17 @@ void path_demo_0(const util::Path &path) { if (ev.type() == QEvent::KeyRelease) { if (ev.key() == Qt::Key_F1) { // Show cost field render_manager->show_cost_field(cost_field); - current_field = RenderManager::field_t::COST; + current_field = RenderManager0::field_t::COST; log::log(INFO << "Showing cost field"); } else if (ev.key() == Qt::Key_F2) { // Show integration field render_manager->show_integration_field(integration_field); - current_field = RenderManager::field_t::INTEGRATION; + current_field = RenderManager0::field_t::INTEGRATION; log::log(INFO << "Showing integration field"); } else if (ev.key() == Qt::Key_F3) { // Show flow field render_manager->show_flow_field(flow_field); - current_field = RenderManager::field_t::FLOW; + current_field = RenderManager0::field_t::FLOW; log::log(INFO << "Showing flow field"); } else if (ev.key() == Qt::Key_F4) { // Show steering vectors @@ -156,9 +156,9 @@ void path_demo_0(const util::Path &path) { } -RenderManager::RenderManager(const std::shared_ptr &app, - const std::shared_ptr &window, - const util::Path &path) : +RenderManager0::RenderManager0(const std::shared_ptr &app, + const std::shared_ptr &window, + const util::Path &path) : path{path}, app{app}, window{window}, @@ -175,7 +175,7 @@ RenderManager::RenderManager(const std::shared_ptrwindow->should_close()) { this->app->process_events(); @@ -192,7 +192,7 @@ void RenderManager::run() { this->window->close(); } -void RenderManager::show_cost_field(const std::shared_ptr &field) { +void RenderManager0::show_cost_field(const std::shared_ptr &field) { Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); auto unifs = this->cost_shader->new_uniform_input( "model", @@ -201,7 +201,7 @@ void RenderManager::show_cost_field(const std::shared_ptr &fiel this->camera->get_view_matrix(), "proj", this->camera->get_projection_matrix()); - auto mesh = RenderManager::get_cost_field_mesh(field); + auto mesh = RenderManager0::get_cost_field_mesh(field); auto geometry = this->renderer->add_mesh_geometry(mesh); renderer::Renderable renderable{ unifs, @@ -212,7 +212,7 @@ void RenderManager::show_cost_field(const std::shared_ptr &fiel this->field_pass->set_renderables({renderable}); } -void RenderManager::show_integration_field(const std::shared_ptr &field) { +void RenderManager0::show_integration_field(const std::shared_ptr &field) { Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); auto unifs = this->integration_shader->new_uniform_input( "model", @@ -232,7 +232,7 @@ void RenderManager::show_integration_field(const std::shared_ptrfield_pass->set_renderables({renderable}); } -void RenderManager::show_flow_field(const std::shared_ptr &field) { +void RenderManager0::show_flow_field(const std::shared_ptr &field) { Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); auto unifs = this->flow_shader->new_uniform_input( "model", @@ -252,7 +252,7 @@ void RenderManager::show_flow_field(const std::shared_ptr &fiel this->field_pass->set_renderables({renderable}); } -void RenderManager::show_vectors(const std::shared_ptr &field) { +void RenderManager0::show_vectors(const std::shared_ptr &field) { static const std::unordered_map offset_dir{ {flow_dir_t::NORTH, {-0.25f, 0.0f, 0.0f}}, {flow_dir_t::NORTH_EAST, {-0.25f, 0.0f, -0.25f}}, @@ -299,11 +299,11 @@ void RenderManager::show_vectors(const std::shared_ptr &field) } } -void RenderManager::hide_vectors() { +void RenderManager0::hide_vectors() { this->vector_pass->clear_renderables(); } -std::pair RenderManager::select_tile(double x, double y) { +std::pair RenderManager0::select_tile(double x, double y) { auto grid_plane_normal = Eigen::Vector3f{0, 1, 0}; auto grid_plane_point = Eigen::Vector3f{0, 0, 0}; auto camera_direction = renderer::camera::cam_direction; @@ -317,7 +317,7 @@ std::pair RenderManager::select_tile(double x, double y) { return {grid_x, grid_y}; } -void RenderManager::init_shaders() { +void RenderManager0::init_shaders() { // Shader sources auto shaderdir = this->path / "assets" / "test" / "shaders" / "pathfinding"; @@ -422,7 +422,7 @@ void RenderManager::init_shaders() { this->display_shader = renderer->add_shader({display_vshader_src, display_fshader_src}); } -void RenderManager::init_passes() { +void RenderManager0::init_passes() { auto size = this->window->get_size(); // Make a framebuffer for the background render pass to draw into @@ -539,8 +539,8 @@ void RenderManager::init_passes() { renderer->get_display_target()); } -renderer::resources::MeshData RenderManager::get_cost_field_mesh(const std::shared_ptr &field, - size_t resolution) { +renderer::resources::MeshData RenderManager0::get_cost_field_mesh(const std::shared_ptr &field, + size_t resolution) { // increase by 1 in every dimension because to get the vertex length // of each dimension util::Vector2s size{ @@ -625,8 +625,8 @@ renderer::resources::MeshData RenderManager::get_cost_field_mesh(const std::shar return {std::move(vert_data), std::move(idx_data), info}; } -renderer::resources::MeshData RenderManager::get_integration_field_mesh(const std::shared_ptr &field, - size_t resolution) { +renderer::resources::MeshData RenderManager0::get_integration_field_mesh(const std::shared_ptr &field, + size_t resolution) { // increase by 1 in every dimension because to get the vertex length // of each dimension util::Vector2s size{ @@ -711,8 +711,8 @@ renderer::resources::MeshData RenderManager::get_integration_field_mesh(const st return {std::move(vert_data), std::move(idx_data), info}; } -renderer::resources::MeshData RenderManager::get_flow_field_mesh(const std::shared_ptr &field, - size_t resolution) { +renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::shared_ptr &field, + size_t resolution) { // increase by 1 in every dimension because to get the vertex length // of each dimension util::Vector2s size{ @@ -797,7 +797,7 @@ renderer::resources::MeshData RenderManager::get_flow_field_mesh(const std::shar return {std::move(vert_data), std::move(idx_data), info}; } -renderer::resources::MeshData RenderManager::get_arrow_mesh() { +renderer::resources::MeshData RenderManager0::get_arrow_mesh() { // vertices for the arrow // x, y, z std::vector verts{ @@ -821,7 +821,7 @@ renderer::resources::MeshData RenderManager::get_arrow_mesh() { return {std::move(vert_data), info}; } -renderer::resources::MeshData RenderManager::get_grid_mesh(size_t side_length) { +renderer::resources::MeshData RenderManager0::get_grid_mesh(size_t side_length) { // increase by 1 in every dimension because to get the vertex length // of each dimension util::Vector2s size{side_length + 1, side_length + 1}; diff --git a/libopenage/pathfinding/demo/demo_0.h b/libopenage/pathfinding/demo/demo_0.h index f84472ee54..7a07ebfd7e 100644 --- a/libopenage/pathfinding/demo/demo_0.h +++ b/libopenage/pathfinding/demo/demo_0.h @@ -50,7 +50,7 @@ void path_demo_0(const util::Path &path); /** * Manages the graphical display of the pathfinding demo. */ -class RenderManager { +class RenderManager0 { public: enum class field_t { COST, @@ -65,10 +65,10 @@ class RenderManager { * @param window Window to render to. * @param path Path to the project rootdir. */ - RenderManager(const std::shared_ptr &app, - const std::shared_ptr &window, - const util::Path &path); - ~RenderManager() = default; + RenderManager0(const std::shared_ptr &app, + const std::shared_ptr &window, + const util::Path &path); + ~RenderManager0() = default; /** * Run the render loop. @@ -126,6 +126,7 @@ class RenderManager { * Create the following render passes for the demo: * - Background pass: Mono-colored background object. * - Field pass; Renders the cost, integration and flow fields. + * - Vector pass: Renders the steering vectors of a flow field. * - Grid pass: Renders a grid on top of the fields. * - Display pass: Draws the results of previous passes to the screen. */ From 8ab759d700f7f0299cb1b385660d6d38157a2984 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Mar 2024 23:19:20 +0100 Subject: [PATCH 360/771] path: Draw grid in demo 1. --- .../pathfinding/demo_1_display.frag.glsl | 10 + .../pathfinding/demo_1_display.vert.glsl | 10 + .../shaders/pathfinding/demo_1_grid.frag.glsl | 8 + .../shaders/pathfinding/demo_1_grid.vert.glsl | 11 + .../shaders/pathfinding/demo_1_obj.frag.glsl | 9 + .../shaders/pathfinding/demo_1_obj.vert.glsl | 11 + libopenage/pathfinding/demo/demo_0.cpp | 2 +- libopenage/pathfinding/demo/demo_1.cpp | 322 ++++++++++++++++++ libopenage/pathfinding/demo/demo_1.h | 146 +++++++- 9 files changed, 526 insertions(+), 3 deletions(-) create mode 100644 assets/test/shaders/pathfinding/demo_1_display.frag.glsl create mode 100644 assets/test/shaders/pathfinding/demo_1_display.vert.glsl create mode 100644 assets/test/shaders/pathfinding/demo_1_grid.frag.glsl create mode 100644 assets/test/shaders/pathfinding/demo_1_grid.vert.glsl create mode 100644 assets/test/shaders/pathfinding/demo_1_obj.frag.glsl create mode 100644 assets/test/shaders/pathfinding/demo_1_obj.vert.glsl diff --git a/assets/test/shaders/pathfinding/demo_1_display.frag.glsl b/assets/test/shaders/pathfinding/demo_1_display.frag.glsl new file mode 100644 index 0000000000..a6732d0c7f --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_1_display.frag.glsl @@ -0,0 +1,10 @@ +#version 330 + +uniform sampler2D color_texture; + +in vec2 v_uv; +out vec4 col; + +void main() { + col = texture(color_texture, v_uv); +} diff --git a/assets/test/shaders/pathfinding/demo_1_display.vert.glsl b/assets/test/shaders/pathfinding/demo_1_display.vert.glsl new file mode 100644 index 0000000000..6112530242 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_1_display.vert.glsl @@ -0,0 +1,10 @@ +#version 330 + +layout(location=0) in vec2 position; +layout(location=1) in vec2 uv; +out vec2 v_uv; + +void main() { + gl_Position = vec4(position, 0.0, 1.0); + v_uv = uv; +} diff --git a/assets/test/shaders/pathfinding/demo_1_grid.frag.glsl b/assets/test/shaders/pathfinding/demo_1_grid.frag.glsl new file mode 100644 index 0000000000..19d2e459c3 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_1_grid.frag.glsl @@ -0,0 +1,8 @@ +#version 330 + +out vec4 out_col; + +void main() +{ + out_col = vec4(0.0, 0.0, 0.0, 0.3); +} diff --git a/assets/test/shaders/pathfinding/demo_1_grid.vert.glsl b/assets/test/shaders/pathfinding/demo_1_grid.vert.glsl new file mode 100644 index 0000000000..cbf6712eef --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_1_grid.vert.glsl @@ -0,0 +1,11 @@ +#version 330 + +layout (location = 0) in vec2 position; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 proj; + +void main() { + gl_Position = proj * view * model * vec4(position, 0.0, 1.0); +} diff --git a/assets/test/shaders/pathfinding/demo_1_obj.frag.glsl b/assets/test/shaders/pathfinding/demo_1_obj.frag.glsl new file mode 100644 index 0000000000..ebbb10bc8f --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_1_obj.frag.glsl @@ -0,0 +1,9 @@ +#version 330 + +uniform vec4 color; + +out vec4 outcol; + +void main() { + outcol = color; +} diff --git a/assets/test/shaders/pathfinding/demo_1_obj.vert.glsl b/assets/test/shaders/pathfinding/demo_1_obj.vert.glsl new file mode 100644 index 0000000000..bf804ffe53 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_1_obj.vert.glsl @@ -0,0 +1,11 @@ +#version 330 + +layout(location=0) in vec2 position; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 proj; + +void main() { + gl_Position = proj * view * model * vec4(position, 0.0, 1.0); +} diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 23a5f12d46..e87b44d1f0 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -493,7 +493,7 @@ void RenderManager0::init_passes() { camera->get_view_matrix(), "proj", camera->get_projection_matrix()); - auto grid_mesh = get_grid_mesh(10); + auto grid_mesh = this->get_grid_mesh(10); auto grid_geometry = renderer->add_mesh_geometry(grid_mesh); renderer::Renderable grid_obj{ grid_unifs, diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 44cbfd906a..c25bda4390 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -7,6 +7,13 @@ #include "pathfinding/portal.h" #include "pathfinding/sector.h" +#include "renderer/gui/integration/public/gui_application_with_logger.h" +#include "renderer/opengl/window.h" +#include "renderer/resources/shader_source.h" +#include "renderer/resources/texture_info.h" +#include "renderer/shader_program.h" +#include "renderer/window.h" + namespace openage::path::tests { @@ -62,5 +69,320 @@ void path_demo_1(const util::Path &path) { log::log(MSG(info) << " Connected portals: " << portal->get_connected(sector->get_id()).size()); } } + + // Render the grid + auto qtapp = std::make_shared(); + auto window = std::make_shared("openage pathfinding test", 1024, 768); + auto render_manager = std::make_shared(qtapp, window, path, grid); + log::log(INFO << "Created render manager for pathfinding demo"); + + render_manager->run(); +} + + +RenderManager1::RenderManager1(const std::shared_ptr &app, + const std::shared_ptr &window, + const util::Path &path, + const std::shared_ptr &grid) : + path{path}, + grid{grid}, + app{app}, + window{window}, + renderer{window->make_renderer()} { + this->init_shaders(); + this->init_passes(); +} + +void RenderManager1::run() { + while (not this->window->should_close()) { + this->app->process_events(); + + this->renderer->render(this->background_pass); + this->renderer->render(this->field_pass); + this->renderer->render(this->display_pass); + + this->window->update(); + + this->renderer->check_error(); + } + this->window->close(); +} + +void RenderManager1::init_passes() { + auto size = this->window->get_size(); + + // Make a framebuffer for the background render pass to draw into + auto background_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto fbo = renderer->create_texture_target({background_texture, depth_texture}); + + // Background object for contrast between field and display + Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity(); + auto background_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{1.0, 1.0, 1.0, 1.0}, + "model", + id_matrix, + "view", + id_matrix, + "proj", + id_matrix); + auto background = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); + renderer::Renderable background_obj{ + background_unifs, + background, + true, + true, + }; + this->background_pass = renderer->add_render_pass({background_obj}, fbo); + + // Make a framebuffer for the field render pass to draw into + auto field_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture_2 = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto field_fbo = renderer->create_texture_target({field_texture, depth_texture_2}); + this->field_pass = renderer->add_render_pass({}, field_fbo); + this->create_impassible_tiles(this->grid); + + // Make a framebuffer for the grid render passes to draw into + auto grid_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture_3 = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto grid_fbo = renderer->create_texture_target({grid_texture, depth_texture_3}); + + // Create object for the grid + auto model = Eigen::Affine3f::Identity(); + model.prescale(Eigen::Vector3f{ + 1.0f / (this->grid->get_size()[0] * this->grid->get_sector_size()), + 1.0f / (this->grid->get_size()[1] * this->grid->get_sector_size()), + 1.0f}); + auto grid_unifs = grid_shader->new_uniform_input( + "model", + model.matrix(), + "view", + id_matrix, + "proj", + id_matrix); + auto grid_mesh = this->get_grid_mesh(this->grid); + auto grid_geometry = renderer->add_mesh_geometry(grid_mesh); + renderer::Renderable grid_obj{ + grid_unifs, + grid_geometry, + true, + true, + }; + this->grid_pass = renderer->add_render_pass({grid_obj}, grid_fbo); + + // Make two objects that draw the results of the previous passes onto the screen + // in the display render pass + auto bg_texture_unif = display_shader->new_uniform_input("color_texture", background_texture); + auto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); + renderer::Renderable bg_pass_obj{ + bg_texture_unif, + quad, + true, + true, + }; + auto field_texture_unif = display_shader->new_uniform_input("color_texture", field_texture); + renderer::Renderable field_pass_obj{ + field_texture_unif, + quad, + true, + true, + }; + auto grid_texture_unif = display_shader->new_uniform_input("color_texture", grid_texture); + renderer::Renderable grid_pass_obj{ + grid_texture_unif, + quad, + true, + true, + }; + this->display_pass = renderer->add_render_pass( + {bg_pass_obj, field_pass_obj, grid_pass_obj}, + renderer->get_display_target()); +} + +void RenderManager1::init_shaders() { + // Shader sources + auto shaderdir = this->path / "assets" / "test" / "shaders" / "pathfinding"; + + // Shader for rendering the grid + auto grid_vshader_file = shaderdir / "demo_1_grid.vert.glsl"; + auto grid_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + grid_vshader_file); + + auto grid_fshader_file = shaderdir / "demo_1_grid.frag.glsl"; + auto grid_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + grid_fshader_file); + + // Shader for 2D monocolored objects + auto obj_vshader_file = shaderdir / "demo_1_obj.vert.glsl"; + auto obj_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + obj_vshader_file); + + auto obj_fshader_file = shaderdir / "demo_1_obj.frag.glsl"; + auto obj_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + obj_fshader_file); + + // Shader for rendering to the display target + auto display_vshader_file = shaderdir / "demo_1_display.vert.glsl"; + auto display_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + display_vshader_file); + + auto display_fshader_file = shaderdir / "demo_1_display.frag.glsl"; + auto display_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + display_fshader_file); + + // Create the shaders + this->grid_shader = renderer->add_shader({grid_vshader_src, grid_fshader_src}); + this->obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src}); + this->display_shader = renderer->add_shader({display_vshader_src, display_fshader_src}); +} + + +renderer::resources::MeshData RenderManager1::get_grid_mesh(const std::shared_ptr &grid) { + // increase by 1 in every dimension because to get the vertex length + // of each dimension + util::Vector2s size{ + grid->get_size()[0] * grid->get_sector_size() + 1, + grid->get_size()[1] * grid->get_sector_size() + 1}; + + // add vertices for the cells of the grid + std::vector verts{}; + auto vert_count = size[0] * size[1]; + verts.reserve(vert_count * 2); + for (int i = 0; i < (int)size[0]; ++i) { + for (int j = 0; j < (int)size[1]; ++j) { + verts.push_back(i); + verts.push_back(j); + } + } + + // split the grid into lines using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 8); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we just draw a square using the 4 vertices + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + i * size[1]); // bottom left + } + } + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V2F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::LINES, + renderer::resources::index_t::U16}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); + + return {std::move(vert_data), std::move(idx_data), info}; } + +void RenderManager1::create_impassible_tiles(const std::shared_ptr &grid) { + auto width = grid->get_size()[0]; + auto height = grid->get_size()[1]; + auto sector_size = grid->get_sector_size(); + + float tile_offset_width = 2.0f / (width * sector_size); + float tile_offset_height = 2.0f / (height * sector_size); + + for (size_t sector_x = 0; sector_x < width; sector_x++) { + for (size_t sector_y = 0; sector_y < height; sector_y++) { + auto sector = grid->get_sector(sector_x, sector_y); + auto cost_field = sector->get_cost_field(); + for (size_t y = 0; y < sector_size; y++) { + for (size_t x = 0; x < sector_size; x++) { + auto cost = cost_field->get_cost(coord::tile{x, y}); + if (cost == COST_IMPASSABLE) { + std::array tile_data{ + -1.0f + x * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - y * tile_offset_height - sector_size * sector_y * tile_offset_height, + -1.0f + x * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - (y + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height, + -1.0f + (x + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - y * tile_offset_height - sector_size * sector_y * tile_offset_height, + -1.0f + (x + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - (y + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height, + }; + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V2F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLE_STRIP}; + + auto const data_size = tile_data.size() * sizeof(float); + std::vector verts(data_size); + std::memcpy(verts.data(), tile_data.data(), data_size); + + auto tile_mesh = renderer::resources::MeshData(std::move(verts), info); + auto tile_geometry = renderer->add_mesh_geometry(tile_mesh); + + Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity(); + auto tile_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{0.0, 0.0, 0.0, 1.0}, + "model", + id_matrix, + "view", + id_matrix, + "proj", + id_matrix); + auto tile_obj = renderer::Renderable{ + tile_unifs, + tile_geometry, + true, + true, + }; + this->field_pass->add_renderables({tile_obj}); + } + } + } + } + } +} + } // namespace openage::path::tests diff --git a/libopenage/pathfinding/demo/demo_1.h b/libopenage/pathfinding/demo/demo_1.h index 933d12e7c3..b067ff7553 100644 --- a/libopenage/pathfinding/demo/demo_1.h +++ b/libopenage/pathfinding/demo/demo_1.h @@ -3,10 +3,27 @@ #pragma once #include "pathfinding/definitions.h" +#include "renderer/resources/mesh_data.h" #include "util/path.h" -namespace openage::path::tests { +namespace openage { +namespace renderer { +class Renderer; +class RenderPass; +class ShaderProgram; +class Window; + +namespace gui { +class GuiApplicationWithLogger; +} // namespace gui + +} // namespace renderer + +namespace path { +class Grid; + +namespace tests { /** @@ -22,6 +39,129 @@ namespace openage::path::tests { void path_demo_1(const util::Path &path); +/** + * Manages the graphical display of the pathfinding demo. + */ +class RenderManager1 { +public: + /** + * Create a new render manager. + * + * @param app GUI application. + * @param window Window to render to. + * @param path Path to the project rootdir. + */ + RenderManager1(const std::shared_ptr &app, + const std::shared_ptr &window, + const util::Path &path, + const std::shared_ptr &grid); + ~RenderManager1() = default; + + /** + * Run the render loop. + */ + void run(); + +private: + /** + * Load the shader sources for the demo and create the shader programs. + */ + void init_shaders(); + + /** + * Create the following render passes for the demo: + * - Background pass: Mono-colored background object. + * - Field pass; Renders the cost, integration and flow fields. + * - Grid pass: Renders a grid on top of the cost field. + * - Display pass: Draws the results of previous passes to the screen. + */ + void init_passes(); + + /** + * Create a mesh for the grid. + * + * @param grid Pathing grid. + * + * @return Mesh data for the grid. + */ + static renderer::resources::MeshData get_grid_mesh(const std::shared_ptr &grid); + + /** + * Create objects for the impassible tiles in the grid. + * + * @param grid Pathing grid. + * + * @return Mesh data for the grid. + */ + void create_impassible_tiles(const std::shared_ptr &grid); + + /** + * Path to the project rootdir. + */ + const util::Path &path; + + /** + * Pathing grid of the demo. + */ + std::shared_ptr grid; + + /* Renderer objects */ + + /** + * Qt GUI application. + */ + std::shared_ptr app; + + /** + * openage window to render to. + */ + std::shared_ptr window; + + /** + * openage renderer instance. + */ + std::shared_ptr renderer; + + /* Shader programs */ + + /** + * Shader program for rendering a grid. + */ + std::shared_ptr grid_shader; + + /** + * Shader program for rendering 2D mono-colored objects. + */ + std::shared_ptr obj_shader; + + /** + * Shader program for rendering the final display. + */ + std::shared_ptr display_shader; + + /* Render passes */ + + /** + * Background pass: Mono-colored background object. + */ + std::shared_ptr background_pass; + + /** + * Field pass: Renders the cost field. + */ + std::shared_ptr field_pass; + + /** + * Grid pass: Renders a grid on top of the cost field. + */ + std::shared_ptr grid_pass; + + /** + * Display pass: Draws the results of previous passes to the screen. + */ + std::shared_ptr display_pass; +}; + // Cost for the sectors in the grid // taken from Figure 23.1 in "Crowd Pathfinding and Steering Using Flow Field Tiles" const std::vector> sectors_cost = { @@ -194,4 +334,6 @@ const std::vector> sectors_cost = { // clang-format on }}; -} // namespace openage::path::tests +} // namespace tests +} // namespace path +} // namespace openage From 9d9000496c49ffef04e29f4dcad8cbd21c842684 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 27 Mar 2024 23:12:43 +0100 Subject: [PATCH 361/771] path: Render grid in demo 1. --- libopenage/pathfinding/demo/demo_1.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index c25bda4390..a9a3171a67 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -99,6 +99,7 @@ void RenderManager1::run() { this->renderer->render(this->background_pass); this->renderer->render(this->field_pass); + this->renderer->render(this->grid_pass); this->renderer->render(this->display_pass); this->window->update(); @@ -169,9 +170,10 @@ void RenderManager1::init_passes() { // Create object for the grid auto model = Eigen::Affine3f::Identity(); model.prescale(Eigen::Vector3f{ - 1.0f / (this->grid->get_size()[0] * this->grid->get_sector_size()), - 1.0f / (this->grid->get_size()[1] * this->grid->get_sector_size()), + 2.0f / (this->grid->get_size()[0] * this->grid->get_sector_size()), + 2.0f / (this->grid->get_size()[1] * this->grid->get_sector_size()), 1.0f}); + model.pretranslate(Eigen::Vector3f{-1.0f, -1.0f, 0.0f}); auto grid_unifs = grid_shader->new_uniform_input( "model", model.matrix(), From ca2de6979d592385f1a9a02ba17ad67ba0a192b3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 27 Mar 2024 23:35:19 +0100 Subject: [PATCH 362/771] path: Daw portal tiles on grid. --- libopenage/pathfinding/demo/demo_1.cpp | 78 ++++++++++++++++++++++++++ libopenage/pathfinding/demo/demo_1.h | 11 +++- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index a9a3171a67..c5a2e54755 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -155,6 +155,7 @@ void RenderManager1::init_passes() { auto field_fbo = renderer->create_texture_target({field_texture, depth_texture_2}); this->field_pass = renderer->add_render_pass({}, field_fbo); this->create_impassible_tiles(this->grid); + this->create_portal_tiles(this->grid); // Make a framebuffer for the grid render passes to draw into auto grid_texture = renderer->add_texture( @@ -387,4 +388,81 @@ void RenderManager1::create_impassible_tiles(const std::shared_ptr & } } +void RenderManager1::create_portal_tiles(const std::shared_ptr &grid) { + auto width = grid->get_size()[0]; + auto height = grid->get_size()[1]; + auto sector_size = grid->get_sector_size(); + + float tile_offset_width = 2.0f / (width * sector_size); + float tile_offset_height = 2.0f / (height * sector_size); + + for (size_t sector_x = 0; sector_x < width; sector_x++) { + for (size_t sector_y = 0; sector_y < height; sector_y++) { + auto sector = grid->get_sector(sector_x, sector_y); + + for (auto &portal : sector->get_portals()) { + auto start = portal->get_entry_start(sector->get_id()); + auto end = portal->get_entry_end(sector->get_id()); + auto direction = portal->get_direction(); + + std::vector tiles; + if (direction == PortalDirection::NORTH_SOUTH) { + auto y = start.se; + for (size_t x = start.ne; x <= end.ne; ++x) { + tiles.push_back(coord::tile{x, y}); + } + } + else { + auto x = start.ne; + for (size_t y = start.se; y <= end.se; ++y) { + tiles.push_back(coord::tile{x, y}); + } + } + + for (auto tile : tiles) { + std::array tile_data{ + -1.0f + tile.ne * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - tile.se * tile_offset_height - sector_size * sector_y * tile_offset_height, + -1.0f + tile.ne * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - (tile.se + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height, + -1.0f + (tile.ne + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - tile.se * tile_offset_height - sector_size * sector_y * tile_offset_height, + -1.0f + (tile.ne + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - (tile.se + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height, + }; + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V2F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLE_STRIP}; + + auto const data_size = tile_data.size() * sizeof(float); + std::vector verts(data_size); + std::memcpy(verts.data(), tile_data.data(), data_size); + + auto tile_mesh = renderer::resources::MeshData(std::move(verts), info); + auto tile_geometry = renderer->add_mesh_geometry(tile_mesh); + + Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity(); + auto tile_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{0.0, 0.0, 0.0, 0.3}, + "model", + id_matrix, + "view", + id_matrix, + "proj", + id_matrix); + auto tile_obj = renderer::Renderable{ + tile_unifs, + tile_geometry, + true, + true, + }; + this->field_pass->add_renderables({tile_obj}); + } + } + } + } +} + } // namespace openage::path::tests diff --git a/libopenage/pathfinding/demo/demo_1.h b/libopenage/pathfinding/demo/demo_1.h index b067ff7553..52ed7d1f65 100644 --- a/libopenage/pathfinding/demo/demo_1.h +++ b/libopenage/pathfinding/demo/demo_1.h @@ -87,14 +87,19 @@ class RenderManager1 { static renderer::resources::MeshData get_grid_mesh(const std::shared_ptr &grid); /** - * Create objects for the impassible tiles in the grid. + * Create renderables for the impassible tiles in the grid. * * @param grid Pathing grid. - * - * @return Mesh data for the grid. */ void create_impassible_tiles(const std::shared_ptr &grid); + /** + * Create renderables for the portal tiles in the grid. + * + * @param grid Pathing grid. + */ + void create_portal_tiles(const std::shared_ptr &grid); + /** * Path to the project rootdir. */ From ea9f14306e1713b8407daa2cb55cef101e7d9c13 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 29 Mar 2024 14:47:27 +0100 Subject: [PATCH 363/771] path: Remove last tile check in portal search. --- libopenage/pathfinding/sector.cpp | 81 +++++++++++++------------------ 1 file changed, 34 insertions(+), 47 deletions(-) diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index 14c0ddeb36..c095c7b995 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -52,6 +52,7 @@ std::vector> Sector::find_portals(const std::shared_ptr< // compare the edges of the sectors size_t start; + size_t end; bool passable_edge; for (size_t i = 0; i < this->cost_field->get_size(); ++i) { auto coord_this = coord::tile{0, 0}; @@ -69,62 +70,48 @@ std::vector> Sector::find_portals(const std::shared_ptr< if (this->cost_field->get_cost(coord_this) != COST_IMPASSABLE and other_cost->get_cost(coord_other) != COST_IMPASSABLE) { + // both sides of the edge are passable if (not passable_edge) { + // start a new portal start = i; passable_edge = true; } - } - else { - if (passable_edge) { - auto coord_start = coord::tile{0, 0}; - auto coord_end = coord::tile{0, 0}; - if (direction == PortalDirection::NORTH_SOUTH) { - // right edge; top to bottom - coord_start = coord::tile{start, this->cost_field->get_size() - 1}; - coord_end = coord::tile{i - 1, this->cost_field->get_size() - 1}; - } - else if (direction == PortalDirection::EAST_WEST) { - // bottom edge; east to west - coord_start = coord::tile{this->cost_field->get_size() - 1, start}; - coord_end = coord::tile{this->cost_field->get_size() - 1, i - 1}; - } + // else: we already started a portal - result.push_back( - std::make_shared( - next_id, - this->id, - other->get_id(), - direction, - coord_start, - coord_end)); - passable_edge = false; - next_id += 1; + end = i; + if (i != this->cost_field->get_size() - 1) { + // continue to next tile unless we are at the last tile + // then we have to end the current portal + continue; } } - } - // recheck for the last tile on the edge - // because it may be the end of a portal - if (passable_edge) { - auto coord_start = coord::tile{0, 0}; - auto coord_end = coord::tile{0, 0}; - if (direction == PortalDirection::NORTH_SOUTH) { - coord_start = coord::tile{start, this->cost_field->get_size() - 1}; - coord_end = coord::tile{this->cost_field->get_size() - 1, this->cost_field->get_size() - 1}; - } - else if (direction == PortalDirection::EAST_WEST) { - coord_start = coord::tile{this->cost_field->get_size() - 1, start}; - coord_end = coord::tile{this->cost_field->get_size() - 1, this->cost_field->get_size() - 1}; - } + if (passable_edge) { + // create a new portal + auto coord_start = coord::tile{0, 0}; + auto coord_end = coord::tile{0, 0}; + if (direction == PortalDirection::NORTH_SOUTH) { + // right edge; top to bottom + coord_start = coord::tile{start, this->cost_field->get_size() - 1}; + coord_end = coord::tile{end, this->cost_field->get_size() - 1}; + } + else if (direction == PortalDirection::EAST_WEST) { + // bottom edge; east to west + coord_start = coord::tile{this->cost_field->get_size() - 1, start}; + coord_end = coord::tile{this->cost_field->get_size() - 1, end}; + } - result.push_back( - std::make_shared( - next_id, - this->id, - other->get_id(), - direction, - coord_start, - coord_end)); + result.push_back( + std::make_shared( + next_id, + this->id, + other->get_id(), + direction, + coord_start, + coord_end)); + passable_edge = false; + next_id += 1; + } } return result; From 3993d414a71af3095e2561ee784ab91857ad9e63 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 29 Mar 2024 14:49:51 +0100 Subject: [PATCH 364/771] path: Use matching types in flood fill algorithm. --- libopenage/pathfinding/sector.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index c095c7b995..09f35e16f8 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -144,13 +144,13 @@ void Sector::connect_exits() { if (portal->get_direction() == PortalDirection::NORTH_SOUTH) { // right edge; top to bottom - for (size_t i = start.se; i <= end.se; ++i) { + for (auto i = start.se; i <= end.se; ++i) { open_list.push_back(start.ne + i * this->cost_field->get_size()); } } else if (portal->get_direction() == PortalDirection::EAST_WEST) { // bottom edge; east to west - for (size_t i = start.ne; i <= end.ne; ++i) { + for (auto i = start.ne; i <= end.ne; ++i) { open_list.push_back(i + start.se * this->cost_field->get_size()); } } From d0f88dc0ad20fa3bcc1280550df74e174f31ec0d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 30 Mar 2024 21:20:27 +0100 Subject: [PATCH 365/771] path: Store sector position relative to grid origin --- libopenage/pathfinding/grid.cpp | 7 ++++++- libopenage/pathfinding/portal.cpp | 28 ++++++++++++++++++++++++++++ libopenage/pathfinding/portal.h | 22 ++++++++++++++++++++-- libopenage/pathfinding/sector.cpp | 10 ++++++++-- libopenage/pathfinding/sector.h | 17 +++++++++++++++++ 5 files changed, 79 insertions(+), 5 deletions(-) diff --git a/libopenage/pathfinding/grid.cpp b/libopenage/pathfinding/grid.cpp index 182bd71fc6..51940d2bf3 100644 --- a/libopenage/pathfinding/grid.cpp +++ b/libopenage/pathfinding/grid.cpp @@ -4,6 +4,7 @@ #include "error/error.h" +#include "coord/chunk.h" #include "pathfinding/cost_field.h" #include "pathfinding/sector.h" @@ -18,7 +19,11 @@ Grid::Grid(grid_id_t id, sector_size{sector_size} { for (size_t y = 0; y < size[1]; y++) { for (size_t x = 0; x < size[0]; x++) { - this->sectors.push_back(std::make_shared(x + y * this->size[0], sector_size)); + this->sectors.push_back( + std::make_shared( + x + y * this->size[0], + coord::chunk{x, y}, + sector_size)); } } } diff --git a/libopenage/pathfinding/portal.cpp b/libopenage/pathfinding/portal.cpp index 142780843b..da3581047f 100644 --- a/libopenage/pathfinding/portal.cpp +++ b/libopenage/pathfinding/portal.cpp @@ -75,6 +75,20 @@ const coord::tile Portal::get_entry_start(sector_id_t entry_sector) const { return this->get_sector1_start(); } +const coord::tile Portal::get_entry_center(sector_id_t entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + auto start = this->get_sector0_start(); + auto end = this->get_sector0_end(); + return {(start.ne + end.ne) / 2, (start.se + end.se) / 2}; + } + + auto start = this->get_sector1_start(); + auto end = this->get_sector1_end(); + return {(start.ne + end.ne) / 2, (start.se + end.se) / 2}; +} + const coord::tile Portal::get_entry_end(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); @@ -95,6 +109,20 @@ const coord::tile Portal::get_exit_start(sector_id_t entry_sector) const { return this->get_sector0_start(); } +const coord::tile Portal::get_exit_center(sector_id_t entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + auto start = this->get_sector1_start(); + auto end = this->get_sector1_end(); + return {(start.ne + end.ne) / 2, (start.se + end.se) / 2}; + } + + auto start = this->get_sector0_start(); + auto end = this->get_sector0_end(); + return {(start.ne + end.ne) / 2, (start.se + end.se) / 2}; +} + const coord::tile Portal::get_exit_end(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); diff --git a/libopenage/pathfinding/portal.h b/libopenage/pathfinding/portal.h index 509ef0b318..60a6bf9e48 100644 --- a/libopenage/pathfinding/portal.h +++ b/libopenage/pathfinding/portal.h @@ -106,14 +106,23 @@ class Portal { sector_id_t get_exit_sector(sector_id_t entry_sector) const; /** - * Get the cost field of the sector from which the portal is entered. + * Get the cell coordinates of the start of the portal in the entry sector. * * @param entry_sector Sector from which the portal is entered. * - * @return Cost field of the sector from which the portal is entered. + * @return Cell coordinates of the start of the portal in the entry sector. */ const coord::tile get_entry_start(sector_id_t entry_sector) const; + /** + * Get the cell coordinates of the center of the portal in the entry sector. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Cell coordinates of the center of the portal in the entry sector. + */ + const coord::tile get_entry_center(sector_id_t entry_sector) const; + /** * Get the cell coordinates of the start of the portal in the entry sector. * @@ -132,6 +141,15 @@ class Portal { */ const coord::tile get_exit_start(sector_id_t entry_sector) const; + /** + * Get the cell coordinates of the center of the portal in the exit sector. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Cell coordinates of the center of the portal in the exit sector. + */ + const coord::tile get_exit_center(sector_id_t entry_sector) const; + /** * Get the cell coordinates of the end of the portal in the exit sector. * diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index 09f35e16f8..bc75894cb8 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -14,13 +14,15 @@ namespace openage::path { -Sector::Sector(sector_id_t id, size_t field_size) : +Sector::Sector(sector_id_t id, const coord::chunk &position, size_t field_size) : id{id}, + position{position}, cost_field{std::make_shared(field_size)} { } -Sector::Sector(sector_id_t id, const std::shared_ptr &cost_field) : +Sector::Sector(sector_id_t id, const coord::chunk &position, const std::shared_ptr &cost_field) : id{id}, + position{position}, cost_field{cost_field} { } @@ -28,6 +30,10 @@ const sector_id_t &Sector::get_id() const { return this->id; } +const coord::chunk &Sector::get_position() const { + return this->position; +} + const std::shared_ptr &Sector::get_cost_field() const { return this->cost_field; } diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h index 84f4bea0c4..f39f1904a9 100644 --- a/libopenage/pathfinding/sector.h +++ b/libopenage/pathfinding/sector.h @@ -6,6 +6,7 @@ #include #include +#include "coord/chunk.h" #include "pathfinding/portal.h" #include "pathfinding/types.h" @@ -26,18 +27,22 @@ class Sector { * Create a new sector with a specified ID and an uninitialized cost field. * * @param id ID of the sector. Should be unique per grid. + * @param position Position of the sector in the grid. * @param field_size Size of the cost field. */ Sector(sector_id_t id, + const coord::chunk &position, size_t field_size); /** * Create a new sector with a specified ID and an existing cost field. * * @param id ID of the sector. Should be unique per grid. + * @param position Position of the sector in the grid. * @param cost_field Cost field of the sector. */ Sector(sector_id_t id, + const coord::chunk &position, const std::shared_ptr &cost_field); /** @@ -49,6 +54,13 @@ class Sector { */ const sector_id_t &get_id() const; + /** + * Get the position of this sector in the grid. + * + * @return Position of the sector. + */ + const coord::chunk &get_position() const; + /** * Get the cost field of this sector. * @@ -97,6 +109,11 @@ class Sector { */ sector_id_t id; + /** + * Position of the sector in the grid. + */ + coord::chunk position; + /** * Cost field of the sector. */ From bc9279a22b6933617c70b237f5680ba3876fdede Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 31 Mar 2024 00:37:59 +0100 Subject: [PATCH 366/771] path: Port A* algorithm code to grid portals. --- libopenage/pathfinding/pathfinder.cpp | 191 ++++++++++++++++++++++++++ libopenage/pathfinding/pathfinder.h | 123 ++++++++++++++++- 2 files changed, 312 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 44be92ccd1..c158c57bee 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -2,7 +2,11 @@ #include "pathfinder.h" +#include "coord/phys.h" +#include "pathfinding/grid.h" #include "pathfinding/integrator.h" +#include "pathfinding/portal.h" +#include "pathfinding/sector.h" namespace openage::path { @@ -12,8 +16,195 @@ Pathfinder::Pathfinder() : integrator{std::make_shared()} { } +const Path Pathfinder::get_path(PathRequest &request) { + auto portal_path = this->portal_a_star(request); + + // TODO: Implement the rest of the pathfinding process +} + const std::shared_ptr &Pathfinder::get_grid(size_t id) const { return this->grids.at(id); } +const std::vector> Pathfinder::portal_a_star(PathRequest &request) const { + std::vector> result; + + auto grid = this->grids.at(request.grid_id); + auto sector_size = grid->get_sector_size(); + + auto start_sector_x = request.start.ne / sector_size; + auto start_sector_y = request.start.se / sector_size; + auto start_sector = grid->get_sector(start_sector_x, start_sector_y); + + auto target_sector_x = request.target.ne / sector_size; + auto target_sector_y = request.target.se / sector_size; + auto target_sector = grid->get_sector(target_sector_x, target_sector_y); + + // path node storage, always provides cheapest next node. + PortalNode::heap_t node_candidates; + + // list of known portals and corresponding node. + PortalNode::nodemap_t visited_portals; + + // Cost to travel from one portal to another + // TODO: Determine this cost for each portal + const int distance_cost = 1; + + // start nodes: all portals in the start sector + for (auto &portal : start_sector->get_portals()) { + auto portal_node = std::make_shared(portal, start_sector->get_id(), nullptr); + + auto sector_pos = grid->get_sector(portal->get_exit_sector(start_sector->get_id()))->get_position(); + auto portal_pos = portal->get_exit_center(start_sector->get_id()); + auto portal_abs_pos = portal_pos + coord::tile_delta{sector_pos.ne * sector_size, sector_pos.se * sector_size}; + auto heuristic_cost = Pathfinder::heuristic_cost(portal_abs_pos, request.target); + + portal_node->current_cost = 0; + portal_node->heuristic_cost = heuristic_cost; + portal_node->future_cost = portal_node->current_cost + heuristic_cost; + + portal_node->heap_node = node_candidates.push(portal_node); + visited_portals[portal->get_id()] = portal_node; + } + + // track the closest we can get to the end position + // used when no path is found + auto closest_node = node_candidates.top(); + + // while there are candidates to visit + while (not node_candidates.empty()) { + auto current_node = node_candidates.pop(); + + current_node->was_best = true; + + // check if the current node is the target + if (current_node->portal->get_exit_sector(current_node->entry_sector) == target_sector->get_id()) { + auto backtrace = current_node->generate_backtrace(); + for (auto &node : backtrace) { + result.push_back(node->portal); + } + return result; + } + + // check if the current node is the closest to the target + if (current_node->heuristic_cost < closest_node->heuristic_cost) { + closest_node = current_node; + } + + // get the exits of the current node + auto exits = current_node->get_exits(visited_portals, current_node->entry_sector); + + // evaluate all neighbors of the current candidate for further progress + for (auto &exit : exits) { + if (exit->was_best) { + continue; + } + + bool not_visited = !visited_portals.contains(exit->portal->get_id()); + auto tentative_cost = current_node->current_cost + distance_cost; + + if (not_visited or tentative_cost < exit->current_cost) { + if (not_visited) { + // calculate the heuristic cost + exit->heuristic_cost = Pathfinder::heuristic_cost(exit->portal->get_exit_center(exit->entry_sector), request.target); + } + + // update the cost knowledge + exit->current_cost = tentative_cost; + exit->future_cost = exit->current_cost + exit->heuristic_cost; + exit->prev_portal = current_node; + + if (not_visited) { + exit->heap_node = node_candidates.push(exit); + visited_portals[exit->portal->get_id()] = exit; + } + else { + node_candidates.decrease(exit->heap_node); + } + } + } + } + + // no path found, return the closest node + auto backtrace = closest_node->generate_backtrace(); + for (auto &node : backtrace) { + result.push_back(node->portal); + } + + return result; +} + +int Pathfinder::heuristic_cost(const coord::tile &portal_pos, + const coord::tile &target_pos) { + auto portal_phys_pos = portal_pos.to_phys2(); + auto target_phys_pos = target_pos.to_phys2(); + auto delta = target_phys_pos - portal_phys_pos; + + return delta.length(); +} + +PortalNode::PortalNode(const std::shared_ptr &portal, + sector_id_t entry_sector, + const node_t &prev_portal) : + portal{portal}, + entry_sector{entry_sector}, + was_best{false}, + prev_portal{prev_portal}, + heap_node{nullptr} {} + +PortalNode::PortalNode(const std::shared_ptr &portal, + sector_id_t entry_sector, + const node_t &prev_portal, + int past_cost, + int heuristic_cost) : + portal{portal}, + entry_sector{entry_sector}, + future_cost{past_cost + heuristic_cost}, + current_cost{past_cost}, + heuristic_cost{heuristic_cost}, + was_best{false}, + prev_portal{prev_portal}, + heap_node{nullptr} { +} + +bool PortalNode::operator<(const PortalNode &other) const { + return this->future_cost < other.future_cost; +} + +bool PortalNode::operator==(const PortalNode &other) const { + return this->portal->get_id() == other.portal->get_id(); +} + +std::vector PortalNode::generate_backtrace() { + std::vector waypoints; + + node_t current = this->shared_from_this(); + do { + node_t other = current; + waypoints.push_back(current); + current = current->prev_portal; + } + while (current != nullptr); + waypoints.pop_back(); // remove start + + return waypoints; +} + +std::vector PortalNode::get_exits(const nodemap_t &nodes, + sector_id_t entry_sector) { + std::vector exits; + for (auto &exit : this->portal->get_exits(entry_sector)) { + auto exit_id = exit->get_id(); + + if (nodes.contains(exit_id)) { + exits.push_back(nodes.at(exit_id)); + } + else { + exits.push_back(std::make_shared(exit, entry_sector, this->shared_from_this())); + } + } + return exits; +} + + } // namespace openage::path diff --git a/libopenage/pathfinding/pathfinder.h b/libopenage/pathfinding/pathfinder.h index d423272ff1..e1e04a4a17 100644 --- a/libopenage/pathfinding/pathfinder.h +++ b/libopenage/pathfinding/pathfinder.h @@ -5,10 +5,16 @@ #include #include +#include "coord/tile.h" +#include "datastructure/pairing_heap.h" +#include "pathfinding/path.h" +#include "pathfinding/types.h" + namespace openage::path { class Grid; class Integrator; +class Portal; /** * Pathfinder for flow field pathfinding. @@ -36,15 +42,36 @@ class Pathfinder { * * @return Pathfinding grid. */ - const std::shared_ptr &get_grid(size_t id) const; + const std::shared_ptr &get_grid(grid_id_t id) const; + + /** + * Get the path for a pathfinding request. + * + * @param request Pathfinding request. + * + * @return Path found by the pathfinder. + */ + const Path get_path(PathRequest &request); private: + const std::vector> portal_a_star(PathRequest &request) const; + + /** + * Calculate the heuristic cost between a portal and a target cell. + * + * @param portal_pos Position of the portal. This should be the center of the portal exit. + * @param target_pos Position of the target cell. + * + * @return Heuristic cost between the cells. + */ + static int heuristic_cost(const coord::tile &portal_pos, const coord::tile &target_pos); + /** * Grids managed by this pathfinder. * * Each grid can have separate pathing. */ - std::unordered_map> grids; + std::unordered_map> grids; /** * Integrator for flow field calculations. @@ -52,4 +79,96 @@ class Pathfinder { std::shared_ptr integrator; }; + +/** + * One navigation waypoint in a path. + */ +class PortalNode : public std::enable_shared_from_this { +public: + using node_t = std::shared_ptr; + using heap_t = datastructure::PairingHeap; + using nodemap_t = std::unordered_map; + + PortalNode(const std::shared_ptr &portal, + sector_id_t entry_sector, + const node_t &prev_portal); + PortalNode(const std::shared_ptr &portal, + sector_id_t entry_sector, + const node_t &prev_portal, + int past_cost, + int heuristic_cost); + + /** + * Orders nodes according to their future cost value. + */ + bool operator<(const PortalNode &other) const; + + /** + * Compare the node to another one. + * They are the same if their portal is. + */ + bool operator==(const PortalNode &other) const; + + /** + * Calculates the actual movement cose to another node. + */ + int cost_to(const PortalNode &other) const; + + /** + * Create a backtrace path beginning at this node. + */ + std::vector generate_backtrace(); + + /** + * Get all exits of a node. + */ + std::vector get_exits(const nodemap_t &nodes, sector_id_t entry_sector); + + /** + * The portal this node is associated to. + */ + std::shared_ptr portal; + + /** + * Sector where the portal is entered. + */ + sector_id_t entry_sector; + + /** + * Future cost estimation value for this node. + */ + int future_cost; + + /** + * Evaluated past cost value for the node. + * This stores the actual cost from start to this node. + */ + int current_cost; + + /** + * Heuristic cost cache. + * Calculated once, is the heuristic distance from this node + * to the goal. + */ + int heuristic_cost; + + /** + * Does this node already have an alternative path? + * If the node was once selected as the best next hop, + * this is set to true. + */ + bool was_best = false; + + /** + * Node where this one was reached by least cost. + */ + node_t prev_portal; + + /** + * Priority queue node that contains this path node. + */ + heap_t::element_t heap_node; +}; + + } // namespace openage::path From 42393b84c514a0765d61faaa179f629677f62ca2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 31 Mar 2024 17:03:27 +0200 Subject: [PATCH 367/771] path: Fix node cost comparison on heap. --- libopenage/pathfinding/pathfinder.cpp | 35 ++++++++++++++++++++------- libopenage/pathfinding/pathfinder.h | 29 +++++++++++++++++++--- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index c158c57bee..5d1e255d84 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -20,12 +20,17 @@ const Path Pathfinder::get_path(PathRequest &request) { auto portal_path = this->portal_a_star(request); // TODO: Implement the rest of the pathfinding process + return Path{}; } -const std::shared_ptr &Pathfinder::get_grid(size_t id) const { +const std::shared_ptr &Pathfinder::get_grid(grid_id_t id) const { return this->grids.at(id); } +void Pathfinder::add_grid(const std::shared_ptr &grid) { + this->grids[grid->get_id()] = grid; +} + const std::vector> Pathfinder::portal_a_star(PathRequest &request) const { std::vector> result; @@ -41,10 +46,10 @@ const std::vector> Pathfinder::portal_a_star(PathRequest auto target_sector = grid->get_sector(target_sector_x, target_sector_y); // path node storage, always provides cheapest next node. - PortalNode::heap_t node_candidates; + heap_t node_candidates; // list of known portals and corresponding node. - PortalNode::nodemap_t visited_portals; + nodemap_t visited_portals; // Cost to travel from one portal to another // TODO: Determine this cost for each portal @@ -106,7 +111,9 @@ const std::vector> Pathfinder::portal_a_star(PathRequest if (not_visited or tentative_cost < exit->current_cost) { if (not_visited) { // calculate the heuristic cost - exit->heuristic_cost = Pathfinder::heuristic_cost(exit->portal->get_exit_center(exit->entry_sector), request.target); + exit->heuristic_cost = Pathfinder::heuristic_cost( + exit->portal->get_exit_center(exit->entry_sector), + request.target); } // update the cost knowledge @@ -148,6 +155,9 @@ PortalNode::PortalNode(const std::shared_ptr &portal, const node_t &prev_portal) : portal{portal}, entry_sector{entry_sector}, + future_cost{std::numeric_limits::max()}, + current_cost{std::numeric_limits::max()}, + heuristic_cost{std::numeric_limits::max()}, was_best{false}, prev_portal{prev_portal}, heap_node{nullptr} {} @@ -175,7 +185,7 @@ bool PortalNode::operator==(const PortalNode &other) const { return this->portal->get_id() == other.portal->get_id(); } -std::vector PortalNode::generate_backtrace() { +std::vector PortalNode::generate_backtrace() { std::vector waypoints; node_t current = this->shared_from_this(); @@ -185,14 +195,15 @@ std::vector PortalNode::generate_backtrace() { current = current->prev_portal; } while (current != nullptr); - waypoints.pop_back(); // remove start return waypoints; } -std::vector PortalNode::get_exits(const nodemap_t &nodes, - sector_id_t entry_sector) { +std::vector PortalNode::get_exits(const nodemap_t &nodes, + sector_id_t entry_sector) { std::vector exits; + + auto exit_sector = this->portal->get_exit_sector(entry_sector); for (auto &exit : this->portal->get_exits(entry_sector)) { auto exit_id = exit->get_id(); @@ -200,11 +211,17 @@ std::vector PortalNode::get_exits(const nodemap_t &nodes, exits.push_back(nodes.at(exit_id)); } else { - exits.push_back(std::make_shared(exit, entry_sector, this->shared_from_this())); + exits.push_back(std::make_shared(exit, + exit_sector, + this->shared_from_this())); } } return exits; } +bool compare_node_cost::operator()(const node_t &lhs, const node_t &rhs) const { + return *lhs < *rhs; +} + } // namespace openage::path diff --git a/libopenage/pathfinding/pathfinder.h b/libopenage/pathfinding/pathfinder.h index e1e04a4a17..b89f994bc8 100644 --- a/libopenage/pathfinding/pathfinder.h +++ b/libopenage/pathfinding/pathfinder.h @@ -44,6 +44,13 @@ class Pathfinder { */ const std::shared_ptr &get_grid(grid_id_t id) const; + /** + * Add a grid to the pathfinder. + * + * @param grid Grid to add. + */ + void add_grid(const std::shared_ptr &grid); + /** * Get the path for a pathfinding request. * @@ -80,15 +87,29 @@ class Pathfinder { }; +class PortalNode; + +using node_t = std::shared_ptr; + +/** + * Cost comparison for node_t on the pairing heap. + * + * Extracts the nodes from the shared_ptr and compares them. We have + * to use a custom comparison function because otherwise the shared_ptr + * would be compared instead of the actual node. + */ +struct compare_node_cost { + bool operator()(const node_t &lhs, const node_t &rhs) const; +}; + +using heap_t = datastructure::PairingHeap; +using nodemap_t = std::unordered_map; + /** * One navigation waypoint in a path. */ class PortalNode : public std::enable_shared_from_this { public: - using node_t = std::shared_ptr; - using heap_t = datastructure::PairingHeap; - using nodemap_t = std::unordered_map; - PortalNode(const std::shared_ptr &portal, sector_id_t entry_sector, const node_t &prev_portal); From 16710e912c38e5b2b6fa128de770338e463a38df Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 31 Mar 2024 17:03:43 +0200 Subject: [PATCH 368/771] path: Calculate example path in demo. --- libopenage/pathfinding/demo/demo_1.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index c5a2e54755..c2f5ba19de 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -4,6 +4,8 @@ #include "pathfinding/cost_field.h" #include "pathfinding/grid.h" +#include "pathfinding/path.h" +#include "pathfinding/pathfinder.h" #include "pathfinding/portal.h" #include "pathfinding/sector.h" @@ -70,6 +72,16 @@ void path_demo_1(const util::Path &path) { } } + auto pathfinder = std::make_shared(); + pathfinder->add_grid(grid); + + auto path_request = path::PathRequest{ + 0, + coord::tile{2, 26}, + coord::tile{36, 2}, + }; + auto path_result = pathfinder->get_path(path_request); + // Render the grid auto qtapp = std::make_shared(); auto window = std::make_shared("openage pathfinding test", 1024, 768); From de9dc91abbd3b60a2332374ddbb9481dc6e8edcd Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 31 Mar 2024 17:10:25 +0200 Subject: [PATCH 369/771] path: Remove unnecessary parameter. --- libopenage/pathfinding/pathfinder.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 5d1e255d84..22fa2b0534 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -190,7 +190,6 @@ std::vector PortalNode::generate_backtrace() { node_t current = this->shared_from_this(); do { - node_t other = current; waypoints.push_back(current); current = current->prev_portal; } From 46a544f427b54a3725d7874144fdbf8be2d721df Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 1 Apr 2024 23:48:48 +0200 Subject: [PATCH 370/771] path: Remove cost field storage from integrator. --- libopenage/pathfinding/integrator.cpp | 15 ++++++--------- libopenage/pathfinding/integrator.h | 16 ++++------------ libopenage/pathfinding/tests.cpp | 3 +-- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 3e9ecbe902..9c5d6020e0 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -9,16 +9,13 @@ namespace openage::path { -void Integrator::set_cost_field(const std::shared_ptr &cost_field) { - this->cost_field = cost_field; -} - -std::shared_ptr Integrator::build(const coord::tile &target) { - auto flow_field = std::make_shared(this->cost_field->get_size()); - auto integrate_field = std::make_shared(this->cost_field->get_size()); +std::shared_ptr Integrator::build(const std::shared_ptr &cost_field, + const coord::tile &target) { + auto flow_field = std::make_shared(cost_field->get_size()); + auto integrate_field = std::make_shared(cost_field->get_size()); - auto wavefront_blocked = integrate_field->integrate_los(this->cost_field, target); - integrate_field->integrate_cost(this->cost_field, std::move(wavefront_blocked)); + auto wavefront_blocked = integrate_field->integrate_los(cost_field, target); + integrate_field->integrate_cost(cost_field, std::move(wavefront_blocked)); flow_field->build(integrate_field); return flow_field; diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index fc0d433c1d..2af2bf1e0c 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -24,26 +24,18 @@ class Integrator { ~Integrator() = default; /** - * Set the cost field. + * Build the flow field for a target. * * @param cost_field Cost field. - */ - void set_cost_field(const std::shared_ptr &cost_field); - - /** - * Build the flow field for a target cell. - * * @param target Coordinates of the target cell. * * @return Flow field. */ - std::shared_ptr build(const coord::tile &target); + std::shared_ptr build(const std::shared_ptr &cost_field, + const coord::tile &target); private: - /** - * Cost field. - */ - std::shared_ptr cost_field; + // TODO: Cache field results. }; } // namespace path diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index ab62d2ff2f..ea30a3d2a5 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -83,10 +83,9 @@ void flow_field() { { // Integrator for managing the flow field auto integrator = std::make_shared(); - integrator->set_cost_field(cost_field); // Build the flow field - auto flow_field = integrator->build(coord::tile{2, 2}); + auto flow_field = integrator->build(cost_field, coord::tile{2, 2}); auto ff_cells = flow_field->get_cells(); // The flow field for targeting (2, 2) hould look like this: From 481465974fe81a1dc5ff68584384058ed59d9e46 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 1 Apr 2024 23:54:05 +0200 Subject: [PATCH 371/771] path: Use legacy namespace for old code. --- libopenage/pathfinding/legacy/a_star.cpp | 4 +- libopenage/pathfinding/legacy/a_star.h | 4 +- libopenage/pathfinding/legacy/heuristics.cpp | 5 +- libopenage/pathfinding/legacy/heuristics.h | 4 +- libopenage/pathfinding/legacy/path.cpp | 4 +- libopenage/pathfinding/legacy/path.h | 10 +-- libopenage/pathfinding/legacy/path_utils.h | 2 +- libopenage/pathfinding/legacy/tests.cpp | 64 ++++++++++---------- 8 files changed, 49 insertions(+), 48 deletions(-) diff --git a/libopenage/pathfinding/legacy/a_star.cpp b/libopenage/pathfinding/legacy/a_star.cpp index 640a20bb32..afeabeeefd 100644 --- a/libopenage/pathfinding/legacy/a_star.cpp +++ b/libopenage/pathfinding/legacy/a_star.cpp @@ -22,7 +22,7 @@ namespace openage { -namespace path { +namespace path::legacy { Path to_point(coord::phys3 start, @@ -128,5 +128,5 @@ Path a_star(coord::phys3 start, } -} // namespace path +} // namespace path::legacy } // namespace openage diff --git a/libopenage/pathfinding/legacy/a_star.h b/libopenage/pathfinding/legacy/a_star.h index 24e09fee2e..a2704bb5f1 100644 --- a/libopenage/pathfinding/legacy/a_star.h +++ b/libopenage/pathfinding/legacy/a_star.h @@ -9,7 +9,7 @@ namespace openage { -namespace path { +namespace path::legacy { /** * path between two static points @@ -38,5 +38,5 @@ Path a_star(coord::phys3 start, std::function heuristic, std::function passable); -} // namespace path +} // namespace path::legacy } // namespace openage diff --git a/libopenage/pathfinding/legacy/heuristics.cpp b/libopenage/pathfinding/legacy/heuristics.cpp index 51a24618a0..e064fa1ad7 100644 --- a/libopenage/pathfinding/legacy/heuristics.cpp +++ b/libopenage/pathfinding/legacy/heuristics.cpp @@ -7,7 +7,7 @@ namespace openage { -namespace path { +namespace path::legacy { cost_old_t manhattan_cost(const coord::phys3 &start, const coord::phys3 &end) { cost_old_t dx = std::abs(start.ne - end.ne).to_float(); @@ -31,4 +31,5 @@ cost_old_t euclidean_squared_cost(const coord::phys3 &start, const coord::phys3 return dx * dx + dy * dy; } -}} // openage::path +} // namespace path::legacy +} // namespace openage diff --git a/libopenage/pathfinding/legacy/heuristics.h b/libopenage/pathfinding/legacy/heuristics.h index e9e9a03709..77707fbc9f 100644 --- a/libopenage/pathfinding/legacy/heuristics.h +++ b/libopenage/pathfinding/legacy/heuristics.h @@ -5,7 +5,7 @@ #include "pathfinding/legacy/path.h" namespace openage { -namespace path { +namespace path::legacy { /** * function pointer type for distance estimation functions. @@ -41,5 +41,5 @@ cost_old_t euclidean_squared_cost(const coord::phys3 &start, const coord::phys3 */ cost_old_t euclidean_squared_to_euclidean_cost(const cost_old_t euclidean_squared_value); -} // namespace path +} // namespace path::legacy } // namespace openage diff --git a/libopenage/pathfinding/legacy/path.cpp b/libopenage/pathfinding/legacy/path.cpp index 6db3c528f9..dafb63e686 100644 --- a/libopenage/pathfinding/legacy/path.cpp +++ b/libopenage/pathfinding/legacy/path.cpp @@ -4,7 +4,7 @@ #include "pathfinding/legacy/path.h" -namespace openage::path { +namespace openage::path::legacy { bool compare_node_cost::operator()(const node_pt &lhs, const node_pt &rhs) const { @@ -112,4 +112,4 @@ Path::Path(const std::vector &nodes) : waypoints{nodes} {} -} // namespace openage::path +} // namespace openage::path::legacy diff --git a/libopenage/pathfinding/legacy/path.h b/libopenage/pathfinding/legacy/path.h index a6d7aa80ae..59851ecd51 100644 --- a/libopenage/pathfinding/legacy/path.h +++ b/libopenage/pathfinding/legacy/path.h @@ -16,7 +16,7 @@ namespace openage { -namespace path { +namespace path::legacy { class Node; class Path; @@ -185,7 +185,7 @@ class Path { std::vector waypoints; }; -} // namespace path +} // namespace path::legacy } // namespace openage @@ -196,10 +196,10 @@ namespace std { * Just uses their position. */ template <> -struct hash { - size_t operator()(const openage::path::Node &x) const { +struct hash { + size_t operator()(const openage::path::legacy::Node &x) const { openage::coord::phys3 node_pos = x.position; - size_t hash = openage::util::type_hash(); + size_t hash = openage::util::type_hash(); hash = openage::util::hash_combine(hash, std::hash{}(node_pos.ne)); hash = openage::util::hash_combine(hash, std::hash{}(node_pos.se)); return hash; diff --git a/libopenage/pathfinding/legacy/path_utils.h b/libopenage/pathfinding/legacy/path_utils.h index de1aa99da8..2c87923ad4 100644 --- a/libopenage/pathfinding/legacy/path_utils.h +++ b/libopenage/pathfinding/legacy/path_utils.h @@ -3,7 +3,7 @@ #pragma once namespace openage { -namespace path { +namespace path::legacy { } // namespace path } // namespace openage diff --git a/libopenage/pathfinding/legacy/tests.cpp b/libopenage/pathfinding/legacy/tests.cpp index f68e512660..b160eff91a 100644 --- a/libopenage/pathfinding/legacy/tests.cpp +++ b/libopenage/pathfinding/legacy/tests.cpp @@ -23,11 +23,11 @@ void node_0() { coord::phys3 p5{2, 2, 0}; coord::phys3 p6{2, -2, 0}; - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, n0); - node_pt n2 = std::make_unique(p2, n1); - node_pt n3 = std::make_unique(p3, n1); - node_pt n4 = std::make_unique(p0, n1); + legacy::node_pt n0 = std::make_unique(p0, nullptr); + legacy::node_pt n1 = std::make_unique(p1, n0); + legacy::node_pt n2 = std::make_unique(p2, n1); + legacy::node_pt n3 = std::make_unique(p3, n1); + legacy::node_pt n4 = std::make_unique(p0, n1); // Testing how the factor is effected from the change in // direction from one node to another @@ -58,10 +58,10 @@ void node_0() { // Testing that the distance from the previous node noes not // effect the factor, only change in direction - n1 = std::make_unique(p4, n0); - n2 = std::make_unique(p5, n1); - n3 = std::make_unique(p6, n1); - n4 = std::make_unique(p0, n1); + n1 = std::make_unique(p4, n0); + n2 = std::make_unique(p5, n1); + n3 = std::make_unique(p6, n1); + n4 = std::make_unique(p0, n1); TESTEQUALS(n1->direction.ne, 1); TESTEQUALS(n1->direction.se, 0); @@ -95,8 +95,8 @@ void node_cost_to_0() { coord::phys3 p0{0, 0, 0}; coord::phys3 p1{10, 0, 0}; - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, nullptr); + legacy::node_pt n0 = std::make_unique(p0, nullptr); + legacy::node_pt n1 = std::make_unique(p1, nullptr); TESTEQUALS(n0->cost_to(*n1), 10); TESTEQUALS(n1->cost_to(*n0), 10); @@ -104,7 +104,7 @@ void node_cost_to_0() { // Testing basic cost_to with se only coord::phys3 p2{0, 5, 0}; - node_pt n2 = std::make_unique(p2, nullptr); + legacy::node_pt n2 = std::make_unique(p2, nullptr); TESTEQUALS(n0->cost_to(*n2), 5); TESTEQUALS(n2->cost_to(*n0), 5); @@ -112,14 +112,14 @@ void node_cost_to_0() { // Testing cost_to with both se and ne: coord::phys3 p3{3, 4, 0}; // -> sqrt(3*3 + 4*4) == 5 - node_pt n3 = std::make_unique(p3, nullptr); + legacy::node_pt n3 = std::make_unique(p3, nullptr); TESTEQUALS(n0->cost_to(*n3), 5); TESTEQUALS(n3->cost_to(*n0), 5); // Test cost_to and check that `up` has no effect coord::phys3 p4{3, 4, 8}; - node_pt n4 = std::make_unique(p4, nullptr); + legacy::node_pt n4 = std::make_unique(p4, nullptr); TESTEQUALS(n0->cost_to(*n4), 5); TESTEQUALS(n4->cost_to(*n0), 5); @@ -135,20 +135,20 @@ void node_cost_to_1() { coord::phys3 p0{-0.125, 0, 0}; coord::phys3 p1{0.125, 0, 0}; - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, n0); + legacy::node_pt n0 = std::make_unique(p0, nullptr); + legacy::node_pt n1 = std::make_unique(p1, n0); // We expect twice the normal cost since n0 had not direction // thus we get a factor of 2 on n1 TESTEQUALS_FLOAT(n0->cost_to(*n1), 0.5, 0.001); TESTEQUALS_FLOAT(n1->cost_to(*n0), 0.5, 0.001); - nodemap_t visited_tiles; + legacy::nodemap_t visited_tiles; visited_tiles[n0->position] = n0; // Collect the costs to go to all the neighbors of n1 std::vector costs; - for (node_pt neighbor : n1->get_neighbors(visited_tiles, 1)) { + for (legacy::node_pt neighbor : n1->get_neighbors(visited_tiles, 1)) { costs.push_back(n1->cost_to(*neighbor)); } @@ -172,12 +172,12 @@ void node_generate_backtrace_0() { coord::phys3 p2{20, 0, 0}; coord::phys3 p3{30, 0, 0}; - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, n0); - node_pt n2 = std::make_unique(p2, n1); - node_pt n3 = std::make_unique(p3, n2); + legacy::node_pt n0 = std::make_unique(p0, nullptr); + legacy::node_pt n1 = std::make_unique(p1, n0); + legacy::node_pt n2 = std::make_unique(p2, n1); + legacy::node_pt n3 = std::make_unique(p3, n2); - Path path = n3->generate_backtrace(); + legacy::Path path = n3->generate_backtrace(); (path.waypoints[0] == *n3) or TESTFAIL; (path.waypoints[1] == *n2) or TESTFAIL; @@ -191,13 +191,13 @@ void node_generate_backtrace_0() { void node_get_neighbors_0() { coord::phys3 p0{0, 0, 0}; - node_pt n0 = std::make_unique(p0, nullptr); - nodemap_t map; + legacy::node_pt n0 = std::make_unique(p0, nullptr); + legacy::nodemap_t map; // Testing get_neighbors returning all surounding tiles with // a factor of 1 - std::vector neighbors = n0->get_neighbors(map, 1); + std::vector neighbors = n0->get_neighbors(map, 1); TESTEQUALS(neighbors.size(), 8); TESTEQUALS_FLOAT(neighbors[0]->position.ne.to_double(), 0.125, 0.001); @@ -288,16 +288,16 @@ void node_passable_line_0() { coord::phys3 p0{0, 0, 0}; coord::phys3 p1{1000, 0, 0}; - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, n0); + legacy::node_pt n0 = std::make_unique(p0, nullptr); + legacy::node_pt n1 = std::make_unique(p1, n0); - TESTEQUALS(path::passable_line(n0, n1, path::tests::always_passable), true); - TESTEQUALS(path::passable_line(n0, n1, path::tests::not_passable), false); + TESTEQUALS(path::legacy::passable_line(n0, n1, path::tests::always_passable), true); + TESTEQUALS(path::legacy::passable_line(n0, n1, path::tests::not_passable), false); // The next 2 cases show that a different sample can change the results // for the same path - TESTEQUALS(path::passable_line(n0, n1, path::tests::sometimes_passable, 10), true); - TESTEQUALS(path::passable_line(n0, n1, path::tests::sometimes_passable, 50), false); + TESTEQUALS(path::legacy::passable_line(n0, n1, path::tests::sometimes_passable, 10), true); + TESTEQUALS(path::legacy::passable_line(n0, n1, path::tests::sometimes_passable, 50), false); } /** From ba505e9755be6a6bff200227ec1f085a76c01558 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 2 Apr 2024 23:39:03 +0200 Subject: [PATCH 372/771] path: Integrate cost via portal. --- libopenage/pathfinding/integration_field.cpp | 46 ++++++++++++++++++++ libopenage/pathfinding/integration_field.h | 20 ++++++++- libopenage/pathfinding/pathfinder.cpp | 17 ++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 9881ec0f3c..9ff6e0854a 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -10,6 +10,7 @@ #include "coord/tile.h" #include "pathfinding/cost_field.h" #include "pathfinding/definitions.h" +#include "pathfinding/portal.h" namespace openage::path { @@ -163,6 +164,51 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie this->integrate_cost(cost_field, {target_idx}); } +void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal) { + ENSURE(cost_field->get_size() == this->get_size(), + "cost field size " + << cost_field->get_size() << "x" << cost_field->get_size() + << " does not match integration field size " + << this->get_size() << "x" << this->get_size()); + ENSURE(other->get_size() == this->get_size(), + "other integration field size " + << other->get_size() << "x" << other->get_size() + << " does not match integration field size " + << this->get_size() << "x" << this->get_size()); + + // Get the cost of the cells on the entry side (other) of the portal + std::vector other_cells; + auto other_start = portal->get_entry_start(other_sector_id); + auto other_end = portal->get_entry_end(other_sector_id); + for (auto y = other_start.se; y <= other_end.se; ++y) { + for (auto x = other_start.ne; x <= other_end.ne; ++x) { + other_cells.push_back(x + y * this->size); + } + } + + // Integrate the cost of the cells on the exit side (this) of the portal + std::vector start_cells; + auto exit_start = portal->get_exit_start(other_sector_id); + auto exit_end = portal->get_exit_end(other_sector_id); + for (auto y = exit_start.se; y <= exit_end.se; ++y) { + for (auto x = exit_start.ne; x <= exit_end.ne; ++x) { + auto idx = x + y * this->size; + start_cells.push_back(idx); + + // Integrate the cells on this side of the portal + this->cells[idx].cost = other->get_cell(x + y * this->size).cost + cost_field->get_cost(idx); + + // TODO: Transfer flags from other to this + } + } + + // Integrate the rest of the cost field + this->integrate_cost(cost_field, std::move(start_cells)); +} + void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, std::vector &&start_cells) { // Lookup table for cells that are in the open list diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 3e01275d9d..02bce34b31 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -18,6 +18,7 @@ struct tile; namespace path { class CostField; +class Portal; /** * Integration field in the flow-field pathfinding algorithm. @@ -73,11 +74,26 @@ class IntegrationField { * * @param cost_field Cost field to integrate. * @param target Coordinates of the target cell. - * @param start_cells Cells flagged as "wavefront blocked" from a LOS pass. */ void integrate_cost(const std::shared_ptr &cost_field, const coord::tile &target); + /** + * Calculate the cost integration field starting from a portal to another + * integration field. + * + * The other integration field must already be integrated. + * + * @param cost_field Cost field to integrate. + * @param other Other integration field. + * @param other_sector_id Sector ID of the other integration field. + * @param portal Portal connecting the two fields. + */ + void integrate_cost(const std::shared_ptr &cost_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal); + /** * Calculate the cost integration field starting from a wavefront. * @@ -85,7 +101,7 @@ class IntegrationField { * @param start_cells Cells flagged as "wavefront blocked" from a LOS pass. */ void integrate_cost(const std::shared_ptr &cost_field, - std::vector &&start_cells = {}); + std::vector &&start_cells); /** * Get the integration field values. diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 22fa2b0534..a70392f054 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -17,8 +17,25 @@ Pathfinder::Pathfinder() : } const Path Pathfinder::get_path(PathRequest &request) { + // High-level pathfinding + // Find the portals to use to get from the start to the target auto portal_path = this->portal_a_star(request); + // Low-level pathfinding + // Find the path within the sectors + auto grid = this->grids.at(request.grid_id); + auto sector_size = grid->get_sector_size(); + + auto target_sector_x = request.target.ne / sector_size; + auto target_sector_y = request.target.se / sector_size; + auto target_sector = grid->get_sector(target_sector_x, target_sector_y); + + auto target_x = request.target.ne % sector_size; + auto target_y = request.target.se % sector_size; + auto target = coord::tile{target_x, target_y}; + + auto flow_field = this->integrator->build(target_sector->get_cost_field(), target); + // TODO: Implement the rest of the pathfinding process return Path{}; } From e701d8c636d543e81daed9fd80785013bd64e2ed Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 2 Apr 2024 23:43:53 +0200 Subject: [PATCH 373/771] path: More fine-grained control in integrator. --- libopenage/pathfinding/integrator.cpp | 37 +++++++++++++++++---- libopenage/pathfinding/integrator.h | 48 ++++++++++++++++++++++++--- libopenage/pathfinding/tests.cpp | 2 +- 3 files changed, 74 insertions(+), 13 deletions(-) diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 9c5d6020e0..2d773bf735 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -9,16 +9,39 @@ namespace openage::path { -std::shared_ptr Integrator::build(const std::shared_ptr &cost_field, - const coord::tile &target) { - auto flow_field = std::make_shared(cost_field->get_size()); - auto integrate_field = std::make_shared(cost_field->get_size()); +std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field, + const coord::tile &target) { + auto integration_field = std::make_shared(cost_field->get_size()); + + auto wavefront_blocked = integration_field->integrate_los(cost_field, target); + integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); + + return integration_field; +} + +std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal) { + auto integration_field = std::make_shared(cost_field->get_size()); + integration_field->integrate_cost(cost_field, other, other_sector_id, portal); - auto wavefront_blocked = integrate_field->integrate_los(cost_field, target); - integrate_field->integrate_cost(cost_field, std::move(wavefront_blocked)); - flow_field->build(integrate_field); + return integration_field; +} + +std::shared_ptr Integrator::build(const std::shared_ptr &integration_field) { + auto flow_field = std::make_shared(integration_field->get_size()); + flow_field->build(integration_field); return flow_field; } +Integrator::build_return_t Integrator::build(const std::shared_ptr &cost_field, + const coord::tile &target) { + auto integration_field = this->integrate(cost_field, target); + auto flow_field = this->build(integration_field); + + return std::make_pair(integration_field, flow_field); +} + } // namespace openage::path diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index 2af2bf1e0c..807c25a13c 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -5,6 +5,8 @@ #include #include +#include "pathfinding/types.h" + namespace openage { namespace coord { @@ -14,6 +16,8 @@ struct tile; namespace path { class CostField; class FlowField; +class IntegrationField; +class Portal; /** * Integrator for the flow field pathfinding algorithm. @@ -24,18 +28,52 @@ class Integrator { ~Integrator() = default; /** - * Build the flow field for a target. + * Integrate the cost field for a target. * * @param cost_field Cost field. * @param target Coordinates of the target cell. * + * @return Integration field. + */ + std::shared_ptr integrate(const std::shared_ptr &cost_field, + const coord::tile &target); + + /** + * Integrate the cost field from a portal. + * + * @param cost_field Cost field. + * @param other Integration field from the other side of the portal. + * @param other_sector_id Sector ID of the other side of the portal. + * @param portal Portal. + * + * @return Integration field. + */ + std::shared_ptr integrate(const std::shared_ptr &cost_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal); + + /** + * Build the flow field from an integration field. + * + * @param integration_field Integration field. + * * @return Flow field. */ - std::shared_ptr build(const std::shared_ptr &cost_field, - const coord::tile &target); + std::shared_ptr build(const std::shared_ptr &integration_field); + + using build_return_t = std::pair, std::shared_ptr>; -private: - // TODO: Cache field results. + /** + * Build the flow field for a target. + * + * @param cost_field Cost field. + * @param target Coordinates of the target cell. + * + * @return Flow field. + */ + build_return_t build(const std::shared_ptr &cost_field, + const coord::tile &target); }; } // namespace path diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index ea30a3d2a5..2c9775ca36 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -85,7 +85,7 @@ void flow_field() { auto integrator = std::make_shared(); // Build the flow field - auto flow_field = integrator->build(cost_field, coord::tile{2, 2}); + auto flow_field = integrator->build(cost_field, coord::tile{2, 2}).second; auto ff_cells = flow_field->get_cells(); // The flow field for targeting (2, 2) hould look like this: From 1466aa1c2412f9afc37ff7ce24d05793cb9dcce2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 3 Apr 2024 21:59:33 +0200 Subject: [PATCH 374/771] path: Build flow field via portal. (partial) --- libopenage/pathfinding/flow_field.cpp | 38 ++++++++++++++++++++++----- libopenage/pathfinding/flow_field.h | 14 ++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 96c1a0cd56..a85a4d6769 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -8,6 +8,7 @@ #include "coord/tile.h" #include "pathfinding/definitions.h" #include "pathfinding/integration_field.h" +#include "pathfinding/portal.h" namespace openage::path { @@ -18,10 +19,10 @@ FlowField::FlowField(size_t size) : log::log(DBG << "Created flow field with size " << this->size << "x" << this->size); } -FlowField::FlowField(const std::shared_ptr &integrate_field) : - size{integrate_field->get_size()}, +FlowField::FlowField(const std::shared_ptr &integration_field) : + size{integration_field->get_size()}, cells(this->size * this->size, FLOW_INIT) { - this->build(integrate_field); + this->build(integration_field); } size_t FlowField::get_size() const { @@ -36,14 +37,14 @@ flow_dir_t FlowField::get_dir(const coord::tile &pos) const { return static_cast(this->get_cell(pos) & FLOW_DIR_MASK); } -void FlowField::build(const std::shared_ptr &integrate_field) { - ENSURE(integrate_field->get_size() == this->get_size(), +void FlowField::build(const std::shared_ptr &integration_field) { + ENSURE(integration_field->get_size() == this->get_size(), "integration field size " - << integrate_field->get_size() << "x" << integrate_field->get_size() + << integration_field->get_size() << "x" << integration_field->get_size() << " does not match flow field size " << this->get_size() << "x" << this->get_size()); - auto &integrate_cells = integrate_field->get_cells(); + auto &integrate_cells = integration_field->get_cells(); auto &flow_cells = this->cells; for (size_t y = 0; y < this->size; ++y) { @@ -139,6 +140,29 @@ void FlowField::build(const std::shared_ptr &integrate_field) } } +void FlowField::build(const std::shared_ptr &integration_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal) { + ENSURE(integration_field->get_size() == this->get_size(), + "integration field size " + << integration_field->get_size() << "x" << integration_field->get_size() + << " does not match flow field size " + << this->get_size() << "x" << this->get_size()); + + auto &integrate_cells = integration_field->get_cells(); + auto &flow_cells = this->cells; + auto direction = portal->get_direction(); + + if (direction == PortalDirection::NORTH_SOUTH) { + } + else if (direction == PortalDirection::EAST_WEST) { + } + else { + throw Error(ERR << "Invalid portal direction"); + } +} + const std::vector &FlowField::get_cells() const { return this->cells; } diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index 8389b2a0f7..32ebdee6b1 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -17,6 +17,7 @@ struct tile; namespace path { class IntegrationField; +class Portal; class FlowField { public: @@ -66,6 +67,19 @@ class FlowField { */ void build(const std::shared_ptr &integrate_field); + /** + * Build the flow field for a portal. + * + * @param integrate_field Integration field. + * @param other Other integration field. + * @param other_sector_id Sector ID of the other field. + * @param portal Portal connecting the two fields. + */ + void build(const std::shared_ptr &integrate_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal); + /** * Get the flow field values. * From b74d19cd1ecaca40e4997388a4c98f98e4cb8a41 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 7 Apr 2024 20:26:07 +0200 Subject: [PATCH 375/771] etc: Pretty printers for path::flow_t and path::integrated_flags_t. --- etc/gdb_pretty/printers.py | 84 +++++++++++++++++++++++++++ libopenage/pathfinding/flow_field.cpp | 1 - libopenage/pathfinding/types.h | 8 +-- 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index ca8f92083c..cdf1e15858 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -279,6 +279,90 @@ def children(self): yield ('time', self.__val['time']) yield ('value', self.__val['value']) + +@printer_typedef('openage::path::flow_t') +class PathFlowTypePrinter: + """ + Pretty printer for openage::path::flow_t. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + FLOW_FLAGS = { + 0x10: 'PATHABLE', + 0x20: 'LOS', + 0x40: 'WAVEFRONT_BLOCKED', + 0x80: 'UNUSED', + } + + FLOW_DIRECTION = { + 0x00: 'NORTH', + 0x01: 'NORTHEAST', + 0x02: 'EAST', + 0x03: 'SOUTHEAST', + 0x04: 'SOUTH', + 0x05: 'SOUTHWEST', + 0x06: 'WEST', + 0x07: 'NORTHWEST', + } + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the flow type as a string. + """ + flow = int(self.__val) + flags = flow & 0xF0 + direction = flow & 0x0F + return (f"{self.FLOW_DIRECTION.get(direction, 'INVALID')} | (" + f"{', '.join([self.FLOW_FLAGS[f] for f in self.FLOW_FLAGS if f & flags])})") + + def children(self): + """ + Get the displayed children of the flow type. + """ + flow = int(self.__val) + flags = flow & 0xF0 + direction = flow & 0x0F + yield ('direction', self.FLOW_DIRECTION[direction]) + for mask, flag in self.FLOW_FLAGS.items(): + yield (flag, bool(flags & mask)) + + +@printer_typedef('openage::path::integrated_flags_t') +class PathIntegratedFlagsTypePrinter: + """ + Pretty printer for openage::path::integrated_flags_t. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + INTEGRATED_FLAGS = { + 0x01: 'LOS', + 0x02: 'WAVEFRONT_BLOCKED', + } + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the integrate type as a string. + """ + integrate = int(self.__val) + return f"{', '.join([self.INTEGRATED_FLAGS[f] for f in self.INTEGRATED_FLAGS if f & integrate])}" + + def children(self): + """ + Get the displayed children of the integrate type. + """ + integrate = int(self.__val) + for mask, flag in self.INTEGRATED_FLAGS.items(): + yield (flag, bool(integrate & mask)) + + # TODO: curve types # TODO: pathfinding types # TODO: input event codes diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index a85a4d6769..1eee7132f5 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -6,7 +6,6 @@ #include "log/log.h" #include "coord/tile.h" -#include "pathfinding/definitions.h" #include "pathfinding/integration_field.h" #include "pathfinding/portal.h" diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index 6ed735e896..57a52b9783 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -48,10 +48,10 @@ struct integrate_t { /** * Flow field cell value. * - * Bit 0: Line of sight flag. - * Bit 1: Pathable flag. - * Bit 2: Wavefront blocked flag. - * Bit 3: Unused. + * Bit 0: Unused. + * Bit 1: Wave front blocked flag. + * Bit 2: Line of sight flag. + * Bit 3: Pathable flag. * Bits 4-7: flow direction. */ using flow_t = uint8_t; From 555d19934e291315c4691f2f73e66da78755d107 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 7 Apr 2024 20:31:33 +0200 Subject: [PATCH 376/771] patg: Rename integrate_t to integrated_t. --- libopenage/pathfinding/definitions.h | 2 +- libopenage/pathfinding/integration_field.cpp | 6 +++--- libopenage/pathfinding/integration_field.h | 8 ++++---- libopenage/pathfinding/types.h | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h index 3d8584616b..cd12b6fe10 100644 --- a/libopenage/pathfinding/definitions.h +++ b/libopenage/pathfinding/definitions.h @@ -55,7 +55,7 @@ constexpr integrated_flags_t INTEGRATE_WAVEFRONT_BLOCKED_MASK = 0x02; /** * Initial value for a cell in the integration grid. */ -constexpr integrate_t INTEGRATE_INIT = {INTEGRATED_COST_UNREACHABLE, 0}; +constexpr integrated_t INTEGRATE_INIT = {INTEGRATED_COST_UNREACHABLE, 0}; /** diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 9ff6e0854a..7e03bde8d8 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -25,11 +25,11 @@ size_t IntegrationField::get_size() const { return this->size; } -const integrate_t &IntegrationField::get_cell(const coord::tile &pos) const { +const integrated_t &IntegrationField::get_cell(const coord::tile &pos) const { return this->cells.at(pos.ne + pos.se * this->size); } -const integrate_t &IntegrationField::get_cell(size_t idx) const { +const integrated_t &IntegrationField::get_cell(size_t idx) const { return this->cells.at(idx); } @@ -266,7 +266,7 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie } } -const std::vector &IntegrationField::get_cells() const { +const std::vector &IntegrationField::get_cells() const { return this->cells; } diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 02bce34b31..07f3ece839 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -45,7 +45,7 @@ class IntegrationField { * @param pos Coordinates of the cell. * @return Integration value at the specified position. */ - const integrate_t &get_cell(const coord::tile &pos) const; + const integrated_t &get_cell(const coord::tile &pos) const; /** * Get the integration value at a specified position. @@ -53,7 +53,7 @@ class IntegrationField { * @param idx Index of the cell. * @return Integration value at the specified position. */ - const integrate_t &get_cell(size_t idx) const; + const integrated_t &get_cell(size_t idx) const; /** * Calculate the line-of-sight integration flags for a target cell. @@ -108,7 +108,7 @@ class IntegrationField { * * @return Integration field values. */ - const std::vector &get_cells() const; + const std::vector &get_cells() const; /** * Reset the integration field for a new integration. @@ -172,7 +172,7 @@ class IntegrationField { /** * Integration field values. */ - std::vector cells; + std::vector cells; }; } // namespace path diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index 57a52b9783..f543f269a4 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -30,7 +30,7 @@ using integrated_flags_t = uint8_t; /** * Integration field cell value. */ -struct integrate_t { +struct integrated_t { /** * Total integrated cost. */ From 6e19321b8bba126a2b4e20aa77941f851ddaec55 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 7 Apr 2024 21:08:07 +0200 Subject: [PATCH 377/771] etc: Pretty printer for path::integrated_t. --- etc/gdb_pretty/printers.py | 64 ++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index cdf1e15858..53dd9cb8ef 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -288,14 +288,14 @@ class PathFlowTypePrinter: TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. """ - FLOW_FLAGS = { + FLOW_FLAGS: dict = { 0x10: 'PATHABLE', 0x20: 'LOS', 0x40: 'WAVEFRONT_BLOCKED', 0x80: 'UNUSED', } - FLOW_DIRECTION = { + FLOW_DIRECTION: dict = { 0x00: 'NORTH', 0x01: 'NORTHEAST', 0x02: 'EAST', @@ -316,7 +316,7 @@ def to_string(self): flow = int(self.__val) flags = flow & 0xF0 direction = flow & 0x0F - return (f"{self.FLOW_DIRECTION.get(direction, 'INVALID')} | (" + return (f"{self.FLOW_DIRECTION.get(direction, 'INVALID')} (" f"{', '.join([self.FLOW_FLAGS[f] for f in self.FLOW_FLAGS if f & flags])})") def children(self): @@ -331,6 +331,28 @@ def children(self): yield (flag, bool(flags & mask)) +# Integrated flags +INTEGRATED_FLAGS: dict = { + 0x01: 'LOS', + 0x02: 'WAVEFRONT_BLOCKED', +} + + +def get_integrated_flags_list(value: int) -> str: + """ + Get the list of flags as a string. + + :param value: The value to get the flags for. + :type value: int + """ + flags = [] + for mask, flag in INTEGRATED_FLAGS.items(): + if value & mask: + flags.append(flag) + + return ' | '.join(flags) + + @printer_typedef('openage::path::integrated_flags_t') class PathIntegratedFlagsTypePrinter: """ @@ -339,11 +361,6 @@ class PathIntegratedFlagsTypePrinter: TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. """ - INTEGRATED_FLAGS = { - 0x01: 'LOS', - 0x02: 'WAVEFRONT_BLOCKED', - } - def __init__(self, val: gdb.Value): self.__val = val @@ -352,7 +369,7 @@ def to_string(self): Get the integrate type as a string. """ integrate = int(self.__val) - return f"{', '.join([self.INTEGRATED_FLAGS[f] for f in self.INTEGRATED_FLAGS if f & integrate])}" + return get_integrated_flags_list(integrate) def children(self): """ @@ -363,6 +380,35 @@ def children(self): yield (flag, bool(integrate & mask)) +@printer_typedef('openage::path::integrated_t') +class PathIntegratedTypePrinter: + """ + Pretty printer for openage::path::integrated_t. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the integrate type as a string. + """ + output_str = f'cost = {self.__val["cost"]}' + flags = get_integrated_flags_list(int(self.__val['flags'])) + if len(flags) > 0: + output_str += f' ({flags})' + return output_str + + def children(self): + """ + Get the displayed children of the integrate type. + """ + yield ('cost', self.__val['cost']) + yield ('flags', self.__val['flags']) + + # TODO: curve types # TODO: pathfinding types # TODO: input event codes From 5c708eb336098f05fc985533889780011dd9d373 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 8 Apr 2024 02:14:03 +0200 Subject: [PATCH 378/771] path: Don't use other integration field. Unnecessary because the flow fields will be the same. --- libopenage/pathfinding/integration_field.cpp | 24 +------------------- libopenage/pathfinding/integration_field.h | 4 ---- libopenage/pathfinding/integrator.cpp | 3 +-- libopenage/pathfinding/integrator.h | 2 -- libopenage/pathfinding/pathfinder.cpp | 4 ++-- 5 files changed, 4 insertions(+), 33 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 7e03bde8d8..5824bdd1e4 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -165,7 +165,6 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie } void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, - const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal) { ENSURE(cost_field->get_size() == this->get_size(), @@ -173,21 +172,6 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie << cost_field->get_size() << "x" << cost_field->get_size() << " does not match integration field size " << this->get_size() << "x" << this->get_size()); - ENSURE(other->get_size() == this->get_size(), - "other integration field size " - << other->get_size() << "x" << other->get_size() - << " does not match integration field size " - << this->get_size() << "x" << this->get_size()); - - // Get the cost of the cells on the entry side (other) of the portal - std::vector other_cells; - auto other_start = portal->get_entry_start(other_sector_id); - auto other_end = portal->get_entry_end(other_sector_id); - for (auto y = other_start.se; y <= other_end.se; ++y) { - for (auto x = other_start.ne; x <= other_end.ne; ++x) { - other_cells.push_back(x + y * this->size); - } - } // Integrate the cost of the cells on the exit side (this) of the portal std::vector start_cells; @@ -195,13 +179,7 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie auto exit_end = portal->get_exit_end(other_sector_id); for (auto y = exit_start.se; y <= exit_end.se; ++y) { for (auto x = exit_start.ne; x <= exit_end.ne; ++x) { - auto idx = x + y * this->size; - start_cells.push_back(idx); - - // Integrate the cells on this side of the portal - this->cells[idx].cost = other->get_cell(x + y * this->size).cost + cost_field->get_cost(idx); - - // TODO: Transfer flags from other to this + start_cells.push_back(x + y * this->size); } } diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 07f3ece839..183fb9cff6 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -82,15 +82,11 @@ class IntegrationField { * Calculate the cost integration field starting from a portal to another * integration field. * - * The other integration field must already be integrated. - * * @param cost_field Cost field to integrate. - * @param other Other integration field. * @param other_sector_id Sector ID of the other integration field. * @param portal Portal connecting the two fields. */ void integrate_cost(const std::shared_ptr &cost_field, - const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal); diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 2d773bf735..61c8cd033a 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -20,11 +20,10 @@ std::shared_ptr Integrator::integrate(const std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field, - const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal) { auto integration_field = std::make_shared(cost_field->get_size()); - integration_field->integrate_cost(cost_field, other, other_sector_id, portal); + integration_field->integrate_cost(cost_field, other_sector_id, portal); return integration_field; } diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index 807c25a13c..3c2f401c47 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -42,14 +42,12 @@ class Integrator { * Integrate the cost field from a portal. * * @param cost_field Cost field. - * @param other Integration field from the other side of the portal. * @param other_sector_id Sector ID of the other side of the portal. * @param portal Portal. * * @return Integration field. */ std::shared_ptr integrate(const std::shared_ptr &cost_field, - const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal); diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index a70392f054..68b3bc80b8 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -30,8 +30,8 @@ const Path Pathfinder::get_path(PathRequest &request) { auto target_sector_y = request.target.se / sector_size; auto target_sector = grid->get_sector(target_sector_x, target_sector_y); - auto target_x = request.target.ne % sector_size; - auto target_y = request.target.se % sector_size; + coord::tile_t target_x = request.target.ne % sector_size; + coord::tile_t target_y = request.target.se % sector_size; auto target = coord::tile{target_x, target_y}; auto flow_field = this->integrator->build(target_sector->get_cost_field(), target); From 9b4e7fbbda3655dcd18b316939b7b2cd54281520 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 8 Apr 2024 02:23:18 +0200 Subject: [PATCH 379/771] etc: Fix pathfinding pretty printers flag lookups. --- etc/gdb_pretty/printers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index 53dd9cb8ef..0dfac99af5 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -317,7 +317,7 @@ def to_string(self): flags = flow & 0xF0 direction = flow & 0x0F return (f"{self.FLOW_DIRECTION.get(direction, 'INVALID')} (" - f"{', '.join([self.FLOW_FLAGS[f] for f in self.FLOW_FLAGS if f & flags])})") + f"{', '.join([flag for mask, flag in self.FLOW_FLAGS.items() if mask & flags])})") def children(self): """ @@ -376,7 +376,7 @@ def children(self): Get the displayed children of the integrate type. """ integrate = int(self.__val) - for mask, flag in self.INTEGRATED_FLAGS.items(): + for mask, flag in INTEGRATED_FLAGS.items(): yield (flag, bool(integrate & mask)) From 479e64710e53a06f5c3179ce633fd7a6506da2fd Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 8 Apr 2024 02:30:01 +0200 Subject: [PATCH 380/771] path: Exit high-level pathfinding early if start and target are in same sector. --- libopenage/pathfinding/pathfinder.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 68b3bc80b8..759eb3f2a1 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -62,6 +62,11 @@ const std::vector> Pathfinder::portal_a_star(PathRequest auto target_sector_y = request.target.se / sector_size; auto target_sector = grid->get_sector(target_sector_x, target_sector_y); + if (start_sector == target_sector) { + // exit early if the start and target are in the same sector + return result; + } + // path node storage, always provides cheapest next node. heap_t node_candidates; From ae2785db05dc6cd4abedc1a399ef7ee6eb58b231 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 9 Apr 2024 23:48:06 +0200 Subject: [PATCH 381/771] path: Correctly set starting cost for portal target. --- libopenage/pathfinding/integration_field.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 5824bdd1e4..a38b06e67a 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -179,7 +179,12 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie auto exit_end = portal->get_exit_end(other_sector_id); for (auto y = exit_start.se; y <= exit_end.se; ++y) { for (auto x = exit_start.ne; x <= exit_end.ne; ++x) { - start_cells.push_back(x + y * this->size); + // every portal cell is a target cell + auto target_idx = x + y * this->size; + + // Set the cost of all target cells to the start value + this->cells[target_idx].cost = INTEGRATED_COST_START; + start_cells.push_back(target_idx); } } From d93380ae7aa295dd8dc09fb9d18a731fe8bcf678 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 10 Apr 2024 00:36:20 +0200 Subject: [PATCH 382/771] path: Build flow field via portal. --- libopenage/pathfinding/flow_field.cpp | 52 ++++++++++++++++++++++++--- libopenage/pathfinding/flow_field.h | 14 ++++---- libopenage/pathfinding/integrator.cpp | 20 +++++++++++ libopenage/pathfinding/integrator.h | 30 +++++++++++++++- libopenage/pathfinding/pathfinder.cpp | 27 ++++++++++++-- 5 files changed, 129 insertions(+), 14 deletions(-) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 1eee7132f5..c649c13a29 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -71,7 +71,7 @@ void FlowField::build(const std::shared_ptr &integration_field } // Find the neighbor with the smallest cost. - flow_dir_t direction; + flow_dir_t direction = static_cast(this->cells[idx] & FLOW_DIR_MASK); auto smallest_cost = INTEGRATED_COST_UNREACHABLE; if (y > 0) { auto cost = integrate_cells[idx - this->size].cost; @@ -140,7 +140,7 @@ void FlowField::build(const std::shared_ptr &integration_field } void FlowField::build(const std::shared_ptr &integration_field, - const std::shared_ptr &other, + const std::shared_ptr & /* other */, sector_id_t other_sector_id, const std::shared_ptr &portal) { ENSURE(integration_field->get_size() == this->get_size(), @@ -149,17 +149,61 @@ void FlowField::build(const std::shared_ptr &integration_field << " does not match flow field size " << this->get_size() << "x" << this->get_size()); - auto &integrate_cells = integration_field->get_cells(); auto &flow_cells = this->cells; auto direction = portal->get_direction(); + // portal entry and exit cell coordinates + auto entry_start = portal->get_entry_start(other_sector_id); + auto entry_end = portal->get_entry_end(other_sector_id); + auto exit_start = portal->get_exit_start(other_sector_id); + + // TODO: Compare integration values from other side of portal + // auto &integrate_cells = integration_field->get_cells(); + + // set the direction for the flow field cells that are part of the portal if (direction == PortalDirection::NORTH_SOUTH) { + bool other_is_north = entry_start.se > exit_start.se; + if (other_is_north) { + auto y = entry_start.se; + for (auto x = entry_start.ne; x <= entry_end.ne; ++x) { + auto idx = y * this->size + x; + flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; + flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::NORTH); + } + } + else { + auto y = entry_start.se; + for (auto x = entry_start.ne; x <= entry_end.ne; ++x) { + auto idx = y * this->size + x; + flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; + flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::SOUTH); + } + } } else if (direction == PortalDirection::EAST_WEST) { + bool other_is_east = entry_start.ne > exit_start.ne; + if (other_is_east) { + auto x = entry_start.ne; + for (auto y = entry_start.se; y <= entry_end.se; ++y) { + auto idx = y * this->size + x; + flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; + flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::EAST); + } + } + else { + auto x = entry_start.ne; + for (auto y = entry_start.se; y <= entry_end.se; ++y) { + auto idx = y * this->size + x; + flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; + flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::WEST); + } + } } else { - throw Error(ERR << "Invalid portal direction"); + throw Error(ERR << "Invalid portal direction: " << static_cast(direction)); } + + this->build(integration_field); } const std::vector &FlowField::get_cells() const { diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index 32ebdee6b1..beb5bccf06 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -31,9 +31,9 @@ class FlowField { /** * Create a flow field from an existing integration field. * - * @param integrate_field Integration field. + * @param integration_field Integration field. */ - FlowField(const std::shared_ptr &integrate_field); + FlowField(const std::shared_ptr &integration_field); /** * Get the size of the flow field. @@ -63,19 +63,19 @@ class FlowField { /** * Build the flow field. * - * @param integrate_field Integration field. + * @param integration_field Integration field. */ - void build(const std::shared_ptr &integrate_field); + void build(const std::shared_ptr &integration_field); /** * Build the flow field for a portal. * - * @param integrate_field Integration field. - * @param other Other integration field. + * @param integration_field Integration field. + * @param other Integration field of the other sector. * @param other_sector_id Sector ID of the other field. * @param portal Portal connecting the two fields. */ - void build(const std::shared_ptr &integrate_field, + void build(const std::shared_ptr &integration_field, const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal); diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 61c8cd033a..28f8a829ca 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -35,6 +35,16 @@ std::shared_ptr Integrator::build(const std::shared_ptr Integrator::build(const std::shared_ptr &integration_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal) { + auto flow_field = std::make_shared(integration_field->get_size()); + flow_field->build(integration_field, other, other_sector_id, portal); + + return flow_field; +} + Integrator::build_return_t Integrator::build(const std::shared_ptr &cost_field, const coord::tile &target) { auto integration_field = this->integrate(cost_field, target); @@ -43,4 +53,14 @@ Integrator::build_return_t Integrator::build(const std::shared_ptr &c return std::make_pair(integration_field, flow_field); } +Integrator::build_return_t Integrator::build(const std::shared_ptr &cost_field, + const std::shared_ptr &other_integration_field, + sector_id_t other_sector_id, + const std::shared_ptr &portal) { + auto integration_field = this->integrate(cost_field, other_sector_id, portal); + auto flow_field = this->build(integration_field, other_integration_field, other_sector_id, portal); + + return std::make_pair(integration_field, flow_field); +} + } // namespace openage::path diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index 3c2f401c47..2b44d3f18f 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -60,10 +60,25 @@ class Integrator { */ std::shared_ptr build(const std::shared_ptr &integration_field); + /** + * Build the flow field from a portal. + * + * @param integration_field Integration field. + * @param other Integration field of the other side of the portal. + * @param other_sector_id Sector ID of the other side of the portal. + * @param portal Portal. + * + * @return Flow field. + */ + std::shared_ptr build(const std::shared_ptr &integration_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal); + using build_return_t = std::pair, std::shared_ptr>; /** - * Build the flow field for a target. + * Build the integration field and flow field for a target. * * @param cost_field Cost field. * @param target Coordinates of the target cell. @@ -72,6 +87,19 @@ class Integrator { */ build_return_t build(const std::shared_ptr &cost_field, const coord::tile &target); + + /** + * Build the integration field and flow field from a portal. + * + * @param cost_field Cost field. + * @param other_integration_field Integration field of the other side of the portal. + * @param other_sector_id Sector ID of the other side of the portal. + * @param portal Portal. + */ + build_return_t build(const std::shared_ptr &cost_field, + const std::shared_ptr &other_integration_field, + sector_id_t other_sector_id, + const std::shared_ptr &portal); }; } // namespace path diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 759eb3f2a1..b42f3471b2 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -34,10 +34,33 @@ const Path Pathfinder::get_path(PathRequest &request) { coord::tile_t target_y = request.target.se % sector_size; auto target = coord::tile{target_x, target_y}; - auto flow_field = this->integrator->build(target_sector->get_cost_field(), target); + auto sector_fields = this->integrator->build(target_sector->get_cost_field(), target); + auto prev_integration_field = sector_fields.first; + auto prev_sector_id = target_sector->get_id(); + + if (not portal_path.empty()) { + std::vector flow_fields; + flow_fields.reserve(portal_path.size() + 1); + + flow_fields.push_back(sector_fields); + for (auto &portal : portal_path) { + auto prev_sector = grid->get_sector(prev_sector_id); + auto next_sector_id = portal->get_exit_sector(prev_sector_id); + auto next_sector = grid->get_sector(next_sector_id); + + sector_fields = this->integrator->build(next_sector->get_cost_field(), + prev_integration_field, + prev_sector_id, + portal); + flow_fields.push_back(sector_fields); + + prev_integration_field = sector_fields.first; + prev_sector_id = next_sector_id; + } + } // TODO: Implement the rest of the pathfinding process - return Path{}; + return Path{request.grid_id, {}}; } const std::shared_ptr &Pathfinder::get_grid(grid_id_t id) const { From f7913587ed27db88634b1d553ce4bcc164601ae7 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Apr 2024 00:20:08 +0200 Subject: [PATCH 383/771] path: Exclude target cells from search in flow field. --- libopenage/pathfinding/flow_field.cpp | 37 ++++++++++++++++++--------- libopenage/pathfinding/flow_field.h | 6 ++++- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index c649c13a29..0aa8daf84b 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -36,7 +36,8 @@ flow_dir_t FlowField::get_dir(const coord::tile &pos) const { return static_cast(this->get_cell(pos) & FLOW_DIR_MASK); } -void FlowField::build(const std::shared_ptr &integration_field) { +void FlowField::build(const std::shared_ptr &integration_field, + const std::unordered_set &target_cells) { ENSURE(integration_field->get_size() == this->get_size(), "integration field size " << integration_field->get_size() << "x" << integration_field->get_size() @@ -50,6 +51,11 @@ void FlowField::build(const std::shared_ptr &integration_field for (size_t x = 0; x < this->size; ++x) { size_t idx = y * this->size + x; + if (target_cells.contains(idx)) { + // Ignore target cells + continue; + } + if (integrate_cells[idx].cost == INTEGRATED_COST_UNREACHABLE) { // Cell cannot be used as path continue; @@ -154,48 +160,55 @@ void FlowField::build(const std::shared_ptr &integration_field // portal entry and exit cell coordinates auto entry_start = portal->get_entry_start(other_sector_id); - auto entry_end = portal->get_entry_end(other_sector_id); auto exit_start = portal->get_exit_start(other_sector_id); + auto exit_end = portal->get_exit_end(other_sector_id); // TODO: Compare integration values from other side of portal // auto &integrate_cells = integration_field->get_cells(); + // cells that are part of the portal + std::unordered_set portal_cells; + // set the direction for the flow field cells that are part of the portal if (direction == PortalDirection::NORTH_SOUTH) { bool other_is_north = entry_start.se > exit_start.se; if (other_is_north) { - auto y = entry_start.se; - for (auto x = entry_start.ne; x <= entry_end.ne; ++x) { + auto y = exit_start.se; + for (auto x = exit_start.ne; x <= exit_end.ne; ++x) { auto idx = y * this->size + x; flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::NORTH); + portal_cells.insert(idx); } } else { - auto y = entry_start.se; - for (auto x = entry_start.ne; x <= entry_end.ne; ++x) { + auto y = exit_start.se; + for (auto x = exit_start.ne; x <= exit_end.ne; ++x) { auto idx = y * this->size + x; flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::SOUTH); + portal_cells.insert(idx); } } } else if (direction == PortalDirection::EAST_WEST) { - bool other_is_east = entry_start.ne > exit_start.ne; + bool other_is_east = entry_start.ne < exit_start.ne; if (other_is_east) { - auto x = entry_start.ne; - for (auto y = entry_start.se; y <= entry_end.se; ++y) { + auto x = exit_start.ne; + for (auto y = exit_start.se; y <= exit_end.se; ++y) { auto idx = y * this->size + x; flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::EAST); + portal_cells.insert(idx); } } else { - auto x = entry_start.ne; - for (auto y = entry_start.se; y <= entry_end.se; ++y) { + auto x = exit_start.ne; + for (auto y = exit_start.se; y <= exit_end.se; ++y) { auto idx = y * this->size + x; flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::WEST); + portal_cells.insert(idx); } } } @@ -203,7 +216,7 @@ void FlowField::build(const std::shared_ptr &integration_field throw Error(ERR << "Invalid portal direction: " << static_cast(direction)); } - this->build(integration_field); + this->build(integration_field, portal_cells); } const std::vector &FlowField::get_cells() const { diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index beb5bccf06..1c1adcfe33 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -4,6 +4,7 @@ #include #include +#include #include #include "pathfinding/definitions.h" @@ -64,8 +65,11 @@ class FlowField { * Build the flow field. * * @param integration_field Integration field. + * @param target_cells Target cells of the flow field. These cells are ignored + * when building the field. */ - void build(const std::shared_ptr &integration_field); + void build(const std::shared_ptr &integration_field, + const std::unordered_set &target_cells = {}); /** * Build the flow field for a portal. From bbb5ab30e127c6983f63aa9e6c79549724a43532 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Apr 2024 00:20:44 +0200 Subject: [PATCH 384/771] path: Traverse flow field to find waypoints. --- libopenage/pathfinding/pathfinder.cpp | 169 +++++++++++++++++++++++--- libopenage/pathfinding/pathfinder.h | 8 +- 2 files changed, 156 insertions(+), 21 deletions(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index b42f3471b2..9515d8f8e5 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -3,6 +3,7 @@ #include "pathfinder.h" #include "coord/phys.h" +#include "pathfinding/flow_field.h" #include "pathfinding/grid.h" #include "pathfinding/integrator.h" #include "pathfinding/portal.h" @@ -16,7 +17,7 @@ Pathfinder::Pathfinder() : integrator{std::make_shared()} { } -const Path Pathfinder::get_path(PathRequest &request) { +const Path Pathfinder::get_path(const PathRequest &request) { // High-level pathfinding // Find the portals to use to get from the start to the target auto portal_path = this->portal_a_star(request); @@ -38,29 +39,33 @@ const Path Pathfinder::get_path(PathRequest &request) { auto prev_integration_field = sector_fields.first; auto prev_sector_id = target_sector->get_id(); - if (not portal_path.empty()) { - std::vector flow_fields; - flow_fields.reserve(portal_path.size() + 1); + std::vector waypoints; + std::vector>> flow_fields; + flow_fields.reserve(portal_path.size() + 1); - flow_fields.push_back(sector_fields); - for (auto &portal : portal_path) { - auto prev_sector = grid->get_sector(prev_sector_id); - auto next_sector_id = portal->get_exit_sector(prev_sector_id); - auto next_sector = grid->get_sector(next_sector_id); + flow_fields.push_back(std::make_pair(target_sector->get_id(), sector_fields.second)); + for (auto &portal : portal_path) { + auto prev_sector = grid->get_sector(prev_sector_id); + auto next_sector_id = portal->get_exit_sector(prev_sector_id); + auto next_sector = grid->get_sector(next_sector_id); - sector_fields = this->integrator->build(next_sector->get_cost_field(), - prev_integration_field, - prev_sector_id, - portal); - flow_fields.push_back(sector_fields); + sector_fields = this->integrator->build(next_sector->get_cost_field(), + prev_integration_field, + prev_sector_id, + portal); + flow_fields.push_back(std::make_pair(next_sector_id, sector_fields.second)); - prev_integration_field = sector_fields.first; - prev_sector_id = next_sector_id; - } + prev_integration_field = sector_fields.first; + prev_sector_id = next_sector_id; } + // reverse the flow fields so they are ordered from start to target + std::reverse(flow_fields.begin(), flow_fields.end()); + + waypoints = this->get_waypoints(flow_fields, request); + // TODO: Implement the rest of the pathfinding process - return Path{request.grid_id, {}}; + return Path{request.grid_id, waypoints}; } const std::shared_ptr &Pathfinder::get_grid(grid_id_t id) const { @@ -71,7 +76,7 @@ void Pathfinder::add_grid(const std::shared_ptr &grid) { this->grids[grid->get_id()] = grid; } -const std::vector> Pathfinder::portal_a_star(PathRequest &request) const { +const std::vector> Pathfinder::portal_a_star(const PathRequest &request) const { std::vector> result; auto grid = this->grids.at(request.grid_id); @@ -186,6 +191,132 @@ const std::vector> Pathfinder::portal_a_star(PathRequest return result; } +const std::vector Pathfinder::get_waypoints(const std::vector>> &flow_fields, + const PathRequest &request) const { + ENSURE(flow_fields.size() > 1, "At least 1 flow field is required for finding waypoints."); + + std::vector waypoints; + + auto grid = this->get_grid(request.grid_id); + auto sector_size = grid->get_sector_size(); + coord::tile_t start_x = request.start.ne % sector_size; + coord::tile_t start_y = request.start.se % sector_size; + coord::tile_t target_x = request.target.ne % sector_size; + coord::tile_t target_y = request.target.se % sector_size; + + coord::tile_t current_x = start_x; + coord::tile_t current_y = start_y; + flow_dir_t current_direction = flow_fields.at(0).second->get_dir(coord::tile{current_x, current_y}); + for (size_t i = 0; i < flow_fields.size(); ++i) { + auto sector = grid->get_sector(flow_fields.at(i).first); + auto flow_field = flow_fields.at(i).second; + bool target_field = i == flow_fields.size() - 1; + + // navigate the flow field vectors until we reach its edge (or the target) + while (current_x < sector_size and current_y < sector_size + and current_x >= 0 and current_y >= 0) { + auto cell = flow_field->get_cell(coord::tile{current_x, current_y}); + if (cell & FLOW_LOS_MASK) { + // check if we reached an LOS cell + auto sector_pos = sector->get_position(); + auto cell_pos = coord::tile{sector_pos.ne * sector_size, + sector_pos.se * sector_size} + + coord::tile_delta{current_x, current_y}; + waypoints.push_back(cell_pos); + break; + } + + // check if we need to change direction + auto cell_direction = flow_field->get_dir(coord::tile{current_x, current_y}); + if (cell_direction != current_direction) { + // add the current cell as a waypoint + auto sector_pos = sector->get_position(); + auto cell_pos = coord::tile{sector_pos.ne * sector_size, + sector_pos.se * sector_size} + + coord::tile_delta{current_x, current_y}; + waypoints.push_back(cell_pos); + current_direction = cell_direction; + } + + // move to the next cell + switch (current_direction) { + case flow_dir_t::NORTH: + current_y -= 1; + break; + case flow_dir_t::NORTH_EAST: + current_x += 1; + current_y -= 1; + break; + case flow_dir_t::EAST: + current_x += 1; + break; + case flow_dir_t::SOUTH_EAST: + current_x += 1; + current_y += 1; + break; + case flow_dir_t::SOUTH: + current_y += 1; + break; + case flow_dir_t::SOUTH_WEST: + current_x -= 1; + current_y += 1; + break; + case flow_dir_t::WEST: + current_x -= 1; + break; + case flow_dir_t::NORTH_WEST: + current_x -= 1; + current_y -= 1; + break; + default: + throw Error{ERR << "Invalid flow direction: " << static_cast(current_direction)}; + } + } + + // reset the current position for the next flow field + switch (current_direction) { + case flow_dir_t::NORTH: + current_y = sector_size - 1; + break; + case flow_dir_t::NORTH_EAST: + current_x = current_x + 1; + current_y = sector_size - 1; + break; + case flow_dir_t::EAST: + current_x = 0; + break; + case flow_dir_t::SOUTH_EAST: + current_x = 0; + current_y = current_y + 1; + break; + case flow_dir_t::SOUTH: + current_y = 0; + break; + case flow_dir_t::SOUTH_WEST: + current_x = current_x - 1; + current_y = 0; + break; + case flow_dir_t::WEST: + current_x = sector_size - 1; + break; + case flow_dir_t::NORTH_WEST: + current_x = sector_size - 1; + current_y = current_y - 1; + break; + default: + throw Error{ERR << "Invalid flow direction: " << static_cast(current_direction)}; + } + } + + // add the target position as the last waypoint + auto sector_pos = grid->get_sector(flow_fields.back().first)->get_position(); + auto target_pos = coord::tile{sector_pos.ne * sector_size, sector_pos.se * sector_size} + + coord::tile_delta{target_x, target_y}; + waypoints.push_back(target_pos); + + return waypoints; +} + int Pathfinder::heuristic_cost(const coord::tile &portal_pos, const coord::tile &target_pos) { auto portal_phys_pos = portal_pos.to_phys2(); diff --git a/libopenage/pathfinding/pathfinder.h b/libopenage/pathfinding/pathfinder.h index b89f994bc8..935a7db488 100644 --- a/libopenage/pathfinding/pathfinder.h +++ b/libopenage/pathfinding/pathfinder.h @@ -15,6 +15,7 @@ namespace openage::path { class Grid; class Integrator; class Portal; +class FlowField; /** * Pathfinder for flow field pathfinding. @@ -58,10 +59,13 @@ class Pathfinder { * * @return Path found by the pathfinder. */ - const Path get_path(PathRequest &request); + const Path get_path(const PathRequest &request); private: - const std::vector> portal_a_star(PathRequest &request) const; + const std::vector> portal_a_star(const PathRequest &request) const; + + const std::vector get_waypoints(const std::vector>> &flow_fields, + const PathRequest &request) const; /** * Calculate the heuristic cost between a portal and a target cell. From 8fff7012f20ce850006db68013c0bf5e95d706a3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Apr 2024 00:21:03 +0200 Subject: [PATCH 385/771] path: Show waypoints in demo 1. --- libopenage/pathfinding/demo/demo_1.cpp | 64 ++++++++++++++++++++++++-- libopenage/pathfinding/demo/demo_1.h | 8 ++++ 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index c2f5ba19de..a8eb68f00c 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -75,6 +75,12 @@ void path_demo_1(const util::Path &path) { auto pathfinder = std::make_shared(); pathfinder->add_grid(grid); + // Render the grid + auto qtapp = std::make_shared(); + auto window = std::make_shared("openage pathfinding test", 1024, 768); + auto render_manager = std::make_shared(qtapp, window, path, grid); + log::log(INFO << "Created render manager for pathfinding demo"); + auto path_request = path::PathRequest{ 0, coord::tile{2, 26}, @@ -82,11 +88,7 @@ void path_demo_1(const util::Path &path) { }; auto path_result = pathfinder->get_path(path_request); - // Render the grid - auto qtapp = std::make_shared(); - auto window = std::make_shared("openage pathfinding test", 1024, 768); - auto render_manager = std::make_shared(qtapp, window, path, grid); - log::log(INFO << "Created render manager for pathfinding demo"); + render_manager->create_waypoint_tiles(path_result); render_manager->run(); } @@ -477,4 +479,56 @@ void RenderManager1::create_portal_tiles(const std::shared_ptr &grid } } +void RenderManager1::create_waypoint_tiles(const Path &path) { + auto width = grid->get_size()[0]; + auto height = grid->get_size()[1]; + auto sector_size = grid->get_sector_size(); + + float tile_offset_width = 2.0f / (width * sector_size); + float tile_offset_height = 2.0f / (height * sector_size); + + for (auto &tile : path.waypoints) { + std::array tile_data{ + -1.0f + tile.ne * tile_offset_width, + 1.0f - tile.se * tile_offset_height, + -1.0f + tile.ne * tile_offset_width, + 1.0f - (tile.se + 1) * tile_offset_height, + -1.0f + (tile.ne + 1) * tile_offset_width, + 1.0f - tile.se * tile_offset_height, + -1.0f + (tile.ne + 1) * tile_offset_width, + 1.0f - (tile.se + 1) * tile_offset_height, + }; + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V2F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLE_STRIP}; + + auto const data_size = tile_data.size() * sizeof(float); + std::vector verts(data_size); + std::memcpy(verts.data(), tile_data.data(), data_size); + + auto tile_mesh = renderer::resources::MeshData(std::move(verts), info); + auto tile_geometry = renderer->add_mesh_geometry(tile_mesh); + + Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity(); + auto tile_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{1.0, 0.0, 0.0, 1.0}, + "model", + id_matrix, + "view", + id_matrix, + "proj", + id_matrix); + auto tile_obj = renderer::Renderable{ + tile_unifs, + tile_geometry, + true, + true, + }; + this->field_pass->add_renderables({tile_obj}); + } +} + } // namespace openage::path::tests diff --git a/libopenage/pathfinding/demo/demo_1.h b/libopenage/pathfinding/demo/demo_1.h index 52ed7d1f65..eafcc3d45f 100644 --- a/libopenage/pathfinding/demo/demo_1.h +++ b/libopenage/pathfinding/demo/demo_1.h @@ -3,6 +3,7 @@ #pragma once #include "pathfinding/definitions.h" +#include "pathfinding/path.h" #include "renderer/resources/mesh_data.h" #include "util/path.h" @@ -62,6 +63,13 @@ class RenderManager1 { */ void run(); + /** + * Create renderables for the waypoint tiles of a path. + * + * @param path Path object. + */ + void create_waypoint_tiles(const Path &path); + private: /** * Load the shader sources for the demo and create the shader programs. From 6f568b1d8d354ac3d34ebe96b209327a8c1fbdb0 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Apr 2024 00:25:12 +0200 Subject: [PATCH 386/771] path: Include path start in waypoints. --- libopenage/pathfinding/path.h | 2 ++ libopenage/pathfinding/pathfinder.cpp | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/path.h index cb08e09991..8d64f5914f 100644 --- a/libopenage/pathfinding/path.h +++ b/libopenage/pathfinding/path.h @@ -28,6 +28,8 @@ struct Path { // ID of the grid to used for pathfinding. size_t grid_id; // Waypoints of the path. + // First waypoint is the start position of the path request. + // Last waypoint is the target position of the path request. std::vector waypoints; }; diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 9515d8f8e5..2568f59f87 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -39,7 +39,6 @@ const Path Pathfinder::get_path(const PathRequest &request) { auto prev_integration_field = sector_fields.first; auto prev_sector_id = target_sector->get_id(); - std::vector waypoints; std::vector>> flow_fields; flow_fields.reserve(portal_path.size() + 1); @@ -62,9 +61,11 @@ const Path Pathfinder::get_path(const PathRequest &request) { // reverse the flow fields so they are ordered from start to target std::reverse(flow_fields.begin(), flow_fields.end()); - waypoints = this->get_waypoints(flow_fields, request); + // traverse the flow fields to get the waypoints + std::vector waypoints{request.start}; + auto flow_field_waypoints = this->get_waypoints(flow_fields, request); + waypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end()); - // TODO: Implement the rest of the pathfinding process return Path{request.grid_id, waypoints}; } From 07de19bab8845a62de2782a7f32fa757c647e99e Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Apr 2024 01:03:43 +0200 Subject: [PATCH 387/771] path: Draw start and end of path with separate colors. --- libopenage/pathfinding/demo/demo_1.cpp | 91 +++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 3 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index a8eb68f00c..8811634c95 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -487,7 +487,11 @@ void RenderManager1::create_waypoint_tiles(const Path &path) { float tile_offset_width = 2.0f / (width * sector_size); float tile_offset_height = 2.0f / (height * sector_size); - for (auto &tile : path.waypoints) { + Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity(); + + // Draw in-between waypoints + for (size_t i = 1; i < path.waypoints.size() - 1; i++) { + auto tile = path.waypoints[i]; std::array tile_data{ -1.0f + tile.ne * tile_offset_width, 1.0f - tile.se * tile_offset_height, @@ -511,10 +515,9 @@ void RenderManager1::create_waypoint_tiles(const Path &path) { auto tile_mesh = renderer::resources::MeshData(std::move(verts), info); auto tile_geometry = renderer->add_mesh_geometry(tile_mesh); - Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity(); auto tile_unifs = obj_shader->new_uniform_input( "color", - Eigen::Vector4f{1.0, 0.0, 0.0, 1.0}, + Eigen::Vector4f{0.0, 0.25, 1.0, 1.0}, "model", id_matrix, "view", @@ -529,6 +532,88 @@ void RenderManager1::create_waypoint_tiles(const Path &path) { }; this->field_pass->add_renderables({tile_obj}); } + + // Draw start and end waypoints with different colors + auto start_tile = path.waypoints.front(); + std::array start_tile_data{ + -1.0f + start_tile.ne * tile_offset_width, + 1.0f - start_tile.se * tile_offset_height, + -1.0f + start_tile.ne * tile_offset_width, + 1.0f - (start_tile.se + 1) * tile_offset_height, + -1.0f + (start_tile.ne + 1) * tile_offset_width, + 1.0f - start_tile.se * tile_offset_height, + -1.0f + (start_tile.ne + 1) * tile_offset_width, + 1.0f - (start_tile.se + 1) * tile_offset_height, + }; + + renderer::resources::VertexInputInfo start_info{ + {renderer::resources::vertex_input_t::V2F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLE_STRIP}; + + auto const start_data_size = start_tile_data.size() * sizeof(float); + std::vector start_verts(start_data_size); + std::memcpy(start_verts.data(), start_tile_data.data(), start_data_size); + + auto start_tile_mesh = renderer::resources::MeshData(std::move(start_verts), start_info); + auto start_tile_geometry = renderer->add_mesh_geometry(start_tile_mesh); + auto start_tile_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{0.0, 0.5, 0.0, 1.0}, + "model", + id_matrix, + "view", + id_matrix, + "proj", + id_matrix); + auto start_tile_obj = renderer::Renderable{ + start_tile_unifs, + start_tile_geometry, + true, + true, + }; + + auto end_tile = path.waypoints.back(); + std::array end_tile_data{ + -1.0f + end_tile.ne * tile_offset_width, + 1.0f - end_tile.se * tile_offset_height, + -1.0f + end_tile.ne * tile_offset_width, + 1.0f - (end_tile.se + 1) * tile_offset_height, + -1.0f + (end_tile.ne + 1) * tile_offset_width, + 1.0f - end_tile.se * tile_offset_height, + -1.0f + (end_tile.ne + 1) * tile_offset_width, + 1.0f - (end_tile.se + 1) * tile_offset_height, + }; + + renderer::resources::VertexInputInfo end_info{ + {renderer::resources::vertex_input_t::V2F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLE_STRIP}; + + auto const end_data_size = end_tile_data.size() * sizeof(float); + std::vector end_verts(end_data_size); + std::memcpy(end_verts.data(), end_tile_data.data(), end_data_size); + + auto end_tile_mesh = renderer::resources::MeshData(std::move(end_verts), end_info); + auto end_tile_geometry = renderer->add_mesh_geometry(end_tile_mesh); + auto end_tile_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{1.0, 0.5, 0.0, 1.0}, + "model", + id_matrix, + "view", + id_matrix, + "proj", + id_matrix); + + auto end_tile_obj = renderer::Renderable{ + end_tile_unifs, + end_tile_geometry, + true, + true, + }; + + this->field_pass->add_renderables({start_tile_obj, end_tile_obj}); } } // namespace openage::path::tests From 17b53aa83e13842dbe06d9fb7843ae4e6b01b8e3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Apr 2024 01:10:54 +0200 Subject: [PATCH 388/771] path: Make some of the variable types in the demo more obvious. --- libopenage/pathfinding/demo/demo_1.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 8811634c95..1472590535 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -23,15 +23,15 @@ void path_demo_1(const util::Path &path) { auto grid = std::make_shared(0, util::Vector2s{4, 3}, 10); // Initialize the cost field for each sector. - for (auto sector : grid->get_sectors()) { + for (auto §or : grid->get_sectors()) { auto cost_field = sector->get_cost_field(); - auto sector_cost = sectors_cost.at(sector->get_id()); + std::vector sector_cost = sectors_cost.at(sector->get_id()); cost_field->set_costs(std::move(sector_cost)); } // Initialize portals between sectors. - auto grid_size = grid->get_size(); - auto portal_id = 0; + util::Vector2s grid_size = grid->get_size(); + portal_id_t portal_id = 0; for (size_t y = 0; y < grid_size[1]; y++) { for (size_t x = 0; x < grid_size[0]; x++) { auto sector = grid->get_sector(x, y); @@ -39,7 +39,7 @@ void path_demo_1(const util::Path &path) { if (x < grid_size[0] - 1) { auto neighbor = grid->get_sector(x + 1, y); auto portals = sector->find_portals(neighbor, PortalDirection::EAST_WEST, portal_id); - for (auto portal : portals) { + for (auto &portal : portals) { sector->add_portal(portal); neighbor->add_portal(portal); } @@ -48,7 +48,7 @@ void path_demo_1(const util::Path &path) { if (y < grid_size[1] - 1) { auto neighbor = grid->get_sector(x, y + 1); auto portals = sector->find_portals(neighbor, PortalDirection::NORTH_SOUTH, portal_id); - for (auto portal : portals) { + for (auto &portal : portals) { sector->add_portal(portal); neighbor->add_portal(portal); } @@ -81,12 +81,13 @@ void path_demo_1(const util::Path &path) { auto render_manager = std::make_shared(qtapp, window, path, grid); log::log(INFO << "Created render manager for pathfinding demo"); - auto path_request = path::PathRequest{ + // TODO: Make the path request interactive with window callbacks + PathRequest path_request{ 0, coord::tile{2, 26}, coord::tile{36, 2}, }; - auto path_result = pathfinder->get_path(path_request); + Path path_result = pathfinder->get_path(path_request); render_manager->create_waypoint_tiles(path_result); From 1d384a6d315a07b329aebe784aedd862fe505c17 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Apr 2024 01:23:05 +0200 Subject: [PATCH 389/771] path: Fix warnings about type conversions. --- libopenage/pathfinding/demo/demo_1.cpp | 21 ++++++++++++-------- libopenage/pathfinding/integration_field.cpp | 4 ++-- libopenage/pathfinding/pathfinder.cpp | 7 ++++--- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 1472590535..752854a3e5 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -72,15 +72,11 @@ void path_demo_1(const util::Path &path) { } } + // Create a pathfinder for searching paths on the grid auto pathfinder = std::make_shared(); pathfinder->add_grid(grid); - // Render the grid - auto qtapp = std::make_shared(); - auto window = std::make_shared("openage pathfinding test", 1024, 768); - auto render_manager = std::make_shared(qtapp, window, path, grid); - log::log(INFO << "Created render manager for pathfinding demo"); - + // Create a path request and get the path // TODO: Make the path request interactive with window callbacks PathRequest path_request{ 0, @@ -89,8 +85,17 @@ void path_demo_1(const util::Path &path) { }; Path path_result = pathfinder->get_path(path_request); + // Create a renderer to display the grid and path + auto qtapp = std::make_shared(); + auto window = std::make_shared("openage pathfinding test", 1024, 768); + auto render_manager = std::make_shared(qtapp, window, path, grid); + log::log(INFO << "Created render manager for pathfinding demo"); + + // Create renderables for the waypoints of the path render_manager->create_waypoint_tiles(path_result); + // Run the renderer pss to draw the grid and path into a window + // TODO: Make this a while (not window.should_close()) loop render_manager->run(); } @@ -423,13 +428,13 @@ void RenderManager1::create_portal_tiles(const std::shared_ptr &grid std::vector tiles; if (direction == PortalDirection::NORTH_SOUTH) { auto y = start.se; - for (size_t x = start.ne; x <= end.ne; ++x) { + for (auto x = start.ne; x <= end.ne; ++x) { tiles.push_back(coord::tile{x, y}); } } else { auto x = start.ne; - for (size_t y = start.se; y <= end.se; ++y) { + for (auto y = start.se; y <= end.se; ++y) { tiles.push_back(coord::tile{x, y}); } } diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index a38b06e67a..0deed51073 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -311,10 +311,10 @@ std::vector> IntegrationField::get_los_corners(const std::sh if (blocker.ne > 0) { left_cost = cost_field->get_cost(coord::tile{blocker.ne - 1, blocker.se}); } - if (blocker.se < this->size - 1) { + if (static_cast(blocker.se) < this->size - 1) { bottom_cost = cost_field->get_cost(coord::tile{blocker.ne, blocker.se + 1}); } - if (blocker.ne < this->size - 1) { + if (static_cast(blocker.ne) < this->size - 1) { right_cost = cost_field->get_cost(coord::tile{blocker.ne + 1, blocker.se}); } diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 2568f59f87..3f3c7fb24f 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -211,11 +211,12 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_sector(flow_fields.at(i).first); auto flow_field = flow_fields.at(i).second; - bool target_field = i == flow_fields.size() - 1; // navigate the flow field vectors until we reach its edge (or the target) - while (current_x < sector_size and current_y < sector_size - and current_x >= 0 and current_y >= 0) { + while (current_x < static_cast(sector_size) + and current_y < static_cast(sector_size) + and current_x >= 0 + and current_y >= 0) { auto cell = flow_field->get_cell(coord::tile{current_x, current_y}); if (cell & FLOW_LOS_MASK) { // check if we reached an LOS cell From ed5d6c834df572df064e42b3afe5614225ef388c Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Apr 2024 01:34:58 +0200 Subject: [PATCH 390/771] path: Initialize portals in Grid object. --- libopenage/pathfinding/grid.cpp | 34 +++++++++++++++++++++++++++++++++ libopenage/pathfinding/grid.h | 7 +++++++ 2 files changed, 41 insertions(+) diff --git a/libopenage/pathfinding/grid.cpp b/libopenage/pathfinding/grid.cpp index 51940d2bf3..4100492c16 100644 --- a/libopenage/pathfinding/grid.cpp +++ b/libopenage/pathfinding/grid.cpp @@ -65,4 +65,38 @@ const std::vector> &Grid::get_sectors() const { return this->sectors; } +void Grid::init_portals() { + // Create portals between neighboring sectors. + portal_id_t portal_id = 0; + for (size_t y = 0; y < this->size[1]; y++) { + for (size_t x = 0; x < this->size[0]; x++) { + auto sector = this->get_sector(x, y); + + if (x < this->size[0] - 1) { + auto neighbor = this->get_sector(x + 1, y); + auto portals = sector->find_portals(neighbor, PortalDirection::EAST_WEST, portal_id); + for (auto &portal : portals) { + sector->add_portal(portal); + neighbor->add_portal(portal); + } + portal_id += portals.size(); + } + if (y < this->size[1] - 1) { + auto neighbor = this->get_sector(x, y + 1); + auto portals = sector->find_portals(neighbor, PortalDirection::NORTH_SOUTH, portal_id); + for (auto &portal : portals) { + sector->add_portal(portal); + neighbor->add_portal(portal); + } + portal_id += portals.size(); + } + } + } + + // Connect mutually reachable exits of sectors. + for (auto §or : this->sectors) { + sector->connect_exits(); + } +} + } // namespace openage::path diff --git a/libopenage/pathfinding/grid.h b/libopenage/pathfinding/grid.h index c5e783ddfa..83b4c2b721 100644 --- a/libopenage/pathfinding/grid.h +++ b/libopenage/pathfinding/grid.h @@ -88,6 +88,13 @@ class Grid { */ const std::vector> &get_sectors() const; + /** + * Initialize the portals of the sectors on the grid. + * + * This should be called after all sectors' cost fields have been initialized. + */ + void init_portals(); + private: /** * ID of the grid. From 9cc8710d546f3b2965378ed91fcbb35fdd8935fb Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Apr 2024 01:51:26 +0200 Subject: [PATCH 391/771] path: Move flow_dir_t to types.h. --- libopenage/pathfinding/definitions.h | 16 ---------------- libopenage/pathfinding/types.h | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h index cd12b6fe10..d61c5fe43c 100644 --- a/libopenage/pathfinding/definitions.h +++ b/libopenage/pathfinding/definitions.h @@ -58,22 +58,6 @@ constexpr integrated_flags_t INTEGRATE_WAVEFRONT_BLOCKED_MASK = 0x02; constexpr integrated_t INTEGRATE_INIT = {INTEGRATED_COST_UNREACHABLE, 0}; -/** - * Flow field direction types. - * - * Encoded into the flow_t values. - */ -enum class flow_dir_t : uint8_t { - NORTH = 0x00, - NORTH_EAST = 0x01, - EAST = 0x02, - SOUTH_EAST = 0x03, - SOUTH = 0x04, - SOUTH_WEST = 0x05, - WEST = 0x06, - NORTH_WEST = 0x07, -}; - /** * Initial value for a flow field cell. */ diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index f543f269a4..1d0623328e 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -45,6 +45,22 @@ struct integrated_t { integrated_flags_t flags; }; +/** + * Flow field direction types. + * + * Encoded into the flow_t values. + */ +enum class flow_dir_t : uint8_t { + NORTH = 0x00, + NORTH_EAST = 0x01, + EAST = 0x02, + SOUTH_EAST = 0x03, + SOUTH = 0x04, + SOUTH_WEST = 0x05, + WEST = 0x06, + NORTH_WEST = 0x07, +}; + /** * Flow field cell value. * From 1f882f0fd05c715cd51578dc9193ebb489907612 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Apr 2024 02:08:15 +0200 Subject: [PATCH 392/771] path: Document pathfinding functions. --- libopenage/pathfinding/pathfinder.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/libopenage/pathfinding/pathfinder.h b/libopenage/pathfinding/pathfinder.h index 935a7db488..e6e43a7abf 100644 --- a/libopenage/pathfinding/pathfinder.h +++ b/libopenage/pathfinding/pathfinder.h @@ -62,8 +62,23 @@ class Pathfinder { const Path get_path(const PathRequest &request); private: + /** + * High-level pathfinder. Uses A* to find the path through the portals of sectors. + * + * @param request Pathfinding request. + * + * @return Portals to traverse in order to reach the target. + */ const std::vector> portal_a_star(const PathRequest &request) const; + /** + * Low-level pathfinder. Uses flow fields to find the path through the sectors. + * + * @param flow_fields Flow fields for the sectors. + * @param request Pathfinding request. + * + * @return Waypoint coordinates to traverse in order to reach the target. + */ const std::vector get_waypoints(const std::vector>> &flow_fields, const PathRequest &request) const; From 78657a3e8678bab62495b79e88777a133ab7cc57 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Apr 2024 02:48:23 +0200 Subject: [PATCH 393/771] path: Add timer for path request to demo 1. --- libopenage/pathfinding/demo/demo_1.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 752854a3e5..d8da5193e8 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -8,6 +8,7 @@ #include "pathfinding/pathfinder.h" #include "pathfinding/portal.h" #include "pathfinding/sector.h" +#include "util/timer.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" @@ -76,6 +77,8 @@ void path_demo_1(const util::Path &path) { auto pathfinder = std::make_shared(); pathfinder->add_grid(grid); + util::Timer timer; + // Create a path request and get the path // TODO: Make the path request interactive with window callbacks PathRequest path_request{ @@ -83,7 +86,11 @@ void path_demo_1(const util::Path &path) { coord::tile{2, 26}, coord::tile{36, 2}, }; + timer.start(); Path path_result = pathfinder->get_path(path_request); + timer.stop(); + + log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps"); // Create a renderer to display the grid and path auto qtapp = std::make_shared(); From 9ff58024f659ac39a8dfd4f3fae7479fba051f0d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Apr 2024 02:48:43 +0200 Subject: [PATCH 394/771] path: Add TODOs. --- libopenage/pathfinding/integration_field.cpp | 2 ++ libopenage/pathfinding/integrator.h | 3 +++ libopenage/pathfinding/portal.h | 4 ++++ libopenage/pathfinding/sector.cpp | 1 + libopenage/pathfinding/types.h | 2 ++ 5 files changed, 12 insertions(+) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 0deed51073..201823eec4 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -185,6 +185,8 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie // Set the cost of all target cells to the start value this->cells[target_idx].cost = INTEGRATED_COST_START; start_cells.push_back(target_idx); + + // TODO: Transfer flags and cost from the other integration field } } diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index 2b44d3f18f..027f627274 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -100,6 +100,9 @@ class Integrator { const std::shared_ptr &other_integration_field, sector_id_t other_sector_id, const std::shared_ptr &portal); + +private: + // TODO: Cache created flow fields. }; } // namespace path diff --git a/libopenage/pathfinding/portal.h b/libopenage/pathfinding/portal.h index 60a6bf9e48..2487e449c7 100644 --- a/libopenage/pathfinding/portal.h +++ b/libopenage/pathfinding/portal.h @@ -212,11 +212,15 @@ class Portal { /** * Exits in sector 0 reachable from the portal. + * + * TODO: Also store avarage cost to reach each exit. */ std::vector> sector0_exits; /** * Exits in sector 1 reachable from the portal. + * + * TODO: Also store avarage cost to reach each exit. */ std::vector> sector1_exits; diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index bc75894cb8..8dcdfb0e5a 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -197,6 +197,7 @@ void Sector::connect_exits() { neighbors.clear(); // mark the current cell as visited + // TODO: Record the cost of reaching this cell visited.insert(current); } diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index 1d0623328e..11190f6551 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -11,6 +11,8 @@ namespace openage::path { /** * Movement cost in the cost field. * + * TODO: Cost stamps + * * 0: uninitialized * 1-254: normal cost * 255: impassable From e9470220473de22973efa23568bb6fa586bfa56f Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Apr 2024 20:42:03 +0200 Subject: [PATCH 395/771] path: Fix an issue with gcc not default initializing correctly in release build. --- libopenage/pathfinding/sector.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index 8dcdfb0e5a..6b56114e47 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -57,9 +57,9 @@ std::vector> Sector::find_portals(const std::shared_ptr< auto other_cost = other->get_cost_field(); // compare the edges of the sectors - size_t start; - size_t end; - bool passable_edge; + size_t start = 0; + size_t end = 0; + bool passable_edge = false; for (size_t i = 0; i < this->cost_field->get_size(); ++i) { auto coord_this = coord::tile{0, 0}; auto coord_other = coord::tile{0, 0}; From 066dd06abef0b30fee21eeed8697c300a73b1aed Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 14 Apr 2024 01:27:40 +0200 Subject: [PATCH 396/771] doc: Pathfinding documentaton text. --- doc/code/pathfinding/README.md | 122 +++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 doc/code/pathfinding/README.md diff --git a/doc/code/pathfinding/README.md b/doc/code/pathfinding/README.md new file mode 100644 index 0000000000..f7c5ce6d5b --- /dev/null +++ b/doc/code/pathfinding/README.md @@ -0,0 +1,122 @@ +# Pathfinding + +openage's pathfinding subsystem implements structures used for navigating game entities in the +game world. These structures define movement costs for a map and allow search for a path +from one coordinate in the game world to another. + +Pathfinding is a subsystem of the [game simulation](/doc/code/game_simulation/README.md) where +it is primarily used for movement and placement of game entities. + +1. [Architecture](#architecture) +2. [Workflow](#workflow) + + +## Architecture + +The architecture of the pathfinder is heavily based on the article *Crowd Pathfinding and Steering* +*Using Flow Field Tiles* by Elijah Emerson (available [here](http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf)). A core design +decision taken from this article was the usage of flow fields as the basis for the pathing algorithm. + +Flow fields offer a few advantages for large group movements, i.e. the movement you typically see in +RTS games like Age of Empires. For example, they allow reusing already computed path results for +subsequent pathing requests and can also be used for smart collision avoidance. One downside +of using flow fields can be the heavy upfront cost of pathing calculations. However, the downsides +can be mitigated to some degree with caching and high-level optimizations. + +The openage pathfinder is tile-based, like most flow field pathfinding implementations. Every tile +has a movement cost associated with it that represents the cost of a game entity moving on that tile. +When computing a path from A to B, the pathfinder tries to find the sequence of tiles with the cheapest +accumulated movement cost. + +In openage, the pathfinding subsystem is independent from the terrain implementation in the game +simulation. Terrain data may be used to influence movement cost during gameplay, but pathfinding +can be initialized without any requirements for terrain code. By only loosely connecting these two system, +we have a much more fine-grained control that allows for better optimization of the pathfinder. +The most relevant similarity between terrain and pathfinding code is that they use the same +[coordinate systems](/doc/code/coordinate-systems.md#tiletile3). To make the distinction between +pathfinding and terrain more clear, we use the term *cells* for tiles in the pathfinder. + +![UML pathfinding classes]() + +The relationship between classes in the pathfinder can be seen above. `Grid` is the top-level structure +for storage of movement cost. There may be multiple grids defined, one for each movement type +used by game entities in the game simulation. + +Every grid is subdivided into sectors, represented by the `Sector` class. Each sectors holds a +pointer to a `CostField` which stores the movement cost of individual cells. All sectors on the +same grid have a fixed square size that is determined by the grid. Thus, the sector size on +a grid is always consistent but different grid may utilize different sector sizes. + +Sectors on a grid are connect to each other with so-called portals, see the `Portal` +class. Portals represent a passable gateway between two sectors where game entities +can pass through. As such, portals store the coordinates of the cells in each sector +where game entities can pass. `Portal` objects are created for every continuous sequence of cells +on the edges between two sectors where the cells are passable on both sides of the two sectors. +Therefore, there may be multiple portals defined for the edge between two sectors. + +Additionally, each portal stores which other portals it can reach in a specific sector. The +result is a portal "graph" that can be searched separately to determine which portals +and sectors are visited for a specific pathing request. This is used in the high-level +pathfinder to preselect the sectors that flow fields need to be generated for (see the +[Workflow section](#workflow)). + +The individual movement cost of each cell in a sector are recorded in a `CostField` object. +The cost of a cell can range from 1 (minimum cost) to 254 (maximum cost), while a cost of 255 +makes a cell impassible. For Age of Empires, usually only the minimum and impassable cost +values are relevant. The cost field is built when the grid is first initialized and +individual cost of cells can be altered during gameplay events. + +To get a path between two coordinates, the game simulation mainly interfaces with a `Pathfinder` +object. The `Pathfinder` class calls the actual pathfinding algorithms used for searching +for a path and stores references to all available grids in the pathfinding subsystems. It can receive +`PathRequest` objects that contain information about the grid that should be searched as well as +the start and target coordinates of the desired path. Found paths are returned as `Path` objects +which store the waypoint coordinates for the computed path. + +Flow field calculations are controlled by an `Integrator` object, which process a cost field +from a sector as well as target coordinates to compute an `IntegrationField` and a `FlowField` object. +`IntegrationField`s are intermediary objects that store the accumulated cost of reaching +the target cell for each other cell in the field, using the cell values in the cost field as basis. +From the integration field, a `FlowField` object is created. Cells in the flow field store +movement vectors that point to their next cheapest neighbor cell in the integration field. `Path`s +may be computed from these flow field by simply following the movement vectors until the +target coordinate is reached. + + +## Workflow + +To initiate a new search, the pathfinder receives a `PathRequest` object, e.g. from the game simulation. +Every path request contains the following information: + +- ID of the grid to search +- start cell coordinates +- target cell coordinates + +The actual pathfinding algorithm is split into two stages. + +1. High-level portal-based search to identify the visited sectors on the grid +2. Low-level flow field-based search in the identified sectors to find the waypoints for the path + +The high-level search is accomplished by utilizing the portal graph, i.e. the +connections between portals in each sector. From the portal graph, a node mesh is created +that can be searched with a graph-traversal algorithm. For this purpose, the A\* algorithm +is used. The result of the high-level search is a list of sectors and portals that are +traversed by the path. + +The high-level search is mainly motivated by the need to avoid costly flow field +calculations on the whole grid. As the portal graph should already be precomputed when +a path request is made, the main influence on performance is the A\* algorithm. Given +a limited number of portals, the A\* search should overall be very cheap. + +The resulting list of sectors and portals is subsequently used in the low-level flow +field calculations. As a first step, the pathfinder uses its integrator to generate +a flow field for each identified sector. Generation starts with the target sector +and ends with the start sector. Flow field results are passed through at the cells +of the identified portals to make the flow between sectors seamless. + + + +In a second step, the pathfinder follows the movement vectors in the flow fields from +the start cell to the target cell. Waypoints are created for every direction change, so +that game entities can travel in straight lines between them. The list of waypoints +from start to target is then returned by the pathfinder via a `Path` object. From f9f9ae1af3ae4fd872e5b78ddcf851dc9b2245c8 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 6 May 2024 20:15:17 +0200 Subject: [PATCH 397/771] docs: Pathfinding documentation diagrams. --- doc/code/images/pathfinder_architecture.svg | 175 +++++++++++++ doc/code/images/pathfinder_architecture.uxf | 271 ++++++++++++++++++++ doc/code/pathfinding/README.md | 2 +- 3 files changed, 447 insertions(+), 1 deletion(-) create mode 100644 doc/code/images/pathfinder_architecture.svg create mode 100644 doc/code/images/pathfinder_architecture.uxf diff --git a/doc/code/images/pathfinder_architecture.svg b/doc/code/images/pathfinder_architecture.svg new file mode 100644 index 0000000000..2046a1a486 --- /dev/null +++ b/doc/code/images/pathfinder_architecture.svg @@ -0,0 +1,175 @@ + + +GamestateMovement,collision, etc.PathfinderPathFlowFieldIntegrationFieldIntegratorPathfinderPortalCostFieldSectorGrid diff --git a/doc/code/images/pathfinder_architecture.uxf b/doc/code/images/pathfinder_architecture.uxf new file mode 100644 index 0000000000..3a051b2acf --- /dev/null +++ b/doc/code/images/pathfinder_architecture.uxf @@ -0,0 +1,271 @@ + + + 10 + + UMLClass + + 710 + 510 + 120 + 60 + + +*Grid* + + + + UMLClass + + 710 + 620 + 120 + 60 + + +*Sector* + + + + UMLClass + + 630 + 730 + 120 + 60 + + +*CostField* + + + + UMLClass + + 790 + 730 + 120 + 60 + + +*Portal* + + + + UMLClass + + 710 + 400 + 120 + 60 + + +*Pathfinder* + + + + UMLClass + + 980 + 400 + 120 + 60 + + +*Integrator* + + + + UMLClass + + 1040 + 490 + 160 + 60 + + +*IntegrationField* + + + + UMLClass + + 1040 + 560 + 120 + 60 + + +*FlowField* + + + + UMLClass + + 520 + 400 + 120 + 60 + + +*Path* + + + + Relation + + 760 + 450 + 30 + 80 + + lt=<. + 10.0;60.0;10.0;10.0 + + + Relation + + 760 + 560 + 30 + 80 + + lt=<. + 10.0;60.0;10.0;10.0 + + + Relation + + 720 + 670 + 30 + 80 + + lt=<. + 10.0;60.0;10.0;10.0 + + + Relation + + 800 + 670 + 30 + 80 + + lt=<. + 10.0;60.0;10.0;10.0 + + + Relation + + 820 + 420 + 180 + 30 + + lt=<. + 160.0;10.0;10.0;10.0 + + + Relation + + 990 + 450 + 30 + 160 + + lt=. + 10.0;140.0;10.0;10.0 + + + Relation + + 990 + 510 + 70 + 30 + + lt=<. + 50.0;10.0;10.0;10.0 + + + Relation + + 990 + 580 + 70 + 30 + + lt=<. + 50.0;10.0;10.0;10.0 + + + Relation + + 630 + 420 + 100 + 30 + + lt=<. + 10.0;10.0;80.0;10.0 + + + Relation + + 760 + 200 + 30 + 220 + + lt=<. + 10.0;200.0;10.0;10.0 + + + Text + + 460 + 330 + 240 + 70 + + *Pathfinder* +fontsize=22 +style=wordwrap + + + + Relation + + 450 + 300 + 770 + 30 + + lt=- + 10.0;10.0;750.0;10.0 + + + Text + + 730 + 160 + 130 + 70 + + Movement, collision, etc. +style=wordwrap + + + + Text + + 460 + 160 + 240 + 70 + + *Gamestate* +fontsize=22 +style=wordwrap + + + diff --git a/doc/code/pathfinding/README.md b/doc/code/pathfinding/README.md index f7c5ce6d5b..a7ec70d1bd 100644 --- a/doc/code/pathfinding/README.md +++ b/doc/code/pathfinding/README.md @@ -36,7 +36,7 @@ The most relevant similarity between terrain and pathfinding code is that they u [coordinate systems](/doc/code/coordinate-systems.md#tiletile3). To make the distinction between pathfinding and terrain more clear, we use the term *cells* for tiles in the pathfinder. -![UML pathfinding classes]() +![UML pathfinding classes](/doc/code/images/pathfinder_architecture.svg) The relationship between classes in the pathfinder can be seen above. `Grid` is the top-level structure for storage of movement cost. There may be multiple grids defined, one for each movement type From 50797682e37859263d194dd5de1550d75b415e75 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 6 May 2024 20:32:50 +0200 Subject: [PATCH 398/771] path: Set start/target in demo with mouse callbacks. --- libopenage/pathfinding/demo/demo_1.cpp | 61 ++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index d8da5193e8..7fa71997ba 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -2,6 +2,8 @@ #include "demo_1.h" +#include + #include "pathfinding/cost_field.h" #include "pathfinding/grid.h" #include "pathfinding/path.h" @@ -80,11 +82,13 @@ void path_demo_1(const util::Path &path) { util::Timer timer; // Create a path request and get the path - // TODO: Make the path request interactive with window callbacks + coord::tile start{2, 26}; + coord::tile target{36, 2}; + PathRequest path_request{ - 0, - coord::tile{2, 26}, - coord::tile{36, 2}, + grid->get_id(), + start, + target, }; timer.start(); Path path_result = pathfinder->get_path(path_request); @@ -98,6 +102,55 @@ void path_demo_1(const util::Path &path) { auto render_manager = std::make_shared(qtapp, window, path, grid); log::log(INFO << "Created render manager for pathfinding demo"); + window->add_mouse_button_callback([&](const QMouseEvent &ev) { + if (ev.type() == QEvent::MouseButtonRelease) { + auto cell_count_x = grid->get_size()[0] * grid->get_sector_size(); + auto cell_count_y = grid->get_size()[1] * grid->get_sector_size(); + auto window_size = window->get_size(); + + auto cell_size_x = window_size[0] / cell_count_x; + auto cell_size_y = window_size[1] / cell_count_y; + + coord::tile_t grid_x = ev.position().x() / cell_size_x; + coord::tile_t grid_y = ev.position().y() / cell_size_y; + + if (ev.button() == Qt::RightButton) { // Set target cell + target = coord::tile{grid_x, grid_y}; + PathRequest new_path_request{ + grid->get_id(), + start, + target, + }; + + timer.start(); + path_result = pathfinder->get_path(new_path_request); + timer.stop(); + + log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps"); + + // Create renderables for the waypoints of the path + render_manager->create_waypoint_tiles(path_result); + } + else if (ev.button() == Qt::LeftButton) { // Set start cell + start = coord::tile{grid_x, grid_y}; + PathRequest new_path_request{ + grid->get_id(), + start, + target, + }; + + timer.start(); + path_result = pathfinder->get_path(new_path_request); + timer.stop(); + + log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps"); + + // Create renderables for the waypoints of the path + render_manager->create_waypoint_tiles(path_result); + } + } + }); + // Create renderables for the waypoints of the path render_manager->create_waypoint_tiles(path_result); From 405e2dd32fa2a9c93aacded7091fff9a01189c80 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 6 May 2024 20:44:23 +0200 Subject: [PATCH 399/771] path: Draw waypoints in separate render pass. --- libopenage/pathfinding/demo/demo_1.cpp | 29 +++++++++++++++++++++++--- libopenage/pathfinding/demo/demo_1.h | 5 +++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 7fa71997ba..b352f8a91b 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -179,6 +179,7 @@ void RenderManager1::run() { this->renderer->render(this->background_pass); this->renderer->render(this->field_pass); + this->renderer->render(this->waypoint_pass); this->renderer->render(this->grid_pass); this->renderer->render(this->display_pass); @@ -248,6 +249,18 @@ void RenderManager1::init_passes() { renderer::resources::pixel_format::depth24)); auto grid_fbo = renderer->create_texture_target({grid_texture, depth_texture_3}); + // Make a framebuffer for the waypoint render pass to draw into + auto waypoint_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture_4 = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto waypoint_fbo = renderer->create_texture_target({waypoint_texture, depth_texture_4}); + this->waypoint_pass = renderer->add_render_pass({}, waypoint_fbo); + // Create object for the grid auto model = Eigen::Affine3f::Identity(); model.prescale(Eigen::Vector3f{ @@ -289,6 +302,13 @@ void RenderManager1::init_passes() { true, true, }; + auto waypoint_texture_unif = display_shader->new_uniform_input("color_texture", waypoint_texture); + renderer::Renderable waypoint_pass_obj{ + waypoint_texture_unif, + quad, + true, + true, + }; auto grid_texture_unif = display_shader->new_uniform_input("color_texture", grid_texture); renderer::Renderable grid_pass_obj{ grid_texture_unif, @@ -296,8 +316,9 @@ void RenderManager1::init_passes() { true, true, }; + this->display_pass = renderer->add_render_pass( - {bg_pass_obj, field_pass_obj, grid_pass_obj}, + {bg_pass_obj, field_pass_obj, waypoint_pass_obj, grid_pass_obj}, renderer->get_display_target()); } @@ -555,6 +576,8 @@ void RenderManager1::create_waypoint_tiles(const Path &path) { Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity(); + this->waypoint_pass->clear_renderables(); + // Draw in-between waypoints for (size_t i = 1; i < path.waypoints.size() - 1; i++) { auto tile = path.waypoints[i]; @@ -596,7 +619,7 @@ void RenderManager1::create_waypoint_tiles(const Path &path) { true, true, }; - this->field_pass->add_renderables({tile_obj}); + this->waypoint_pass->add_renderables({tile_obj}); } // Draw start and end waypoints with different colors @@ -679,7 +702,7 @@ void RenderManager1::create_waypoint_tiles(const Path &path) { true, }; - this->field_pass->add_renderables({start_tile_obj, end_tile_obj}); + this->waypoint_pass->add_renderables({start_tile_obj, end_tile_obj}); } } // namespace openage::path::tests diff --git a/libopenage/pathfinding/demo/demo_1.h b/libopenage/pathfinding/demo/demo_1.h index eafcc3d45f..db70efe084 100644 --- a/libopenage/pathfinding/demo/demo_1.h +++ b/libopenage/pathfinding/demo/demo_1.h @@ -169,6 +169,11 @@ class RenderManager1 { */ std::shared_ptr grid_pass; + /** + * Waypoint pass: Renders the path and its waypoints. + */ + std::shared_ptr waypoint_pass; + /** * Display pass: Draws the results of previous passes to the screen. */ From e0b63a270bf6548e6112a4c7ed7db0cba696de8b Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 6 May 2024 20:49:33 +0200 Subject: [PATCH 400/771] path: Fix check for flow field count. --- libopenage/pathfinding/pathfinder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 3f3c7fb24f..1b6cfe91e2 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -194,7 +194,7 @@ const std::vector> Pathfinder::portal_a_star(const PathR const std::vector Pathfinder::get_waypoints(const std::vector>> &flow_fields, const PathRequest &request) const { - ENSURE(flow_fields.size() > 1, "At least 1 flow field is required for finding waypoints."); + ENSURE(flow_fields.size() > 0, "At least 1 flow field is required for finding waypoints."); std::vector waypoints; From 8b1d427e847c49d353e07317697cfdb679bde433 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 6 May 2024 21:14:07 +0200 Subject: [PATCH 401/771] path: Use distance between portal centers for A star distance cost. --- libopenage/pathfinding/pathfinder.cpp | 15 ++++++++++++++- libopenage/pathfinding/pathfinder.h | 10 ++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 1b6cfe91e2..b5322f140c 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -104,7 +104,7 @@ const std::vector> Pathfinder::portal_a_star(const PathR // Cost to travel from one portal to another // TODO: Determine this cost for each portal - const int distance_cost = 1; + // const int distance_cost = 1; // start nodes: all portals in the start sector for (auto &portal : start_sector->get_portals()) { @@ -156,6 +156,11 @@ const std::vector> Pathfinder::portal_a_star(const PathR continue; } + // Get distance cost from current node to exit + auto distance_cost = Pathfinder::distance_cost( + current_node->portal->get_exit_center(current_node->entry_sector), + exit->portal->get_entry_center(exit->entry_sector)); + bool not_visited = !visited_portals.contains(exit->portal->get_id()); auto tentative_cost = current_node->current_cost + distance_cost; @@ -328,6 +333,14 @@ int Pathfinder::heuristic_cost(const coord::tile &portal_pos, return delta.length(); } +int Pathfinder::distance_cost(const coord::tile &portal1_pos, + const coord::tile &portal2_pos) { + auto delta = portal2_pos.to_phys2() - portal1_pos.to_phys2(); + + return delta.length(); +} + + PortalNode::PortalNode(const std::shared_ptr &portal, sector_id_t entry_sector, const node_t &prev_portal) : diff --git a/libopenage/pathfinding/pathfinder.h b/libopenage/pathfinding/pathfinder.h index e6e43a7abf..a4df0350d8 100644 --- a/libopenage/pathfinding/pathfinder.h +++ b/libopenage/pathfinding/pathfinder.h @@ -92,6 +92,16 @@ class Pathfinder { */ static int heuristic_cost(const coord::tile &portal_pos, const coord::tile &target_pos); + /** + * Calculate the distance cost between two portals. + * + * @param portal1_pos Center of the first portal. + * @param portal2_pos Center of the second portal. + * + * @return Distance cost between the portal centers. + */ + static int distance_cost(const coord::tile &portal1_pos, const coord::tile &portal2_pos); + /** * Grids managed by this pathfinder. * From 9ebe429c391c23afd005a1a87eecd23ebee1e97e Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 6 May 2024 21:19:58 +0200 Subject: [PATCH 402/771] path: Fix setting start/target position. --- libopenage/pathfinding/demo/demo_1.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index b352f8a91b..f3bd42f625 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -108,8 +108,8 @@ void path_demo_1(const util::Path &path) { auto cell_count_y = grid->get_size()[1] * grid->get_sector_size(); auto window_size = window->get_size(); - auto cell_size_x = window_size[0] / cell_count_x; - auto cell_size_y = window_size[1] / cell_count_y; + double cell_size_x = static_cast(window_size[0]) / cell_count_x; + double cell_size_y = static_cast(window_size[1]) / cell_count_y; coord::tile_t grid_x = ev.position().x() / cell_size_x; coord::tile_t grid_y = ev.position().y() / cell_size_y; From c6b7b28653ceba2dab2b2c7ac685a121adcda6bd Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 6 May 2024 21:53:24 +0200 Subject: [PATCH 403/771] path: Use window_settings type to set window size, --- libopenage/pathfinding/demo/demo_0.cpp | 7 ++++++- libopenage/pathfinding/demo/demo_1.cpp | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index e87b44d1f0..f8970459d4 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -59,7 +59,12 @@ void path_demo_0(const util::Path &path) { // Render the grid and the pathfinding results auto qtapp = std::make_shared(); - auto window = std::make_shared("openage pathfinding test", 1440, 720); + + // Create a window for rendering + renderer::window_settings settings; + settings.width = 1440; + settings.height = 720; + auto window = std::make_shared("openage pathfinding test", settings); auto render_manager = std::make_shared(qtapp, window, path); log::log(INFO << "Created render manager for pathfinding demo"); diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index f3bd42f625..be34e95af2 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -98,7 +98,11 @@ void path_demo_1(const util::Path &path) { // Create a renderer to display the grid and path auto qtapp = std::make_shared(); - auto window = std::make_shared("openage pathfinding test", 1024, 768); + + renderer::window_settings settings; + settings.width = 1024; + settings.height = 768; + auto window = std::make_shared("openage pathfinding test", settings); auto render_manager = std::make_shared(qtapp, window, path, grid); log::log(INFO << "Created render manager for pathfinding demo"); From 9a5ac8d925b48bf282d032ef779189ae2551d659 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 7 May 2024 00:24:21 +0200 Subject: [PATCH 404/771] path: Avoid narrowing conversion to coord types. --- libopenage/pathfinding/demo/demo_0.cpp | 26 ++++++++++---------- libopenage/pathfinding/demo/demo_1.cpp | 6 ++--- libopenage/pathfinding/grid.cpp | 7 +++--- libopenage/pathfinding/integration_field.cpp | 2 +- libopenage/pathfinding/pathfinder.cpp | 18 +++++++------- libopenage/pathfinding/sector.cpp | 16 ++++++------ 6 files changed, 37 insertions(+), 38 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index f8970459d4..9918d22bdc 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -272,7 +272,7 @@ void RenderManager0::show_vectors(const std::shared_ptr &field) this->vector_pass->clear_renderables(); for (size_t y = 0; y < field->get_size(); ++y) { for (size_t x = 0; x < field->get_size(); ++x) { - auto cell = field->get_cell(coord::tile{x, y}); + auto cell = field->get_cell(coord::tile(x, y)); if (cell & FLOW_PATHABLE_MASK and not(cell & FLOW_LOS_MASK)) { Eigen::Affine3f arrow_model = Eigen::Affine3f::Identity(); arrow_model.translate(Eigen::Vector3f(y + 0.5, 0, -1.0f * x - 0.5)); @@ -563,19 +563,19 @@ renderer::resources::MeshData RenderManager0::get_cost_field_mesh(const std::sha // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cost(coord::tile{(i - 1) / resolution, (j - 1) / resolution}); + auto cost = field->get_cost(coord::tile((i - 1) / resolution, (j - 1) / resolution)); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cost(coord::tile{(i - 1) / resolution, j / resolution}); + auto cost = field->get_cost(coord::tile((i - 1) / resolution, j / resolution)); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cost(coord::tile{i / resolution, j / resolution}); + auto cost = field->get_cost(coord::tile(i / resolution, j / resolution)); surround.push_back(cost); } if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cost(coord::tile{i / resolution, (j - 1) / resolution}); + auto cost = field->get_cost(coord::tile(i / resolution, (j - 1) / resolution)); surround.push_back(cost); } // use the cost of the most expensive surrounding tile @@ -649,19 +649,19 @@ renderer::resources::MeshData RenderManager0::get_integration_field_mesh(const s // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile{(i - 1) / resolution, (j - 1) / resolution}).cost; + auto cost = field->get_cell(coord::tile((i - 1) / resolution, (j - 1) / resolution)).cost; surround.push_back(cost); } if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile{(i - 1) / resolution, j / resolution}).cost; + auto cost = field->get_cell(coord::tile((i - 1) / resolution, j / resolution)).cost; surround.push_back(cost); } if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile{i / resolution, j / resolution}).cost; + auto cost = field->get_cell(coord::tile(i / resolution, j / resolution)).cost; surround.push_back(cost); } if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile{i / resolution, (j - 1) / resolution}).cost; + auto cost = field->get_cell(coord::tile(i / resolution, (j - 1) / resolution)).cost; surround.push_back(cost); } // use the cost of the most expensive surrounding tile @@ -735,19 +735,19 @@ renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::sha // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile{(i - 1) / resolution, (j - 1) / resolution}); + auto cost = field->get_cell(coord::tile((i - 1) / resolution, (j - 1) / resolution)); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile{(i - 1) / resolution, j / resolution}); + auto cost = field->get_cell(coord::tile((i - 1) / resolution, j / resolution)); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile{i / resolution, j / resolution}); + auto cost = field->get_cell(coord::tile(i / resolution, j / resolution)); surround.push_back(cost); } if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile{i / resolution, (j - 1) / resolution}); + auto cost = field->get_cell(coord::tile(i / resolution, (j - 1) / resolution)); surround.push_back(cost); } // use the cost of the most expensive surrounding tile diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index be34e95af2..1a4ea42ac6 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -445,7 +445,7 @@ void RenderManager1::create_impassible_tiles(const std::shared_ptr & auto cost_field = sector->get_cost_field(); for (size_t y = 0; y < sector_size; y++) { for (size_t x = 0; x < sector_size; x++) { - auto cost = cost_field->get_cost(coord::tile{x, y}); + auto cost = cost_field->get_cost(coord::tile(x, y)); if (cost == COST_IMPASSABLE) { std::array tile_data{ -1.0f + x * tile_offset_width + sector_size * sector_x * tile_offset_width, @@ -514,13 +514,13 @@ void RenderManager1::create_portal_tiles(const std::shared_ptr &grid if (direction == PortalDirection::NORTH_SOUTH) { auto y = start.se; for (auto x = start.ne; x <= end.ne; ++x) { - tiles.push_back(coord::tile{x, y}); + tiles.push_back(coord::tile(x, y)); } } else { auto x = start.ne; for (auto y = start.se; y <= end.se; ++y) { - tiles.push_back(coord::tile{x, y}); + tiles.push_back(coord::tile(x, y)); } } diff --git a/libopenage/pathfinding/grid.cpp b/libopenage/pathfinding/grid.cpp index 4100492c16..62a2d3156f 100644 --- a/libopenage/pathfinding/grid.cpp +++ b/libopenage/pathfinding/grid.cpp @@ -20,10 +20,9 @@ Grid::Grid(grid_id_t id, for (size_t y = 0; y < size[1]; y++) { for (size_t x = 0; x < size[0]; x++) { this->sectors.push_back( - std::make_shared( - x + y * this->size[0], - coord::chunk{x, y}, - sector_size)); + std::make_shared(x + y * this->size[0], + coord::chunk(x, y), + sector_size)); } } } diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 201823eec4..bc2745879f 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -98,7 +98,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrget_los_corners(cost_field, target, coord::tile{x, y}); + auto corners = this->get_los_corners(cost_field, target, coord::tile(x, y)); for (auto &corner : corners) { // draw a line from the corner to the edge of the field // to get the cells blocked by the corner diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index b5322f140c..7e09278f7c 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -112,7 +112,7 @@ const std::vector> Pathfinder::portal_a_star(const PathR auto sector_pos = grid->get_sector(portal->get_exit_sector(start_sector->get_id()))->get_position(); auto portal_pos = portal->get_exit_center(start_sector->get_id()); - auto portal_abs_pos = portal_pos + coord::tile_delta{sector_pos.ne * sector_size, sector_pos.se * sector_size}; + auto portal_abs_pos = portal_pos + coord::tile_delta(sector_pos.ne * sector_size, sector_pos.se * sector_size); auto heuristic_cost = Pathfinder::heuristic_cost(portal_abs_pos, request.target); portal_node->current_cost = 0; @@ -226,21 +226,21 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_position(); - auto cell_pos = coord::tile{sector_pos.ne * sector_size, - sector_pos.se * sector_size} - + coord::tile_delta{current_x, current_y}; + auto cell_pos = coord::tile(sector_pos.ne * sector_size, + sector_pos.se * sector_size) + + coord::tile_delta(current_x, current_y); waypoints.push_back(cell_pos); break; } // check if we need to change direction - auto cell_direction = flow_field->get_dir(coord::tile{current_x, current_y}); + auto cell_direction = flow_field->get_dir(coord::tile(current_x, current_y)); if (cell_direction != current_direction) { // add the current cell as a waypoint auto sector_pos = sector->get_position(); - auto cell_pos = coord::tile{sector_pos.ne * sector_size, - sector_pos.se * sector_size} - + coord::tile_delta{current_x, current_y}; + auto cell_pos = coord::tile(sector_pos.ne * sector_size, + sector_pos.se * sector_size) + + coord::tile_delta(current_x, current_y); waypoints.push_back(cell_pos); current_direction = cell_direction; } @@ -317,7 +317,7 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_sector(flow_fields.back().first)->get_position(); - auto target_pos = coord::tile{sector_pos.ne * sector_size, sector_pos.se * sector_size} + auto target_pos = coord::tile(sector_pos.ne * sector_size, sector_pos.se * sector_size) + coord::tile_delta{target_x, target_y}; waypoints.push_back(target_pos); diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index 6b56114e47..cf6ffaa755 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -65,13 +65,13 @@ std::vector> Sector::find_portals(const std::shared_ptr< auto coord_other = coord::tile{0, 0}; if (direction == PortalDirection::NORTH_SOUTH) { // right edge; top to bottom - coord_this = coord::tile{i, this->cost_field->get_size() - 1}; - coord_other = coord::tile{i, 0}; + coord_this = coord::tile(i, this->cost_field->get_size() - 1); + coord_other = coord::tile(i, 0); } else if (direction == PortalDirection::EAST_WEST) { // bottom edge; east to west - coord_this = coord::tile{this->cost_field->get_size() - 1, i}; - coord_other = coord::tile{0, i}; + coord_this = coord::tile(this->cost_field->get_size() - 1, i); + coord_other = coord::tile(0, i); } if (this->cost_field->get_cost(coord_this) != COST_IMPASSABLE @@ -98,13 +98,13 @@ std::vector> Sector::find_portals(const std::shared_ptr< auto coord_end = coord::tile{0, 0}; if (direction == PortalDirection::NORTH_SOUTH) { // right edge; top to bottom - coord_start = coord::tile{start, this->cost_field->get_size() - 1}; - coord_end = coord::tile{end, this->cost_field->get_size() - 1}; + coord_start = coord::tile(start, this->cost_field->get_size() - 1); + coord_end = coord::tile(end, this->cost_field->get_size() - 1); } else if (direction == PortalDirection::EAST_WEST) { // bottom edge; east to west - coord_start = coord::tile{this->cost_field->get_size() - 1, start}; - coord_end = coord::tile{this->cost_field->get_size() - 1, end}; + coord_start = coord::tile(this->cost_field->get_size() - 1, start); + coord_end = coord::tile(this->cost_field->get_size() - 1, end); } result.push_back( From cfebd22862a1374d3bbac6428e60b491a8b1881b Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 7 May 2024 23:04:01 +0200 Subject: [PATCH 405/771] path: Reset timer when path is recalculated. --- libopenage/pathfinding/demo/demo_1.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 1a4ea42ac6..5a90f97218 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -126,6 +126,7 @@ void path_demo_1(const util::Path &path) { target, }; + timer.reset(); timer.start(); path_result = pathfinder->get_path(new_path_request); timer.stop(); @@ -143,6 +144,7 @@ void path_demo_1(const util::Path &path) { target, }; + timer.reset(); timer.start(); path_result = pathfinder->get_path(new_path_request); timer.stop(); From 12cd944d805abe483b3edbae724de24f3a2e8e42 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 24 May 2024 03:27:07 +0200 Subject: [PATCH 406/771] path: Fix includes after rebase. --- libopenage/pathfinding/demo/demo_0.cpp | 4 +++- libopenage/pathfinding/demo/demo_1.cpp | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 9918d22bdc..da7ae17492 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -12,6 +12,8 @@ #include "renderer/camera/camera.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" +#include "renderer/render_pass.h" +#include "renderer/renderable.h" #include "renderer/renderer.h" #include "renderer/resources/mesh_data.h" #include "renderer/resources/shader_source.h" @@ -298,7 +300,7 @@ void RenderManager0::show_vectors(const std::shared_ptr &field) true, true, }; - this->vector_pass->add_renderables(arrow_renderable); + this->vector_pass->add_renderables(std::move(arrow_renderable)); } } } diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 5a90f97218..21aaa2c9d8 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -14,6 +14,8 @@ #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" +#include "renderer/render_pass.h" +#include "renderer/renderable.h" #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" #include "renderer/shader_program.h" From 8dcd86d2ee5ae9211516690497a30a9641e63a30 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 24 May 2024 03:55:06 +0200 Subject: [PATCH 407/771] demo: Fix missing imports for gcc 14. --- .../main/demo/interactive/interactive.cpp | 2 ++ .../renderer/vulkan/graphics_device.cpp | 31 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/libopenage/main/demo/interactive/interactive.cpp b/libopenage/main/demo/interactive/interactive.cpp index 69a8d83948..9a4f904697 100644 --- a/libopenage/main/demo/interactive/interactive.cpp +++ b/libopenage/main/demo/interactive/interactive.cpp @@ -2,6 +2,8 @@ #include "interactive.h" +#include + #include "log/log.h" #include "assets/mod_manager.h" diff --git a/libopenage/renderer/vulkan/graphics_device.cpp b/libopenage/renderer/vulkan/graphics_device.cpp index 2853c3f34e..2190501646 100644 --- a/libopenage/renderer/vulkan/graphics_device.cpp +++ b/libopenage/renderer/vulkan/graphics_device.cpp @@ -2,6 +2,7 @@ #include "graphics_device.h" +#include #include #include "../../error/error.h" @@ -21,7 +22,7 @@ std::optional VlkGraphicsDevice::find_device_surface_supp // Figure out if any of the families supports graphics for (size_t i = 0; i < q_fams.size(); i++) { - auto const& q_fam = q_fams[i]; + auto const &q_fam = q_fams[i]; if (q_fam.queueCount > 0) { if ((q_fam.queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0u) { @@ -52,10 +53,11 @@ std::optional VlkGraphicsDevice::find_device_surface_supp if (maybe_present_fam) { details.graphics_fam = *maybe_graphics_fam; details.maybe_present_fam = {}; - } else { + } + else { // Otherwise look for a present-only queue for (size_t i = 0; i < q_fams.size(); i++) { - auto const& q_fam = q_fams[i]; + auto const &q_fam = q_fams[i]; if (q_fam.queueCount > 0) { VkBool32 support = VK_FALSE; vkGetPhysicalDeviceSurfaceSupportKHR(dev, i, surf, &support); @@ -88,9 +90,8 @@ std::optional VlkGraphicsDevice::find_device_surface_supp return details; } -VlkGraphicsDevice::VlkGraphicsDevice(VkPhysicalDevice dev, std::vector const& q_fams) - : phys_device(dev) -{ +VlkGraphicsDevice::VlkGraphicsDevice(VkPhysicalDevice dev, std::vector const &q_fams) : + phys_device(dev) { // Prepare queue creation info for each family requested std::vector q_infos(q_fams.size()); const float p = 1.0f; @@ -103,15 +104,15 @@ VlkGraphicsDevice::VlkGraphicsDevice(VkPhysicalDevice dev, std::vector } // Request these extensions - std::vector ext_names = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; + std::vector ext_names = {VK_KHR_SWAPCHAIN_EXTENSION_NAME}; // Check if extensions are available auto exts = vk_do_ritual(vkEnumerateDeviceExtensionProperties, dev, nullptr); for (auto ext : ext_names) { - if (std::count_if(exts.begin(), exts.end(), [=] (VkExtensionProperties const& p) { - return std::strcmp(p.extensionName, ext) == 0; - } ) == 0 - ) { + if (std::count_if(exts.begin(), exts.end(), [=](VkExtensionProperties const &p) { + return std::strcmp(p.extensionName, ext) == 0; + }) + == 0) { throw Error(MSG(err) << "Tried to instantiate device, but it's missing this extension: " << ext); } } @@ -122,21 +123,21 @@ VlkGraphicsDevice::VlkGraphicsDevice(VkPhysicalDevice dev, std::vector vkGetPhysicalDeviceProperties(this->phys_device, &dev_props); log::log(MSG(dbg) << "Chosen Vulkan graphics device: " << dev_props.deviceName); log::log(MSG(dbg) << "Device extensions:"); - for (auto const& ext : exts) { + for (auto const &ext : exts) { log::log(MSG(dbg) << "\t" << ext.extensionName); } } #endif // Prepare device creation - VkDeviceCreateInfo create_dev {}; + VkDeviceCreateInfo create_dev{}; create_dev.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; create_dev.queueCreateInfoCount = q_infos.size(); create_dev.pQueueCreateInfos = q_infos.data(); create_dev.enabledExtensionCount = ext_names.size(); create_dev.ppEnabledExtensionNames = ext_names.data(); - VkPhysicalDeviceFeatures features {}; + VkPhysicalDeviceFeatures features{}; // TODO request features create_dev.pEnabledFeatures = &features; @@ -165,4 +166,4 @@ VlkGraphicsDevice::~VlkGraphicsDevice() { vkDestroyDevice(this->device, nullptr); } -} // openage::renderer::vulkan +} // namespace openage::renderer::vulkan From 2de580db3403b6bd053267691da18488975ca7ce Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 25 May 2024 12:30:51 +0200 Subject: [PATCH 408/771] Fix copyright years. --- libopenage/main/demo/interactive/interactive.cpp | 2 +- libopenage/renderer/vulkan/graphics_device.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/main/demo/interactive/interactive.cpp b/libopenage/main/demo/interactive/interactive.cpp index 9a4f904697..d38e8b2c85 100644 --- a/libopenage/main/demo/interactive/interactive.cpp +++ b/libopenage/main/demo/interactive/interactive.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 "interactive.h" diff --git a/libopenage/renderer/vulkan/graphics_device.cpp b/libopenage/renderer/vulkan/graphics_device.cpp index 2190501646..25874225d3 100644 --- a/libopenage/renderer/vulkan/graphics_device.cpp +++ b/libopenage/renderer/vulkan/graphics_device.cpp @@ -1,4 +1,4 @@ -// Copyright 2017-2019 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #include "graphics_device.h" From 9f478eef78671d82d0e9f6e9f2bea40d0ee92411 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 May 2024 23:47:22 +0200 Subject: [PATCH 409/771] ci: Fix cask install for brew. --- .github/workflows/macosx-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/macosx-ci.yml b/.github/workflows/macosx-ci.yml index 253a5f359b..e30906228d 100644 --- a/.github/workflows/macosx-ci.yml +++ b/.github/workflows/macosx-ci.yml @@ -53,7 +53,7 @@ jobs: mv clang+llvm-15.0.0-x86_64-apple-darwin clang-15.0.0 ~/clang-15.0.0/bin/clang++ --version - name: Brew install DeJaVu fonts - run: brew tap homebrew/cask-fonts && brew install --force font-dejavu + run: brew install --cask font-dejavu - name: Remove python's 2to3 link so that 'brew link' does not fail run: rm /usr/local/bin/2to3* && rm /usr/local/bin/idle3* - name: Install environment helpers with homebrew From 4cdf85aa64a0e5ff5c66b06636b3d84fce9a81de Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 8 Jul 2024 00:38:51 +0200 Subject: [PATCH 410/771] log: Make common log functions consteval. --- libopenage/log/message.h | 6 +++--- libopenage/util/{constexpr.h => consteval.h} | 22 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) rename libopenage/util/{constexpr.h => consteval.h} (79%) diff --git a/libopenage/log/message.h b/libopenage/log/message.h index 09b888a9ac..d46ed18c7d 100644 --- a/libopenage/log/message.h +++ b/libopenage/log/message.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -11,7 +11,7 @@ #include #include "../util/compiler.h" -#include "../util/constexpr.h" +#include "../util/consteval.h" #include "../util/stringformatter.h" #include "config.h" #include "logsink.h" @@ -165,7 +165,7 @@ class OAAPI MessageBuilder : public util::StringFormatter { // for use with existing log::level objects #define MSG_LVLOBJ(LVLOBJ) \ ::openage::log::MessageBuilder( \ - ::openage::util::constexpr_::strip_prefix( \ + ::openage::util::consteval_::strip_prefix( \ __FILE__, \ ::openage::config::buildsystem_sourcefile_dir), \ __LINE__, \ diff --git a/libopenage/util/constexpr.h b/libopenage/util/consteval.h similarity index 79% rename from libopenage/util/constexpr.h rename to libopenage/util/consteval.h index 20b8f15c4a..8ddcd22f09 100644 --- a/libopenage/util/constexpr.h +++ b/libopenage/util/consteval.h @@ -6,15 +6,15 @@ /** - * This namespace contains constexpr functions, i.e. C++14 functions that are designed - * to run at compile-time. + * This namespace contains consteval functions, i.e. C++20 functions that are designed + * to be evaluated at compile-time. */ -namespace openage::util::constexpr_ { +namespace openage::util::consteval_ { /** * Returns true IFF the string literals have equal content. */ -constexpr bool streq(const char *a, const char *b) { +consteval bool streq(const char *a, const char *b) { for (; *a == *b; ++a, ++b) { if (*a == '\0') { return true; @@ -27,7 +27,7 @@ constexpr bool streq(const char *a, const char *b) { /** * Returns the length of the string literal, excluding the terminating NULL byte. */ -constexpr size_t strlen(const char *str) { +consteval size_t strlen(const char *str) { for (size_t len = 0;; ++len) { if (str[len] == '\0') { return len; @@ -56,7 +56,7 @@ struct truncated_string_literal { * * Raises 'false' if str doesn't end in the given suffix. */ -constexpr truncated_string_literal get_prefix(const char *str, const char *suffix) { +consteval truncated_string_literal get_prefix(const char *str, const char *suffix) { if (strlen(str) < strlen(suffix)) { // suffix is longer than str throw false; @@ -74,14 +74,14 @@ constexpr truncated_string_literal get_prefix(const char *str, const char *suffi /** * Creates a truncated_string_literal from a regular string literal. */ -constexpr truncated_string_literal create_truncated_string_literal(const char *str) { +consteval truncated_string_literal create_truncated_string_literal(const char *str) { return truncated_string_literal{str, strlen(str)}; } /** * Tests whether a string literal starts with the given prefix. */ -constexpr bool has_prefix(const char *str, const truncated_string_literal prefix) { +consteval bool has_prefix(const char *str, const truncated_string_literal prefix) { for (size_t pos = 0; pos < prefix.length; ++pos) { if (str[pos] != prefix.literal[pos]) { return false; @@ -96,7 +96,7 @@ constexpr bool has_prefix(const char *str, const truncated_string_literal prefix * * If the string literal doesn't have that prefix, returns the string literal itself. */ -constexpr const char *strip_prefix(const char *str, const truncated_string_literal prefix) { +consteval const char *strip_prefix(const char *str, const truncated_string_literal prefix) { if (has_prefix(str, prefix)) { return str + prefix.length; } @@ -111,8 +111,8 @@ constexpr const char *strip_prefix(const char *str, const truncated_string_liter * * If the string literal doesn't have that prefix, returns the string literal itself. */ -constexpr const char *strip_prefix(const char *str, const char *prefix) { +consteval const char *strip_prefix(const char *str, const char *prefix) { return strip_prefix(str, create_truncated_string_literal(prefix)); } -} // namespace openage::util::constexpr_ +} // namespace openage::util::consteval_ From ad6a87052a135d7a75df8c601c5c8ee56f5e2b9c Mon Sep 17 00:00:00 2001 From: Nikhil Ghosh Date: Thu, 25 Apr 2024 23:14:02 -0400 Subject: [PATCH 411/771] Fixed bug with opening files with \r on windows --- libopenage/renderer/resources/parser/common.cpp | 8 +++++++- libopenage/renderer/resources/parser/parse_sprite.cpp | 4 ++-- libopenage/renderer/resources/parser/parse_terrain.cpp | 4 ++-- libopenage/renderer/resources/parser/parse_texture.cpp | 9 +++++++-- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/libopenage/renderer/resources/parser/common.cpp b/libopenage/renderer/resources/parser/common.cpp index 30fa97118c..ab6a1b205f 100644 --- a/libopenage/renderer/resources/parser/common.cpp +++ b/libopenage/renderer/resources/parser/common.cpp @@ -20,7 +20,13 @@ TextureData parse_texture(const std::vector &args) { texture.texture_id = std::stoul(args[1]); // Call substr() to get rid of the quotes - texture.path = args[2].substr(1, args[2].size() - 2); + // If the line ends in a carriage return, remove it as well + if (args[2][args[2].size() - 1] == '\r') { + texture.path = args[2].substr(1, args[2].size() - 3); + } + else { + texture.path = args[2].substr(1, args[2].size() - 2); + } return texture; } diff --git a/libopenage/renderer/resources/parser/parse_sprite.cpp b/libopenage/renderer/resources/parser/parse_sprite.cpp index 9ea4afac1c..0974f98342 100644 --- a/libopenage/renderer/resources/parser/parse_sprite.cpp +++ b/libopenage/renderer/resources/parser/parse_sprite.cpp @@ -195,8 +195,8 @@ Animation2dInfo parse_sprite_file(const util::Path &file, })}; for (auto line : lines) { - // Skip empty lines and comments - if (line.empty() || line.substr(0, 1) == "#") { + // Skip empty lines, lines with carriage returns, and comments + if (line.empty() || line.substr(0, 1) == "#" || line[0] == '\r') { continue; } std::vector args{util::split(line, ' ')}; diff --git a/libopenage/renderer/resources/parser/parse_terrain.cpp b/libopenage/renderer/resources/parser/parse_terrain.cpp index 4330606091..26dc77f10b 100644 --- a/libopenage/renderer/resources/parser/parse_terrain.cpp +++ b/libopenage/renderer/resources/parser/parse_terrain.cpp @@ -200,8 +200,8 @@ TerrainInfo parse_terrain_file(const util::Path &file, })}; for (auto line : lines) { - // Skip empty lines and comments - if (line.empty() || line.substr(0, 1) == "#") { + // Skip empty lines, lines with carriage returns, and comments + if (line.empty() || line.substr(0, 1) == "#" || line[0] == '\r') { continue; } std::vector args{util::split(line, ' ')}; diff --git a/libopenage/renderer/resources/parser/parse_texture.cpp b/libopenage/renderer/resources/parser/parse_texture.cpp index 7da91211f4..a0af8d6f73 100644 --- a/libopenage/renderer/resources/parser/parse_texture.cpp +++ b/libopenage/renderer/resources/parser/parse_texture.cpp @@ -56,6 +56,11 @@ std::string parse_imagefile(const std::vector &args) { // it should result in an error if wrongly used here. // Call substr() to get rid of the quotes + // If the line ends in a carriage return, remove it as well + if (args[1][args[1].size() - 1] == '\r') { + return args[1].substr(1, args[1].size() - 3); + } + return args[1].substr(1, args[1].size() - 2); } @@ -179,8 +184,8 @@ Texture2dInfo parse_texture_file(const util::Path &file) { })}; for (auto line : lines) { - // Skip empty lines and comments - if (line.empty() || line.substr(0, 1) == "#") { + // Skip empty lines, lines with carriage returns, and comments + if (line.empty() || line.substr(0, 1) == "#" || line[0] == '\r') { continue; } std::vector args{util::split(line, ' ')}; From 07e7c62408460d4da33ee7b2907eebd1c7f635bf Mon Sep 17 00:00:00 2001 From: Nikhil Ghosh Date: Thu, 25 Apr 2024 23:14:54 -0400 Subject: [PATCH 412/771] Implemented frustum culling --- libopenage/renderer/camera/CMakeLists.txt | 1 + libopenage/renderer/camera/camera.cpp | 30 +++- libopenage/renderer/camera/camera.h | 24 ++- libopenage/renderer/camera/frustum.cpp | 165 ++++++++++++++++++ libopenage/renderer/camera/frustum.h | 46 +++++ libopenage/renderer/stages/world/object.cpp | 9 + libopenage/renderer/stages/world/object.h | 2 + .../renderer/stages/world/render_stage.cpp | 6 + 8 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 libopenage/renderer/camera/frustum.cpp create mode 100644 libopenage/renderer/camera/frustum.h diff --git a/libopenage/renderer/camera/CMakeLists.txt b/libopenage/renderer/camera/CMakeLists.txt index f4ae421320..43a49cc1c7 100644 --- a/libopenage/renderer/camera/CMakeLists.txt +++ b/libopenage/renderer/camera/CMakeLists.txt @@ -1,3 +1,4 @@ add_sources(libopenage camera.cpp + frustum.cpp ) diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index 884aceca1b..d43d30b102 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -25,11 +25,15 @@ Camera::Camera(const std::shared_ptr &renderer, moved{true}, zoom_changed{true}, view{Eigen::Matrix4f::Identity()}, - proj{Eigen::Matrix4f::Identity()} { + proj{Eigen::Matrix4f::Identity()}, + frustum_culling{false} { this->look_at_scene(Eigen::Vector3f(0.0f, 0.0f, 0.0f)); this->init_uniform_buffer(renderer); + float real_zoom = 0.7f * this->default_zoom_ratio * this->zoom; + frustum.Recalculate(this->viewport_size, near_distance, far_distance, this->scene_pos, cam_direction, Eigen::Vector3f(0.0f, 1.0f, 0.0f), real_zoom); + log::log(INFO << "Created new camera at position " << "(" << this->scene_pos[0] << ", " << this->scene_pos[1] @@ -41,7 +45,8 @@ Camera::Camera(const std::shared_ptr &renderer, Eigen::Vector3f scene_pos, float zoom, float max_zoom_out, - float default_zoom_ratio) : + float default_zoom_ratio, + bool frustum_culling) : scene_pos{scene_pos}, viewport_size{viewport_size}, zoom{zoom}, @@ -51,9 +56,13 @@ Camera::Camera(const std::shared_ptr &renderer, zoom_changed{true}, viewport_changed{true}, view{Eigen::Matrix4f::Identity()}, - proj{Eigen::Matrix4f::Identity()} { + proj{Eigen::Matrix4f::Identity()}, + frustum_culling{frustum_culling} { this->init_uniform_buffer(renderer); + float real_zoom = 0.7f * this->default_zoom_ratio * this->zoom; + frustum.Recalculate(this->viewport_size, near_distance, far_distance, this->scene_pos, cam_direction, Eigen::Vector3f(0.0f, 1.0f, 0.0f), real_zoom); + log::log(INFO << "Created new camera at position " << "(" << this->scene_pos[0] << ", " << this->scene_pos[1] @@ -107,6 +116,9 @@ void Camera::move_to(Eigen::Vector3f scene_pos) { this->scene_pos = scene_pos; this->moved = true; + + float real_zoom = 0.7f * this->default_zoom_ratio * this->zoom; + frustum.Recalculate(viewport_size, near_distance, far_distance, scene_pos, cam_direction, Eigen::Vector3f(0.0f, 1.0f, 0.0f), real_zoom); } void Camera::move_rel(Eigen::Vector3f direction, float delta) { @@ -274,4 +286,16 @@ void Camera::init_uniform_buffer(const std::shared_ptr &renderer) { this->uniform_buffer = renderer->add_uniform_buffer(ubo_info); } +bool Camera::using_frustum_culling() const { + return this->frustum_culling; +} + +bool Camera::is_in_frustum(Eigen::Vector3f pos) const{ + if (!this->frustum_culling) { + return true; + } + + return frustum.is_in_frustum(pos); +} + } // namespace openage::renderer::camera diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index c8c5c23076..56fdccef8a 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -12,6 +12,8 @@ #include "coord/scene.h" #include "util/vector.h" +#include "frustum.h" + namespace openage::renderer { class Renderer; class UniformBuffer; @@ -30,6 +32,9 @@ static const Eigen::Vector3f cam_direction{ -1 * (std::sqrt(6.f) / 4), }; +static const float near_distance = 0.01f; +static const float far_distance = 100.0f; + /** * Camera for selecting what part of the ingame world is displayed. * @@ -58,13 +63,15 @@ class Camera { * @param zoom Zoom level of the camera (defaults to 1.0f). * @param max_zoom_out Maximum zoom out level (defaults to 64.0f). * @param default_zoom_ratio Default zoom level calibration (defaults to (1.0f / 49)). + * @param frustum_culling Is frustum culling enables (defaults to false). */ Camera(const std::shared_ptr &renderer, util::Vector2s viewport_size, Eigen::Vector3f scene_pos, float zoom = 1.0f, float max_zoom_out = 64.0f, - float default_zoom_ratio = 1.0f / 49); + float default_zoom_ratio = 1.0f / 49, + bool frustum_culling = false); ~Camera() = default; /** @@ -188,6 +195,10 @@ class Camera { */ const std::shared_ptr &get_uniform_buffer() const; + bool using_frustum_culling() const; + + bool is_in_frustum(Eigen::Vector3f pos) const; + private: /** * Create the uniform buffer for the camera. @@ -281,6 +292,17 @@ class Camera { * Uniform buffer for the camera matrices. */ std::shared_ptr uniform_buffer; + + /** + * Is frustum culling enabled? If true, + */ + bool frustum_culling; + + /** + * The frustum used to cull objects + * Will be recalculated regardless of whether frustum culling is enabled + */ + Frustum frustum; }; } // namespace camera diff --git a/libopenage/renderer/camera/frustum.cpp b/libopenage/renderer/camera/frustum.cpp new file mode 100644 index 0000000000..d79058f411 --- /dev/null +++ b/libopenage/renderer/camera/frustum.cpp @@ -0,0 +1,165 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "frustum.h" + +#include +#include +#include +#include + +#include "coord/pixel.h" +#include "coord/scene.h" +#include "renderer/renderer.h" +#include "renderer/resources/buffer_info.h" + +namespace openage::renderer::camera { + +/* + Eigen::Vector3f top_face_normal; + float top_face_distance; + + Eigen::Vector3f bottom_face_normal; + float bottom_face_distance; + + Eigen::Vector3f right_face_normal; + float right_face_distance; + + Eigen::Vector3f left_face_normal; + float left_face_distance; + + Eigen::Vector3f far_face_normal; + float far_face_distance; + + Eigen::Vector3f near_face_normal; + float near_face_distance; +*/ + +Frustum::Frustum() : + top_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, + top_face_distance{0.0f}, + bottom_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, + bottom_face_distance{0.0f}, + right_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, + right_face_distance{0.0f}, + left_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, + left_face_distance{0.0f}, + far_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, + far_face_distance{0.0f}, + near_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, + near_face_distance{0.0f} +{ +} + +void Frustum::Recalculate(util::Vector2s &viewport_size, float near_distance, float far_distance, Eigen::Vector3f &scene_pos, Eigen::Vector3f cam_direction, Eigen::Vector3f up_direction, float real_zoom) { + // offsets are adjusted by zoom + // this is the same calculation as for the projection matrix + float halfscreenwidth = viewport_size[0] / 2; + float halfscreenheight = viewport_size[1] / 2; + + float halfwidth = halfscreenwidth * real_zoom; + float halfheight = halfscreenheight * real_zoom; + + Eigen::Vector3f direction = cam_direction.normalized(); + Eigen::Vector3f eye = scene_pos; + Eigen::Vector3f center = scene_pos + direction; + + // calculate up (u) and right (s) vectors for camera + // these define the camera plane in 3D space that the input + Eigen::Vector3f f = center - eye; + f.normalize(); + Eigen::Vector3f s = f.cross(up_direction); + s.normalize(); + Eigen::Vector3f u = s.cross(f); + u.normalize(); + + Eigen::Vector3f near_position = scene_pos - direction * near_distance; + Eigen::Vector3f far_position = scene_pos + direction * far_distance; + + Eigen::Vector3f near_top_left = near_position - s * halfwidth + u * halfheight; + Eigen::Vector3f near_top_right = near_position + s * halfwidth + u * halfheight; + Eigen::Vector3f near_bottom_left = near_position - s * halfwidth - u * halfheight; + Eigen::Vector3f near_bottom_right = near_position + s * halfwidth - u * halfheight; + + Eigen::Vector3f far_top_left = far_position - s * halfwidth + u * halfheight; + Eigen::Vector3f far_top_right = far_position + s * halfwidth + u * halfheight; + Eigen::Vector3f far_bottom_left = far_position - s * halfwidth - u * halfheight; + Eigen::Vector3f far_bottom_right = far_position + s * halfwidth - u * halfheight; + + this->near_face_normal = -1.0f * cam_direction.normalized(); + this->far_face_normal = cam_direction.normalized(); + + this->near_face_distance = this->near_face_normal.dot(near_position) * -1.0f; + this->far_face_distance = this->far_face_normal.dot(far_position) * -1.0f; + + this->left_face_normal = (near_bottom_left - near_top_left).cross(far_bottom_left - near_bottom_left); + this->right_face_normal = (far_bottom_right - near_bottom_right).cross(near_bottom_right - near_top_right); + this->top_face_normal = (near_top_right - near_top_left).cross(near_top_left - far_top_left); + this->bottom_face_normal = (near_bottom_left - far_bottom_left).cross(near_bottom_right - near_bottom_left); + + log::log(INFO << "Compute near and far points"); + log::log(INFO << "Near Top Left: " << near_top_left[0] << ", " << near_top_left[1] << ", " << near_top_left[2]); + log::log(INFO << "Near Top Right: " << near_top_right[0] << ", " << near_top_right[1] << ", " << near_top_right[2]); + log::log(INFO << "Near Bottom Left: " << near_bottom_left[0] << ", " << near_bottom_left[1] << ", " << near_bottom_left[2]); + log::log(INFO << "Near Bottom Right: " << near_bottom_right[0] << ", " << near_bottom_right[1] << ", " << near_bottom_right[2]); + log::log(INFO << "Far Top Left: " << far_top_left[0] << ", " << far_top_left[1] << ", " << far_top_left[2]); + log::log(INFO << "Far Top Right: " << far_top_right[0] << ", " << far_top_right[1] << ", " << far_top_right[2]); + log::log(INFO << "Far Bottom Left: " << far_bottom_left[0] << ", " << far_bottom_left[1] << ", " << far_bottom_left[2]); + log::log(INFO << "Far Bottom Right: " << far_bottom_right[0] << ", " << far_bottom_right[1] << ", " << far_bottom_right[2]); + + this->left_face_normal.normalize(); + this->right_face_normal.normalize(); + this->top_face_normal.normalize(); + this->bottom_face_normal.normalize(); + + this->left_face_distance = 1.f * this->left_face_normal.dot(near_bottom_left); + this->right_face_distance = 1.f * this->right_face_normal.dot(far_bottom_right); + this->top_face_distance = 1.f * this->top_face_normal.dot(near_top_right); + this->bottom_face_distance = 1.f * this->bottom_face_normal.dot(near_bottom_left); + + log::log(INFO << "Computed Frustum with planes \n" + << "Top: <" << this->top_face_normal[0] << ", " << this->top_face_normal[1] << ", " << this->top_face_normal[2] << ">, " << this->top_face_distance << "\n" + << "Bottom: <" << this->bottom_face_normal[0] << ", " << this->bottom_face_normal[1] << ", " << this->bottom_face_normal[2] << ">, " << this->bottom_face_distance << "\n" + << "Left: <" << this->left_face_normal[0] << ", " << this->left_face_normal[1] << ", " << this->left_face_normal[2] << ">, " << this->left_face_distance << "\n" + << "Right: <" << this->right_face_normal[0] << ", " << this->right_face_normal[1] << ", " << this->right_face_normal[2] << ">, " << this->right_face_distance << "\n" + << "Far: <" << this->far_face_normal[0] << ", " << this->far_face_normal[1] << ", " << this->far_face_normal[2] << ">, " << this->far_face_distance << "\n" + << "Near: <" << this->near_face_normal[0] << ", " << this->near_face_normal[1] << ", " << this->near_face_normal[2] << ">, " << this->near_face_distance << "\n" + ); +} + +bool Frustum::is_in_frustum(Eigen::Vector3f& pos) const { + float distance; + + distance = this->top_face_normal.dot(pos) - this->top_face_distance; + if (distance < 0) { + return false; + } + + distance = this->bottom_face_normal.dot(pos) - this->bottom_face_distance; + if (distance < 0) { + return false; + } + + distance = this->left_face_normal.dot(pos) - this->left_face_distance; + if (distance < 0) { + return false; + } + + distance = this->right_face_normal.dot(pos) - this->right_face_distance; + if (distance < 0) { + return false; + } + + distance = this->far_face_normal.dot(pos) - this->far_face_distance; + if (distance < 0) { + return false; + } + + distance = this->bottom_face_normal.dot(pos) - this->bottom_face_distance; + if (distance < 0) { + return false; + } + + return true; +} + +} \ No newline at end of file diff --git a/libopenage/renderer/camera/frustum.h b/libopenage/renderer/camera/frustum.h new file mode 100644 index 0000000000..8dfe9372c5 --- /dev/null +++ b/libopenage/renderer/camera/frustum.h @@ -0,0 +1,46 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include + +#include + +#include "coord/pixel.h" +#include "coord/scene.h" +#include "util/vector.h" + +namespace openage::renderer::camera { + +class Frustum +{ +public: + Frustum(); + + void Recalculate(util::Vector2s& viewport_size, float near_distance, float far_distance, Eigen::Vector3f& scene_pos, Eigen::Vector3f cam_direction, Eigen::Vector3f up_direction, float real_zoom); + + bool is_in_frustum(Eigen::Vector3f& pos) const; + +private: + Eigen::Vector3f top_face_normal; + float top_face_distance; + + Eigen::Vector3f bottom_face_normal; + float bottom_face_distance; + + Eigen::Vector3f right_face_normal; + float right_face_distance; + + Eigen::Vector3f left_face_normal; + float left_face_distance; + + Eigen::Vector3f far_face_normal; + float far_face_distance; + + Eigen::Vector3f near_face_normal; + float near_face_distance; + +}; +} // namespace openage::renderer::camera diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index 3b9edd6d3d..d8d9120c69 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -224,4 +224,13 @@ void WorldObject::set_uniforms(std::vectorlayer_uniforms = std::move(uniforms); } +bool WorldObject::within_camera_frustum(const std::shared_ptr &camera) { + if (!camera->using_frustum_culling()) { + return true; + } + + Eigen::Vector3f current_pos = this->position.get(this->last_update).to_world_space(); + return camera->is_in_frustum(current_pos); +} + } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/object.h b/libopenage/renderer/stages/world/object.h index c5e0aa8197..17da70be75 100644 --- a/libopenage/renderer/stages/world/object.h +++ b/libopenage/renderer/stages/world/object.h @@ -133,6 +133,8 @@ class WorldObject { */ void set_uniforms(std::vector> &&uniforms); + bool within_camera_frustum(const std::shared_ptr &camera); + /** * Shader uniform IDs for setting uniform values. */ diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 05827eb645..08bb7c7a8d 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -59,6 +59,10 @@ void WorldRenderStage::update() { std::unique_lock lock{this->mutex}; auto current_time = this->clock->get_real_time(); for (auto &obj : this->render_objects) { + if (!obj->within_camera_frustum(this->camera)) { + continue; + } + obj->fetch_updates(current_time); if (obj->is_changed()) { if (obj->requires_renderable()) { @@ -96,6 +100,8 @@ void WorldRenderStage::update() { } obj->update_uniforms(current_time); } + + std::cout << this->render_pass->renderables.size() << std::flush; } void WorldRenderStage::resize(size_t width, size_t height) { From e463962edab570b43bca5b7b1993fe4f2bef4475 Mon Sep 17 00:00:00 2001 From: Nikhil Ghosh Date: Thu, 25 Apr 2024 23:15:14 -0400 Subject: [PATCH 413/771] Implemented stresstest 1 --- libopenage/renderer/demo/CMakeLists.txt | 1 + libopenage/renderer/demo/stresstest_1.cpp | 222 ++++++++++++++++++++++ libopenage/renderer/demo/stresstest_1.h | 16 ++ libopenage/renderer/demo/tests.cpp | 6 +- 4 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 libopenage/renderer/demo/stresstest_1.cpp create mode 100644 libopenage/renderer/demo/stresstest_1.h diff --git a/libopenage/renderer/demo/CMakeLists.txt b/libopenage/renderer/demo/CMakeLists.txt index 24a5c20d84..aaa30bc906 100644 --- a/libopenage/renderer/demo/CMakeLists.txt +++ b/libopenage/renderer/demo/CMakeLists.txt @@ -6,6 +6,7 @@ add_sources(libopenage demo_4.cpp demo_5.cpp stresstest_0.cpp + stresstest_1.cpp tests.cpp util.cpp ) diff --git a/libopenage/renderer/demo/stresstest_1.cpp b/libopenage/renderer/demo/stresstest_1.cpp new file mode 100644 index 0000000000..05a357a2ef --- /dev/null +++ b/libopenage/renderer/demo/stresstest_1.cpp @@ -0,0 +1,222 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "stresstest_1.h" + +#include + +#include "coord/tile.h" +#include "renderer/camera/camera.h" +#include "renderer/gui/integration/public/gui_application_with_logger.h" +#include "renderer/opengl/window.h" +#include "renderer/render_factory.h" +#include "renderer/resources/assets/asset_manager.h" +#include "renderer/resources/shader_source.h" +#include "renderer/stages/camera/manager.h" +#include "renderer/stages/screen/render_stage.h" +#include "renderer/stages/skybox/render_stage.h" +#include "renderer/stages/terrain/render_entity.h" +#include "renderer/stages/terrain/render_stage.h" +#include "renderer/stages/world/render_entity.h" +#include "renderer/stages/world/render_stage.h" +#include "renderer/uniform_buffer.h" +#include "time/clock.h" +#include "util/fps.h" + +namespace openage::renderer::tests { +void renderer_stresstest_1(const util::Path &path) { + auto qtapp = std::make_shared(); + + auto window = std::make_shared("openage renderer test", 1024, 768, true); + auto renderer = window->make_renderer(); + + // Clock required by world renderer for timing animation frames + auto clock = std::make_shared(); + + // Camera + // our viewport into the game world + // on this one, enable frustum culling + auto camera = std::make_shared(renderer, + window->get_size(), + Eigen::Vector3f{17.0f, 10.0f, 7.0f}, + 1.f, + 64.f, + 1.f / 49.f, + true); + auto cam_unifs = camera->get_uniform_buffer()->create_empty_input(); + cam_unifs->update( + "view", + camera->get_view_matrix(), + "proj", + camera->get_projection_matrix()); + camera->get_uniform_buffer()->update_uniforms(cam_unifs); + + // Render stages + // every stage use a different subrenderer that manages renderables, + // shaders, textures & more. + std::vector> + render_passes{}; + + // TODO: Make this optional for subrenderers? + auto asset_manager = std::make_shared( + renderer, + path["assets"]["test"]); + + // Renders the background + auto skybox_renderer = std::make_shared( + window, + renderer, + path["assets"]["shaders"]); + skybox_renderer->set_color(1.0f, 0.5f, 0.0f, 1.0f); // orange color + + // Renders the terrain in 3D + auto terrain_renderer = std::make_shared( + window, + renderer, + camera, + path["assets"]["shaders"], + asset_manager, + clock); + + // Renders units/buildings/other objects + auto world_renderer = std::make_shared( + window, + renderer, + camera, + path["assets"]["shaders"], + asset_manager, + clock); + + // Store the render passes of the renderers + // The order is important as its also the order in which they + // are rendered and drawn onto the screen. + render_passes.push_back(skybox_renderer->get_render_pass()); + render_passes.push_back(terrain_renderer->get_render_pass()); + render_passes.push_back(world_renderer->get_render_pass()); + + // Final output on screen has its own subrenderer + // It takes the outputs of all previous render passes + // and blends them together + auto screen_renderer = std::make_shared( + window, + renderer, + path["assets"]["shaders"]); + std::vector> targets{}; + for (auto &pass : render_passes) { + targets.push_back(pass->get_target()); + } + screen_renderer->set_render_targets(targets); + + window->add_resize_callback([&](size_t, size_t, double /*scale*/) { + // Acquire the render targets for all previous passes + std::vector> targets{}; + for (size_t i = 0; i < render_passes.size() - 1; ++i) { + targets.push_back(render_passes[i]->get_target()); + } + screen_renderer->set_render_targets(targets); + }); + + render_passes.push_back(screen_renderer->get_render_pass()); + + // Create some entities to populate the scene + auto render_factory = std::make_shared(terrain_renderer, world_renderer); + + // Fill a 10x10 terrain grid with height values + auto terrain_size = util::Vector2s{10, 10}; + std::vector> tiles{}; + tiles.reserve(terrain_size[0] * terrain_size[1]); + for (size_t i = 0; i < terrain_size[0] * terrain_size[1]; ++i) { + tiles.emplace_back(0.0f, "./textures/test_terrain.terrain"); + } + + // Create entity for terrain rendering + auto terrain0 = render_factory->add_terrain_render_entity(terrain_size, + coord::tile_delta{0, 0}); + + // send the terrain data to the terrain renderer + terrain0->update(terrain_size, tiles); + + std::vector> render_entities{}; + auto add_world_entity = [&](const coord::phys3 initial_pos, + const time::time_t time) { + const auto animation_path = "./textures/test_tank_mirrored.sprite"; + + auto position = curve::Continuous{nullptr, 0, "", nullptr, coord::phys3(0, 0, 0)}; + position.set_insert(time, initial_pos); + position.set_insert(time + 1, initial_pos + coord::phys3_delta{0, 4, 0}); + position.set_insert(time + 2, initial_pos + coord::phys3_delta{4, 8, 0}); + position.set_insert(time + 3, initial_pos + coord::phys3_delta{8, 8, 0}); + position.set_insert(time + 4, initial_pos + coord::phys3_delta{12, 4, 0}); + position.set_insert(time + 5, initial_pos + coord::phys3_delta{12, 0, 0}); + position.set_insert(time + 6, initial_pos + coord::phys3_delta{12, -4, 0}); + position.set_insert(time + 7, initial_pos + coord::phys3_delta{4, -4, 0}); + position.set_insert(time + 8, initial_pos); + + auto angle = curve::Segmented{nullptr, 0}; + angle.set_insert(time, coord::phys_angle_t::from_int(315)); + angle.set_insert_jump(time + 1, coord::phys_angle_t::from_int(315), coord::phys_angle_t::from_int(270)); + angle.set_insert_jump(time + 2, coord::phys_angle_t::from_int(270), coord::phys_angle_t::from_int(225)); + angle.set_insert_jump(time + 3, coord::phys_angle_t::from_int(225), coord::phys_angle_t::from_int(180)); + angle.set_insert_jump(time + 4, coord::phys_angle_t::from_int(180), coord::phys_angle_t::from_int(135)); + angle.set_insert_jump(time + 5, coord::phys_angle_t::from_int(135), coord::phys_angle_t::from_int(90)); + angle.set_insert_jump(time + 6, coord::phys_angle_t::from_int(90), coord::phys_angle_t::from_int(45)); + angle.set_insert_jump(time + 7, coord::phys_angle_t::from_int(45), coord::phys_angle_t::from_int(0)); + angle.set_insert_jump(time + 8, coord::phys_angle_t::from_int(0), coord::phys_angle_t::from_int(315)); + + auto entity = render_factory->add_world_render_entity(); + entity->update(render_entities.size(), + position, + angle, + animation_path, + time); + render_entities.push_back(entity); + }; + + // Stop after 8000 entities + size_t entity_limit = 8000; + + clock->start(); + + util::FrameCounter timer; + + add_world_entity(coord::phys3(0.0f, 10.0f, 0.0f), clock->get_time()); + time::time_t next_entity = clock->get_real_time() + 0.1; + while (render_entities.size() <= entity_limit) { + // Print FPS + timer.frame(); + std::cout + << "Entities: " << render_entities.size() + << " -- " + << "FPS: " << timer.fps << "\r" << std::flush; + + qtapp->process_events(); + + // Advance time + clock->update_time(); + auto current_time = clock->get_real_time(); + if (current_time > next_entity) { + add_world_entity(coord::phys3(0.0f, 10.0f, 0.0f), clock->get_time()); + add_world_entity(coord::phys3(-1.f, 9.0f, 0.0f), clock->get_time()); + next_entity = current_time + 0.1; + } + + // Update the renderables of the subrenderers + terrain_renderer->update(); + world_renderer->update(); + + // Draw everything + for (auto &pass : render_passes) { + renderer->render(pass); + } + + renderer->check_error(); + + // Display final output on screen + window->update(); + } + + clock->stop(); + log::log(MSG(info) << "Stopped after rendering " << render_entities.size() << " entities"); + + window->close(); +} +} \ No newline at end of file diff --git a/libopenage/renderer/demo/stresstest_1.h b/libopenage/renderer/demo/stresstest_1.h new file mode 100644 index 0000000000..260296201f --- /dev/null +++ b/libopenage/renderer/demo/stresstest_1.h @@ -0,0 +1,16 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include "util/path.h" + +namespace openage::renderer::tests { + +/** + * Stresstest for the renderer's frustum culling feature. + * + * @param path Path to the openage asset directory. + */ +void renderer_stresstest_1(const util::Path &path); + +} \ No newline at end of file diff --git a/libopenage/renderer/demo/tests.cpp b/libopenage/renderer/demo/tests.cpp index 95f6bd40af..30961a6c95 100644 --- a/libopenage/renderer/demo/tests.cpp +++ b/libopenage/renderer/demo/tests.cpp @@ -12,7 +12,7 @@ #include "renderer/demo/demo_4.h" #include "renderer/demo/demo_5.h" #include "renderer/demo/stresstest_0.h" - +#include "renderer/demo/stresstest_1.h" namespace openage::renderer::tests { @@ -53,7 +53,9 @@ OAAPI void renderer_stresstest(int demo_id, const util::Path &path) { case 0: renderer_stresstest_0(path); break; - + case 1: + renderer_stresstest_1(path); + break; default: log::log(MSG(err) << "Unknown renderer stresstest requested: " << demo_id << "."); break; From 84fa57bc485c1c5ac7027dea05234ab3e71f46b6 Mon Sep 17 00:00:00 2001 From: Nikhil Ghosh Date: Thu, 25 Apr 2024 23:32:55 -0400 Subject: [PATCH 414/771] Added additional comments to the frustum culling logic --- libopenage/renderer/camera/camera.cpp | 3 ++ libopenage/renderer/camera/camera.h | 3 +- libopenage/renderer/camera/frustum.cpp | 51 ++++---------------------- 3 files changed, 13 insertions(+), 44 deletions(-) diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index d43d30b102..fd94c2ce57 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -31,6 +31,7 @@ Camera::Camera(const std::shared_ptr &renderer, this->init_uniform_buffer(renderer); + // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered float real_zoom = 0.7f * this->default_zoom_ratio * this->zoom; frustum.Recalculate(this->viewport_size, near_distance, far_distance, this->scene_pos, cam_direction, Eigen::Vector3f(0.0f, 1.0f, 0.0f), real_zoom); @@ -60,6 +61,7 @@ Camera::Camera(const std::shared_ptr &renderer, frustum_culling{frustum_culling} { this->init_uniform_buffer(renderer); + // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered float real_zoom = 0.7f * this->default_zoom_ratio * this->zoom; frustum.Recalculate(this->viewport_size, near_distance, far_distance, this->scene_pos, cam_direction, Eigen::Vector3f(0.0f, 1.0f, 0.0f), real_zoom); @@ -117,6 +119,7 @@ void Camera::move_to(Eigen::Vector3f scene_pos) { this->scene_pos = scene_pos; this->moved = true; + // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered float real_zoom = 0.7f * this->default_zoom_ratio * this->zoom; frustum.Recalculate(viewport_size, near_distance, far_distance, scene_pos, cam_direction, Eigen::Vector3f(0.0f, 1.0f, 0.0f), real_zoom); } diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index 56fdccef8a..bc4959f589 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -294,7 +294,8 @@ class Camera { std::shared_ptr uniform_buffer; /** - * Is frustum culling enabled? If true, + * Is frustum culling enabled? If true, perform frustum culling. + * If false, all frustum checks will return true */ bool frustum_culling; diff --git a/libopenage/renderer/camera/frustum.cpp b/libopenage/renderer/camera/frustum.cpp index d79058f411..459cc7226c 100644 --- a/libopenage/renderer/camera/frustum.cpp +++ b/libopenage/renderer/camera/frustum.cpp @@ -14,26 +14,6 @@ namespace openage::renderer::camera { -/* - Eigen::Vector3f top_face_normal; - float top_face_distance; - - Eigen::Vector3f bottom_face_normal; - float bottom_face_distance; - - Eigen::Vector3f right_face_normal; - float right_face_distance; - - Eigen::Vector3f left_face_normal; - float left_face_distance; - - Eigen::Vector3f far_face_normal; - float far_face_distance; - - Eigen::Vector3f near_face_normal; - float near_face_distance; -*/ - Frustum::Frustum() : top_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, top_face_distance{0.0f}, @@ -75,6 +55,7 @@ void Frustum::Recalculate(util::Vector2s &viewport_size, float near_distance, fl Eigen::Vector3f near_position = scene_pos - direction * near_distance; Eigen::Vector3f far_position = scene_pos + direction * far_distance; + // The frustum is a box with 8 points defining it (4 on the near plane, 4 on the far plane) Eigen::Vector3f near_top_left = near_position - s * halfwidth + u * halfheight; Eigen::Vector3f near_top_right = near_position + s * halfwidth + u * halfheight; Eigen::Vector3f near_bottom_left = near_position - s * halfwidth - u * halfheight; @@ -85,48 +66,32 @@ void Frustum::Recalculate(util::Vector2s &viewport_size, float near_distance, fl Eigen::Vector3f far_bottom_left = far_position - s * halfwidth - u * halfheight; Eigen::Vector3f far_bottom_right = far_position + s * halfwidth - u * halfheight; + // The near and far planes are easiest to compute, as they should be in the direction of the camera this->near_face_normal = -1.0f * cam_direction.normalized(); this->far_face_normal = cam_direction.normalized(); this->near_face_distance = this->near_face_normal.dot(near_position) * -1.0f; this->far_face_distance = this->far_face_normal.dot(far_position) * -1.0f; + // Each left, right, top, and bottom plane are defined by three points on the plane this->left_face_normal = (near_bottom_left - near_top_left).cross(far_bottom_left - near_bottom_left); this->right_face_normal = (far_bottom_right - near_bottom_right).cross(near_bottom_right - near_top_right); this->top_face_normal = (near_top_right - near_top_left).cross(near_top_left - far_top_left); this->bottom_face_normal = (near_bottom_left - far_bottom_left).cross(near_bottom_right - near_bottom_left); - log::log(INFO << "Compute near and far points"); - log::log(INFO << "Near Top Left: " << near_top_left[0] << ", " << near_top_left[1] << ", " << near_top_left[2]); - log::log(INFO << "Near Top Right: " << near_top_right[0] << ", " << near_top_right[1] << ", " << near_top_right[2]); - log::log(INFO << "Near Bottom Left: " << near_bottom_left[0] << ", " << near_bottom_left[1] << ", " << near_bottom_left[2]); - log::log(INFO << "Near Bottom Right: " << near_bottom_right[0] << ", " << near_bottom_right[1] << ", " << near_bottom_right[2]); - log::log(INFO << "Far Top Left: " << far_top_left[0] << ", " << far_top_left[1] << ", " << far_top_left[2]); - log::log(INFO << "Far Top Right: " << far_top_right[0] << ", " << far_top_right[1] << ", " << far_top_right[2]); - log::log(INFO << "Far Bottom Left: " << far_bottom_left[0] << ", " << far_bottom_left[1] << ", " << far_bottom_left[2]); - log::log(INFO << "Far Bottom Right: " << far_bottom_right[0] << ", " << far_bottom_right[1] << ", " << far_bottom_right[2]); - this->left_face_normal.normalize(); this->right_face_normal.normalize(); this->top_face_normal.normalize(); this->bottom_face_normal.normalize(); - this->left_face_distance = 1.f * this->left_face_normal.dot(near_bottom_left); - this->right_face_distance = 1.f * this->right_face_normal.dot(far_bottom_right); - this->top_face_distance = 1.f * this->top_face_normal.dot(near_top_right); - this->bottom_face_distance = 1.f * this->bottom_face_normal.dot(near_bottom_left); - - log::log(INFO << "Computed Frustum with planes \n" - << "Top: <" << this->top_face_normal[0] << ", " << this->top_face_normal[1] << ", " << this->top_face_normal[2] << ">, " << this->top_face_distance << "\n" - << "Bottom: <" << this->bottom_face_normal[0] << ", " << this->bottom_face_normal[1] << ", " << this->bottom_face_normal[2] << ">, " << this->bottom_face_distance << "\n" - << "Left: <" << this->left_face_normal[0] << ", " << this->left_face_normal[1] << ", " << this->left_face_normal[2] << ">, " << this->left_face_distance << "\n" - << "Right: <" << this->right_face_normal[0] << ", " << this->right_face_normal[1] << ", " << this->right_face_normal[2] << ">, " << this->right_face_distance << "\n" - << "Far: <" << this->far_face_normal[0] << ", " << this->far_face_normal[1] << ", " << this->far_face_normal[2] << ">, " << this->far_face_distance << "\n" - << "Near: <" << this->near_face_normal[0] << ", " << this->near_face_normal[1] << ", " << this->near_face_normal[2] << ">, " << this->near_face_distance << "\n" - ); + this->left_face_distance = this->left_face_normal.dot(near_bottom_left); + this->right_face_distance = this->right_face_normal.dot(far_bottom_right); + this->top_face_distance = this->top_face_normal.dot(near_top_right); + this->bottom_face_distance = this->bottom_face_normal.dot(near_bottom_left); } bool Frustum::is_in_frustum(Eigen::Vector3f& pos) const { + // For each plane, if a point is behind one of the frustum planes, it is not within the frustum float distance; distance = this->top_face_normal.dot(pos) - this->top_face_distance; From 94f9a46ebeccf09b128ac77197a1c6327efd8dc9 Mon Sep 17 00:00:00 2001 From: Nikhil Ghosh Date: Fri, 26 Apr 2024 12:03:07 -0400 Subject: [PATCH 415/771] Fixed styling --- libopenage/renderer/camera/frustum.cpp | 2 +- libopenage/renderer/demo/stresstest_1.cpp | 4 ++-- libopenage/renderer/demo/stresstest_1.h | 4 ++-- libopenage/renderer/resources/parser/parse_sprite.h | 2 +- libopenage/renderer/resources/parser/parse_terrain.h | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/libopenage/renderer/camera/frustum.cpp b/libopenage/renderer/camera/frustum.cpp index 459cc7226c..fb22e69988 100644 --- a/libopenage/renderer/camera/frustum.cpp +++ b/libopenage/renderer/camera/frustum.cpp @@ -14,7 +14,7 @@ namespace openage::renderer::camera { -Frustum::Frustum() : +Frustum::Frustum() : top_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, top_face_distance{0.0f}, bottom_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, diff --git a/libopenage/renderer/demo/stresstest_1.cpp b/libopenage/renderer/demo/stresstest_1.cpp index 05a357a2ef..85a60196bd 100644 --- a/libopenage/renderer/demo/stresstest_1.cpp +++ b/libopenage/renderer/demo/stresstest_1.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2024-2024 the openage authors. See copying.md for legal info. #include "stresstest_1.h" @@ -219,4 +219,4 @@ void renderer_stresstest_1(const util::Path &path) { window->close(); } -} \ No newline at end of file +} diff --git a/libopenage/renderer/demo/stresstest_1.h b/libopenage/renderer/demo/stresstest_1.h index 260296201f..a2dbde6136 100644 --- a/libopenage/renderer/demo/stresstest_1.h +++ b/libopenage/renderer/demo/stresstest_1.h @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2024-2024 the openage authors. See copying.md for legal info. #pragma once @@ -13,4 +13,4 @@ namespace openage::renderer::tests { */ void renderer_stresstest_1(const util::Path &path); -} \ No newline at end of file +} diff --git a/libopenage/renderer/resources/parser/parse_sprite.h b/libopenage/renderer/resources/parser/parse_sprite.h index b2021190b3..5f989f5aa3 100644 --- a/libopenage/renderer/resources/parser/parse_sprite.h +++ b/libopenage/renderer/resources/parser/parse_sprite.h @@ -1,4 +1,4 @@ -// 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 diff --git a/libopenage/renderer/resources/parser/parse_terrain.h b/libopenage/renderer/resources/parser/parse_terrain.h index 6aad27ce6e..ea5c582f38 100644 --- a/libopenage/renderer/resources/parser/parse_terrain.h +++ b/libopenage/renderer/resources/parser/parse_terrain.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 From e62b1b962a0641b0070b57f4e727c67bcd6ccde4 Mon Sep 17 00:00:00 2001 From: Nikhil Ghosh Date: Fri, 26 Apr 2024 12:16:40 -0400 Subject: [PATCH 416/771] Fixed various checks --- libopenage/renderer/camera/frustum.cpp | 2 +- libopenage/renderer/demo/tests.cpp | 2 +- libopenage/renderer/resources/parser/common.cpp | 2 +- libopenage/renderer/resources/parser/parse_texture.cpp | 2 +- libopenage/renderer/stages/world/render_stage.cpp | 2 -- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/libopenage/renderer/camera/frustum.cpp b/libopenage/renderer/camera/frustum.cpp index fb22e69988..912cf97a5b 100644 --- a/libopenage/renderer/camera/frustum.cpp +++ b/libopenage/renderer/camera/frustum.cpp @@ -127,4 +127,4 @@ bool Frustum::is_in_frustum(Eigen::Vector3f& pos) const { return true; } -} \ No newline at end of file +} diff --git a/libopenage/renderer/demo/tests.cpp b/libopenage/renderer/demo/tests.cpp index 30961a6c95..c997a06763 100644 --- a/libopenage/renderer/demo/tests.cpp +++ b/libopenage/renderer/demo/tests.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "tests.h" diff --git a/libopenage/renderer/resources/parser/common.cpp b/libopenage/renderer/resources/parser/common.cpp index ab6a1b205f..483c2cb0dd 100644 --- a/libopenage/renderer/resources/parser/common.cpp +++ b/libopenage/renderer/resources/parser/common.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 "common.h" diff --git a/libopenage/renderer/resources/parser/parse_texture.cpp b/libopenage/renderer/resources/parser/parse_texture.cpp index a0af8d6f73..c3d90fa30a 100644 --- a/libopenage/renderer/resources/parser/parse_texture.cpp +++ b/libopenage/renderer/resources/parser/parse_texture.cpp @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the openage authors. See copying.md for legal info. +// Copyright 2021-2024 the openage authors. See copying.md for legal info. #include "parse_texture.h" diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 08bb7c7a8d..816203b9d8 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -100,8 +100,6 @@ void WorldRenderStage::update() { } obj->update_uniforms(current_time); } - - std::cout << this->render_pass->renderables.size() << std::flush; } void WorldRenderStage::resize(size_t width, size_t height) { From d70ae7fe1e773a300bdf7cf8c291a37f2eff8c53 Mon Sep 17 00:00:00 2001 From: Nikhil Ghosh Date: Fri, 26 Apr 2024 12:25:01 -0400 Subject: [PATCH 417/771] Added name to mailmap --- .mailmap | 1 + 1 file changed, 1 insertion(+) diff --git a/.mailmap b/.mailmap index 300076fb84..fbf10ad63f 100644 --- a/.mailmap +++ b/.mailmap @@ -20,3 +20,4 @@ Tobias Feldballe Tobias Feldballe Jonas Borchelt Derek Frogget <114030121+derekfrogget@users.noreply.github.com> +Nikhil Ghosh \ No newline at end of file From 03da156788eb98506b26701a6979de6003fb1ee8 Mon Sep 17 00:00:00 2001 From: Nikhil Ghosh Date: Fri, 26 Apr 2024 12:34:20 -0400 Subject: [PATCH 418/771] Added name to copying.md --- copying.md | 1 + 1 file changed, 1 insertion(+) diff --git a/copying.md b/copying.md index 13124fb993..46c28990b4 100644 --- a/copying.md +++ b/copying.md @@ -153,6 +153,7 @@ _the openage authors_ are: | Astitva Kamble | askastitva | astitvakamble5 à gmail dawt com | | Haoyang Bi | AyiStar | ayistar à outlook dawt com | | Michael Seibt | RoboSchmied | github à roboschmie dawt de | +| Nikhil Ghosh | NikhilGhosh75 | nghosh606 à gmail dawt com If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. From 87a21a831e088c3289c47e523ecb4ecf7bb62085 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Jul 2024 14:56:10 +0200 Subject: [PATCH 419/771] renderer: Fix imports and windows settings. --- libopenage/renderer/demo/stresstest_1.cpp | 29 ++++++++++++++--------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/libopenage/renderer/demo/stresstest_1.cpp b/libopenage/renderer/demo/stresstest_1.cpp index 85a60196bd..8e2e2887f9 100644 --- a/libopenage/renderer/demo/stresstest_1.cpp +++ b/libopenage/renderer/demo/stresstest_1.cpp @@ -9,6 +9,7 @@ #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" #include "renderer/render_factory.h" +#include "renderer/render_pass.h" #include "renderer/resources/assets/asset_manager.h" #include "renderer/resources/shader_source.h" #include "renderer/stages/camera/manager.h" @@ -24,9 +25,15 @@ namespace openage::renderer::tests { void renderer_stresstest_1(const util::Path &path) { - auto qtapp = std::make_shared(); - - auto window = std::make_shared("openage renderer test", 1024, 768, true); + auto qtapp = std::make_shared(); + + // Create the window and renderer + window_settings settings; + settings.width = 1024; + settings.height = 768; + settings.vsync = false; + settings.debug = true; + auto window = std::make_shared("openage renderer test", settings); auto renderer = window->make_renderer(); // Clock required by world renderer for timing animation frames @@ -38,10 +45,10 @@ void renderer_stresstest_1(const util::Path &path) { auto camera = std::make_shared(renderer, window->get_size(), Eigen::Vector3f{17.0f, 10.0f, 7.0f}, - 1.f, - 64.f, - 1.f / 49.f, - true); + 1.f, + 64.f, + 1.f / 49.f, + true); auto cam_unifs = camera->get_uniform_buffer()->create_empty_input(); cam_unifs->update( "view", @@ -50,7 +57,7 @@ void renderer_stresstest_1(const util::Path &path) { camera->get_projection_matrix()); camera->get_uniform_buffer()->update_uniforms(cam_unifs); - // Render stages + // Render stages // every stage use a different subrenderer that manages renderables, // shaders, textures & more. std::vector> @@ -135,7 +142,7 @@ void renderer_stresstest_1(const util::Path &path) { // send the terrain data to the terrain renderer terrain0->update(terrain_size, tiles); - std::vector> render_entities{}; + std::vector> render_entities{}; auto add_world_entity = [&](const coord::phys3 initial_pos, const time::time_t time) { const auto animation_path = "./textures/test_tank_mirrored.sprite"; @@ -195,7 +202,7 @@ void renderer_stresstest_1(const util::Path &path) { auto current_time = clock->get_real_time(); if (current_time > next_entity) { add_world_entity(coord::phys3(0.0f, 10.0f, 0.0f), clock->get_time()); - add_world_entity(coord::phys3(-1.f, 9.0f, 0.0f), clock->get_time()); + add_world_entity(coord::phys3(-1.f, 9.0f, 0.0f), clock->get_time()); next_entity = current_time + 0.1; } @@ -219,4 +226,4 @@ void renderer_stresstest_1(const util::Path &path) { window->close(); } -} +} // namespace openage::renderer::tests From c3805cd49fea6f4e0d6a9df50055c774d49996f1 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Jul 2024 14:58:52 +0200 Subject: [PATCH 420/771] Fix name parsing in copying.md --- buildsystem/codecompliance/authors.py | 4 ++-- copying.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/buildsystem/codecompliance/authors.py b/buildsystem/codecompliance/authors.py index 502e3b2648..98b3a50e18 100644 --- a/buildsystem/codecompliance/authors.py +++ b/buildsystem/codecompliance/authors.py @@ -1,4 +1,4 @@ -# Copyright 2014-2023 the openage authors. See copying.md for legal info. +# Copyright 2014-2024 the openage authors. See copying.md for legal info. """ Checks whether all authors are properly listed in copying.md. @@ -39,7 +39,7 @@ def get_author_emails_copying_md(): """ with open("copying.md", encoding='utf8') as fobj: for line in fobj: - match = re.match("^.*\\|[^|]*\\|[^|]*\\|([^|]+)\\|.*$", line) + match = re.match(r"^.*\|[^|]*\|[^|]*\|([^|]+)\|.*$", line) if not match: continue diff --git a/copying.md b/copying.md index 46c28990b4..fb6501bab0 100644 --- a/copying.md +++ b/copying.md @@ -153,7 +153,7 @@ _the openage authors_ are: | Astitva Kamble | askastitva | astitvakamble5 à gmail dawt com | | Haoyang Bi | AyiStar | ayistar à outlook dawt com | | Michael Seibt | RoboSchmied | github à roboschmie dawt de | -| Nikhil Ghosh | NikhilGhosh75 | nghosh606 à gmail dawt com +| Nikhil Ghosh | NikhilGhosh75 | nghosh606 à gmail dawt com | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. From 94461652347df8418f55ad032e7cb4bf529e1808 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Jul 2024 16:08:31 +0200 Subject: [PATCH 421/771] renderer: Fix camera unifs for stresstest 1. --- libopenage/renderer/demo/stresstest_1.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libopenage/renderer/demo/stresstest_1.cpp b/libopenage/renderer/demo/stresstest_1.cpp index 8e2e2887f9..9a113eb85d 100644 --- a/libopenage/renderer/demo/stresstest_1.cpp +++ b/libopenage/renderer/demo/stresstest_1.cpp @@ -54,7 +54,14 @@ void renderer_stresstest_1(const util::Path &path) { "view", camera->get_view_matrix(), "proj", - camera->get_projection_matrix()); + camera->get_projection_matrix(), + "inv_zoom", + 1.0f / camera->get_zoom()); + auto viewport_size = camera->get_viewport_size(); + Eigen::Vector2f viewport_size_vec{ + 1.0f / static_cast(viewport_size[0]), + 1.0f / static_cast(viewport_size[1])}; + cam_unifs->update("inv_viewport_size", viewport_size_vec); camera->get_uniform_buffer()->update_uniforms(cam_unifs); // Render stages From acc9c96907c730b4d42c4a14b32903fe5d8abf6e Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Jul 2024 16:09:06 +0200 Subject: [PATCH 422/771] renderer: Get frustum from camera. --- libopenage/renderer/camera/camera.cpp | 45 +++-- libopenage/renderer/camera/camera.h | 21 +- libopenage/renderer/camera/frustum.cpp | 189 +++++++++--------- libopenage/renderer/camera/frustum.h | 46 ++--- libopenage/renderer/demo/stresstest_1.cpp | 3 +- libopenage/renderer/stages/world/object.cpp | 8 +- .../renderer/stages/world/render_stage.cpp | 2 +- 7 files changed, 159 insertions(+), 155 deletions(-) diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index fd94c2ce57..0e2112b6c7 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -25,15 +25,20 @@ Camera::Camera(const std::shared_ptr &renderer, moved{true}, zoom_changed{true}, view{Eigen::Matrix4f::Identity()}, - proj{Eigen::Matrix4f::Identity()}, - frustum_culling{false} { + proj{Eigen::Matrix4f::Identity()} { this->look_at_scene(Eigen::Vector3f(0.0f, 0.0f, 0.0f)); this->init_uniform_buffer(renderer); // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered float real_zoom = 0.7f * this->default_zoom_ratio * this->zoom; - frustum.Recalculate(this->viewport_size, near_distance, far_distance, this->scene_pos, cam_direction, Eigen::Vector3f(0.0f, 1.0f, 0.0f), real_zoom); + frustum.Recalculate(this->viewport_size, + near_distance, + far_distance, + this->scene_pos, + cam_direction, + Eigen::Vector3f(0.0f, 1.0f, 0.0f), + real_zoom); log::log(INFO << "Created new camera at position " << "(" << this->scene_pos[0] @@ -46,8 +51,7 @@ Camera::Camera(const std::shared_ptr &renderer, Eigen::Vector3f scene_pos, float zoom, float max_zoom_out, - float default_zoom_ratio, - bool frustum_culling) : + float default_zoom_ratio) : scene_pos{scene_pos}, viewport_size{viewport_size}, zoom{zoom}, @@ -57,13 +61,18 @@ Camera::Camera(const std::shared_ptr &renderer, zoom_changed{true}, viewport_changed{true}, view{Eigen::Matrix4f::Identity()}, - proj{Eigen::Matrix4f::Identity()}, - frustum_culling{frustum_culling} { + proj{Eigen::Matrix4f::Identity()} { this->init_uniform_buffer(renderer); // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered float real_zoom = 0.7f * this->default_zoom_ratio * this->zoom; - frustum.Recalculate(this->viewport_size, near_distance, far_distance, this->scene_pos, cam_direction, Eigen::Vector3f(0.0f, 1.0f, 0.0f), real_zoom); + frustum.Recalculate(this->viewport_size, + near_distance, + far_distance, + this->scene_pos, + cam_direction, + Eigen::Vector3f(0.0f, 1.0f, 0.0f), + real_zoom); log::log(INFO << "Created new camera at position " << "(" << this->scene_pos[0] @@ -121,7 +130,13 @@ void Camera::move_to(Eigen::Vector3f scene_pos) { // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered float real_zoom = 0.7f * this->default_zoom_ratio * this->zoom; - frustum.Recalculate(viewport_size, near_distance, far_distance, scene_pos, cam_direction, Eigen::Vector3f(0.0f, 1.0f, 0.0f), real_zoom); + frustum.Recalculate(viewport_size, + near_distance, + far_distance, + scene_pos, + cam_direction, + Eigen::Vector3f(0.0f, 1.0f, 0.0f), + real_zoom); } void Camera::move_rel(Eigen::Vector3f direction, float delta) { @@ -289,16 +304,8 @@ void Camera::init_uniform_buffer(const std::shared_ptr &renderer) { this->uniform_buffer = renderer->add_uniform_buffer(ubo_info); } -bool Camera::using_frustum_culling() const { - return this->frustum_culling; -} - -bool Camera::is_in_frustum(Eigen::Vector3f pos) const{ - if (!this->frustum_culling) { - return true; - } - - return frustum.is_in_frustum(pos); +Frustum Camera::get_frustum() const { + return this->frustum; } } // namespace openage::renderer::camera diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index bc4959f589..26bebd7e60 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -12,7 +12,7 @@ #include "coord/scene.h" #include "util/vector.h" -#include "frustum.h" +#include "renderer/camera/frustum.h" namespace openage::renderer { class Renderer; @@ -63,15 +63,13 @@ class Camera { * @param zoom Zoom level of the camera (defaults to 1.0f). * @param max_zoom_out Maximum zoom out level (defaults to 64.0f). * @param default_zoom_ratio Default zoom level calibration (defaults to (1.0f / 49)). - * @param frustum_culling Is frustum culling enables (defaults to false). */ Camera(const std::shared_ptr &renderer, util::Vector2s viewport_size, Eigen::Vector3f scene_pos, float zoom = 1.0f, float max_zoom_out = 64.0f, - float default_zoom_ratio = 1.0f / 49, - bool frustum_culling = false); + float default_zoom_ratio = 1.0f / 49); ~Camera() = default; /** @@ -195,9 +193,12 @@ class Camera { */ const std::shared_ptr &get_uniform_buffer() const; - bool using_frustum_culling() const; - - bool is_in_frustum(Eigen::Vector3f pos) const; + /** + * Get a frustum object for this camera. + * + * @return Frustum object. + */ + Frustum get_frustum() const; private: /** @@ -293,12 +294,6 @@ class Camera { */ std::shared_ptr uniform_buffer; - /** - * Is frustum culling enabled? If true, perform frustum culling. - * If false, all frustum checks will return true - */ - bool frustum_culling; - /** * The frustum used to cull objects * Will be recalculated regardless of whether frustum culling is enabled diff --git a/libopenage/renderer/camera/frustum.cpp b/libopenage/renderer/camera/frustum.cpp index 912cf97a5b..03735247df 100644 --- a/libopenage/renderer/camera/frustum.cpp +++ b/libopenage/renderer/camera/frustum.cpp @@ -15,116 +15,121 @@ namespace openage::renderer::camera { Frustum::Frustum() : - top_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, - top_face_distance{0.0f}, - bottom_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, - bottom_face_distance{0.0f}, - right_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, - right_face_distance{0.0f}, - left_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, - left_face_distance{0.0f}, - far_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, - far_face_distance{0.0f}, - near_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, - near_face_distance{0.0f} -{ + top_face_distance{0.0f}, + bottom_face_distance{0.0f}, + right_face_distance{0.0f}, + left_face_distance{0.0f}, + far_face_distance{0.0f}, + near_face_distance{0.0f}, + top_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, + bottom_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, + right_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, + left_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, + far_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, + near_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)} { } -void Frustum::Recalculate(util::Vector2s &viewport_size, float near_distance, float far_distance, Eigen::Vector3f &scene_pos, Eigen::Vector3f cam_direction, Eigen::Vector3f up_direction, float real_zoom) { - // offsets are adjusted by zoom +void Frustum::Recalculate(util::Vector2s &viewport_size, + float near_distance, + float far_distance, + Eigen::Vector3f &scene_pos, + Eigen::Vector3f cam_direction, + Eigen::Vector3f up_direction, + float real_zoom) { + // offsets are adjusted by zoom // this is the same calculation as for the projection matrix float halfscreenwidth = viewport_size[0] / 2; float halfscreenheight = viewport_size[1] / 2; - float halfwidth = halfscreenwidth * real_zoom; + float halfwidth = halfscreenwidth * real_zoom; float halfheight = halfscreenheight * real_zoom; - Eigen::Vector3f direction = cam_direction.normalized(); - Eigen::Vector3f eye = scene_pos; + Eigen::Vector3f direction = cam_direction.normalized(); + Eigen::Vector3f eye = scene_pos; Eigen::Vector3f center = scene_pos + direction; - // calculate up (u) and right (s) vectors for camera + // calculate up (u) and right (s) vectors for camera // these define the camera plane in 3D space that the input - Eigen::Vector3f f = center - eye; + Eigen::Vector3f f = center - eye; f.normalize(); Eigen::Vector3f s = f.cross(up_direction); s.normalize(); Eigen::Vector3f u = s.cross(f); u.normalize(); - Eigen::Vector3f near_position = scene_pos - direction * near_distance; - Eigen::Vector3f far_position = scene_pos + direction * far_distance; - - // The frustum is a box with 8 points defining it (4 on the near plane, 4 on the far plane) - Eigen::Vector3f near_top_left = near_position - s * halfwidth + u * halfheight; - Eigen::Vector3f near_top_right = near_position + s * halfwidth + u * halfheight; - Eigen::Vector3f near_bottom_left = near_position - s * halfwidth - u * halfheight; - Eigen::Vector3f near_bottom_right = near_position + s * halfwidth - u * halfheight; - - Eigen::Vector3f far_top_left = far_position - s * halfwidth + u * halfheight; - Eigen::Vector3f far_top_right = far_position + s * halfwidth + u * halfheight; - Eigen::Vector3f far_bottom_left = far_position - s * halfwidth - u * halfheight; - Eigen::Vector3f far_bottom_right = far_position + s * halfwidth - u * halfheight; - - // The near and far planes are easiest to compute, as they should be in the direction of the camera - this->near_face_normal = -1.0f * cam_direction.normalized(); - this->far_face_normal = cam_direction.normalized(); - - this->near_face_distance = this->near_face_normal.dot(near_position) * -1.0f; - this->far_face_distance = this->far_face_normal.dot(far_position) * -1.0f; - - // Each left, right, top, and bottom plane are defined by three points on the plane - this->left_face_normal = (near_bottom_left - near_top_left).cross(far_bottom_left - near_bottom_left); - this->right_face_normal = (far_bottom_right - near_bottom_right).cross(near_bottom_right - near_top_right); - this->top_face_normal = (near_top_right - near_top_left).cross(near_top_left - far_top_left); - this->bottom_face_normal = (near_bottom_left - far_bottom_left).cross(near_bottom_right - near_bottom_left); - - this->left_face_normal.normalize(); - this->right_face_normal.normalize(); - this->top_face_normal.normalize(); - this->bottom_face_normal.normalize(); - - this->left_face_distance = this->left_face_normal.dot(near_bottom_left); - this->right_face_distance = this->right_face_normal.dot(far_bottom_right); - this->top_face_distance = this->top_face_normal.dot(near_top_right); - this->bottom_face_distance = this->bottom_face_normal.dot(near_bottom_left); + Eigen::Vector3f near_position = scene_pos - direction * near_distance; + Eigen::Vector3f far_position = scene_pos + direction * far_distance; + + // The frustum is a box with 8 points defining it (4 on the near plane, 4 on the far plane) + Eigen::Vector3f near_top_left = near_position - s * halfwidth + u * halfheight; + Eigen::Vector3f near_top_right = near_position + s * halfwidth + u * halfheight; + Eigen::Vector3f near_bottom_left = near_position - s * halfwidth - u * halfheight; + Eigen::Vector3f near_bottom_right = near_position + s * halfwidth - u * halfheight; + + Eigen::Vector3f far_top_left = far_position - s * halfwidth + u * halfheight; + Eigen::Vector3f far_top_right = far_position + s * halfwidth + u * halfheight; + Eigen::Vector3f far_bottom_left = far_position - s * halfwidth - u * halfheight; + Eigen::Vector3f far_bottom_right = far_position + s * halfwidth - u * halfheight; + + // The near and far planes are easiest to compute, as they should be in the direction of the camera + this->near_face_normal = -1.0f * cam_direction.normalized(); + this->far_face_normal = cam_direction.normalized(); + + this->near_face_distance = this->near_face_normal.dot(near_position) * -1.0f; + this->far_face_distance = this->far_face_normal.dot(far_position) * -1.0f; + + // Each left, right, top, and bottom plane are defined by three points on the plane + this->left_face_normal = (near_bottom_left - near_top_left).cross(far_bottom_left - near_bottom_left); + this->right_face_normal = (far_bottom_right - near_bottom_right).cross(near_bottom_right - near_top_right); + this->top_face_normal = (near_top_right - near_top_left).cross(near_top_left - far_top_left); + this->bottom_face_normal = (near_bottom_left - far_bottom_left).cross(near_bottom_right - near_bottom_left); + + this->left_face_normal.normalize(); + this->right_face_normal.normalize(); + this->top_face_normal.normalize(); + this->bottom_face_normal.normalize(); + + this->left_face_distance = this->left_face_normal.dot(near_bottom_left); + this->right_face_distance = this->right_face_normal.dot(far_bottom_right); + this->top_face_distance = this->top_face_normal.dot(near_top_right); + this->bottom_face_distance = this->bottom_face_normal.dot(near_bottom_left); } -bool Frustum::is_in_frustum(Eigen::Vector3f& pos) const { - // For each plane, if a point is behind one of the frustum planes, it is not within the frustum - float distance; - - distance = this->top_face_normal.dot(pos) - this->top_face_distance; - if (distance < 0) { - return false; - } - - distance = this->bottom_face_normal.dot(pos) - this->bottom_face_distance; - if (distance < 0) { - return false; - } - - distance = this->left_face_normal.dot(pos) - this->left_face_distance; - if (distance < 0) { - return false; - } - - distance = this->right_face_normal.dot(pos) - this->right_face_distance; - if (distance < 0) { - return false; - } - - distance = this->far_face_normal.dot(pos) - this->far_face_distance; - if (distance < 0) { - return false; - } - - distance = this->bottom_face_normal.dot(pos) - this->bottom_face_distance; - if (distance < 0) { - return false; - } - - return true; +bool Frustum::is_in_frustum(Eigen::Vector3f &pos) const { + // For each plane, if a point is behind one of the frustum planes, it is not within the frustum + float distance; + + distance = this->top_face_normal.dot(pos) - this->top_face_distance; + if (distance < 0) { + return false; + } + + distance = this->bottom_face_normal.dot(pos) - this->bottom_face_distance; + if (distance < 0) { + return false; + } + + distance = this->left_face_normal.dot(pos) - this->left_face_distance; + if (distance < 0) { + return false; + } + + distance = this->right_face_normal.dot(pos) - this->right_face_distance; + if (distance < 0) { + return false; + } + + distance = this->far_face_normal.dot(pos) - this->far_face_distance; + if (distance < 0) { + return false; + } + + distance = this->bottom_face_normal.dot(pos) - this->bottom_face_distance; + if (distance < 0) { + return false; + } + + return true; } -} +} // namespace openage::renderer::camera diff --git a/libopenage/renderer/camera/frustum.h b/libopenage/renderer/camera/frustum.h index 8dfe9372c5..bd87147177 100644 --- a/libopenage/renderer/camera/frustum.h +++ b/libopenage/renderer/camera/frustum.h @@ -14,33 +14,33 @@ namespace openage::renderer::camera { -class Frustum -{ +class Frustum { public: - Frustum(); + Frustum(); - void Recalculate(util::Vector2s& viewport_size, float near_distance, float far_distance, Eigen::Vector3f& scene_pos, Eigen::Vector3f cam_direction, Eigen::Vector3f up_direction, float real_zoom); + void Recalculate(util::Vector2s &viewport_size, + float near_distance, + float far_distance, + Eigen::Vector3f &scene_pos, + Eigen::Vector3f cam_direction, + Eigen::Vector3f up_direction, + float real_zoom); - bool is_in_frustum(Eigen::Vector3f& pos) const; + bool is_in_frustum(Eigen::Vector3f &pos) const; private: - Eigen::Vector3f top_face_normal; - float top_face_distance; - - Eigen::Vector3f bottom_face_normal; - float bottom_face_distance; - - Eigen::Vector3f right_face_normal; - float right_face_distance; - - Eigen::Vector3f left_face_normal; - float left_face_distance; - - Eigen::Vector3f far_face_normal; - float far_face_distance; - - Eigen::Vector3f near_face_normal; - float near_face_distance; - + Eigen::Vector3f top_face_normal; + Eigen::Vector3f bottom_face_normal; + Eigen::Vector3f right_face_normal; + Eigen::Vector3f left_face_normal; + Eigen::Vector3f far_face_normal; + Eigen::Vector3f near_face_normal; + + float top_face_distance; + float bottom_face_distance; + float right_face_distance; + float left_face_distance; + float far_face_distance; + float near_face_distance; }; } // namespace openage::renderer::camera diff --git a/libopenage/renderer/demo/stresstest_1.cpp b/libopenage/renderer/demo/stresstest_1.cpp index 9a113eb85d..99e6e253ef 100644 --- a/libopenage/renderer/demo/stresstest_1.cpp +++ b/libopenage/renderer/demo/stresstest_1.cpp @@ -47,8 +47,7 @@ void renderer_stresstest_1(const util::Path &path) { Eigen::Vector3f{17.0f, 10.0f, 7.0f}, 1.f, 64.f, - 1.f / 49.f, - true); + 1.f / 49.f); auto cam_unifs = camera->get_uniform_buffer()->create_empty_input(); cam_unifs->update( "view", diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index d8d9120c69..da326a4faf 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -11,6 +11,7 @@ #include #include "renderer/camera/camera.h" +#include "renderer/camera/frustum.h" #include "renderer/definitions.h" #include "renderer/resources/animation/angle_info.h" #include "renderer/resources/animation/animation_info.h" @@ -225,12 +226,9 @@ void WorldObject::set_uniforms(std::vector &camera) { - if (!camera->using_frustum_culling()) { - return true; - } - Eigen::Vector3f current_pos = this->position.get(this->last_update).to_world_space(); - return camera->is_in_frustum(current_pos); + auto frustum = camera->get_frustum(); + return frustum.is_in_frustum(current_pos); } } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 816203b9d8..bfa745d19d 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -59,7 +59,7 @@ void WorldRenderStage::update() { std::unique_lock lock{this->mutex}; auto current_time = this->clock->get_real_time(); for (auto &obj : this->render_objects) { - if (!obj->within_camera_frustum(this->camera)) { + if (not obj->within_camera_frustum(this->camera)) { continue; } From 34cc059308fc8023cc9eb4d55d5b840824eb8a94 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 14 Jul 2024 13:34:39 +0200 Subject: [PATCH 423/771] renderer: Fix object position fetching. Needed the current `time` for getting the correct position. --- libopenage/renderer/camera/camera.cpp | 44 +++++++++---------- libopenage/renderer/camera/frustum.cpp | 28 ++++++------ libopenage/renderer/camera/frustum.h | 43 ++++++++++++++---- libopenage/renderer/stages/world/object.cpp | 7 +-- libopenage/renderer/stages/world/object.h | 3 +- .../renderer/stages/world/render_stage.cpp | 2 +- 6 files changed, 78 insertions(+), 49 deletions(-) diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index 0e2112b6c7..01e9896c35 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -32,13 +32,13 @@ Camera::Camera(const std::shared_ptr &renderer, // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered float real_zoom = 0.7f * this->default_zoom_ratio * this->zoom; - frustum.Recalculate(this->viewport_size, - near_distance, - far_distance, - this->scene_pos, - cam_direction, - Eigen::Vector3f(0.0f, 1.0f, 0.0f), - real_zoom); + frustum.update(this->viewport_size, + near_distance, + far_distance, + this->scene_pos, + cam_direction, + Eigen::Vector3f(0.0f, 1.0f, 0.0f), + real_zoom); log::log(INFO << "Created new camera at position " << "(" << this->scene_pos[0] @@ -66,13 +66,13 @@ Camera::Camera(const std::shared_ptr &renderer, // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered float real_zoom = 0.7f * this->default_zoom_ratio * this->zoom; - frustum.Recalculate(this->viewport_size, - near_distance, - far_distance, - this->scene_pos, - cam_direction, - Eigen::Vector3f(0.0f, 1.0f, 0.0f), - real_zoom); + frustum.update(this->viewport_size, + near_distance, + far_distance, + this->scene_pos, + cam_direction, + Eigen::Vector3f(0.0f, 1.0f, 0.0f), + real_zoom); log::log(INFO << "Created new camera at position " << "(" << this->scene_pos[0] @@ -129,14 +129,14 @@ void Camera::move_to(Eigen::Vector3f scene_pos) { this->moved = true; // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered - float real_zoom = 0.7f * this->default_zoom_ratio * this->zoom; - frustum.Recalculate(viewport_size, - near_distance, - far_distance, - scene_pos, - cam_direction, - Eigen::Vector3f(0.0f, 1.0f, 0.0f), - real_zoom); + float real_zoom = 0.5f * this->default_zoom_ratio * this->zoom; + frustum.update(viewport_size, + near_distance, + far_distance, + scene_pos, + cam_direction, + Eigen::Vector3f(0.0f, 1.0f, 0.0f), + real_zoom); } void Camera::move_rel(Eigen::Vector3f direction, float delta) { diff --git a/libopenage/renderer/camera/frustum.cpp b/libopenage/renderer/camera/frustum.cpp index 03735247df..234dbe500a 100644 --- a/libopenage/renderer/camera/frustum.cpp +++ b/libopenage/renderer/camera/frustum.cpp @@ -29,13 +29,13 @@ Frustum::Frustum() : near_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)} { } -void Frustum::Recalculate(util::Vector2s &viewport_size, - float near_distance, - float far_distance, - Eigen::Vector3f &scene_pos, - Eigen::Vector3f cam_direction, - Eigen::Vector3f up_direction, - float real_zoom) { +void Frustum::update(util::Vector2s &viewport_size, + float near_distance, + float far_distance, + Eigen::Vector3f &scene_pos, + Eigen::Vector3f cam_direction, + Eigen::Vector3f up_direction, + float real_zoom) { // offsets are adjusted by zoom // this is the same calculation as for the projection matrix float halfscreenwidth = viewport_size[0] / 2; @@ -57,10 +57,10 @@ void Frustum::Recalculate(util::Vector2s &viewport_size, Eigen::Vector3f u = s.cross(f); u.normalize(); - Eigen::Vector3f near_position = scene_pos - direction * near_distance; + Eigen::Vector3f near_position = scene_pos + direction * near_distance; Eigen::Vector3f far_position = scene_pos + direction * far_distance; - // The frustum is a box with 8 points defining it (4 on the near plane, 4 on the far plane) + // The frustum is a cuboid box with 8 points defining it (4 on the near plane, 4 on the far plane) Eigen::Vector3f near_top_left = near_position - s * halfwidth + u * halfheight; Eigen::Vector3f near_top_right = near_position + s * halfwidth + u * halfheight; Eigen::Vector3f near_bottom_left = near_position - s * halfwidth - u * halfheight; @@ -72,11 +72,11 @@ void Frustum::Recalculate(util::Vector2s &viewport_size, Eigen::Vector3f far_bottom_right = far_position + s * halfwidth - u * halfheight; // The near and far planes are easiest to compute, as they should be in the direction of the camera - this->near_face_normal = -1.0f * cam_direction.normalized(); - this->far_face_normal = cam_direction.normalized(); + this->near_face_normal = cam_direction.normalized(); + this->far_face_normal = -1.0f * cam_direction.normalized(); - this->near_face_distance = this->near_face_normal.dot(near_position) * -1.0f; - this->far_face_distance = this->far_face_normal.dot(far_position) * -1.0f; + this->near_face_distance = this->near_face_normal.dot(near_position); + this->far_face_distance = this->far_face_normal.dot(far_position); // Each left, right, top, and bottom plane are defined by three points on the plane this->left_face_normal = (near_bottom_left - near_top_left).cross(far_bottom_left - near_bottom_left); @@ -95,7 +95,7 @@ void Frustum::Recalculate(util::Vector2s &viewport_size, this->bottom_face_distance = this->bottom_face_normal.dot(near_bottom_left); } -bool Frustum::is_in_frustum(Eigen::Vector3f &pos) const { +bool Frustum::in_frustum(Eigen::Vector3f &pos) const { // For each plane, if a point is behind one of the frustum planes, it is not within the frustum float distance; diff --git a/libopenage/renderer/camera/frustum.h b/libopenage/renderer/camera/frustum.h index bd87147177..d1d32ce296 100644 --- a/libopenage/renderer/camera/frustum.h +++ b/libopenage/renderer/camera/frustum.h @@ -14,19 +14,46 @@ namespace openage::renderer::camera { +/** + * Frustum for a camera. + * + * Used for frustum culling (https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling) + * in the renderer. + */ class Frustum { public: + /** + * Create a new frustum. + */ Frustum(); - void Recalculate(util::Vector2s &viewport_size, - float near_distance, - float far_distance, - Eigen::Vector3f &scene_pos, - Eigen::Vector3f cam_direction, - Eigen::Vector3f up_direction, - float real_zoom); + /** + * Update this frustum with the new camera parameters. + * + * @param viewport_size Size of the viewport (width x height). + * @param near_distance Near distance of the frustum. + * @param far_distance Far distance of the frustum. + * @param scene_pos Scene position of the camera. + * @param cam_direction Direction the camera is looking at. + * @param up_direction Up direction of the camera. + * @param real_zoom Zoom factor of the camera. + */ + void update(util::Vector2s &viewport_size, + float near_distance, + float far_distance, + Eigen::Vector3f &scene_pos, + Eigen::Vector3f cam_direction, + Eigen::Vector3f up_direction, + float real_zoom); - bool is_in_frustum(Eigen::Vector3f &pos) const; + /** + * Check whether a point in the scene is inside the frustum. + * + * @param scene_pos 3D scene coordinates. + * + * @return true if the point is inside the frustum, else false. + */ + bool in_frustum(Eigen::Vector3f &scene_pos) const; private: Eigen::Vector3f top_face_normal; diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index da326a4faf..a33f89f152 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -225,10 +225,11 @@ void WorldObject::set_uniforms(std::vectorlayer_uniforms = std::move(uniforms); } -bool WorldObject::within_camera_frustum(const std::shared_ptr &camera) { - Eigen::Vector3f current_pos = this->position.get(this->last_update).to_world_space(); +bool WorldObject::within_camera_frustum(const std::shared_ptr &camera, + const time::time_t &time) { + Eigen::Vector3f current_pos = this->position.get(time).to_world_space(); auto frustum = camera->get_frustum(); - return frustum.is_in_frustum(current_pos); + return frustum.in_frustum(current_pos); } } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/object.h b/libopenage/renderer/stages/world/object.h index 17da70be75..cdad725ae8 100644 --- a/libopenage/renderer/stages/world/object.h +++ b/libopenage/renderer/stages/world/object.h @@ -133,7 +133,8 @@ class WorldObject { */ void set_uniforms(std::vector> &&uniforms); - bool within_camera_frustum(const std::shared_ptr &camera); + bool within_camera_frustum(const std::shared_ptr &camera, + const time::time_t &time); /** * Shader uniform IDs for setting uniform values. diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index bfa745d19d..51ec4878d6 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -59,7 +59,7 @@ void WorldRenderStage::update() { std::unique_lock lock{this->mutex}; auto current_time = this->clock->get_real_time(); for (auto &obj : this->render_objects) { - if (not obj->within_camera_frustum(this->camera)) { + if (not obj->within_camera_frustum(this->camera, current_time)) { continue; } From 5f95f219ce57c35ba2befdeb1abc019bc456ebde Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 14 Jul 2024 17:16:02 +0200 Subject: [PATCH 424/771] renderer: Fix position curve in stresstest 1. --- libopenage/renderer/demo/stresstest_1.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/renderer/demo/stresstest_1.cpp b/libopenage/renderer/demo/stresstest_1.cpp index 99e6e253ef..6ec2887503 100644 --- a/libopenage/renderer/demo/stresstest_1.cpp +++ b/libopenage/renderer/demo/stresstest_1.cpp @@ -160,7 +160,7 @@ void renderer_stresstest_1(const util::Path &path) { position.set_insert(time + 3, initial_pos + coord::phys3_delta{8, 8, 0}); position.set_insert(time + 4, initial_pos + coord::phys3_delta{12, 4, 0}); position.set_insert(time + 5, initial_pos + coord::phys3_delta{12, 0, 0}); - position.set_insert(time + 6, initial_pos + coord::phys3_delta{12, -4, 0}); + position.set_insert(time + 6, initial_pos + coord::phys3_delta{8, -4, 0}); position.set_insert(time + 7, initial_pos + coord::phys3_delta{4, -4, 0}); position.set_insert(time + 8, initial_pos); From 69841ad5bb8209d15fd509fa5156fa2182806da7 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 14 Jul 2024 17:30:13 +0200 Subject: [PATCH 425/771] etc: Fix time pretty print for Keyframe. --- etc/gdb_pretty/printers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index 0dfac99af5..1a7803619f 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -276,7 +276,7 @@ def children(self): """ Get the displayed children of the keyframe. """ - yield ('time', self.__val['time']) + yield ('time', self.__val['timestamp']) yield ('value', self.__val['value']) From 0df247c69cee6bb5c84e3b7eda8b949301f6f5f3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 14 Jul 2024 17:30:55 +0200 Subject: [PATCH 426/771] renderer: Resize frustum to exact camera boundaries. --- libopenage/renderer/camera/camera.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index 01e9896c35..f0eb67a29c 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -65,7 +65,7 @@ Camera::Camera(const std::shared_ptr &renderer, this->init_uniform_buffer(renderer); // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered - float real_zoom = 0.7f * this->default_zoom_ratio * this->zoom; + float real_zoom = 0.5f * this->default_zoom_ratio * this->zoom; frustum.update(this->viewport_size, near_distance, far_distance, From 189d9162fd33bcbc9da6e4949f2c33097c12a821 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 14 Jul 2024 17:31:31 +0200 Subject: [PATCH 427/771] renderer: Fetch updates before checking that object is in frustrum. --- libopenage/renderer/stages/world/render_stage.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 51ec4878d6..2e70776c18 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -59,11 +59,12 @@ void WorldRenderStage::update() { std::unique_lock lock{this->mutex}; auto current_time = this->clock->get_real_time(); for (auto &obj : this->render_objects) { + obj->fetch_updates(current_time); + if (not obj->within_camera_frustum(this->camera, current_time)) { continue; } - obj->fetch_updates(current_time); if (obj->is_changed()) { if (obj->requires_renderable()) { auto layer_positions = obj->get_layer_positions(current_time); From f114994dfd6eaf6667fbbe1375f523fbb1ff4411 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 15 Jul 2024 00:05:01 +0200 Subject: [PATCH 428/771] renderer: Move camera constants to different file. --- libopenage/coord/pixel.cpp | 6 ++- libopenage/pathfinding/demo/demo_0.cpp | 3 +- libopenage/renderer/camera/CMakeLists.txt | 1 + libopenage/renderer/camera/camera.cpp | 51 +++++++++++--------- libopenage/renderer/camera/camera.h | 33 ++++++------- libopenage/renderer/camera/definitions.cpp | 9 ++++ libopenage/renderer/camera/definitions.h | 56 ++++++++++++++++++++++ 7 files changed, 115 insertions(+), 44 deletions(-) create mode 100644 libopenage/renderer/camera/definitions.cpp create mode 100644 libopenage/renderer/camera/definitions.h diff --git a/libopenage/coord/pixel.cpp b/libopenage/coord/pixel.cpp index c7dac5f815..a480cba2dc 100644 --- a/libopenage/coord/pixel.cpp +++ b/libopenage/coord/pixel.cpp @@ -1,9 +1,11 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. +// Copyright 2016-2024 the openage authors. See copying.md for legal info. #include "pixel.h" #include "coord/phys.h" #include "renderer/camera/camera.h" +#include "renderer/camera/definitions.h" + namespace openage { namespace coord { @@ -54,7 +56,7 @@ phys3 input::to_phys3(const std::shared_ptr &camera) c scene3 input::to_scene3(const std::shared_ptr &camera) const { // Use raycasting to find the position // Direction and origin point are fetched from the camera - auto cam_dir = renderer::camera::cam_direction; + auto cam_dir = renderer::camera::CAM_DIRECTION; auto ray_origin = camera->get_input_pos(*this); // xz plane that we want to intersect with diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index da7ae17492..bcf4cca945 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -10,6 +10,7 @@ #include "pathfinding/flow_field.h" #include "pathfinding/integration_field.h" #include "renderer/camera/camera.h" +#include "renderer/camera/definitions.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" #include "renderer/render_pass.h" @@ -313,7 +314,7 @@ void RenderManager0::hide_vectors() { std::pair RenderManager0::select_tile(double x, double y) { auto grid_plane_normal = Eigen::Vector3f{0, 1, 0}; auto grid_plane_point = Eigen::Vector3f{0, 0, 0}; - auto camera_direction = renderer::camera::cam_direction; + auto camera_direction = renderer::camera::CAM_DIRECTION; auto camera_position = camera->get_input_pos( coord::input(x, y)); diff --git a/libopenage/renderer/camera/CMakeLists.txt b/libopenage/renderer/camera/CMakeLists.txt index 43a49cc1c7..a3ffbd067f 100644 --- a/libopenage/renderer/camera/CMakeLists.txt +++ b/libopenage/renderer/camera/CMakeLists.txt @@ -1,4 +1,5 @@ add_sources(libopenage camera.cpp + definitions.cpp frustum.cpp ) diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index f0eb67a29c..5b43bc1bb5 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -19,9 +19,9 @@ Camera::Camera(const std::shared_ptr &renderer, util::Vector2s viewport_size) : scene_pos{Eigen::Vector3f(0.0f, 10.0f, 0.0f)}, viewport_size{viewport_size}, - zoom{1.0f}, - max_zoom_out{64.0f}, - default_zoom_ratio{1.0f / 49}, + zoom{DEFAULT_ZOOM}, + max_zoom_out{DEFAULT_MAX_ZOOM_OUT}, + default_zoom_ratio{DEFAULT_ZOOM_RATIO}, moved{true}, zoom_changed{true}, view{Eigen::Matrix4f::Identity()}, @@ -31,12 +31,12 @@ Camera::Camera(const std::shared_ptr &renderer, this->init_uniform_buffer(renderer); // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered - float real_zoom = 0.7f * this->default_zoom_ratio * this->zoom; + float real_zoom = get_zoom_factor(); frustum.update(this->viewport_size, - near_distance, - far_distance, + DEFAULT_NEAR_DISTANCE, + DEFAULT_FAR_DISTANCE, this->scene_pos, - cam_direction, + CAM_DIRECTION, Eigen::Vector3f(0.0f, 1.0f, 0.0f), real_zoom); @@ -65,12 +65,12 @@ Camera::Camera(const std::shared_ptr &renderer, this->init_uniform_buffer(renderer); // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered - float real_zoom = 0.5f * this->default_zoom_ratio * this->zoom; + float real_zoom = this->get_zoom_factor(); frustum.update(this->viewport_size, - near_distance, - far_distance, + DEFAULT_NEAR_DISTANCE, + DEFAULT_FAR_DISTANCE, this->scene_pos, - cam_direction, + CAM_DIRECTION, Eigen::Vector3f(0.0f, 1.0f, 0.0f), real_zoom); @@ -129,12 +129,12 @@ void Camera::move_to(Eigen::Vector3f scene_pos) { this->moved = true; // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered - float real_zoom = 0.5f * this->default_zoom_ratio * this->zoom; + float real_zoom = this->get_zoom_factor(); frustum.update(viewport_size, - near_distance, - far_distance, + DEFAULT_NEAR_DISTANCE, + DEFAULT_FAR_DISTANCE, scene_pos, - cam_direction, + CAM_DIRECTION, Eigen::Vector3f(0.0f, 1.0f, 0.0f), real_zoom); } @@ -179,7 +179,7 @@ const Eigen::Matrix4f &Camera::get_view_matrix() { return this->view; } - auto direction = cam_direction.normalized(); + auto direction = CAM_DIRECTION.normalized(); Eigen::Vector3f eye = this->scene_pos; Eigen::Vector3f center = this->scene_pos + direction; // look in the direction of the camera @@ -227,7 +227,7 @@ const Eigen::Matrix4f &Camera::get_projection_matrix() { float halfheight = this->viewport_size[1] / 2; // get zoom level - float real_zoom = 0.5f * this->default_zoom_ratio * this->zoom; + float real_zoom = this->get_zoom_factor(); // zoom by narrowing or widening viewport around focus point. // narrow viewport => zoom in (projected area gets *smaller* while screen size stays the same) @@ -240,10 +240,11 @@ const Eigen::Matrix4f &Camera::get_projection_matrix() { Eigen::Matrix4f mat = Eigen::Matrix4f::Identity(); mat(0, 0) = 2.0f / (right - left); mat(1, 1) = 2.0f / (top - bottom); - mat(2, 2) = -2.0f / (1000.0f - (-1.0f)); // clip near and far planes (TODO: necessary?) + mat(2, 2) = -2.0f / (DEFAULT_FAR_DISTANCE - DEFAULT_NEAR_DISTANCE); // clip near and far planes (TODO: necessary?) mat(0, 3) = -(right + left) / (right - left); mat(1, 3) = -(top + bottom) / (top - bottom); - mat(2, 3) = -(1000.0f + (-1.0f)) / (1000.0f - (-1.0f)); // clip near and far planes (TODO: necessary?) + mat(2, 3) = -(DEFAULT_FAR_DISTANCE + DEFAULT_NEAR_DISTANCE) + / (DEFAULT_FAR_DISTANCE - DEFAULT_NEAR_DISTANCE); // clip near and far planes (TODO: necessary?) // Cache matrix for subsequent calls this->proj = mat; @@ -261,7 +262,7 @@ Eigen::Vector3f Camera::get_input_pos(const coord::input &coord) const { // calculate up (u) and right (s) vectors for camera // these define the camera plane in 3D space that the input // coord exists on - auto direction = cam_direction.normalized(); + auto direction = CAM_DIRECTION.normalized(); Eigen::Vector3f eye = this->scene_pos; Eigen::Vector3f center = this->scene_pos + direction; Eigen::Vector3f up = Eigen::Vector3f(0.0f, 1.0f, 0.0f); @@ -277,7 +278,7 @@ Eigen::Vector3f Camera::get_input_pos(const coord::input &coord) const { // this is the same calculation as for the projection matrix float halfwidth = this->viewport_size[0] / 2; float halfheight = this->viewport_size[1] / 2; - float real_zoom = 0.5f * this->default_zoom_ratio * this->zoom; + float real_zoom = this->get_zoom_factor(); // calculate x and y offset on the camera plane relative to the camera position float x = +(2.0f * coord.x / this->viewport_size[0] - 1) * (halfwidth * real_zoom); @@ -293,6 +294,10 @@ const std::shared_ptr &Camera::get_uniform_buffer() con return this->uniform_buffer; } +Frustum Camera::get_frustum() const { + return this->frustum; +} + void Camera::init_uniform_buffer(const std::shared_ptr &renderer) { resources::UBOInput view_input{"view", resources::ubo_input_t::M4F32}; resources::UBOInput proj_input{"proj", resources::ubo_input_t::M4F32}; @@ -304,8 +309,8 @@ void Camera::init_uniform_buffer(const std::shared_ptr &renderer) { this->uniform_buffer = renderer->add_uniform_buffer(ubo_info); } -Frustum Camera::get_frustum() const { - return this->frustum; +inline float Camera::get_zoom_factor() const { + return 0.5f * this->default_zoom_ratio * this->zoom; } } // namespace openage::renderer::camera diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index 26bebd7e60..e1fd0f0f44 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -12,6 +12,7 @@ #include "coord/scene.h" #include "util/vector.h" +#include "renderer/camera/definitions.h" #include "renderer/camera/frustum.h" namespace openage::renderer { @@ -20,21 +21,6 @@ class UniformBuffer; namespace camera { -/** - * Camera direction (= where it looks at). - * Uses a dimetric perspective like in AoE with the (fixed) angles - * yaw = -135 degrees - * pitch = -30 degrees - */ -static const Eigen::Vector3f cam_direction{ - -1 * (std::sqrt(6.f) / 4), - -0.5f, - -1 * (std::sqrt(6.f) / 4), -}; - -static const float near_distance = 0.01f; -static const float far_distance = 100.0f; - /** * Camera for selecting what part of the ingame world is displayed. * @@ -67,9 +53,9 @@ class Camera { Camera(const std::shared_ptr &renderer, util::Vector2s viewport_size, Eigen::Vector3f scene_pos, - float zoom = 1.0f, - float max_zoom_out = 64.0f, - float default_zoom_ratio = 1.0f / 49); + float zoom = DEFAULT_ZOOM, + float max_zoom_out = DEFAULT_MAX_ZOOM_OUT, + float default_zoom_ratio = DEFAULT_ZOOM_RATIO); ~Camera() = default; /** @@ -208,6 +194,17 @@ class Camera { */ void init_uniform_buffer(const std::shared_ptr &renderer); + /** + * Get the zoom factor applied to the camera projection. + * + * The zoom factor is calculated as + * + * zoom * zoom_ratio * 0.5f + * + * @return Zoom factor for projection. + */ + inline float get_zoom_factor() const; + /** * Position in the 3D scene. */ diff --git a/libopenage/renderer/camera/definitions.cpp b/libopenage/renderer/camera/definitions.cpp new file mode 100644 index 0000000000..9011323855 --- /dev/null +++ b/libopenage/renderer/camera/definitions.cpp @@ -0,0 +1,9 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "definitions.h" + +namespace openage::renderer::camera { + +// this file is intentionally empty + +} // namespace openage::renderer::camera diff --git a/libopenage/renderer/camera/definitions.h b/libopenage/renderer/camera/definitions.h new file mode 100644 index 0000000000..3ce6aa4f22 --- /dev/null +++ b/libopenage/renderer/camera/definitions.h @@ -0,0 +1,56 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + + +namespace openage::renderer::camera { + +/** + * Camera direction (= where it looks at). + * Uses a dimetric perspective like in AoE with the (fixed) angles + * yaw = -135 degrees + * pitch = -30 degrees + */ +static const Eigen::Vector3f CAM_DIRECTION{ + -1 * (std::sqrt(6.f) / 4), + -0.5f, + -1 * (std::sqrt(6.f) / 4), +}; + +/** + * Default near distance of the camera. + * + * Determines how close objects can be to the camera before they are not rendered anymore. + */ +static constexpr float DEFAULT_NEAR_DISTANCE = 0.01f; + +/** + * Default far distance of the camera. + * + * Determines how far objects can be from the camera before they are not rendered anymore. + */ +static constexpr float DEFAULT_FAR_DISTANCE = 100.0f; + +/** + * Default zoom level of the camera. + */ +static constexpr float DEFAULT_ZOOM = 1.0f; + +/** + * Maximum zoom out level of the camera. + */ +static constexpr float DEFAULT_MAX_ZOOM_OUT = 64.0f; + +/** + * Default zoom ratio. + * + * This adjusts the zoom level of the camera to the size of sprites in the game, + * so that 1 pixel in a sprite == 1 pixel on the screen. + * + * 1.0f / 49 is the default value for the AoE2 sprites. + */ +static constexpr float DEFAULT_ZOOM_RATIO = 1.0f / 49; + +} // namespace openage::renderer::camera From f3365cf8cb7b30852fe23ade23bcb82c0cf79209 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 15 Jul 2024 00:51:29 +0200 Subject: [PATCH 429/771] renderer: Construct frustum with parameters. --- libopenage/renderer/camera/camera.cpp | 49 +++++------ libopenage/renderer/camera/camera.h | 8 +- libopenage/renderer/camera/definitions.h | 5 ++ libopenage/renderer/camera/frustum.cpp | 62 +++++++------ libopenage/renderer/camera/frustum.h | 88 ++++++++++++++++--- libopenage/renderer/stages/world/object.cpp | 6 +- libopenage/renderer/stages/world/object.h | 14 ++- .../renderer/stages/world/render_stage.cpp | 4 +- 8 files changed, 157 insertions(+), 79 deletions(-) diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index 5b43bc1bb5..309b29737c 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -25,21 +25,18 @@ Camera::Camera(const std::shared_ptr &renderer, moved{true}, zoom_changed{true}, view{Eigen::Matrix4f::Identity()}, - proj{Eigen::Matrix4f::Identity()} { + proj{Eigen::Matrix4f::Identity()}, + frustum{this->viewport_size, + DEFAULT_NEAR_DISTANCE, + DEFAULT_FAR_DISTANCE, + this->scene_pos, + CAM_DIRECTION, + Eigen::Vector3f(0.0f, 1.0f, 0.0f), + this->get_zoom_factor()} { this->look_at_scene(Eigen::Vector3f(0.0f, 0.0f, 0.0f)); this->init_uniform_buffer(renderer); - // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered - float real_zoom = get_zoom_factor(); - frustum.update(this->viewport_size, - DEFAULT_NEAR_DISTANCE, - DEFAULT_FAR_DISTANCE, - this->scene_pos, - CAM_DIRECTION, - Eigen::Vector3f(0.0f, 1.0f, 0.0f), - real_zoom); - log::log(INFO << "Created new camera at position " << "(" << this->scene_pos[0] << ", " << this->scene_pos[1] @@ -61,19 +58,16 @@ Camera::Camera(const std::shared_ptr &renderer, zoom_changed{true}, viewport_changed{true}, view{Eigen::Matrix4f::Identity()}, - proj{Eigen::Matrix4f::Identity()} { + proj{Eigen::Matrix4f::Identity()}, + frustum{this->viewport_size, + DEFAULT_NEAR_DISTANCE, + DEFAULT_FAR_DISTANCE, + this->scene_pos, + CAM_DIRECTION, + Eigen::Vector3f(0.0f, 1.0f, 0.0f), + this->get_zoom_factor()} { this->init_uniform_buffer(renderer); - // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered - float real_zoom = this->get_zoom_factor(); - frustum.update(this->viewport_size, - DEFAULT_NEAR_DISTANCE, - DEFAULT_FAR_DISTANCE, - this->scene_pos, - CAM_DIRECTION, - Eigen::Vector3f(0.0f, 1.0f, 0.0f), - real_zoom); - log::log(INFO << "Created new camera at position " << "(" << this->scene_pos[0] << ", " << this->scene_pos[1] @@ -128,15 +122,14 @@ void Camera::move_to(Eigen::Vector3f scene_pos) { this->scene_pos = scene_pos; this->moved = true; - // Make the frustum slightly bigger than the camera's view to ensure objects on the boundary get rendered - float real_zoom = this->get_zoom_factor(); + // Update frustum frustum.update(viewport_size, DEFAULT_NEAR_DISTANCE, DEFAULT_FAR_DISTANCE, scene_pos, CAM_DIRECTION, Eigen::Vector3f(0.0f, 1.0f, 0.0f), - real_zoom); + this->get_zoom_factor()); } void Camera::move_rel(Eigen::Vector3f direction, float delta) { @@ -183,7 +176,7 @@ const Eigen::Matrix4f &Camera::get_view_matrix() { Eigen::Vector3f eye = this->scene_pos; Eigen::Vector3f center = this->scene_pos + direction; // look in the direction of the camera - Eigen::Vector3f up = Eigen::Vector3f(0.0f, 1.0f, 0.0f); + Eigen::Vector3f up = CAM_UP; Eigen::Vector3f f = center - eye; f.normalize(); @@ -265,7 +258,7 @@ Eigen::Vector3f Camera::get_input_pos(const coord::input &coord) const { auto direction = CAM_DIRECTION.normalized(); Eigen::Vector3f eye = this->scene_pos; Eigen::Vector3f center = this->scene_pos + direction; - Eigen::Vector3f up = Eigen::Vector3f(0.0f, 1.0f, 0.0f); + Eigen::Vector3f up = CAM_UP; Eigen::Vector3f f = center - eye; f.normalize(); @@ -294,7 +287,7 @@ const std::shared_ptr &Camera::get_uniform_buffer() con return this->uniform_buffer; } -Frustum Camera::get_frustum() const { +const Frustum &Camera::get_frustum() const { return this->frustum; } diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index e1fd0f0f44..8fc72b2a72 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -24,6 +24,9 @@ namespace camera { /** * Camera for selecting what part of the ingame world is displayed. * + * The camera uses orthographic projection as it is primarily used for + * 2D rendering. + * * TODO: Vulkan version. */ class Camera { @@ -184,7 +187,7 @@ class Camera { * * @return Frustum object. */ - Frustum get_frustum() const; + const Frustum &get_frustum() const; private: /** @@ -292,8 +295,7 @@ class Camera { std::shared_ptr uniform_buffer; /** - * The frustum used to cull objects - * Will be recalculated regardless of whether frustum culling is enabled + * Frustum (viewing volume) for culling rendering objects. */ Frustum frustum; }; diff --git a/libopenage/renderer/camera/definitions.h b/libopenage/renderer/camera/definitions.h index 3ce6aa4f22..9586239a2f 100644 --- a/libopenage/renderer/camera/definitions.h +++ b/libopenage/renderer/camera/definitions.h @@ -19,6 +19,11 @@ static const Eigen::Vector3f CAM_DIRECTION{ -1 * (std::sqrt(6.f) / 4), }; +/** + * Camera up vector. + */ +static const Eigen::Vector3f CAM_UP{0.0f, 1.0f, 0.0f}; + /** * Default near distance of the camera. * diff --git a/libopenage/renderer/camera/frustum.cpp b/libopenage/renderer/camera/frustum.cpp index 234dbe500a..661672206a 100644 --- a/libopenage/renderer/camera/frustum.cpp +++ b/libopenage/renderer/camera/frustum.cpp @@ -14,39 +14,40 @@ namespace openage::renderer::camera { -Frustum::Frustum() : - top_face_distance{0.0f}, - bottom_face_distance{0.0f}, - right_face_distance{0.0f}, - left_face_distance{0.0f}, - far_face_distance{0.0f}, - near_face_distance{0.0f}, - top_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, - bottom_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, - right_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, - left_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, - far_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)}, - near_face_normal{Eigen::Vector3f(0.0f, 0.0f, 0.0f)} { +Frustum::Frustum(const util::Vector2s &viewport_size, + const float near_distance, + const float far_distance, + const Eigen::Vector3f &camera_pos, + const Eigen::Vector3f &cam_direction, + const Eigen::Vector3f &up_direction, + const float zoom_factor) { + this->update(viewport_size, + near_distance, + far_distance, + camera_pos, + cam_direction, + up_direction, + zoom_factor); } -void Frustum::update(util::Vector2s &viewport_size, - float near_distance, - float far_distance, - Eigen::Vector3f &scene_pos, - Eigen::Vector3f cam_direction, - Eigen::Vector3f up_direction, - float real_zoom) { +void Frustum::update(const util::Vector2s &viewport_size, + const float near_distance, + const float far_distance, + const Eigen::Vector3f &camera_pos, + const Eigen::Vector3f &cam_direction, + const Eigen::Vector3f &up_direction, + const float zoom_factor) { // offsets are adjusted by zoom // this is the same calculation as for the projection matrix float halfscreenwidth = viewport_size[0] / 2; float halfscreenheight = viewport_size[1] / 2; - float halfwidth = halfscreenwidth * real_zoom; - float halfheight = halfscreenheight * real_zoom; + float halfwidth = halfscreenwidth * zoom_factor; + float halfheight = halfscreenheight * zoom_factor; Eigen::Vector3f direction = cam_direction.normalized(); - Eigen::Vector3f eye = scene_pos; - Eigen::Vector3f center = scene_pos + direction; + Eigen::Vector3f eye = camera_pos; + Eigen::Vector3f center = camera_pos + direction; // calculate up (u) and right (s) vectors for camera // these define the camera plane in 3D space that the input @@ -57,8 +58,9 @@ void Frustum::update(util::Vector2s &viewport_size, Eigen::Vector3f u = s.cross(f); u.normalize(); - Eigen::Vector3f near_position = scene_pos + direction * near_distance; - Eigen::Vector3f far_position = scene_pos + direction * far_distance; + // calculate distance of the camera position to the near and far plane + Eigen::Vector3f near_position = camera_pos + direction * near_distance; + Eigen::Vector3f far_position = camera_pos + direction * far_distance; // The frustum is a cuboid box with 8 points defining it (4 on the near plane, 4 on the far plane) Eigen::Vector3f near_top_left = near_position - s * halfwidth + u * halfheight; @@ -67,7 +69,7 @@ void Frustum::update(util::Vector2s &viewport_size, Eigen::Vector3f near_bottom_right = near_position + s * halfwidth - u * halfheight; Eigen::Vector3f far_top_left = far_position - s * halfwidth + u * halfheight; - Eigen::Vector3f far_top_right = far_position + s * halfwidth + u * halfheight; + // Eigen::Vector3f far_top_right = far_position + s * halfwidth + u * halfheight; // unused Eigen::Vector3f far_bottom_left = far_position - s * halfwidth - u * halfheight; Eigen::Vector3f far_bottom_right = far_position + s * halfwidth - u * halfheight; @@ -75,6 +77,7 @@ void Frustum::update(util::Vector2s &viewport_size, this->near_face_normal = cam_direction.normalized(); this->far_face_normal = -1.0f * cam_direction.normalized(); + // The distance of the plane from the origin is the dot product of the normal and a point on the plane this->near_face_distance = this->near_face_normal.dot(near_position); this->far_face_distance = this->far_face_normal.dot(far_position); @@ -84,18 +87,21 @@ void Frustum::update(util::Vector2s &viewport_size, this->top_face_normal = (near_top_right - near_top_left).cross(near_top_left - far_top_left); this->bottom_face_normal = (near_bottom_left - far_bottom_left).cross(near_bottom_right - near_bottom_left); + // for orthographic projection, the normal of left/right should equal -s/s + // and the normal of top/bottom should equal u/-u this->left_face_normal.normalize(); this->right_face_normal.normalize(); this->top_face_normal.normalize(); this->bottom_face_normal.normalize(); + // calculate the distance of the planes to the origin this->left_face_distance = this->left_face_normal.dot(near_bottom_left); this->right_face_distance = this->right_face_normal.dot(far_bottom_right); this->top_face_distance = this->top_face_normal.dot(near_top_right); this->bottom_face_distance = this->bottom_face_normal.dot(near_bottom_left); } -bool Frustum::in_frustum(Eigen::Vector3f &pos) const { +bool Frustum::in_frustum(const Eigen::Vector3f &pos) const { // For each plane, if a point is behind one of the frustum planes, it is not within the frustum float distance; diff --git a/libopenage/renderer/camera/frustum.h b/libopenage/renderer/camera/frustum.h index d1d32ce296..32ae5cb820 100644 --- a/libopenage/renderer/camera/frustum.h +++ b/libopenage/renderer/camera/frustum.h @@ -15,17 +15,35 @@ namespace openage::renderer::camera { /** - * Frustum for a camera. + * Frustum for a camera. The frustum is defined by 6 planes (top, bottom, right, left, far, near) that + * define the viewing volume of the camera. * * Used for frustum culling (https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling) * in the renderer. + * + * As the openage camera uses orthographic projection, the frustum is a box, i.e. plane opposite + * to each other are parallel. */ class Frustum { public: /** * Create a new frustum. + * + * @param viewport_size Size of the viewport (width x height). + * @param near_distance Near distance of the frustum. + * @param far_distance Far distance of the frustum. + * @param camera_pos Scene position of the camera. + * @param cam_direction Direction the camera is looking at. + * @param up_direction Up direction of the camera. + * @param zoom_factor Zoom factor of the camera. */ - Frustum(); + Frustum(const util::Vector2s &viewport_size, + const float near_distance, + const float far_distance, + const Eigen::Vector3f &camera_pos, + const Eigen::Vector3f &cam_direction, + const Eigen::Vector3f &up_direction, + const float zoom_factor); /** * Update this frustum with the new camera parameters. @@ -33,18 +51,18 @@ class Frustum { * @param viewport_size Size of the viewport (width x height). * @param near_distance Near distance of the frustum. * @param far_distance Far distance of the frustum. - * @param scene_pos Scene position of the camera. + * @param camera_pos Scene position of the camera. * @param cam_direction Direction the camera is looking at. * @param up_direction Up direction of the camera. - * @param real_zoom Zoom factor of the camera. + * @param zoom_factor Zoom factor of the camera. */ - void update(util::Vector2s &viewport_size, - float near_distance, - float far_distance, - Eigen::Vector3f &scene_pos, - Eigen::Vector3f cam_direction, - Eigen::Vector3f up_direction, - float real_zoom); + void update(const util::Vector2s &viewport_size, + const float near_distance, + const float far_distance, + const Eigen::Vector3f &camera_pos, + const Eigen::Vector3f &cam_direction, + const Eigen::Vector3f &up_direction, + const float zoom_factor); /** * Check whether a point in the scene is inside the frustum. @@ -53,21 +71,67 @@ class Frustum { * * @return true if the point is inside the frustum, else false. */ - bool in_frustum(Eigen::Vector3f &scene_pos) const; + bool in_frustum(const Eigen::Vector3f &scene_pos) const; private: + /** + * Normal vector of the top face. + */ Eigen::Vector3f top_face_normal; + + /** + * Normal vector of the bottom face. + */ Eigen::Vector3f bottom_face_normal; + + /** + * Normal vector of the right face. + */ Eigen::Vector3f right_face_normal; + + /** + * Normal vector of the left face. + */ Eigen::Vector3f left_face_normal; + + /** + * Normal vector of the far face. + */ Eigen::Vector3f far_face_normal; + + /** + * Normal vector of the near face. + */ Eigen::Vector3f near_face_normal; + /** + * Shortest distance from the top face to the scene origin. + */ float top_face_distance; + + /** + * Shortest distance from the bottom face to the scene origin. + */ float bottom_face_distance; + + /** + * Shortest distance from the right face to the scene origin. + */ float right_face_distance; + + /** + * Shortest distance from the left face to the scene origin. + */ float left_face_distance; + + /** + * Shortest distance from the far face to the scene origin. + */ float far_face_distance; + + /** + * Shortest distance from the near face to the scene origin. + */ float near_face_distance; }; } // namespace openage::renderer::camera diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index a33f89f152..d099e2451a 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -10,7 +10,6 @@ #include -#include "renderer/camera/camera.h" #include "renderer/camera/frustum.h" #include "renderer/definitions.h" #include "renderer/resources/animation/angle_info.h" @@ -225,10 +224,9 @@ void WorldObject::set_uniforms(std::vectorlayer_uniforms = std::move(uniforms); } -bool WorldObject::within_camera_frustum(const std::shared_ptr &camera, - const time::time_t &time) { +bool WorldObject::is_visible(const camera::Frustum &frustum, + const time::time_t &time) { Eigen::Vector3f current_pos = this->position.get(time).to_world_space(); - auto frustum = camera->get_frustum(); return frustum.in_frustum(current_pos); } diff --git a/libopenage/renderer/stages/world/object.h b/libopenage/renderer/stages/world/object.h index cdad725ae8..63a07696eb 100644 --- a/libopenage/renderer/stages/world/object.h +++ b/libopenage/renderer/stages/world/object.h @@ -20,7 +20,7 @@ namespace openage::renderer { class UniformInput; namespace camera { -class Camera; +class Frustum; } namespace resources { @@ -133,8 +133,16 @@ class WorldObject { */ void set_uniforms(std::vector> &&uniforms); - bool within_camera_frustum(const std::shared_ptr &camera, - const time::time_t &time); + /** + * Check whether the object is visible in the camera view. + * + * @param frustum Camera frustum for culling. + * @param time Current simulation time. + * + * @return true if the object is visible, else false. + */ + bool is_visible(const camera::Frustum &frustum, + const time::time_t &time); /** * Shader uniform IDs for setting uniform values. diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 2e70776c18..1b3608885b 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -3,6 +3,7 @@ #include "render_stage.h" #include "renderer/camera/camera.h" +#include "renderer/camera/frustum.h" #include "renderer/opengl/context.h" #include "renderer/render_pass.h" #include "renderer/render_target.h" @@ -58,10 +59,11 @@ void WorldRenderStage::add_render_entity(const std::shared_ptrmutex}; auto current_time = this->clock->get_real_time(); + auto &camera_frustum = this->camera->get_frustum(); for (auto &obj : this->render_objects) { obj->fetch_updates(current_time); - if (not obj->within_camera_frustum(this->camera, current_time)) { + if (not obj->is_visible(camera_frustum, current_time)) { continue; } From d88fb47b7049f0d95470561cb62fd506e742762a Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 15 Jul 2024 22:57:55 +0200 Subject: [PATCH 430/771] renderer: Rename Frustum class to Frustum3d. --- libopenage/renderer/camera/CMakeLists.txt | 2 +- libopenage/renderer/camera/camera.cpp | 2 +- libopenage/renderer/camera/camera.h | 8 ++--- .../camera/{frustum.cpp => frustum_3d.cpp} | 32 +++++++++---------- .../camera/{frustum.h => frustum_3d.h} | 21 ++++++------ libopenage/renderer/stages/world/object.cpp | 4 +-- libopenage/renderer/stages/world/object.h | 4 +-- .../renderer/stages/world/render_stage.cpp | 4 +-- 8 files changed, 39 insertions(+), 38 deletions(-) rename libopenage/renderer/camera/{frustum.cpp => frustum_3d.cpp} (90%) rename libopenage/renderer/camera/{frustum.h => frustum_3d.h} (85%) diff --git a/libopenage/renderer/camera/CMakeLists.txt b/libopenage/renderer/camera/CMakeLists.txt index a3ffbd067f..e639bc75fa 100644 --- a/libopenage/renderer/camera/CMakeLists.txt +++ b/libopenage/renderer/camera/CMakeLists.txt @@ -1,5 +1,5 @@ add_sources(libopenage camera.cpp definitions.cpp - frustum.cpp + frustum_3d.cpp ) diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index 309b29737c..383c2124d6 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -287,7 +287,7 @@ const std::shared_ptr &Camera::get_uniform_buffer() con return this->uniform_buffer; } -const Frustum &Camera::get_frustum() const { +const Frustum3d &Camera::get_frustum_3d() const { return this->frustum; } diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index 8fc72b2a72..6791a6b8d8 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -13,7 +13,7 @@ #include "util/vector.h" #include "renderer/camera/definitions.h" -#include "renderer/camera/frustum.h" +#include "renderer/camera/frustum_3d.h" namespace openage::renderer { class Renderer; @@ -183,11 +183,11 @@ class Camera { const std::shared_ptr &get_uniform_buffer() const; /** - * Get a frustum object for this camera. + * Get a 3D frustum object for this camera. * * @return Frustum object. */ - const Frustum &get_frustum() const; + const Frustum3d &get_frustum_3d() const; private: /** @@ -297,7 +297,7 @@ class Camera { /** * Frustum (viewing volume) for culling rendering objects. */ - Frustum frustum; + Frustum3d frustum; }; } // namespace camera diff --git a/libopenage/renderer/camera/frustum.cpp b/libopenage/renderer/camera/frustum_3d.cpp similarity index 90% rename from libopenage/renderer/camera/frustum.cpp rename to libopenage/renderer/camera/frustum_3d.cpp index 661672206a..f4dddc9f27 100644 --- a/libopenage/renderer/camera/frustum.cpp +++ b/libopenage/renderer/camera/frustum_3d.cpp @@ -1,6 +1,6 @@ // Copyright 2024-2024 the openage authors. See copying.md for legal info. -#include "frustum.h" +#include "frustum_3d.h" #include #include @@ -14,13 +14,13 @@ namespace openage::renderer::camera { -Frustum::Frustum(const util::Vector2s &viewport_size, - const float near_distance, - const float far_distance, - const Eigen::Vector3f &camera_pos, - const Eigen::Vector3f &cam_direction, - const Eigen::Vector3f &up_direction, - const float zoom_factor) { +Frustum3d::Frustum3d(const util::Vector2s &viewport_size, + const float near_distance, + const float far_distance, + const Eigen::Vector3f &camera_pos, + const Eigen::Vector3f &cam_direction, + const Eigen::Vector3f &up_direction, + const float zoom_factor) { this->update(viewport_size, near_distance, far_distance, @@ -30,13 +30,13 @@ Frustum::Frustum(const util::Vector2s &viewport_size, zoom_factor); } -void Frustum::update(const util::Vector2s &viewport_size, - const float near_distance, - const float far_distance, - const Eigen::Vector3f &camera_pos, - const Eigen::Vector3f &cam_direction, - const Eigen::Vector3f &up_direction, - const float zoom_factor) { +void Frustum3d::update(const util::Vector2s &viewport_size, + const float near_distance, + const float far_distance, + const Eigen::Vector3f &camera_pos, + const Eigen::Vector3f &cam_direction, + const Eigen::Vector3f &up_direction, + const float zoom_factor) { // offsets are adjusted by zoom // this is the same calculation as for the projection matrix float halfscreenwidth = viewport_size[0] / 2; @@ -101,7 +101,7 @@ void Frustum::update(const util::Vector2s &viewport_size, this->bottom_face_distance = this->bottom_face_normal.dot(near_bottom_left); } -bool Frustum::in_frustum(const Eigen::Vector3f &pos) const { +bool Frustum3d::in_frustum(const Eigen::Vector3f &pos) const { // For each plane, if a point is behind one of the frustum planes, it is not within the frustum float distance; diff --git a/libopenage/renderer/camera/frustum.h b/libopenage/renderer/camera/frustum_3d.h similarity index 85% rename from libopenage/renderer/camera/frustum.h rename to libopenage/renderer/camera/frustum_3d.h index 32ae5cb820..d777a0d403 100644 --- a/libopenage/renderer/camera/frustum.h +++ b/libopenage/renderer/camera/frustum_3d.h @@ -15,8 +15,9 @@ namespace openage::renderer::camera { /** - * Frustum for a camera. The frustum is defined by 6 planes (top, bottom, right, left, far, near) that - * define the viewing volume of the camera. + * Frustum for culling objects outside of a camera viewcone in 3D space. The frustum + * is defined by 6 planes (top, bottom, right, left, far, near) that define the boundaries + * of the viewing volume of the camera. * * Used for frustum culling (https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling) * in the renderer. @@ -24,7 +25,7 @@ namespace openage::renderer::camera { * As the openage camera uses orthographic projection, the frustum is a box, i.e. plane opposite * to each other are parallel. */ -class Frustum { +class Frustum3d { public: /** * Create a new frustum. @@ -37,13 +38,13 @@ class Frustum { * @param up_direction Up direction of the camera. * @param zoom_factor Zoom factor of the camera. */ - Frustum(const util::Vector2s &viewport_size, - const float near_distance, - const float far_distance, - const Eigen::Vector3f &camera_pos, - const Eigen::Vector3f &cam_direction, - const Eigen::Vector3f &up_direction, - const float zoom_factor); + Frustum3d(const util::Vector2s &viewport_size, + const float near_distance, + const float far_distance, + const Eigen::Vector3f &camera_pos, + const Eigen::Vector3f &cam_direction, + const Eigen::Vector3f &up_direction, + const float zoom_factor); /** * Update this frustum with the new camera parameters. diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index d099e2451a..93ef82f4d2 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -10,7 +10,7 @@ #include -#include "renderer/camera/frustum.h" +#include "renderer/camera/frustum_3d.h" #include "renderer/definitions.h" #include "renderer/resources/animation/angle_info.h" #include "renderer/resources/animation/animation_info.h" @@ -224,7 +224,7 @@ void WorldObject::set_uniforms(std::vectorlayer_uniforms = std::move(uniforms); } -bool WorldObject::is_visible(const camera::Frustum &frustum, +bool WorldObject::is_visible(const camera::Frustum3d &frustum, const time::time_t &time) { Eigen::Vector3f current_pos = this->position.get(time).to_world_space(); return frustum.in_frustum(current_pos); diff --git a/libopenage/renderer/stages/world/object.h b/libopenage/renderer/stages/world/object.h index 63a07696eb..9576d906c9 100644 --- a/libopenage/renderer/stages/world/object.h +++ b/libopenage/renderer/stages/world/object.h @@ -20,7 +20,7 @@ namespace openage::renderer { class UniformInput; namespace camera { -class Frustum; +class Frustum3d; } namespace resources { @@ -141,7 +141,7 @@ class WorldObject { * * @return true if the object is visible, else false. */ - bool is_visible(const camera::Frustum &frustum, + bool is_visible(const camera::Frustum3d &frustum, const time::time_t &time); /** diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 1b3608885b..85378aef09 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -3,7 +3,7 @@ #include "render_stage.h" #include "renderer/camera/camera.h" -#include "renderer/camera/frustum.h" +#include "renderer/camera/frustum_3d.h" #include "renderer/opengl/context.h" #include "renderer/render_pass.h" #include "renderer/render_target.h" @@ -59,7 +59,7 @@ void WorldRenderStage::add_render_entity(const std::shared_ptrmutex}; auto current_time = this->clock->get_real_time(); - auto &camera_frustum = this->camera->get_frustum(); + auto &camera_frustum = this->camera->get_frustum_3d(); for (auto &obj : this->render_objects) { obj->fetch_updates(current_time); From 6926049672cd274bdf39f2743555880dc1013139 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 16 Jul 2024 02:35:34 +0200 Subject: [PATCH 431/771] renderer: 2D Frustum implementation. --- libopenage/renderer/camera/CMakeLists.txt | 1 + libopenage/renderer/camera/frustum_2d.cpp | 63 +++++++++++++++ libopenage/renderer/camera/frustum_2d.h | 79 +++++++++++++++++++ libopenage/renderer/camera/frustum_3d.cpp | 9 --- libopenage/renderer/camera/frustum_3d.h | 7 +- .../renderer/resources/texture_subinfo.h | 2 +- 6 files changed, 145 insertions(+), 16 deletions(-) create mode 100644 libopenage/renderer/camera/frustum_2d.cpp create mode 100644 libopenage/renderer/camera/frustum_2d.h diff --git a/libopenage/renderer/camera/CMakeLists.txt b/libopenage/renderer/camera/CMakeLists.txt index e639bc75fa..aea6391e7a 100644 --- a/libopenage/renderer/camera/CMakeLists.txt +++ b/libopenage/renderer/camera/CMakeLists.txt @@ -1,5 +1,6 @@ add_sources(libopenage camera.cpp definitions.cpp + frustum_2d.cpp frustum_3d.cpp ) diff --git a/libopenage/renderer/camera/frustum_2d.cpp b/libopenage/renderer/camera/frustum_2d.cpp new file mode 100644 index 0000000000..38c1ff537c --- /dev/null +++ b/libopenage/renderer/camera/frustum_2d.cpp @@ -0,0 +1,63 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "frustum_2d.h" + + +namespace openage::renderer::camera { + +Frustum2d::Frustum2d(const util::Vector2s &viewport_size, + const Eigen::Matrix4f &view_matrix, + const Eigen::Matrix4f &projection_matrix, + const float zoom_factor) { + this->update(viewport_size, view_matrix, projection_matrix, zoom_factor); +} + +void Frustum2d::update(const util::Vector2s &viewport_size, + const Eigen::Matrix4f &view_matrix, + const Eigen::Matrix4f &projection_matrix, + const float zoom_factor) { + this->inv_viewport_size = {1.0f / viewport_size[0], 1.0f / viewport_size[1]}; + this->inv_zoom_factor = 1.0f / zoom_factor; + + // calculate the transformation matrix + this->transform_matrix = projection_matrix * view_matrix; +} + +bool Frustum2d::in_frustum(const Eigen::Vector3f &scene_pos, + const float scalefactor, + const Eigen::Vector4i &boundaries) const { + // calculate the position of the scene object in screen space + Eigen::Vector4f screen_pos = this->transform_matrix * Eigen::Vector4f(scene_pos[0], scene_pos[1], scene_pos[2], 1.0f); + float x_ndc = screen_pos[0]; + float y_ndc = screen_pos[1]; + + float zoom_scale = scalefactor * this->inv_zoom_factor; + + // Scale the boundaries by the zoom factor + Eigen::Vector4f scaled_boundaries{boundaries[0] * zoom_scale * this->inv_viewport_size[0], + boundaries[1] * zoom_scale * this->inv_viewport_size[0], + boundaries[2] * zoom_scale * this->inv_viewport_size[1], + boundaries[3] * zoom_scale * this->inv_viewport_size[1]}; + float left_bound = scaled_boundaries[0]; + float right_bound = scaled_boundaries[1]; + float top_bound = scaled_boundaries[2]; + float bottom_bound = scaled_boundaries[3]; + + // check if the object boundaries are inside the frustum + if (x_ndc - left_bound > 1.0f) { + return false; + } + if (x_ndc + right_bound < -1.0f) { + return false; + } + if (y_ndc + top_bound < -1.0f) { + return false; + } + if (y_ndc - bottom_bound > 1.0f) { + return false; + } + + return true; +} + +} // namespace openage::renderer::camera diff --git a/libopenage/renderer/camera/frustum_2d.h b/libopenage/renderer/camera/frustum_2d.h new file mode 100644 index 0000000000..16cb2e8548 --- /dev/null +++ b/libopenage/renderer/camera/frustum_2d.h @@ -0,0 +1,79 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include "util/vector.h" + +namespace openage::renderer::camera { + +/** + * Frustum for culling objects outside of a camera view in 2D screen space. + * This frustum object should be used for sprite culling as sprites do not exist in 3D world space. + * They are directly projected and reszied to the screen space. + * + * Used for frustum culling (https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling) + * in the renderer. + */ +class Frustum2d { +public: + /** + * Create a new 2D frustum. + * + * @param viewport_size Size of the camera viewport (width x height). + * @param view_matrix View matrix of the camera. + * @param projection_matrix Projection matrix of the camera. + * @param zoom_factor Zoom factor of the camera. + */ + Frustum2d(const util::Vector2s &viewport_size, + const Eigen::Matrix4f &view_matrix, + const Eigen::Matrix4f &projection_matrix, + const float zoom_factor); + + /** + * Update the frustum with new camera parameters. + * + * @param viewport_size Size of the camera viewport (width x height). + * @param view_matrix View matrix of the camera. + * @param projection_matrix Projection matrix of the camera. + * @param zoom_factor Zoom factor of the camera. + */ + void update(const util::Vector2s &viewport_size, + const Eigen::Matrix4f &view_matrix, + const Eigen::Matrix4f &projection_matrix, + const float zoom_factor); + + /** + * Check if a scene object is inside the frustum. + * + * @param scene_pos 3D scene coordinates. + * @param scalefactor Scale factor of the animation. + * @param boundaries Boundaries of the animation (in pixels): left, right, top, bottom. + * + * @return true if the object is inside the frustum, false otherwise. + */ + bool in_frustum(const Eigen::Vector3f &scene_pos, + const float scalefactor, + const Eigen::Vector4i &boundaries) const; + +private: + /** + * Camera transformation matrix. + * + * Pre-calculated from view and projection matrix. + */ + Eigen::Matrix4f transform_matrix; + + /** + * Viewport size of the camera (width x height). + */ + Eigen::Vector2f inv_viewport_size; + + /** + * Zoom factor of the camera. + */ + float inv_zoom_factor; +}; + +} // namespace openage::renderer::camera diff --git a/libopenage/renderer/camera/frustum_3d.cpp b/libopenage/renderer/camera/frustum_3d.cpp index f4dddc9f27..0f4b9bb340 100644 --- a/libopenage/renderer/camera/frustum_3d.cpp +++ b/libopenage/renderer/camera/frustum_3d.cpp @@ -2,15 +2,6 @@ #include "frustum_3d.h" -#include -#include -#include -#include - -#include "coord/pixel.h" -#include "coord/scene.h" -#include "renderer/renderer.h" -#include "renderer/resources/buffer_info.h" namespace openage::renderer::camera { diff --git a/libopenage/renderer/camera/frustum_3d.h b/libopenage/renderer/camera/frustum_3d.h index d777a0d403..789b7f1064 100644 --- a/libopenage/renderer/camera/frustum_3d.h +++ b/libopenage/renderer/camera/frustum_3d.h @@ -2,16 +2,11 @@ #pragma once -#include -#include -#include - #include -#include "coord/pixel.h" -#include "coord/scene.h" #include "util/vector.h" + namespace openage::renderer::camera { /** diff --git a/libopenage/renderer/resources/texture_subinfo.h b/libopenage/renderer/resources/texture_subinfo.h index c4114f0fd3..85a0899861 100644 --- a/libopenage/renderer/resources/texture_subinfo.h +++ b/libopenage/renderer/resources/texture_subinfo.h @@ -71,7 +71,7 @@ class Texture2dSubInfo { const Eigen::Vector4f &get_tile_params() const; /** - * Get the anchor parameters of the subtexture center. Used in the model matrix + * Get the anchor parameters of the subtexture center. Used in the shader * to calculate the offset position for displaying the subtexture inside * the OpenGL viewport. * From 0c0efa6b306d158a20b7bb5fc2ea825e78dd2995 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 18 Jul 2024 07:47:06 +0200 Subject: [PATCH 432/771] renderer: Get max bounds for animations. --- libopenage/renderer/camera/frustum_2d.cpp | 2 +- libopenage/renderer/camera/frustum_2d.h | 2 +- .../resources/animation/animation_info.cpp | 47 ++++++++++++++++++- .../resources/animation/animation_info.h | 30 +++++++++++- .../renderer/resources/texture_subinfo.cpp | 4 +- .../renderer/resources/texture_subinfo.h | 4 +- libopenage/renderer/stages/world/object.cpp | 2 +- 7 files changed, 81 insertions(+), 10 deletions(-) diff --git a/libopenage/renderer/camera/frustum_2d.cpp b/libopenage/renderer/camera/frustum_2d.cpp index 38c1ff537c..9e862165ca 100644 --- a/libopenage/renderer/camera/frustum_2d.cpp +++ b/libopenage/renderer/camera/frustum_2d.cpp @@ -25,7 +25,7 @@ void Frustum2d::update(const util::Vector2s &viewport_size, bool Frustum2d::in_frustum(const Eigen::Vector3f &scene_pos, const float scalefactor, - const Eigen::Vector4i &boundaries) const { + const util::Vector4i &boundaries) const { // calculate the position of the scene object in screen space Eigen::Vector4f screen_pos = this->transform_matrix * Eigen::Vector4f(scene_pos[0], scene_pos[1], scene_pos[2], 1.0f); float x_ndc = screen_pos[0]; diff --git a/libopenage/renderer/camera/frustum_2d.h b/libopenage/renderer/camera/frustum_2d.h index 16cb2e8548..68e9686fcb 100644 --- a/libopenage/renderer/camera/frustum_2d.h +++ b/libopenage/renderer/camera/frustum_2d.h @@ -55,7 +55,7 @@ class Frustum2d { */ bool in_frustum(const Eigen::Vector3f &scene_pos, const float scalefactor, - const Eigen::Vector4i &boundaries) const; + const util::Vector4i &boundaries) const; private: /** diff --git a/libopenage/renderer/resources/animation/animation_info.cpp b/libopenage/renderer/resources/animation/animation_info.cpp index 0f309a6fa7..64939a3f56 100644 --- a/libopenage/renderer/resources/animation/animation_info.cpp +++ b/libopenage/renderer/resources/animation/animation_info.cpp @@ -1,7 +1,12 @@ -// Copyright 2021-2023 the openage authors. See copying.md for legal info. +// Copyright 2021-2024 the openage authors. See copying.md for legal info. #include "animation_info.h" +#include "renderer/resources/animation/angle_info.h" +#include "renderer/resources/animation/frame_info.h" +#include "renderer/resources/texture_info.h" + + namespace openage::renderer::resources { Animation2dInfo::Animation2dInfo(const float scalefactor, @@ -9,7 +14,36 @@ Animation2dInfo::Animation2dInfo(const float scalefactor, std::vector &layers) : scalefactor{scalefactor}, texture_infos{textures}, - layers{layers} {} + layers{layers}, + max_bounds{} { + // Calculate max bounds + for (auto &layer : this->layers) { + for (size_t i = 0; i < layer.get_angle_count(); ++i) { + auto &angle = layer.get_angle(i); + for (size_t j = 0; j < angle->get_frame_count(); ++j) { + auto &frame = angle->get_frame(j); + auto tex_idx = frame->get_texture_idx(); + auto subtex_idx = frame->get_subtexture_idx(); + + auto &tex = this->texture_infos.at(tex_idx); + auto &subtex = tex->get_subtex_info(subtex_idx); + + auto subtex_size = subtex.get_size(); + auto anchor_pos = subtex.get_anchor_pos(); + + int left_margin = anchor_pos.x(); + int right_margin = subtex_size.x() - anchor_pos.x(); + int top_margin = anchor_pos.y(); + int bottom_margin = subtex_size.y() - anchor_pos.y(); + + this->max_bounds[0] = std::max(this->max_bounds[0], left_margin); + this->max_bounds[1] = std::max(this->max_bounds[1], right_margin); + this->max_bounds[2] = std::max(this->max_bounds[2], top_margin); + this->max_bounds[3] = std::max(this->max_bounds[3], bottom_margin); + } + } + } +} float Animation2dInfo::get_scalefactor() const { return this->scalefactor; @@ -31,4 +65,13 @@ const LayerInfo &Animation2dInfo::get_layer(size_t idx) const { return this->layers.at(idx); } +const util::Vector4i &Animation2dInfo::get_max_bounds() const { + return this->max_bounds; +} + +const util::Vector2s Animation2dInfo::get_max_size() const { + return {this->max_bounds[0] + this->max_bounds[1], + this->max_bounds[2] + this->max_bounds[3]}; +} + } // namespace openage::renderer::resources diff --git a/libopenage/renderer/resources/animation/animation_info.h b/libopenage/renderer/resources/animation/animation_info.h index be53acb180..1e778d892e 100644 --- a/libopenage/renderer/resources/animation/animation_info.h +++ b/libopenage/renderer/resources/animation/animation_info.h @@ -1,4 +1,4 @@ -// 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 @@ -7,6 +7,7 @@ #include #include "renderer/resources/animation/layer_info.h" +#include "util/vector.h" namespace openage::renderer::resources { @@ -79,6 +80,25 @@ class Animation2dInfo { */ const LayerInfo &get_layer(size_t idx) const; + /** + * Get the maximum boundaries for all frames in the animation. + * + * This represents the maximum distance from the anchor point to the + * left, right, top, bottom edges of the frames (in pixels). + * + * @return Boundaries of the animation (in pixels): [left, right, top, bottom] + */ + const util::Vector4i &get_max_bounds() const; + + /** + * Get the maximum size for all frames in the animation. + * + * This represents the maximum width and height of the frames (in pixels). + * + * @return Size of the animation (in pixels): [width, height] + */ + const util::Vector2s get_max_size() const; + private: /** * Scaling factor of the animation across all layers at default zoom level. @@ -94,6 +114,14 @@ class Animation2dInfo { * Layer information. */ std::vector layers; + + /** + * Maximum boundaries for all frames in the animation. + * + * This represents the maximum distance from the anchor point to the + * left, right, top, bottom edges of the frames (in pixels). + */ + util::Vector4i max_bounds; }; } // namespace openage::renderer::resources diff --git a/libopenage/renderer/resources/texture_subinfo.cpp b/libopenage/renderer/resources/texture_subinfo.cpp index d5c2168c4e..047becc76d 100644 --- a/libopenage/renderer/resources/texture_subinfo.cpp +++ b/libopenage/renderer/resources/texture_subinfo.cpp @@ -1,4 +1,4 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #include "texture_subinfo.h" @@ -38,7 +38,7 @@ const Eigen::Vector2i &Texture2dSubInfo::get_anchor_pos() const { return this->anchor_pos; } -const Eigen::Vector4f &Texture2dSubInfo::get_tile_params() const { +const Eigen::Vector4f &Texture2dSubInfo::get_subtex_coords() const { return this->tile_params; } diff --git a/libopenage/renderer/resources/texture_subinfo.h b/libopenage/renderer/resources/texture_subinfo.h index 85a0899861..70b1d039f3 100644 --- a/libopenage/renderer/resources/texture_subinfo.h +++ b/libopenage/renderer/resources/texture_subinfo.h @@ -53,7 +53,7 @@ class Texture2dSubInfo { const Eigen::Vector2 &get_size() const; /** - * Get the position of the subtexture anchor. + * Get the position of the subtexture anchor (origin == top left). * * @return Anchor coordinates as 2-dimensional Eigen vector: (x, y) */ @@ -68,7 +68,7 @@ class Texture2dSubInfo { * * @return Tile parameters as 4-dimensional Eigen vector: (x, y, width, height) */ - const Eigen::Vector4f &get_tile_params() const; + const Eigen::Vector4f &get_subtex_coords() const; /** * Get the anchor parameters of the subtexture center. Used in the shader diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index 93ef82f4d2..35e541c322 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -143,7 +143,7 @@ void WorldObject::update_uniforms(const time::time_t &time) { layer_unifs->update(this->tex, texture); // Subtexture coordinates.inside texture - auto coords = tex_info->get_subtex_info(subtex_idx).get_tile_params(); + auto coords = tex_info->get_subtex_info(subtex_idx).get_subtex_coords(); layer_unifs->update(this->tile_params, coords); // Animation scale factor From 7391bc2c2af58fab1c503d2c9bd7873325cafed6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 18 Jul 2024 07:47:22 +0200 Subject: [PATCH 433/771] renderer: Create 2D frustum for sprites. --- libopenage/renderer/camera/camera.cpp | 62 +++++++++++++++++---------- libopenage/renderer/camera/camera.h | 18 +++++++- 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index 383c2124d6..7af5b3f761 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -26,13 +26,17 @@ Camera::Camera(const std::shared_ptr &renderer, zoom_changed{true}, view{Eigen::Matrix4f::Identity()}, proj{Eigen::Matrix4f::Identity()}, - frustum{this->viewport_size, - DEFAULT_NEAR_DISTANCE, - DEFAULT_FAR_DISTANCE, - this->scene_pos, - CAM_DIRECTION, - Eigen::Vector3f(0.0f, 1.0f, 0.0f), - this->get_zoom_factor()} { + frustum_2d{this->viewport_size, + this->get_view_matrix(), + this->get_projection_matrix(), + this->get_zoom_factor()}, + frustum_3d{this->viewport_size, + DEFAULT_NEAR_DISTANCE, + DEFAULT_FAR_DISTANCE, + this->scene_pos, + CAM_DIRECTION, + Eigen::Vector3f(0.0f, 1.0f, 0.0f), + this->get_zoom_factor()} { this->look_at_scene(Eigen::Vector3f(0.0f, 0.0f, 0.0f)); this->init_uniform_buffer(renderer); @@ -59,13 +63,17 @@ Camera::Camera(const std::shared_ptr &renderer, viewport_changed{true}, view{Eigen::Matrix4f::Identity()}, proj{Eigen::Matrix4f::Identity()}, - frustum{this->viewport_size, - DEFAULT_NEAR_DISTANCE, - DEFAULT_FAR_DISTANCE, - this->scene_pos, - CAM_DIRECTION, - Eigen::Vector3f(0.0f, 1.0f, 0.0f), - this->get_zoom_factor()} { + frustum_2d{this->viewport_size, + this->get_view_matrix(), + this->get_projection_matrix(), + this->get_zoom_factor()}, + frustum_3d{this->viewport_size, + DEFAULT_NEAR_DISTANCE, + DEFAULT_FAR_DISTANCE, + this->scene_pos, + CAM_DIRECTION, + Eigen::Vector3f(0.0f, 1.0f, 0.0f), + this->get_zoom_factor()} { this->init_uniform_buffer(renderer); log::log(INFO << "Created new camera at position " @@ -122,14 +130,18 @@ void Camera::move_to(Eigen::Vector3f scene_pos) { this->scene_pos = scene_pos; this->moved = true; - // Update frustum - frustum.update(viewport_size, - DEFAULT_NEAR_DISTANCE, - DEFAULT_FAR_DISTANCE, - scene_pos, - CAM_DIRECTION, - Eigen::Vector3f(0.0f, 1.0f, 0.0f), - this->get_zoom_factor()); + // Update frustums + this->frustum_2d.update(viewport_size, + view, + proj, + this->get_zoom_factor()); + this->frustum_3d.update(viewport_size, + DEFAULT_NEAR_DISTANCE, + DEFAULT_FAR_DISTANCE, + scene_pos, + CAM_DIRECTION, + Eigen::Vector3f(0.0f, 1.0f, 0.0f), + this->get_zoom_factor()); } void Camera::move_rel(Eigen::Vector3f direction, float delta) { @@ -287,8 +299,12 @@ const std::shared_ptr &Camera::get_uniform_buffer() con return this->uniform_buffer; } +const Frustum2d &Camera::get_frustum_2d() const { + return this->frustum_2d; +} + const Frustum3d &Camera::get_frustum_3d() const { - return this->frustum; + return this->frustum_3d; } void Camera::init_uniform_buffer(const std::shared_ptr &renderer) { diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index 6791a6b8d8..3cec10da16 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -13,8 +13,10 @@ #include "util/vector.h" #include "renderer/camera/definitions.h" +#include "renderer/camera/frustum_2d.h" #include "renderer/camera/frustum_3d.h" + namespace openage::renderer { class Renderer; class UniformBuffer; @@ -182,6 +184,13 @@ class Camera { */ const std::shared_ptr &get_uniform_buffer() const; + /** + * Get a 2D frustum object for this camera. + * + * @return Frustum object. + */ + const Frustum2d &get_frustum_2d() const; + /** * Get a 3D frustum object for this camera. * @@ -295,9 +304,14 @@ class Camera { std::shared_ptr uniform_buffer; /** - * Frustum (viewing volume) for culling rendering objects. + * 2D frustum (viewbox) for culling rendering objects. + */ + Frustum2d frustum_2d; + + /** + * 3D frustum (viewing volume) for culling rendering objects. */ - Frustum3d frustum; + Frustum3d frustum_3d; }; } // namespace camera From 769154b76f61441f9b3b96494e3f044d57cdda91 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 18 Jul 2024 08:03:16 +0200 Subject: [PATCH 434/771] renderer: Replace 3D frustum with 2D frustum in world renderer. --- libopenage/renderer/stages/world/object.cpp | 9 ++++++--- libopenage/renderer/stages/world/object.h | 4 ++-- libopenage/renderer/stages/world/render_stage.cpp | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index 35e541c322..04bb27e391 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -10,7 +10,7 @@ #include -#include "renderer/camera/frustum_3d.h" +#include "renderer/camera/frustum_2d.h" #include "renderer/definitions.h" #include "renderer/resources/animation/angle_info.h" #include "renderer/resources/animation/animation_info.h" @@ -224,10 +224,13 @@ void WorldObject::set_uniforms(std::vectorlayer_uniforms = std::move(uniforms); } -bool WorldObject::is_visible(const camera::Frustum3d &frustum, +bool WorldObject::is_visible(const camera::Frustum2d &frustum, const time::time_t &time) { Eigen::Vector3f current_pos = this->position.get(time).to_world_space(); - return frustum.in_frustum(current_pos); + auto animation_info = this->animation_info.get(time); + return frustum.in_frustum(current_pos, + animation_info->get_scalefactor(), + animation_info->get_max_bounds()); } } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/object.h b/libopenage/renderer/stages/world/object.h index 9576d906c9..1302733954 100644 --- a/libopenage/renderer/stages/world/object.h +++ b/libopenage/renderer/stages/world/object.h @@ -20,7 +20,7 @@ namespace openage::renderer { class UniformInput; namespace camera { -class Frustum3d; +class Frustum2d; } namespace resources { @@ -141,7 +141,7 @@ class WorldObject { * * @return true if the object is visible, else false. */ - bool is_visible(const camera::Frustum3d &frustum, + bool is_visible(const camera::Frustum2d &frustum, const time::time_t &time); /** diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 85378aef09..4f3354a1fa 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -59,7 +59,7 @@ void WorldRenderStage::add_render_entity(const std::shared_ptrmutex}; auto current_time = this->clock->get_real_time(); - auto &camera_frustum = this->camera->get_frustum_3d(); + auto &camera_frustum = this->camera->get_frustum_2d(); for (auto &obj : this->render_objects) { obj->fetch_updates(current_time); From d7a8e5bddb5c849ab8676e58148eaccf7ce84127 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 18 Jul 2024 08:50:48 +0200 Subject: [PATCH 435/771] renderer: Use correct zoom value for 2D frustum. --- libopenage/renderer/camera/camera.cpp | 22 +++++++++++----------- libopenage/renderer/camera/camera.h | 8 +++++++- libopenage/renderer/camera/frustum_2d.cpp | 22 +++++++++++----------- libopenage/renderer/camera/frustum_2d.h | 8 ++++---- 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index 7af5b3f761..94cd87c00e 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -29,14 +29,14 @@ Camera::Camera(const std::shared_ptr &renderer, frustum_2d{this->viewport_size, this->get_view_matrix(), this->get_projection_matrix(), - this->get_zoom_factor()}, + this->get_zoom()}, frustum_3d{this->viewport_size, DEFAULT_NEAR_DISTANCE, DEFAULT_FAR_DISTANCE, this->scene_pos, CAM_DIRECTION, Eigen::Vector3f(0.0f, 1.0f, 0.0f), - this->get_zoom_factor()} { + this->get_real_zoom_factor()} { this->look_at_scene(Eigen::Vector3f(0.0f, 0.0f, 0.0f)); this->init_uniform_buffer(renderer); @@ -66,14 +66,14 @@ Camera::Camera(const std::shared_ptr &renderer, frustum_2d{this->viewport_size, this->get_view_matrix(), this->get_projection_matrix(), - this->get_zoom_factor()}, + this->get_zoom()}, frustum_3d{this->viewport_size, DEFAULT_NEAR_DISTANCE, DEFAULT_FAR_DISTANCE, this->scene_pos, CAM_DIRECTION, Eigen::Vector3f(0.0f, 1.0f, 0.0f), - this->get_zoom_factor()} { + this->get_real_zoom_factor()} { this->init_uniform_buffer(renderer); log::log(INFO << "Created new camera at position " @@ -132,16 +132,16 @@ void Camera::move_to(Eigen::Vector3f scene_pos) { // Update frustums this->frustum_2d.update(viewport_size, - view, - proj, - this->get_zoom_factor()); + this->get_view_matrix(), + this->get_projection_matrix(), + this->get_zoom()); this->frustum_3d.update(viewport_size, DEFAULT_NEAR_DISTANCE, DEFAULT_FAR_DISTANCE, scene_pos, CAM_DIRECTION, Eigen::Vector3f(0.0f, 1.0f, 0.0f), - this->get_zoom_factor()); + this->get_real_zoom_factor()); } void Camera::move_rel(Eigen::Vector3f direction, float delta) { @@ -232,7 +232,7 @@ const Eigen::Matrix4f &Camera::get_projection_matrix() { float halfheight = this->viewport_size[1] / 2; // get zoom level - float real_zoom = this->get_zoom_factor(); + float real_zoom = this->get_real_zoom_factor(); // zoom by narrowing or widening viewport around focus point. // narrow viewport => zoom in (projected area gets *smaller* while screen size stays the same) @@ -283,7 +283,7 @@ Eigen::Vector3f Camera::get_input_pos(const coord::input &coord) const { // this is the same calculation as for the projection matrix float halfwidth = this->viewport_size[0] / 2; float halfheight = this->viewport_size[1] / 2; - float real_zoom = this->get_zoom_factor(); + float real_zoom = this->get_real_zoom_factor(); // calculate x and y offset on the camera plane relative to the camera position float x = +(2.0f * coord.x / this->viewport_size[0] - 1) * (halfwidth * real_zoom); @@ -318,7 +318,7 @@ void Camera::init_uniform_buffer(const std::shared_ptr &renderer) { this->uniform_buffer = renderer->add_uniform_buffer(ubo_info); } -inline float Camera::get_zoom_factor() const { +inline float Camera::get_real_zoom_factor() const { return 0.5f * this->default_zoom_ratio * this->zoom; } diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index 3cec10da16..e92c345be6 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -139,6 +139,8 @@ class Camera { /** * Get the current zoom level of the camera. * + * Determines the scale of rendered objects. + * * @return Zoom level. */ float get_zoom() const; @@ -213,9 +215,13 @@ class Camera { * * zoom * zoom_ratio * 0.5f * + * Note that this zoom factor should NOT be used for sprite scaling, but + * only for 3D projection matrix calculations. For sprite scaling, use + * \p get_zoom() . + * * @return Zoom factor for projection. */ - inline float get_zoom_factor() const; + inline float get_real_zoom_factor() const; /** * Position in the 3D scene. diff --git a/libopenage/renderer/camera/frustum_2d.cpp b/libopenage/renderer/camera/frustum_2d.cpp index 9e862165ca..0eba531745 100644 --- a/libopenage/renderer/camera/frustum_2d.cpp +++ b/libopenage/renderer/camera/frustum_2d.cpp @@ -8,16 +8,16 @@ namespace openage::renderer::camera { Frustum2d::Frustum2d(const util::Vector2s &viewport_size, const Eigen::Matrix4f &view_matrix, const Eigen::Matrix4f &projection_matrix, - const float zoom_factor) { - this->update(viewport_size, view_matrix, projection_matrix, zoom_factor); + const float zoom) { + this->update(viewport_size, view_matrix, projection_matrix, zoom); } void Frustum2d::update(const util::Vector2s &viewport_size, const Eigen::Matrix4f &view_matrix, const Eigen::Matrix4f &projection_matrix, - const float zoom_factor) { + const float zoom) { this->inv_viewport_size = {1.0f / viewport_size[0], 1.0f / viewport_size[1]}; - this->inv_zoom_factor = 1.0f / zoom_factor; + this->inv_zoom_factor = 1.0f / zoom; // calculate the transformation matrix this->transform_matrix = projection_matrix * view_matrix; @@ -27,9 +27,9 @@ bool Frustum2d::in_frustum(const Eigen::Vector3f &scene_pos, const float scalefactor, const util::Vector4i &boundaries) const { // calculate the position of the scene object in screen space - Eigen::Vector4f screen_pos = this->transform_matrix * Eigen::Vector4f(scene_pos[0], scene_pos[1], scene_pos[2], 1.0f); - float x_ndc = screen_pos[0]; - float y_ndc = screen_pos[1]; + Eigen::Vector4f clip_pos = this->transform_matrix * Eigen::Vector4f(scene_pos[0], scene_pos[1], scene_pos[2], 1.0f); + float x_ndc = clip_pos[0]; + float y_ndc = clip_pos[1]; float zoom_scale = scalefactor * this->inv_zoom_factor; @@ -44,16 +44,16 @@ bool Frustum2d::in_frustum(const Eigen::Vector3f &scene_pos, float bottom_bound = scaled_boundaries[3]; // check if the object boundaries are inside the frustum - if (x_ndc - left_bound > 1.0f) { + if (x_ndc - left_bound >= 1.0f) { return false; } - if (x_ndc + right_bound < -1.0f) { + if (x_ndc + right_bound <= -1.0f) { return false; } - if (y_ndc + top_bound < -1.0f) { + if (y_ndc + top_bound <= -1.0f) { return false; } - if (y_ndc - bottom_bound > 1.0f) { + if (y_ndc - bottom_bound >= 1.0f) { return false; } diff --git a/libopenage/renderer/camera/frustum_2d.h b/libopenage/renderer/camera/frustum_2d.h index 68e9686fcb..21bcd9c7e3 100644 --- a/libopenage/renderer/camera/frustum_2d.h +++ b/libopenage/renderer/camera/frustum_2d.h @@ -24,12 +24,12 @@ class Frustum2d { * @param viewport_size Size of the camera viewport (width x height). * @param view_matrix View matrix of the camera. * @param projection_matrix Projection matrix of the camera. - * @param zoom_factor Zoom factor of the camera. + * @param zoom Zoom of the camera. */ Frustum2d(const util::Vector2s &viewport_size, const Eigen::Matrix4f &view_matrix, const Eigen::Matrix4f &projection_matrix, - const float zoom_factor); + const float zoom); /** * Update the frustum with new camera parameters. @@ -37,12 +37,12 @@ class Frustum2d { * @param viewport_size Size of the camera viewport (width x height). * @param view_matrix View matrix of the camera. * @param projection_matrix Projection matrix of the camera. - * @param zoom_factor Zoom factor of the camera. + * @param zoom Zoom of the camera. */ void update(const util::Vector2s &viewport_size, const Eigen::Matrix4f &view_matrix, const Eigen::Matrix4f &projection_matrix, - const float zoom_factor); + const float zoom); /** * Check if a scene object is inside the frustum. From 33021dc71710d3d00416ed1bef70d24334a178ba Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 19 Jul 2024 01:05:25 +0200 Subject: [PATCH 436/771] renderer: Zig-zag movement in and out of camera view in stressttest 1. --- libopenage/renderer/demo/stresstest_1.cpp | 44 +++++++++++------------ 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/libopenage/renderer/demo/stresstest_1.cpp b/libopenage/renderer/demo/stresstest_1.cpp index 6ec2887503..72c21b5686 100644 --- a/libopenage/renderer/demo/stresstest_1.cpp +++ b/libopenage/renderer/demo/stresstest_1.cpp @@ -155,25 +155,25 @@ void renderer_stresstest_1(const util::Path &path) { auto position = curve::Continuous{nullptr, 0, "", nullptr, coord::phys3(0, 0, 0)}; position.set_insert(time, initial_pos); - position.set_insert(time + 1, initial_pos + coord::phys3_delta{0, 4, 0}); - position.set_insert(time + 2, initial_pos + coord::phys3_delta{4, 8, 0}); - position.set_insert(time + 3, initial_pos + coord::phys3_delta{8, 8, 0}); - position.set_insert(time + 4, initial_pos + coord::phys3_delta{12, 4, 0}); - position.set_insert(time + 5, initial_pos + coord::phys3_delta{12, 0, 0}); - position.set_insert(time + 6, initial_pos + coord::phys3_delta{8, -4, 0}); - position.set_insert(time + 7, initial_pos + coord::phys3_delta{4, -4, 0}); + position.set_insert(time + 1, initial_pos + coord::phys3_delta{5, 0, 0}); + position.set_insert(time + 2, initial_pos + coord::phys3_delta{12, -2, 0}); + position.set_insert(time + 3, initial_pos + coord::phys3_delta{5, 5, 0}); + position.set_insert(time + 4, initial_pos + coord::phys3_delta{12, 12, 0}); + position.set_insert(time + 5, initial_pos + coord::phys3_delta{5, 9, 0}); + position.set_insert(time + 6, initial_pos + coord::phys3_delta{-2, 12, 0}); + position.set_insert(time + 7, initial_pos + coord::phys3_delta{0, 5, 0}); position.set_insert(time + 8, initial_pos); auto angle = curve::Segmented{nullptr, 0}; - angle.set_insert(time, coord::phys_angle_t::from_int(315)); - angle.set_insert_jump(time + 1, coord::phys_angle_t::from_int(315), coord::phys_angle_t::from_int(270)); - angle.set_insert_jump(time + 2, coord::phys_angle_t::from_int(270), coord::phys_angle_t::from_int(225)); - angle.set_insert_jump(time + 3, coord::phys_angle_t::from_int(225), coord::phys_angle_t::from_int(180)); - angle.set_insert_jump(time + 4, coord::phys_angle_t::from_int(180), coord::phys_angle_t::from_int(135)); - angle.set_insert_jump(time + 5, coord::phys_angle_t::from_int(135), coord::phys_angle_t::from_int(90)); - angle.set_insert_jump(time + 6, coord::phys_angle_t::from_int(90), coord::phys_angle_t::from_int(45)); - angle.set_insert_jump(time + 7, coord::phys_angle_t::from_int(45), coord::phys_angle_t::from_int(0)); - angle.set_insert_jump(time + 8, coord::phys_angle_t::from_int(0), coord::phys_angle_t::from_int(315)); + angle.set_insert(time, coord::phys_angle_t::from_int(225)); + angle.set_insert_jump(time + 1, coord::phys_angle_t::from_int(225), coord::phys_angle_t::from_int(210)); + angle.set_insert_jump(time + 2, coord::phys_angle_t::from_int(210), coord::phys_angle_t::from_int(0)); + angle.set_insert_jump(time + 3, coord::phys_angle_t::from_int(0), coord::phys_angle_t::from_int(270)); + angle.set_insert_jump(time + 4, coord::phys_angle_t::from_int(270), coord::phys_angle_t::from_int(60)); + angle.set_insert_jump(time + 5, coord::phys_angle_t::from_int(60), coord::phys_angle_t::from_int(45)); + angle.set_insert_jump(time + 6, coord::phys_angle_t::from_int(45), coord::phys_angle_t::from_int(120)); + angle.set_insert_jump(time + 7, coord::phys_angle_t::from_int(120), coord::phys_angle_t::from_int(135)); + angle.set_insert_jump(time + 8, coord::phys_angle_t::from_int(135), coord::phys_angle_t::from_int(225)); auto entity = render_factory->add_world_render_entity(); entity->update(render_entities.size(), @@ -184,15 +184,14 @@ void renderer_stresstest_1(const util::Path &path) { render_entities.push_back(entity); }; - // Stop after 8000 entities - size_t entity_limit = 8000; + // Stop after 1000 entities + size_t entity_limit = 1000; clock->start(); util::FrameCounter timer; - add_world_entity(coord::phys3(0.0f, 10.0f, 0.0f), clock->get_time()); - time::time_t next_entity = clock->get_real_time() + 0.1; + time::time_t next_entity = clock->get_real_time(); while (render_entities.size() <= entity_limit) { // Print FPS timer.frame(); @@ -207,9 +206,8 @@ void renderer_stresstest_1(const util::Path &path) { clock->update_time(); auto current_time = clock->get_real_time(); if (current_time > next_entity) { - add_world_entity(coord::phys3(0.0f, 10.0f, 0.0f), clock->get_time()); - add_world_entity(coord::phys3(-1.f, 9.0f, 0.0f), clock->get_time()); - next_entity = current_time + 0.1; + add_world_entity(coord::phys3(0.5, 0.5, 0.0f), clock->get_time()); + next_entity = current_time + 0.05; } // Update the renderables of the subrenderers From 04bf89e5c4de21efedc95432e36976cc597bad65 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 19 Jul 2024 01:14:09 +0200 Subject: [PATCH 437/771] renderer: Construct camera frustumson the fly. --- libopenage/renderer/camera/camera.cpp | 56 +++++++-------------------- libopenage/renderer/camera/camera.h | 14 +------ 2 files changed, 17 insertions(+), 53 deletions(-) diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index 94cd87c00e..f70efbee64 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -25,18 +25,7 @@ Camera::Camera(const std::shared_ptr &renderer, moved{true}, zoom_changed{true}, view{Eigen::Matrix4f::Identity()}, - proj{Eigen::Matrix4f::Identity()}, - frustum_2d{this->viewport_size, - this->get_view_matrix(), - this->get_projection_matrix(), - this->get_zoom()}, - frustum_3d{this->viewport_size, - DEFAULT_NEAR_DISTANCE, - DEFAULT_FAR_DISTANCE, - this->scene_pos, - CAM_DIRECTION, - Eigen::Vector3f(0.0f, 1.0f, 0.0f), - this->get_real_zoom_factor()} { + proj{Eigen::Matrix4f::Identity()} { this->look_at_scene(Eigen::Vector3f(0.0f, 0.0f, 0.0f)); this->init_uniform_buffer(renderer); @@ -62,18 +51,7 @@ Camera::Camera(const std::shared_ptr &renderer, zoom_changed{true}, viewport_changed{true}, view{Eigen::Matrix4f::Identity()}, - proj{Eigen::Matrix4f::Identity()}, - frustum_2d{this->viewport_size, - this->get_view_matrix(), - this->get_projection_matrix(), - this->get_zoom()}, - frustum_3d{this->viewport_size, - DEFAULT_NEAR_DISTANCE, - DEFAULT_FAR_DISTANCE, - this->scene_pos, - CAM_DIRECTION, - Eigen::Vector3f(0.0f, 1.0f, 0.0f), - this->get_real_zoom_factor()} { + proj{Eigen::Matrix4f::Identity()} { this->init_uniform_buffer(renderer); log::log(INFO << "Created new camera at position " @@ -129,19 +107,6 @@ void Camera::move_to(Eigen::Vector3f scene_pos) { this->scene_pos = scene_pos; this->moved = true; - - // Update frustums - this->frustum_2d.update(viewport_size, - this->get_view_matrix(), - this->get_projection_matrix(), - this->get_zoom()); - this->frustum_3d.update(viewport_size, - DEFAULT_NEAR_DISTANCE, - DEFAULT_FAR_DISTANCE, - scene_pos, - CAM_DIRECTION, - Eigen::Vector3f(0.0f, 1.0f, 0.0f), - this->get_real_zoom_factor()); } void Camera::move_rel(Eigen::Vector3f direction, float delta) { @@ -299,12 +264,21 @@ const std::shared_ptr &Camera::get_uniform_buffer() con return this->uniform_buffer; } -const Frustum2d &Camera::get_frustum_2d() const { - return this->frustum_2d; +const Frustum2d Camera::get_frustum_2d() { + return {this->viewport_size, + this->get_view_matrix(), + this->get_projection_matrix(), + this->get_zoom()}; } -const Frustum3d &Camera::get_frustum_3d() const { - return this->frustum_3d; +const Frustum3d Camera::get_frustum_3d() const { + return {this->viewport_size, + DEFAULT_NEAR_DISTANCE, + DEFAULT_FAR_DISTANCE, + this->scene_pos, + CAM_DIRECTION, + CAM_UP, + this->get_real_zoom_factor()}; } void Camera::init_uniform_buffer(const std::shared_ptr &renderer) { diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index e92c345be6..eccb5b40e6 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -191,14 +191,14 @@ class Camera { * * @return Frustum object. */ - const Frustum2d &get_frustum_2d() const; + const Frustum2d get_frustum_2d(); /** * Get a 3D frustum object for this camera. * * @return Frustum object. */ - const Frustum3d &get_frustum_3d() const; + const Frustum3d get_frustum_3d() const; private: /** @@ -308,16 +308,6 @@ class Camera { * Uniform buffer for the camera matrices. */ std::shared_ptr uniform_buffer; - - /** - * 2D frustum (viewbox) for culling rendering objects. - */ - Frustum2d frustum_2d; - - /** - * 3D frustum (viewing volume) for culling rendering objects. - */ - Frustum3d frustum_3d; }; } // namespace camera From 8edeb9d79a4c10ff327de99b1081f683efe26067 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 19 Jul 2024 01:43:15 +0200 Subject: [PATCH 438/771] renderer: Skip scaled vector creation in frustum. --- libopenage/renderer/camera/frustum_2d.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/libopenage/renderer/camera/frustum_2d.cpp b/libopenage/renderer/camera/frustum_2d.cpp index 0eba531745..3c5e123d82 100644 --- a/libopenage/renderer/camera/frustum_2d.cpp +++ b/libopenage/renderer/camera/frustum_2d.cpp @@ -33,15 +33,11 @@ bool Frustum2d::in_frustum(const Eigen::Vector3f &scene_pos, float zoom_scale = scalefactor * this->inv_zoom_factor; - // Scale the boundaries by the zoom factor - Eigen::Vector4f scaled_boundaries{boundaries[0] * zoom_scale * this->inv_viewport_size[0], - boundaries[1] * zoom_scale * this->inv_viewport_size[0], - boundaries[2] * zoom_scale * this->inv_viewport_size[1], - boundaries[3] * zoom_scale * this->inv_viewport_size[1]}; - float left_bound = scaled_boundaries[0]; - float right_bound = scaled_boundaries[1]; - float top_bound = scaled_boundaries[2]; - float bottom_bound = scaled_boundaries[3]; + // Scale the boundaries by the zoom factor and the viewport size + float left_bound = boundaries[0] * zoom_scale * this->inv_viewport_size[0]; + float right_bound = boundaries[1] * zoom_scale * this->inv_viewport_size[0]; + float top_bound = boundaries[2] * zoom_scale * this->inv_viewport_size[1]; + float bottom_bound = boundaries[3] * zoom_scale * this->inv_viewport_size[1]; // check if the object boundaries are inside the frustum if (x_ndc - left_bound >= 1.0f) { From b457b877ec25b3e919f5ee2dbbef547453c6dc87 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 19 Jul 2024 02:12:39 +0200 Subject: [PATCH 439/771] renderer: Inclide model matrix in 2D frustum check. --- libopenage/renderer/camera/frustum_2d.cpp | 3 ++- libopenage/renderer/camera/frustum_2d.h | 2 ++ libopenage/renderer/stages/world/object.cpp | 2 ++ libopenage/renderer/stages/world/render_stage.cpp | 4 ++-- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/libopenage/renderer/camera/frustum_2d.cpp b/libopenage/renderer/camera/frustum_2d.cpp index 3c5e123d82..614b2b74a2 100644 --- a/libopenage/renderer/camera/frustum_2d.cpp +++ b/libopenage/renderer/camera/frustum_2d.cpp @@ -24,10 +24,11 @@ void Frustum2d::update(const util::Vector2s &viewport_size, } bool Frustum2d::in_frustum(const Eigen::Vector3f &scene_pos, + const Eigen::Matrix4f &model_matrix, const float scalefactor, const util::Vector4i &boundaries) const { // calculate the position of the scene object in screen space - Eigen::Vector4f clip_pos = this->transform_matrix * Eigen::Vector4f(scene_pos[0], scene_pos[1], scene_pos[2], 1.0f); + Eigen::Vector4f clip_pos = this->transform_matrix * model_matrix * scene_pos.homogeneous(); float x_ndc = clip_pos[0]; float y_ndc = clip_pos[1]; diff --git a/libopenage/renderer/camera/frustum_2d.h b/libopenage/renderer/camera/frustum_2d.h index 21bcd9c7e3..3403d72a43 100644 --- a/libopenage/renderer/camera/frustum_2d.h +++ b/libopenage/renderer/camera/frustum_2d.h @@ -48,12 +48,14 @@ class Frustum2d { * Check if a scene object is inside the frustum. * * @param scene_pos 3D scene coordinates. + * @param model_matrix Model matrix of the object. * @param scalefactor Scale factor of the animation. * @param boundaries Boundaries of the animation (in pixels): left, right, top, bottom. * * @return true if the object is inside the frustum, false otherwise. */ bool in_frustum(const Eigen::Vector3f &scene_pos, + const Eigen::Matrix4f &model_matrix, const float scalefactor, const util::Vector4i &boundaries) const; diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index 04bb27e391..d4629f866b 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -226,9 +226,11 @@ void WorldObject::set_uniforms(std::vectorget_model_matrix(); Eigen::Vector3f current_pos = this->position.get(time).to_world_space(); auto animation_info = this->animation_info.get(time); return frustum.in_frustum(current_pos, + model_matrix, animation_info->get_scalefactor(), animation_info->get_max_bounds()); } diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 4f3354a1fa..d699d1ac33 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -70,14 +70,14 @@ void WorldRenderStage::update() { if (obj->is_changed()) { if (obj->requires_renderable()) { auto layer_positions = obj->get_layer_positions(current_time); - Eigen::Matrix4f model_m = obj->get_model_matrix(); + static const Eigen::Matrix4f model_matrix = obj->get_model_matrix(); std::vector> transform_unifs; for (auto layer_pos : layer_positions) { // Set uniforms that don't change or are not changed often auto layer_unifs = this->display_shader->new_uniform_input( "model", - model_m, + model_matrix, "flip_x", false, "flip_y", From e97a9448a3235dfb678c8e13c24cf42e1e5bb72b Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 19 Jul 2024 02:55:53 +0200 Subject: [PATCH 440/771] renderer: Enable frustum culling with flag in world stage. --- libopenage/renderer/demo/stresstest_1.cpp | 3 +++ libopenage/renderer/stages/world/render_stage.cpp | 5 ++++- libopenage/renderer/stages/world/render_stage.h | 9 ++++++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/libopenage/renderer/demo/stresstest_1.cpp b/libopenage/renderer/demo/stresstest_1.cpp index 72c21b5686..ac6177f819 100644 --- a/libopenage/renderer/demo/stresstest_1.cpp +++ b/libopenage/renderer/demo/stresstest_1.cpp @@ -99,6 +99,9 @@ void renderer_stresstest_1(const util::Path &path) { asset_manager, clock); + // Enable frustum culling + renderer::world::WorldRenderStage::ENABLE_FRUSTUM_CULLING = true; + // Store the render passes of the renderers // The order is important as its also the order in which they // are rendered and drawn onto the screen. diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index d699d1ac33..0434f3cf07 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -19,6 +19,8 @@ namespace openage::renderer::world { +bool WorldRenderStage::ENABLE_FRUSTUM_CULLING = false; + WorldRenderStage::WorldRenderStage(const std::shared_ptr &window, const std::shared_ptr &renderer, const std::shared_ptr &camera, @@ -63,7 +65,8 @@ void WorldRenderStage::update() { for (auto &obj : this->render_objects) { obj->fetch_updates(current_time); - if (not obj->is_visible(camera_frustum, current_time)) { + if (WorldRenderStage::ENABLE_FRUSTUM_CULLING + and not obj->is_visible(camera_frustum, current_time)) { continue; } diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index e22399180b..e6ae39c094 100644 --- a/libopenage/renderer/stages/world/render_stage.h +++ b/libopenage/renderer/stages/world/render_stage.h @@ -39,6 +39,11 @@ class WorldObject; */ class WorldRenderStage { public: + /** + * Enable or disable frustum culling (default = false). + */ + static bool ENABLE_FRUSTUM_CULLING; + /** * Create a new render stage for the game world. * @@ -95,9 +100,7 @@ class WorldRenderStage { * @param height Height of the FBO. * @param shaderdir Directory containg the shader source files. */ - void initialize_render_pass(size_t width, - size_t height, - const util::Path &shaderdir); + void initialize_render_pass(size_t width, size_t height, const util::Path &shaderdir); /** * Fetch the uniform IDs for the uniforms of the world shader from OpenGL From a608d26eb6edaf14e539c32588da3b8421f39d55 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Jul 2024 17:59:51 +0200 Subject: [PATCH 441/771] renderer: Enable GL_LINE_LOOP vertex primitive. --- libopenage/renderer/opengl/lookup.h | 3 ++- libopenage/renderer/resources/mesh_data.h | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/libopenage/renderer/opengl/lookup.h b/libopenage/renderer/opengl/lookup.h index e399e98399..a137101482 100644 --- a/libopenage/renderer/opengl/lookup.h +++ b/libopenage/renderer/opengl/lookup.h @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. // Lookup tables for translating between OpenGL-specific values and generic renderer values, // as well as mapping things like type sizes within OpenGL. @@ -137,6 +137,7 @@ static constexpr auto GL_PRIMITIVE = datastructure::create_const_map Date: Fri, 19 Jul 2024 14:13:11 +0200 Subject: [PATCH 442/771] renderer: Frustum culling demo. --- assets/test/shaders/demo_6_2d.frag.glsl | 37 ++ assets/test/shaders/demo_6_2d.vert.glsl | 83 ++++ assets/test/shaders/demo_6_2d_frame.frag.glsl | 7 + assets/test/shaders/demo_6_2d_frame.vert.glsl | 58 +++ assets/test/shaders/demo_6_3d.frag.glsl | 13 + assets/test/shaders/demo_6_3d.vert.glsl | 24 + assets/test/shaders/demo_6_display.frag.glsl | 10 + assets/test/shaders/demo_6_display.vert.glsl | 10 + libopenage/renderer/demo/CMakeLists.txt | 1 + libopenage/renderer/demo/demo_0.cpp | 2 +- libopenage/renderer/demo/demo_0.h | 2 +- libopenage/renderer/demo/demo_1.cpp | 2 +- libopenage/renderer/demo/demo_1.h | 2 +- libopenage/renderer/demo/demo_2.cpp | 2 +- libopenage/renderer/demo/demo_2.h | 2 +- libopenage/renderer/demo/demo_3.cpp | 2 +- libopenage/renderer/demo/demo_3.h | 3 +- libopenage/renderer/demo/demo_4.h | 2 +- libopenage/renderer/demo/demo_6.cpp | 428 ++++++++++++++++++ libopenage/renderer/demo/demo_6.h | 101 +++++ libopenage/renderer/demo/tests.cpp | 5 + 21 files changed, 786 insertions(+), 10 deletions(-) create mode 100644 assets/test/shaders/demo_6_2d.frag.glsl create mode 100644 assets/test/shaders/demo_6_2d.vert.glsl create mode 100644 assets/test/shaders/demo_6_2d_frame.frag.glsl create mode 100644 assets/test/shaders/demo_6_2d_frame.vert.glsl create mode 100644 assets/test/shaders/demo_6_3d.frag.glsl create mode 100644 assets/test/shaders/demo_6_3d.vert.glsl create mode 100644 assets/test/shaders/demo_6_display.frag.glsl create mode 100644 assets/test/shaders/demo_6_display.vert.glsl create mode 100644 libopenage/renderer/demo/demo_6.cpp create mode 100644 libopenage/renderer/demo/demo_6.h diff --git a/assets/test/shaders/demo_6_2d.frag.glsl b/assets/test/shaders/demo_6_2d.frag.glsl new file mode 100644 index 0000000000..c4beb1dd8e --- /dev/null +++ b/assets/test/shaders/demo_6_2d.frag.glsl @@ -0,0 +1,37 @@ +#version 330 + +in vec2 vert_uv; + +layout(location=0) out vec4 col; + +uniform sampler2D tex; + +// position (top left corner) and size: (x, y, width, height) +uniform vec4 tile_params; + +vec2 uv = vec2( + vert_uv.x * tile_params.z + tile_params.x, + vert_uv.y * tile_params.w + tile_params.y +); + +void main() { + vec4 tex_val = texture(tex, uv); + int alpha = int(round(tex_val.a * 255)); + switch (alpha) { + case 0: + col = tex_val; + discard; + case 254: + col = vec4(1.0f, 0.0f, 0.0f, 1.0f); + break; + case 252: + col = vec4(0.0f, 1.0f, 0.0f, 1.0f); + break; + case 250: + col = vec4(0.0f, 0.0f, 1.0f, 1.0f); + break; + default: + col = tex_val; + break; + } +} diff --git a/assets/test/shaders/demo_6_2d.vert.glsl b/assets/test/shaders/demo_6_2d.vert.glsl new file mode 100644 index 0000000000..6a17baf834 --- /dev/null +++ b/assets/test/shaders/demo_6_2d.vert.glsl @@ -0,0 +1,83 @@ +#version 330 + +layout(location=0) in vec2 v_position; +layout(location=1) in vec2 uv; + +out vec2 vert_uv; + +// camera parameters for transforming the object position +// and scaling the subtex to the correct size +layout (std140) uniform camera { + // view matrix (world to view space) + mat4 view; + // projection matrix (view to clip space) + mat4 proj; + // inverse zoom factor (1.0 / zoom) + // high zoom = upscale subtex + // low zoom = downscale subtex + float inv_zoom; + // inverse viewport size (1.0 / viewport size) + vec2 inv_viewport_size; +}; + +// position of the object in world space +uniform vec3 obj_world_position; + +// parameters for scaling and moving the subtex +// to the correct position in clip space + +// animation scalefactor +// scales the vertex positions so that they +// match the subtex dimensions +// +// high animation scale = downscale subtex +// low animation scale = upscale subtex +uniform float scale; + +// size of the subtex (in pixels) +uniform vec2 subtex_size; + +// offset of the subtex anchor point +// from the subtex center (in pixels) +// used to move the subtex so that the anchor point +// is at the object position +uniform vec2 anchor_offset; + +void main() { + // translate the position of the object from world space to clip space + // this is the position where we want to draw the subtex in 2D + vec4 obj_clip_pos = proj * view * vec4(obj_world_position, 1.0); + + // subtex has to be scaled to account for the zoom factor + // and the animation scale factor. essentially this is (animation scale / zoom). + float zoom_scale = scale * inv_zoom; + + // Scale the subtex vertices + // we have to account for the viewport size to get the correct dimensions + // and then scale the subtex to the zoom factor to get the correct size + vec2 vert_scale = zoom_scale * subtex_size * inv_viewport_size; + + // Scale the anchor offset with the same method as above + // to get the correct anchor position in the viewport + vec2 anchor_scale = zoom_scale * anchor_offset * inv_viewport_size; + + // offset the clip position by the offset of the subtex anchor + // imagine this as pinning the subtex to the object position at the subtex anchor point + obj_clip_pos += vec4(anchor_scale.x, anchor_scale.y, 0.0, 0.0); + + // create a move matrix for positioning the vertices + // uses the vert scale and the transformed object position in clip space + mat4 move = mat4(vert_scale.x, 0.0, 0.0, 0.0, + 0.0, vert_scale.y, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + obj_clip_pos.x, obj_clip_pos.y, obj_clip_pos.z, 1.0); + + // calculate the final vertex position + gl_Position = move * vec4(v_position, 0.0, 1.0); + + // flip y axis because OpenGL uses bottom-left as its origin + float uv_x = uv.x; + float uv_y = 1.0 - uv.y; + + vert_uv = vec2(uv_x, uv_y); +} diff --git a/assets/test/shaders/demo_6_2d_frame.frag.glsl b/assets/test/shaders/demo_6_2d_frame.frag.glsl new file mode 100644 index 0000000000..c67ca484cc --- /dev/null +++ b/assets/test/shaders/demo_6_2d_frame.frag.glsl @@ -0,0 +1,7 @@ +#version 330 + +out vec4 col; + +void main() { + col = vec4(1.0, 0.0, 0.0, 0.8); +} diff --git a/assets/test/shaders/demo_6_2d_frame.vert.glsl b/assets/test/shaders/demo_6_2d_frame.vert.glsl new file mode 100644 index 0000000000..8d7d13b6be --- /dev/null +++ b/assets/test/shaders/demo_6_2d_frame.vert.glsl @@ -0,0 +1,58 @@ +#version 330 + +layout(location=0) in vec2 v_position; + +// camera parameters for transforming the object position +// and scaling the subtex to the correct size +layout (std140) uniform camera { + // view matrix (world to view space) + mat4 view; + // projection matrix (view to clip space) + mat4 proj; + // inverse zoom factor (1.0 / zoom) + float inv_zoom; + // inverse viewport size (1.0 / viewport size) + vec2 inv_viewport_size; +}; + +// position of the object in world space +uniform vec3 obj_world_position; + +// parameters for scaling and moving the subtex +// to the correct position in clip space + +// animation scalefactor +// scales the vertex positions so that they +// match the subtex dimensions +// +// high animation scale = downscale subtex +// low animation scale = upscale subtex +uniform float scale; + +// size of the frame (in pixels) +uniform vec2 frame_size; + +void main() { + // translate the position of the object from world space to clip space + // this is the position where we want to draw the subtex in 2D + vec4 obj_clip_pos = proj * view * vec4(obj_world_position, 1.0); + + // subtex has to be scaled to account for the zoom factor + // and the animation scale factor. essentially this is (animation scale / zoom). + float zoom_scale = scale * inv_zoom; + + // Scale the subtex vertices + // we have to account for the viewport size to get the correct dimensions + // and then scale the frame to the zoom factor to get the correct size + vec2 vert_scale = zoom_scale * frame_size * inv_viewport_size; + + // create a move matrix for positioning the vertices + // uses the vert scale and the transformed object position in clip space + mat4 move = mat4(vert_scale.x, 0.0, 0.0, 0.0, + 0.0, vert_scale.y, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + obj_clip_pos.x, obj_clip_pos.y, obj_clip_pos.z, 1.0); + + // calculate the final vertex position + gl_Position = move * vec4(v_position, 0.0, 1.0); +} diff --git a/assets/test/shaders/demo_6_3d.frag.glsl b/assets/test/shaders/demo_6_3d.frag.glsl new file mode 100644 index 0000000000..f2d3c71bd8 --- /dev/null +++ b/assets/test/shaders/demo_6_3d.frag.glsl @@ -0,0 +1,13 @@ +#version 330 + +in vec2 tex_pos; + +layout(location=0) out vec4 out_col; + +uniform sampler2D tex; + +void main() +{ + vec4 tex_val = texture(tex, tex_pos); + out_col = tex_val; +} diff --git a/assets/test/shaders/demo_6_3d.vert.glsl b/assets/test/shaders/demo_6_3d.vert.glsl new file mode 100644 index 0000000000..95a552dcd6 --- /dev/null +++ b/assets/test/shaders/demo_6_3d.vert.glsl @@ -0,0 +1,24 @@ +#version 330 + +layout (location = 0) in vec3 position; +layout (location = 1) in vec2 uv; + +out vec2 tex_pos; + +// camera parameters for transforming the object position +// and scaling the subtex to the correct size +layout (std140) uniform camera { + // view matrix (world to view space) + mat4 view; + // projection matrix (view to clip space) + mat4 proj; + // inverse zoom factor (1.0 / zoom) + float inv_zoom; + // inverse viewport size (1.0 / viewport size) + vec2 inv_viewport_size; +}; + +void main() { + gl_Position = proj * view * vec4(position, 1.0); + tex_pos = vec2(uv.x, 1.0 - uv.y); +} diff --git a/assets/test/shaders/demo_6_display.frag.glsl b/assets/test/shaders/demo_6_display.frag.glsl new file mode 100644 index 0000000000..a6732d0c7f --- /dev/null +++ b/assets/test/shaders/demo_6_display.frag.glsl @@ -0,0 +1,10 @@ +#version 330 + +uniform sampler2D color_texture; + +in vec2 v_uv; +out vec4 col; + +void main() { + col = texture(color_texture, v_uv); +} diff --git a/assets/test/shaders/demo_6_display.vert.glsl b/assets/test/shaders/demo_6_display.vert.glsl new file mode 100644 index 0000000000..072e5d80b0 --- /dev/null +++ b/assets/test/shaders/demo_6_display.vert.glsl @@ -0,0 +1,10 @@ +#version 330 + +layout(location=0) in vec2 position; +layout(location=1) in vec2 uv; +out vec2 v_uv; + +void main() { + gl_Position = vec4(position, 0.0, 1.0); + v_uv = uv; +} diff --git a/libopenage/renderer/demo/CMakeLists.txt b/libopenage/renderer/demo/CMakeLists.txt index aaa30bc906..fb93e5279b 100644 --- a/libopenage/renderer/demo/CMakeLists.txt +++ b/libopenage/renderer/demo/CMakeLists.txt @@ -5,6 +5,7 @@ add_sources(libopenage demo_3.cpp demo_4.cpp demo_5.cpp + demo_6.cpp stresstest_0.cpp stresstest_1.cpp tests.cpp diff --git a/libopenage/renderer/demo/demo_0.cpp b/libopenage/renderer/demo/demo_0.cpp index d82eb62381..38fd467d0b 100644 --- a/libopenage/renderer/demo/demo_0.cpp +++ b/libopenage/renderer/demo/demo_0.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "demo_0.h" diff --git a/libopenage/renderer/demo/demo_0.h b/libopenage/renderer/demo/demo_0.h index 4cdb1d831b..7ea7e68f19 100644 --- a/libopenage/renderer/demo/demo_0.h +++ b/libopenage/renderer/demo/demo_0.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #pragma once diff --git a/libopenage/renderer/demo/demo_1.cpp b/libopenage/renderer/demo/demo_1.cpp index 1ecd7815a1..23dfdfa67b 100644 --- a/libopenage/renderer/demo/demo_1.cpp +++ b/libopenage/renderer/demo/demo_1.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "demo_1.h" diff --git a/libopenage/renderer/demo/demo_1.h b/libopenage/renderer/demo/demo_1.h index 6b1c3168ee..d978603040 100644 --- a/libopenage/renderer/demo/demo_1.h +++ b/libopenage/renderer/demo/demo_1.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #pragma once diff --git a/libopenage/renderer/demo/demo_2.cpp b/libopenage/renderer/demo/demo_2.cpp index 7f6f398e12..e615d238ba 100644 --- a/libopenage/renderer/demo/demo_2.cpp +++ b/libopenage/renderer/demo/demo_2.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "demo_2.h" diff --git a/libopenage/renderer/demo/demo_2.h b/libopenage/renderer/demo/demo_2.h index 077f61ac33..211e5e9310 100644 --- a/libopenage/renderer/demo/demo_2.h +++ b/libopenage/renderer/demo/demo_2.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #pragma once diff --git a/libopenage/renderer/demo/demo_3.cpp b/libopenage/renderer/demo/demo_3.cpp index 23de242fd7..408e699196 100644 --- a/libopenage/renderer/demo/demo_3.cpp +++ b/libopenage/renderer/demo/demo_3.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "demo_3.h" diff --git a/libopenage/renderer/demo/demo_3.h b/libopenage/renderer/demo/demo_3.h index fda210acbd..9eec5dc74c 100644 --- a/libopenage/renderer/demo/demo_3.h +++ b/libopenage/renderer/demo/demo_3.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #pragma once @@ -10,7 +10,6 @@ namespace openage::renderer::tests { * Show off the render stages in the level 2 renderer and the camera * system. * - Window creation - * - Loading shaders * - Creating a camera * - Initializing the level 2 render stages: skybox, terrain, world, screen * - Adding renderables to the render stages via the render factory diff --git a/libopenage/renderer/demo/demo_4.h b/libopenage/renderer/demo/demo_4.h index 7c80e947e1..c3ce3ad4b5 100644 --- a/libopenage/renderer/demo/demo_4.h +++ b/libopenage/renderer/demo/demo_4.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #pragma once diff --git a/libopenage/renderer/demo/demo_6.cpp b/libopenage/renderer/demo/demo_6.cpp new file mode 100644 index 0000000000..038bb0240e --- /dev/null +++ b/libopenage/renderer/demo/demo_6.cpp @@ -0,0 +1,428 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "demo_6.h" + +#include + +#include "curve/continuous.h" +#include "curve/segmented.h" +#include "renderer/camera/camera.h" +#include "renderer/camera/frustum_2d.h" +#include "renderer/camera/frustum_3d.h" +#include "renderer/gui/integration/public/gui_application_with_logger.h" +#include "renderer/opengl/window.h" +#include "renderer/render_pass.h" +#include "renderer/renderer.h" +#include "renderer/resources/animation/angle_info.h" +#include "renderer/resources/animation/frame_info.h" +#include "renderer/resources/animation/layer_info.h" +#include "renderer/resources/parser/parse_sprite.h" +#include "renderer/resources/parser/parse_terrain.h" +#include "renderer/resources/shader_source.h" +#include "renderer/resources/texture_info.h" +#include "renderer/shader_program.h" +#include "renderer/texture.h" +#include "renderer/uniform_buffer.h" +#include "time/clock.h" +#include "util/path.h" +#include "util/vector.h" + + +namespace openage::renderer::tests { + +void renderer_demo_6(const util::Path &path) { + auto render_mgr = RenderManagerDemo6{path}; + + // Create render objects + auto renderables_2d = render_mgr.create_2d_obj(); + auto renderables_3d = render_mgr.create_3d_obj(); + auto renderables_frame = render_mgr.create_frame_obj(); + + // Add objects to the render passes + render_mgr.obj_2d_pass->add_renderables(std::move(renderables_2d)); + render_mgr.obj_3d_pass->add_renderables({renderables_3d}); + render_mgr.frame_pass->add_renderables(std::move(renderables_frame)); + + render_mgr.window->add_key_callback([&](const QKeyEvent &ev) { + if (ev.type() == QEvent::KeyPress) { + auto key = ev.key(); + + // move_frame moves the camera in the specified direction in the next drawn frame + switch (key) { + case Qt::Key_W: { // forward + render_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), 0.25f); + } break; + case Qt::Key_A: { // left + render_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), 0.25f); + } break; + case Qt::Key_S: { // back + render_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), 0.25f); + } break; + case Qt::Key_D: { // right + render_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), 0.25f); + } break; + default: + break; + } + + auto new_cam_unifs = render_mgr.camera->get_uniform_buffer()->new_uniform_input( + "view", + render_mgr.camera->get_view_matrix(), + "proj", + render_mgr.camera->get_projection_matrix()); + render_mgr.camera->get_uniform_buffer()->update_uniforms(new_cam_unifs); + + auto frustum_camera = *render_mgr.camera; + auto half_cam_size = util::Vector2s{render_mgr.camera->get_viewport_size()[0] * 0.7, + render_mgr.camera->get_viewport_size()[1] * 0.7}; + frustum_camera.resize(half_cam_size[0], half_cam_size[1]); + auto frustum_2d = frustum_camera.get_frustum_2d(); + frustum_2d.update(frustum_camera.get_viewport_size(), + frustum_camera.get_view_matrix(), + frustum_camera.get_projection_matrix(), + frustum_camera.get_zoom()); + + auto renderables_2d = render_mgr.create_2d_obj(); + std::vector renderables_in_frustum{}; + for (size_t i = 0; i < render_mgr.obj_2d_positions.size(); ++i) { + auto pos = render_mgr.obj_2d_positions.at(i); + bool in_frustum = frustum_2d.in_frustum(pos.to_world_space(), + Eigen::Matrix4f::Identity(), + render_mgr.animation_2d_info.get_scalefactor(), + render_mgr.animation_2d_info.get_max_bounds()); + if (in_frustum) { + renderables_in_frustum.push_back(renderables_2d.at(i)); + } + } + render_mgr.obj_2d_pass->clear_renderables(); + render_mgr.obj_2d_pass->add_renderables(std::move(renderables_in_frustum)); + } + }); + + render_mgr.run(); +} + +RenderManagerDemo6::RenderManagerDemo6(const util::Path &path) : + path{path} { + this->setup(); +} + +void RenderManagerDemo6::run() { + while (not window->should_close()) { + this->qtapp->process_events(); + + // Draw everything + renderer->render(this->obj_3d_pass); + renderer->render(this->obj_2d_pass); + renderer->render(this->frame_pass); + renderer->render(this->display_pass); + + // Display final output on screen + this->window->update(); + } +} + +const std::vector RenderManagerDemo6::create_2d_obj() { + std::vector renderables; + for (auto scene_pos : this->obj_2d_positions) { + // Create renderable for 2D animation + auto scale = this->animation_2d_info.get_scalefactor(); + auto tex_id = this->animation_2d_info.get_layer(0).get_angle(0)->get_frame(0)->get_texture_idx(); + auto subtex_id = this->animation_2d_info.get_layer(0).get_angle(0)->get_frame(0)->get_subtexture_idx(); + auto subtex = this->animation_2d_info.get_texture(tex_id)->get_subtex_info(subtex_id); + auto subtex_size = subtex.get_size(); + Eigen::Vector2f subtex_size_vec{ + static_cast(subtex_size[0]), + static_cast(subtex_size[1])}; + auto anchor_params = subtex.get_anchor_params(); + auto anchor_params_vec = Eigen::Vector2f{ + static_cast(anchor_params[0]), + static_cast(anchor_params[1])}; + auto tile_params = subtex.get_subtex_coords(); + auto animation_2d_unifs = this->obj_2d_shader->new_uniform_input( + "obj_world_position", + scene_pos.to_world_space(), + "scale", + scale, + "subtex_size", + subtex_size_vec, + "anchor_offset", + anchor_params_vec, + "tex", + this->obj_2d_texture, + "tile_params", + tile_params); + auto quad = this->renderer->add_mesh_geometry(resources::MeshData::make_quad()); + Renderable animation_2d_obj{ + animation_2d_unifs, + quad, + true, + true, + }; + + renderables.push_back(animation_2d_obj); + } + + return renderables; +} + +const renderer::Renderable RenderManagerDemo6::create_3d_obj() { + auto terrain_tex_info = this->terrain_3d_info.get_texture(0); + auto terrain_tex_data = resources::Texture2dData{*terrain_tex_info}; + this->obj_3d_texture = this->renderer->add_texture(terrain_tex_data); + + // Create renderable for terrain + auto terrain_unifs = this->obj_3d_shader->new_uniform_input( + "tex", + this->obj_3d_texture); + std::vector terrain_pos{}; + terrain_pos.push_back({-25, -25, 0}); + terrain_pos.push_back({25, -25, 0}); + terrain_pos.push_back({-25, 25, 0}); + terrain_pos.push_back({25, 25, 0}); + std::vector terrain_verts{}; + for (size_t i = 0; i < terrain_pos.size(); ++i) { + auto scene_pos = terrain_pos.at(i).to_world_space(); + terrain_verts.push_back(scene_pos[0]); + terrain_verts.push_back(scene_pos[1]); + terrain_verts.push_back(scene_pos[2]); + terrain_verts.push_back(0.0f + i / 2); + terrain_verts.push_back(0.0f + i % 2); + } + auto vert_info = resources::VertexInputInfo{ + {resources::vertex_input_t::V3F32, resources::vertex_input_t::V2F32}, + resources::vertex_layout_t::AOS, + resources::vertex_primitive_t::TRIANGLE_STRIP, + }; + std::vector vert_data(terrain_verts.size() * sizeof(float)); + std::memcpy(vert_data.data(), terrain_verts.data(), vert_data.size()); + auto terrain_mesh = resources::MeshData{std::move(vert_data), vert_info}; + auto terrain_geometry = this->renderer->add_mesh_geometry(terrain_mesh); + Renderable terrain_obj{ + terrain_unifs, + terrain_geometry, + true, + true, + }; + + return terrain_obj; +} + +const std::vector RenderManagerDemo6::create_frame_obj() { + std::vector renderables; + for (auto scene_pos : this->obj_2d_positions) { + // Create renderable for frame + std::array frame_verts{-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f}; + std::vector frame_vert_data(frame_verts.size() * sizeof(float)); + std::memcpy(frame_vert_data.data(), frame_verts.data(), frame_vert_data.size()); + auto frame_vert_info = resources::VertexInputInfo{ + {resources::vertex_input_t::V2F32}, + resources::vertex_layout_t::AOS, + resources::vertex_primitive_t::LINE_LOOP, + }; + auto frame_mesh = resources::MeshData{std::move(frame_vert_data), frame_vert_info}; + auto frame_geometry = this->renderer->add_mesh_geometry(frame_mesh); + + auto scale = this->animation_2d_info.get_scalefactor(); + auto max_frame_size = this->animation_2d_info.get_max_size(); + auto frame_size = Eigen::Vector2f{ + static_cast(max_frame_size[0]), + static_cast(max_frame_size[1])}; + auto frame_unifs = this->frame_shader->new_uniform_input( + "obj_world_position", + scene_pos.to_world_space(), + "scale", + scale, + "frame_size", + frame_size); + Renderable frame_obj{ + frame_unifs, + frame_geometry, + true, + true, + }; + + renderables.push_back(frame_obj); + } + + return renderables; +} + +void RenderManagerDemo6::setup() { + this->qtapp = std::make_shared(); + + // Create the window and renderer + window_settings settings; + settings.width = 1024; + settings.height = 768; + settings.debug = true; + this->window = std::make_shared("openage renderer test", settings); + this->renderer = window->make_renderer(); + + this->load_shaders(); + this->load_assets(); + this->create_camera(); + this->create_render_passes(); +} + +void RenderManagerDemo6::load_shaders() { + // Shader + auto shaderdir = this->path / "assets" / "test" / "shaders"; + + /* Shader for 3D objects*/ + auto obj_vshader_file = (shaderdir / "demo_6_3d.vert.glsl").open(); + auto obj_vshader_src = resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::vertex, + obj_vshader_file.read()); + obj_vshader_file.close(); + + auto obj_fshader_file = (shaderdir / "demo_6_3d.frag.glsl").open(); + auto obj_fshader_src = resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::fragment, + obj_fshader_file.read()); + obj_fshader_file.close(); + + /* Shader for 2D animations */ + auto obj_2d_vshader_file = (shaderdir / "demo_6_2d.vert.glsl").open(); + auto obj_2d_vshader_src = resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::vertex, + obj_2d_vshader_file.read()); + obj_2d_vshader_file.close(); + + auto obj_2d_fshader_file = (shaderdir / "demo_6_2d.frag.glsl").open(); + auto obj_2d_fshader_src = resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::fragment, + obj_2d_fshader_file.read()); + obj_2d_fshader_file.close(); + + /* Shader for frames */ + auto frame_vshader_file = (shaderdir / "demo_6_2d_frame.vert.glsl").open(); + auto frame_vshader_src = resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::vertex, + frame_vshader_file.read()); + frame_vshader_file.close(); + + auto frame_fshader_file = (shaderdir / "demo_6_2d_frame.frag.glsl").open(); + auto frame_fshader_src = resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::fragment, + frame_fshader_file.read()); + frame_fshader_file.close(); + + /* Shader for rendering to the screen */ + auto display_vshader_file = (shaderdir / "demo_6_display.vert.glsl").open(); + auto display_vshader_src = resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::vertex, + display_vshader_file.read()); + display_vshader_file.close(); + + auto display_fshader_file = (shaderdir / "demo_6_display.frag.glsl").open(); + auto display_fshader_src = resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::fragment, + display_fshader_file.read()); + display_fshader_file.close(); + + // Create shader programs + this->obj_3d_shader = this->renderer->add_shader({obj_vshader_src, obj_fshader_src}); + this->obj_2d_shader = this->renderer->add_shader({obj_2d_vshader_src, obj_2d_fshader_src}); + this->frame_shader = this->renderer->add_shader({frame_vshader_src, frame_fshader_src}); + this->display_shader = this->renderer->add_shader({display_vshader_src, display_fshader_src}); +} + +void RenderManagerDemo6::load_assets() { + // Load assets + auto animation_2d_path = this->path / "assets" / "test" / "textures" / "test_tank.sprite"; + this->animation_2d_info = resources::parser::parse_sprite_file(animation_2d_path); + + auto tex_info = this->animation_2d_info.get_texture(0); + auto tex_data = resources::Texture2dData{*tex_info}; + this->obj_2d_texture = this->renderer->add_texture(tex_data); + + // Load assets + auto terrain_path = this->path / "assets" / "test" / "textures" / "test_terrain.terrain"; + this->terrain_3d_info = resources::parser::parse_terrain_file(terrain_path); +} + +void RenderManagerDemo6::create_camera() { + // Camera + this->camera = std::make_shared(renderer, window->get_size()); + this->camera->set_zoom(2.0f); + + // Bind the camera uniform buffer to the shaders + obj_3d_shader->bind_uniform_buffer("camera", this->camera->get_uniform_buffer()); + obj_2d_shader->bind_uniform_buffer("camera", this->camera->get_uniform_buffer()); + frame_shader->bind_uniform_buffer("camera", this->camera->get_uniform_buffer()); + + // Update the camera uniform buffer + auto camera_unifs = camera->get_uniform_buffer()->new_uniform_input( + "view", + this->camera->get_view_matrix(), + "proj", + this->camera->get_projection_matrix(), + "inv_zoom", + 1.0f / this->camera->get_zoom()); + auto viewport_size = this->camera->get_viewport_size(); + Eigen::Vector2f viewport_size_vec{ + 1.0f / static_cast(viewport_size[0]), + 1.0f / static_cast(viewport_size[1])}; + camera_unifs->update("inv_viewport_size", viewport_size_vec); + this->camera->get_uniform_buffer()->update_uniforms(camera_unifs); +} + +void RenderManagerDemo6::create_render_passes() { + // Create render passes + auto window_size = window->get_size(); + auto color_texture0 = renderer->add_texture(resources::Texture2dInfo(window_size[0], + window_size[1], + resources::pixel_format::rgba8)); + auto fbo0 = renderer->create_texture_target({color_texture0}); + this->obj_3d_pass = renderer->add_render_pass({}, fbo0); + + auto color_texture1 = renderer->add_texture(resources::Texture2dInfo(window_size[0], + window_size[1], + resources::pixel_format::rgba8)); + auto fbo1 = renderer->create_texture_target({color_texture1}); + this->obj_2d_pass = renderer->add_render_pass({}, fbo1); + + auto color_texture2 = renderer->add_texture(resources::Texture2dInfo(window_size[0], + window_size[1], + resources::pixel_format::rgba8)); + auto fbo2 = renderer->create_texture_target({color_texture2}); + this->frame_pass = renderer->add_render_pass({}, fbo2); + + // Create render pass for rendering to screen + auto quad = renderer->add_mesh_geometry(resources::MeshData::make_quad()); + auto color_texture0_unif = display_shader->new_uniform_input("color_texture", color_texture0); + Renderable display_obj_3d{ + color_texture0_unif, + quad, + true, + true, + }; + auto color_texture1_unif = display_shader->new_uniform_input("color_texture", color_texture1); + Renderable display_obj_2d{ + color_texture1_unif, + quad, + true, + true, + }; + auto color_texture2_unif = display_shader->new_uniform_input("color_texture", color_texture2); + Renderable display_obj_frame{ + color_texture2_unif, + quad, + true, + true, + }; + this->display_pass = renderer->add_render_pass( + {display_obj_3d, display_obj_2d, display_obj_frame}, + renderer->get_display_target()); +} + +} // namespace openage::renderer::tests diff --git a/libopenage/renderer/demo/demo_6.h b/libopenage/renderer/demo/demo_6.h new file mode 100644 index 0000000000..7c5d3052c5 --- /dev/null +++ b/libopenage/renderer/demo/demo_6.h @@ -0,0 +1,101 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include "coord/scene.h" +#include "renderer/renderable.h" +#include "renderer/resources/animation/animation_info.h" +#include "renderer/resources/terrain/terrain_info.h" +#include "util/path.h" + + +namespace openage::renderer { +class RenderPass; +class Renderer; +class ShaderProgram; +class Texture2d; + +namespace camera { +class Camera; +} + +namespace gui { +class GuiApplicationWithLogger; +} + +namespace opengl { +class GlWindow; +} + +namespace tests { + +/** + * Show the usage of frustum culling in the renderer. + * - Window creation + * - Loading shaders + * - Creating a camera + * - 2D and 3D frustum retrieval + * - Manipulating the frustum + * - Rendering objects with frustum culling + * + * @param path Path to the openage asset directory. + */ +void renderer_demo_6(const util::Path &path); + + +class RenderManagerDemo6 { +public: + RenderManagerDemo6(const util::Path &path); + void run(); + + const std::vector create_2d_obj(); + const renderer::Renderable create_3d_obj(); + const std::vector create_frame_obj(); + + std::shared_ptr qtapp; + + std::shared_ptr window; + + std::shared_ptr camera; + + std::shared_ptr obj_2d_pass; + std::shared_ptr obj_3d_pass; + std::shared_ptr frame_pass; + std::shared_ptr display_pass; + + const std::array obj_2d_positions = { + coord::scene3{0, 0, 0}, + coord::scene3{-4, -4, 0}, + coord::scene3{4, 4, 0}, + coord::scene3{-2, 3, 0}, + coord::scene3{3, -2, 0}, + }; + + resources::Animation2dInfo animation_2d_info; + resources::TerrainInfo terrain_3d_info; + +private: + void setup(); + + void load_shaders(); + void load_assets(); + void create_camera(); + void create_render_passes(); + + util::Path path; + + std::shared_ptr renderer; + + std::shared_ptr obj_2d_shader; + std::shared_ptr obj_3d_shader; + std::shared_ptr frame_shader; + std::shared_ptr display_shader; + + std::shared_ptr obj_2d_texture; + std::shared_ptr obj_3d_texture; +}; + +} // namespace tests +} // namespace openage::renderer diff --git a/libopenage/renderer/demo/tests.cpp b/libopenage/renderer/demo/tests.cpp index c997a06763..d3fb0e3c21 100644 --- a/libopenage/renderer/demo/tests.cpp +++ b/libopenage/renderer/demo/tests.cpp @@ -11,6 +11,7 @@ #include "renderer/demo/demo_3.h" #include "renderer/demo/demo_4.h" #include "renderer/demo/demo_5.h" +#include "renderer/demo/demo_6.h" #include "renderer/demo/stresstest_0.h" #include "renderer/demo/stresstest_1.h" @@ -42,6 +43,10 @@ void renderer_demo(int demo_id, const util::Path &path) { renderer_demo_5(path); break; + case 6: + renderer_demo_6(path); + break; + default: log::log(MSG(err) << "Unknown renderer demo requested: " << demo_id << "."); break; From 9e927c96c393dfc35713e91d35f10d2918c00516 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 May 2024 02:05:43 +0200 Subject: [PATCH 443/771] convert: Load nyan API v0.5.0. --- .../convert/service/read/nyan_api_loader.py | 125 +++++++----------- 1 file changed, 51 insertions(+), 74 deletions(-) diff --git a/openage/convert/service/read/nyan_api_loader.py b/openage/convert/service/read/nyan_api_loader.py index 24bd7c10ef..9980b7fbf4 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-2023 the openage authors. See copying.md for legal info. +# Copyright 2019-2024 the openage authors. See copying.md for legal info. # # pylint: disable=line-too-long,too-many-lines,too-many-statements """ @@ -153,6 +153,13 @@ def _create_objects(api_objects: dict[str, NyanObject]) -> None: nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) + # engine.ability.type.Collision + parents = [api_objects["engine.ability.Ability"]] + nyan_object = NyanObject("Collision", parents) + fqon = "engine.ability.type.Collision" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + # engine.ability.type.Constructable parents = [api_objects["engine.ability.Ability"]] nyan_object = NyanObject("Constructable", parents) @@ -272,13 +279,6 @@ def _create_objects(api_objects: dict[str, NyanObject]) -> None: nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) - # engine.ability.type.Hitbox - parents = [api_objects["engine.ability.Ability"]] - nyan_object = NyanObject("Hitbox", parents) - fqon = "engine.ability.type.Hitbox" - nyan_object.set_fqon(fqon) - api_objects.update({fqon: nyan_object}) - # engine.ability.type.Idle parents = [api_objects["engine.ability.Ability"]] nyan_object = NyanObject("Idle", parents) @@ -328,17 +328,17 @@ def _create_objects(api_objects: dict[str, NyanObject]) -> None: nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) - # engine.ability.type.Passable + # engine.ability.type.PassiveTransformTo parents = [api_objects["engine.ability.Ability"]] - nyan_object = NyanObject("Passable", parents) - fqon = "engine.ability.type.Passable" + nyan_object = NyanObject("PassiveTransformTo", parents) + fqon = "engine.ability.type.PassiveTransformTo" nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) - # engine.ability.type.PassiveTransformTo + # engine.ability.type.Pathable parents = [api_objects["engine.ability.Ability"]] - nyan_object = NyanObject("PassiveTransformTo", parents) - fqon = "engine.ability.type.PassiveTransformTo" + nyan_object = NyanObject("Pathable", parents) + fqon = "engine.ability.type.Pathable" nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) @@ -1372,27 +1372,6 @@ def _create_objects(api_objects: dict[str, NyanObject]) -> None: nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) - # engine.util.passable_mode.PassableMode - parents = [api_objects["engine.root.Object"]] - nyan_object = NyanObject("PassableMode", parents) - fqon = "engine.util.passable_mode.PassableMode" - nyan_object.set_fqon(fqon) - api_objects.update({fqon: nyan_object}) - - # engine.util.passable_mode.type.Gate - parents = [api_objects["engine.util.passable_mode.PassableMode"]] - nyan_object = NyanObject("Gate", parents) - fqon = "engine.util.passable_mode.type.Gate" - nyan_object.set_fqon(fqon) - api_objects.update({fqon: nyan_object}) - - # engine.util.passable_mode.type.Normal - parents = [api_objects["engine.util.passable_mode.PassableMode"]] - nyan_object = NyanObject("Normal", parents) - fqon = "engine.util.passable_mode.type.Normal" - nyan_object.set_fqon(fqon) - api_objects.update({fqon: nyan_object}) - # engine.util.patch.NyanPatch parents = [api_objects["engine.root.Object"]] nyan_object = NyanObject("NyanPatch", parents) @@ -1421,6 +1400,13 @@ def _create_objects(api_objects: dict[str, NyanObject]) -> None: nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) + # engine.util.path_type.PathType + parents = [api_objects["engine.root.Object"]] + nyan_object = NyanObject("PathType", parents) + fqon = "engine.util.path_type.PathType" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + # engine.util.payment_mode.PaymentMode parents = [api_objects["engine.root.Object"]] nyan_object = NyanObject("PaymentMode", parents) @@ -2680,6 +2666,13 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: member = NyanMember("storage_elements", member_type, None, None, 0) api_object.add_member(member) + # engine.ability.type.Collision + api_object = api_objects["engine.ability.type.Collision"] + + member_type = NyanMemberType(api_objects["engine.util.hitbox.Hitbox"]) + member = NyanMember("hitbox", member_type, None, None, 0) + api_object.add_member(member) + # engine.ability.type.Constructable api_object = api_objects["engine.ability.type.Constructable"] @@ -2893,13 +2886,6 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: member = NyanMember("mode", member_type, None, None, 0) api_object.add_member(member) - # engine.ability.type.Hitbox - api_object = api_objects["engine.ability.type.Hitbox"] - - member_type = NyanMemberType(api_objects["engine.util.hitbox.Hitbox"]) - member = NyanMember("hitbox", member_type, None, None, 0) - api_object.add_member(member) - # engine.ability.type.LineOfSight api_object = api_objects["engine.ability.type.LineOfSight"] @@ -2931,6 +2917,10 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: member_type = NyanMemberType(MemberType.SET, (elem_type,)) member = NyanMember("modes", member_type, None, None, 0) api_object.add_member(member) + subtype = NyanMemberType(api_objects["engine.util.path_type.PathType"]) + member_type = NyanMemberType(MemberType.CHILDREN, (subtype,)) + member = NyanMember("path_type", member_type, None, None, 0) + api_object.add_member(member) # engine.ability.type.Named api_object = api_objects["engine.ability.type.Named"] @@ -2955,16 +2945,6 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: member = NyanMember("terrain_overlay", member_type, None, None, 0) api_object.add_member(member) - # engine.ability.type.Passable - api_object = api_objects["engine.ability.type.Passable"] - - member_type = NyanMemberType(api_objects["engine.util.hitbox.Hitbox"]) - member = NyanMember("hitbox", member_type, None, None, 0) - api_object.add_member(member) - member_type = NyanMemberType(api_objects["engine.util.passable_mode.PassableMode"]) - member = NyanMember("mode", member_type, None, None, 0) - api_object.add_member(member) - # engine.ability.type.PassiveTransformTo api_object = api_objects["engine.ability.type.PassiveTransformTo"] @@ -2982,6 +2962,19 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: member = NyanMember("transform_progress", member_type, None, None, 0) api_object.add_member(member) + # engine.ability.type.Pathable + api_object = api_objects["engine.ability.type.Pathable"] + + member_type = NyanMemberType(api_objects["engine.util.hitbox.Hitbox"]) + member = NyanMember("hitbox", member_type, None, None, 0) + api_object.add_member(member) + subtype = NyanMemberType(api_objects["engine.util.path_type.PathType"]) + key_type = NyanMemberType(MemberType.CHILDREN, (subtype,)) + elem_type = N_INT + member_type = NyanMemberType(MemberType.DICT, (key_type, elem_type)) + member = NyanMember("path_costs", member_type, None, None, 0) + api_object.add_member(member) + # engine.ability.type.ProductionQueue api_object = api_objects["engine.ability.type.ProductionQueue"] @@ -3849,28 +3842,6 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: member = NyanMember("range", N_FLOAT, None, None, 0) api_object.add_member(member) - # engine.util.passable_mode.PassableMode - api_object = api_objects["engine.util.passable_mode.PassableMode"] - - subtype = NyanMemberType(api_objects["engine.util.game_entity_type.GameEntityType"]) - elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,)) - member_type = NyanMemberType(MemberType.SET, (elem_type,)) - member = NyanMember("allowed_types", member_type, None, None, 0) - api_object.add_member(member) - elem_type = NyanMemberType(api_objects["engine.util.game_entity.GameEntity"]) - member_type = NyanMemberType(MemberType.SET, (elem_type,)) - member = NyanMember("blacklisted_entities", member_type, None, None, 0) - api_object.add_member(member) - - # engine.util.passable_mode.type.Gate - api_object = api_objects["engine.util.passable_mode.type.Gate"] - - subtype = NyanMemberType(api_objects["engine.util.diplomatic_stance.DiplomaticStance"]) - elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,)) - member_type = NyanMemberType(MemberType.SET, (elem_type,)) - member = NyanMember("stances", member_type, None, None, 0) - api_object.add_member(member) - # engine.util.patch.Patch api_object = api_objects["engine.util.patch.Patch"] @@ -4296,6 +4267,12 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: member_type = NyanMemberType(MemberType.SET, (elem_type,)) member = NyanMember("ambience", member_type, None, None, 0) api_object.add_member(member) + subtype = NyanMemberType(api_objects["engine.util.path_type.PathType"]) + key_type = NyanMemberType(MemberType.CHILDREN, (subtype,)) + elem_type = N_INT + member_type = NyanMemberType(MemberType.DICT, (key_type, elem_type)) + member = NyanMember("path_costs", member_type, None, None, 0) + api_object.add_member(member) # engine.util.terrain.TerrainAmbient api_object = api_objects["engine.util.terrain.TerrainAmbient"] From a25b08c82be380a8799b1871bcfc8c06c154a0d0 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 May 2024 02:06:10 +0200 Subject: [PATCH 444/771] convert: Set exported nyan modpack version to 0.5.0. --- openage/convert/tool/api_export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openage/convert/tool/api_export.py b/openage/convert/tool/api_export.py index b1cd767898..b20752e330 100644 --- a/openage/convert/tool/api_export.py +++ b/openage/convert/tool/api_export.py @@ -75,7 +75,7 @@ def create_modpack() -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("engine", modpack_version="0.4.1", versionstr="0.4.1", repo="openage") + mod_def.set_info("engine", modpack_version="0.5.0", versionstr="0.5.0", repo="openage") mod_def.add_include("**") From b73926c6cd4f8d3900121c78d58b81c7b934f843 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 May 2024 04:05:02 +0200 Subject: [PATCH 445/771] convert: Pregen path types for modpacks. --- .../conversion/aoc/ability_subprocessor.py | 205 ++++++++---------- .../conversion/aoc/pregen_processor.py | 68 +++++- .../conversion/ror/pregen_subprocessor.py | 3 +- .../conversion/swgbcc/pregen_subprocessor.py | 3 +- 4 files changed, 163 insertions(+), 116 deletions(-) diff --git a/openage/convert/processor/conversion/aoc/ability_subprocessor.py b/openage/convert/processor/conversion/aoc/ability_subprocessor.py index ffa0a98e57..867665b3a2 100644 --- a/openage/convert/processor/conversion/aoc/ability_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/ability_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 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 @@ -820,6 +820,65 @@ def collect_storage_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref + @ staticmethod + def collision_ability(line: GenieGameEntityGroup) -> ForwardRef: + """ + Adds the Collision 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 = line.get_head_unit() + 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}.Collision" + ability_raw_api_object = RawAPIObject(ability_ref, "Collision", dataset.nyan_api_objects) + ability_raw_api_object.add_raw_parent("engine.ability.type.Collision") + ability_location = ForwardRef(line, game_entity_name) + ability_raw_api_object.set_location(ability_location) + + # Hitbox object + hitbox_name = f"{game_entity_name}.Collision.{game_entity_name}Hitbox" + hitbox_raw_api_object = RawAPIObject(hitbox_name, + f"{game_entity_name}Hitbox", + dataset.nyan_api_objects) + hitbox_raw_api_object.add_raw_parent("engine.util.hitbox.Hitbox") + hitbox_location = ForwardRef(line, ability_ref) + hitbox_raw_api_object.set_location(hitbox_location) + + radius_x = current_unit["radius_x"].value + radius_y = current_unit["radius_y"].value + radius_z = current_unit["radius_z"].value + + hitbox_raw_api_object.add_raw_member("radius_x", + radius_x, + "engine.util.hitbox.Hitbox") + hitbox_raw_api_object.add_raw_member("radius_y", + radius_y, + "engine.util.hitbox.Hitbox") + hitbox_raw_api_object.add_raw_member("radius_z", + radius_z, + "engine.util.hitbox.Hitbox") + + hitbox_forward_ref = ForwardRef(line, hitbox_name) + ability_raw_api_object.add_raw_member("hitbox", + hitbox_forward_ref, + "engine.ability.type.Collision") + + line.add_raw_api_object(hitbox_raw_api_object) + 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 constructable_ability(line: GenieGameEntityGroup) -> ForwardRef: """ @@ -4015,65 +4074,6 @@ def herdable_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod - def hitbox_ability(line: GenieGameEntityGroup) -> ForwardRef: - """ - Adds the Hitbox 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 = line.get_head_unit() - 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}.Hitbox" - ability_raw_api_object = RawAPIObject(ability_ref, "Hitbox", dataset.nyan_api_objects) - ability_raw_api_object.add_raw_parent("engine.ability.type.Hitbox") - ability_location = ForwardRef(line, game_entity_name) - ability_raw_api_object.set_location(ability_location) - - # Hitbox object - hitbox_name = f"{game_entity_name}.Hitbox.{game_entity_name}Hitbox" - hitbox_raw_api_object = RawAPIObject(hitbox_name, - f"{game_entity_name}Hitbox", - dataset.nyan_api_objects) - hitbox_raw_api_object.add_raw_parent("engine.util.hitbox.Hitbox") - hitbox_location = ForwardRef(line, ability_ref) - hitbox_raw_api_object.set_location(hitbox_location) - - radius_x = current_unit["radius_x"].value - radius_y = current_unit["radius_y"].value - radius_z = current_unit["radius_z"].value - - hitbox_raw_api_object.add_raw_member("radius_x", - radius_x, - "engine.util.hitbox.Hitbox") - hitbox_raw_api_object.add_raw_member("radius_y", - radius_y, - "engine.util.hitbox.Hitbox") - hitbox_raw_api_object.add_raw_member("radius_z", - radius_z, - "engine.util.hitbox.Hitbox") - - hitbox_forward_ref = ForwardRef(line, hitbox_name) - ability_raw_api_object.add_raw_member("hitbox", - hitbox_forward_ref, - "engine.ability.type.Hitbox") - - line.add_raw_api_object(hitbox_raw_api_object) - 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 idle_ability(line: GenieGameEntityGroup) -> ForwardRef: """ @@ -4526,6 +4526,19 @@ def move_ability(line: GenieGameEntityGroup) -> ForwardRef: ability_raw_api_object.add_raw_member("modes", move_modes, "engine.ability.type.Move") + # Path type + path_type = dataset.pregen_nyan_objects["util.path.types.Land"].get_nyan_object() + restrictions = current_unit["terrain_restriction"].value + if restrictions in (0x00, 0x0C, 0x0E, 0x17): + # air units + path_type = dataset.pregen_nyan_objects["util.path.types.Air"].get_nyan_object() + + elif restrictions in (0x03, 0x0D, 0x0F): + # ships + path_type = dataset.pregen_nyan_objects["util.path.types.Water"].get_nyan_object() + + ability_raw_api_object.add_raw_member("path_type", path_type, "engine.ability.type.Move") + ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id()) return ability_forward_ref @@ -4619,6 +4632,10 @@ def move_projectile_ability(line: GenieGameEntityGroup, position: int = -1) -> F ] ability_raw_api_object.add_raw_member("modes", move_modes, "engine.ability.type.Move") + # Path type + path_type = dataset.pregen_nyan_objects["util.path.types.Air"].get_nyan_object() + ability_raw_api_object.add_raw_member("path_type", path_type, "engine.ability.type.Move") + ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id()) return ability_forward_ref @@ -4757,9 +4774,9 @@ def overlay_terrain_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref @ staticmethod - def passable_ability(line: GenieGameEntityGroup) -> ForwardRef: + def pathable_ability(line: GenieGameEntityGroup) -> ForwardRef: """ - Adds the Passable ability to a line. + Adds the Pathable ability to a line. :param line: Unit/Building line that gets the ability. :type line: ...dataformat.converter_object.ConverterObjectGroup @@ -4773,67 +4790,29 @@ def passable_ability(line: GenieGameEntityGroup) -> ForwardRef: game_entity_name = name_lookup_dict[current_unit_id][0] - ability_ref = f"{game_entity_name}.Passable" + ability_ref = f"{game_entity_name}.Pathable" ability_raw_api_object = RawAPIObject(ability_ref, - "Passable", + "Pathable", dataset.nyan_api_objects) - ability_raw_api_object.add_raw_parent("engine.ability.type.Passable") + ability_raw_api_object.add_raw_parent("engine.ability.type.Pathable") ability_location = ForwardRef(line, game_entity_name) ability_raw_api_object.set_location(ability_location) # Hitbox - hitbox_ref = f"{game_entity_name}.Hitbox.{game_entity_name}Hitbox" + hitbox_ref = f"{game_entity_name}.Collision.{game_entity_name}Hitbox" hitbox_forward_ref = ForwardRef(line, hitbox_ref) ability_raw_api_object.add_raw_member("hitbox", hitbox_forward_ref, - "engine.ability.type.Passable") - - # Passable mode - # ===================================================================================== - mode_name = f"{game_entity_name}.Passable.PassableMode" - mode_raw_api_object = RawAPIObject(mode_name, "PassableMode", dataset.nyan_api_objects) - mode_parent = "engine.util.passable_mode.type.Normal" - if isinstance(line, GenieStackBuildingGroup): - if line.is_gate(): - mode_parent = "engine.util.passable_mode.type.Gate" - - mode_raw_api_object.add_raw_parent(mode_parent) - mode_location = ForwardRef(line, ability_ref) - mode_raw_api_object.set_location(mode_location) + "engine.ability.type.Pathable") - # Allowed types - allowed_types = [ - dataset.pregen_nyan_objects["util.game_entity_type.types.Unit"].get_nyan_object(), - dataset.pregen_nyan_objects["util.game_entity_type.types.Building"].get_nyan_object(), - dataset.pregen_nyan_objects["util.game_entity_type.types.Projectile"].get_nyan_object() - ] - mode_raw_api_object.add_raw_member("allowed_types", - allowed_types, - "engine.util.passable_mode.PassableMode") - - # Blacklisted entities - mode_raw_api_object.add_raw_member("blacklisted_entities", - [], - "engine.util.passable_mode.PassableMode") - - if isinstance(line, GenieStackBuildingGroup): - if line.is_gate(): - # Let friendly and own units pass through gate - stances = [ - dataset.pregen_nyan_objects["util.diplomatic_stance.types.Friendly"].get_nyan_object( - ), - dataset.nyan_api_objects["engine.util.diplomatic_stance.type.Self"] - ] - mode_raw_api_object.add_raw_member("stances", - stances, - mode_parent) - - line.add_raw_api_object(mode_raw_api_object) - # ===================================================================================== - mode_forward_ref = ForwardRef(line, mode_name) - ability_raw_api_object.add_raw_member("mode", - mode_forward_ref, - "engine.ability.type.Passable") + # Costs + path_costs = { + dataset.pregen_nyan_objects["util.path.types.Land"]: 255, # impassable + dataset.pregen_nyan_objects["util.path.types.Water"]: 255, # impassable + } + ability_raw_api_object.add_raw_member("path_costs", + path_costs, + "engine.ability.type.Pathable") line.add_raw_api_object(ability_raw_api_object) diff --git a/openage/convert/processor/conversion/aoc/pregen_processor.py b/openage/convert/processor/conversion/aoc/pregen_processor.py index db92c0fa8a..48eb8b97e7 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-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-lines,too-many-locals,too-many-statements # @@ -47,6 +47,7 @@ def generate(cls, full_data_set: GenieObjectContainer) -> None: cls.generate_misc_effect_objects(full_data_set, pregen_converter_group) cls.generate_modifiers(full_data_set, pregen_converter_group) cls.generate_terrain_types(full_data_set, pregen_converter_group) + cls.generate_path_types(full_data_set, pregen_converter_group) cls.generate_resources(full_data_set, pregen_converter_group) cls.generate_death_condition(full_data_set, pregen_converter_group) @@ -1919,6 +1920,71 @@ def generate_terrain_types( pregen_converter_group.add_raw_api_object(type_raw_api_object) pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object}) + @staticmethod + def generate_path_types( + full_data_set: GenieObjectContainer, + pregen_converter_group: ConverterObjectGroup + ) -> None: + """ + Generate PathType objects. + + :param full_data_set: GenieObjectContainer instance that + contains all relevant data for the conversion + process. + :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer + :param pregen_converter_group: GenieObjectGroup instance that stores + pregenerated API objects for referencing with + ForwardRef + :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup + """ + pregen_nyan_objects = full_data_set.pregen_nyan_objects + api_objects = full_data_set.nyan_api_objects + + path_type_parent = "engine.util.path_type.PathType" + path_types_location = "data/util/path_type/" + + # ======================================================================= + # Land + # ======================================================================= + path_type_ref_in_modpack = "util.path.types.Land" + path_type_raw_api_object = RawAPIObject(path_type_ref_in_modpack, + "Land", + api_objects, + path_types_location) + path_type_raw_api_object.set_filename("types") + path_type_raw_api_object.add_raw_parent(path_type_parent) + + pregen_converter_group.add_raw_api_object(path_type_raw_api_object) + pregen_nyan_objects.update({path_type_ref_in_modpack: path_type_raw_api_object}) + + # ======================================================================= + # Water + # ======================================================================= + path_type_ref_in_modpack = "util.path.types.Water" + path_type_raw_api_object = RawAPIObject(path_type_ref_in_modpack, + "Water", + api_objects, + path_types_location) + path_type_raw_api_object.set_filename("types") + path_type_raw_api_object.add_raw_parent(path_type_parent) + + pregen_converter_group.add_raw_api_object(path_type_raw_api_object) + pregen_nyan_objects.update({path_type_ref_in_modpack: path_type_raw_api_object}) + + # ======================================================================= + # Air + # ======================================================================= + path_type_ref_in_modpack = "util.path.types.Air" + path_type_raw_api_object = RawAPIObject(path_type_ref_in_modpack, + "Air", + api_objects, + path_types_location) + path_type_raw_api_object.set_filename("types") + path_type_raw_api_object.add_raw_parent(path_type_parent) + + pregen_converter_group.add_raw_api_object(path_type_raw_api_object) + pregen_nyan_objects.update({path_type_ref_in_modpack: path_type_raw_api_object}) + @staticmethod def generate_resources( full_data_set: GenieObjectContainer, diff --git a/openage/convert/processor/conversion/ror/pregen_subprocessor.py b/openage/convert/processor/conversion/ror/pregen_subprocessor.py index 271110bedc..890ad1250b 100644 --- a/openage/convert/processor/conversion/ror/pregen_subprocessor.py +++ b/openage/convert/processor/conversion/ror/pregen_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals @@ -42,6 +42,7 @@ def generate(cls, full_data_set: GenieObjectContainer) -> None: # TODO: # cls._generate_modifiers(gamedata, pregen_converter_group) AoCPregenSubprocessor.generate_terrain_types(full_data_set, pregen_converter_group) + AoCPregenSubprocessor.generate_path_types(full_data_set, pregen_converter_group) AoCPregenSubprocessor.generate_resources(full_data_set, pregen_converter_group) cls.generate_death_condition(full_data_set, pregen_converter_group) diff --git a/openage/convert/processor/conversion/swgbcc/pregen_subprocessor.py b/openage/convert/processor/conversion/swgbcc/pregen_subprocessor.py index a9114648c7..fb96a60698 100644 --- a/openage/convert/processor/conversion/swgbcc/pregen_subprocessor.py +++ b/openage/convert/processor/conversion/swgbcc/pregen_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals,too-many-statements # @@ -49,6 +49,7 @@ def generate(cls, full_data_set: GenieObjectContainer) -> None: AoCPregenSubprocessor.generate_misc_effect_objects(full_data_set, pregen_converter_group) # cls._generate_modifiers(gamedata, pregen_converter_group) ?? # cls._generate_terrain_types(gamedata, pregen_converter_group) TODO: Create terrain types + AoCPregenSubprocessor.generate_path_types(full_data_set, pregen_converter_group) cls.generate_resources(full_data_set, pregen_converter_group) AoCPregenSubprocessor.generate_death_condition(full_data_set, pregen_converter_group) From 9f69113e3455b54d48ef19ed6ec02c36225f94cc Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 May 2024 04:05:34 +0200 Subject: [PATCH 446/771] convert: Update Genie terrain restrictions definitions. --- .../read/media/datfile/lookup_dicts.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/openage/convert/value_object/read/media/datfile/lookup_dicts.py b/openage/convert/value_object/read/media/datfile/lookup_dicts.py index df0ba23d05..80b0dd5955 100644 --- a/openage/convert/value_object/read/media/datfile/lookup_dicts.py +++ b/openage/convert/value_object/read/media/datfile/lookup_dicts.py @@ -1,4 +1,4 @@ -# Copyright 2021-2023 the openage authors. See copying.md for legal info. +# Copyright 2021-2024 the openage authors. See copying.md for legal info. """ Lookup dicts for the EnumLookupMember instances. @@ -592,28 +592,28 @@ TERRAIN_RESTRICTIONS = { -0x01: "NONE", - 0x00: "ANY", - 0x01: "SHORELINE", - 0x02: "WATER", - 0x03: "WATER_SHIP_0x03", - 0x04: "FOUNDATION", + 0x00: "ANY", # projectiles + 0x01: "SHORELINE", # boar, deer, wolf + 0x02: "WATER", # unused in AoC + 0x03: "WATER_SHIP_0x03", # warships + 0x04: "FOUNDATION", # buildings 0x05: "NOWHERE", # can't place anywhere 0x06: "WATER_DOCK", # shallow water for dock placement - 0x07: "SOLID", - 0x08: "NO_ICE_0x08", + 0x07: "SOLID", # moving land units + 0x08: "NO_ICE_0x08", # resource piles (gold, stone, berries) 0x09: "SWGB_ONLY_WATER0", - 0x0A: "NO_ICE_0x0A", - 0x0B: "FOREST", - 0x0C: "UNKNOWN_0x0C", - 0x0D: "WATER_0x0D", # great fish - 0x0E: "UNKNOWN_0x0E", - 0x0F: "WATER_SHIP_0x0F", # transport ship + 0x0A: "NO_ICE_0x0A", # gate, palisades, walls + 0x0B: "FOREST", # trees + 0x0C: "UNKNOWN_0x0C", # projectile explosions on bridge? + 0x0D: "WATER_0x0D", # great fish, fishtrap, fishing ship + 0x0E: "UNKNOWN_0x0E", # projectile decay on bridge? + 0x0F: "WATER_SHIP_0x0F", # transport ship, longboat 0x10: "GRASS_SHORELINE", # for gates and walls 0x11: "WATER_ANY_0x11", - 0x12: "UNKNOWN_0x12", - 0x13: "FISH_NO_ICE", - 0x14: "WATER_ANY_0x14", - 0x15: "WATER_SHALLOW", + 0x12: "UNKNOWN_0x12", # projectile decay on bridge? + 0x13: "WATER_ICE", # small fish + 0x14: "NO_WATER", # siege units, trade cart + 0x15: "WATER_SHALLOW", # sea walls 0x16: "SWGB_GRASS_SHORE", 0x17: "SWGB_ANY", 0x18: "SWGB_ONLY_WATER1", From 1249cc5608b4979a10068eb930534f683951289b Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 May 2024 04:07:01 +0200 Subject: [PATCH 447/771] convert: Update ability generation with new path types. --- .../conversion/aoc/modpack_subprocessor.py | 4 ++-- .../conversion/aoc/nyan_subprocessor.py | 19 +++++++++---------- .../conversion/de2/nyan_subprocessor.py | 13 ++++++------- .../conversion/ror/nyan_subprocessor.py | 12 ++++++------ .../conversion/swgbcc/ability_subprocessor.py | 8 ++++---- .../conversion/swgbcc/nyan_subprocessor.py | 19 +++++++++---------- 6 files changed, 36 insertions(+), 39 deletions(-) diff --git a/openage/convert/processor/conversion/aoc/modpack_subprocessor.py b/openage/convert/processor/conversion/aoc/modpack_subprocessor.py index 1c823c3ba6..ed5d0a5727 100644 --- a/openage/convert/processor/conversion/aoc/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/modpack_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals,too-many-branches,too-few-public-methods,too-many-statements @@ -198,7 +198,7 @@ def _set_static_aliases(modpack: Modpack, import_tree: ImportTree) -> None: import_tree.add_alias(("engine", "util", "logic", "literal_scope", "type"), "literal_scope") import_tree.add_alias(("engine", "util", "patch"), "patch") import_tree.add_alias(("engine", "util", "patch", "property", "type"), "patch_prop") - import_tree.add_alias(("engine", "util", "passable_mode", "type"), "passable_mode") + import_tree.add_alias(("engine", "util", "path_type"), "path_type") import_tree.add_alias(("engine", "util", "payment_mode", "type"), "payment_mode") import_tree.add_alias(("engine", "util", "placement_mode", "type"), "placement_mode") import_tree.add_alias(("engine", "util", "price_mode", "type"), "price_mode") diff --git a/openage/convert/processor/conversion/aoc/nyan_subprocessor.py b/openage/convert/processor/conversion/aoc/nyan_subprocessor.py index d280b5d9c1..3402938bde 100644 --- a/openage/convert/processor/conversion/aoc/nyan_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/nyan_subprocessor.py @@ -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. # # pylint: disable=too-many-lines,too-many-locals,too-many-statements,too-many-branches # @@ -14,7 +14,7 @@ from ....entity_object.conversion.aoc.genie_tech import UnitLineUpgrade from ....entity_object.conversion.aoc.genie_unit import GenieGarrisonMode, \ - GenieMonkGroup, GenieStackBuildingGroup + GenieMonkGroup from ....entity_object.conversion.aoc.genie_unit import GenieVillagerGroup from ....entity_object.conversion.combined_terrain import CombinedTerrain from ....entity_object.conversion.converter_object import RawAPIObject @@ -224,7 +224,7 @@ def unit_line_to_game_entity(unit_line: GenieUnitLineGroup) -> None: abilities_set.append(AoCAbilitySubprocessor.delete_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.despawn_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.idle_ability(unit_line)) - abilities_set.append(AoCAbilitySubprocessor.hitbox_ability(unit_line)) + abilities_set.append(AoCAbilitySubprocessor.collision_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.live_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.los_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.move_ability(unit_line)) @@ -436,7 +436,7 @@ def building_line_to_game_entity(building_line: GenieBuildingLineGroup) -> None: abilities_set.append(AoCAbilitySubprocessor.delete_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.despawn_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.idle_ability(building_line)) - abilities_set.append(AoCAbilitySubprocessor.hitbox_ability(building_line)) + abilities_set.append(AoCAbilitySubprocessor.collision_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.live_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.los_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.named_ability(building_line)) @@ -450,9 +450,8 @@ def building_line_to_game_entity(building_line: GenieBuildingLineGroup) -> None: if building_line.is_creatable(): abilities_set.append(AoCAbilitySubprocessor.constructable_ability(building_line)) - if building_line.is_passable() or\ - (isinstance(building_line, GenieStackBuildingGroup) and building_line.is_gate()): - abilities_set.append(AoCAbilitySubprocessor.passable_ability(building_line)) + if not building_line.is_passable(): + abilities_set.append(AoCAbilitySubprocessor.pathable_ability(building_line)) if building_line.has_foundation(): if building_line.get_class_id() == 49: @@ -586,7 +585,7 @@ def ambient_group_to_game_entity(ambient_group: GenieAmbientGroup) -> None: if interaction_mode >= 0: abilities_set.append(AoCAbilitySubprocessor.death_ability(ambient_group)) - abilities_set.append(AoCAbilitySubprocessor.hitbox_ability(ambient_group)) + abilities_set.append(AoCAbilitySubprocessor.collision_ability(ambient_group)) abilities_set.append(AoCAbilitySubprocessor.idle_ability(ambient_group)) abilities_set.append(AoCAbilitySubprocessor.live_ability(ambient_group)) abilities_set.append(AoCAbilitySubprocessor.named_ability(ambient_group)) @@ -597,8 +596,8 @@ def ambient_group_to_game_entity(ambient_group: GenieAmbientGroup) -> None: if interaction_mode >= 2: abilities_set.extend(AoCAbilitySubprocessor.selectable_ability(ambient_group)) - if ambient_group.is_passable(): - abilities_set.append(AoCAbilitySubprocessor.passable_ability(ambient_group)) + if not ambient_group.is_passable(): + abilities_set.append(AoCAbilitySubprocessor.pathable_ability(ambient_group)) if ambient_group.is_harvestable(): abilities_set.append(AoCAbilitySubprocessor.harvestable_ability(ambient_group)) diff --git a/openage/convert/processor/conversion/de2/nyan_subprocessor.py b/openage/convert/processor/conversion/de2/nyan_subprocessor.py index 242f67e7cd..c6f779dc4b 100644 --- a/openage/convert/processor/conversion/de2/nyan_subprocessor.py +++ b/openage/convert/processor/conversion/de2/nyan_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-lines,too-many-locals,too-many-statements,too-many-branches # @@ -13,7 +13,7 @@ from ....entity_object.conversion.aoc.genie_tech import UnitLineUpgrade from ....entity_object.conversion.aoc.genie_unit import GenieVillagerGroup, \ - GenieGarrisonMode, GenieMonkGroup, GenieStackBuildingGroup + GenieGarrisonMode, GenieMonkGroup from ....entity_object.conversion.combined_terrain import CombinedTerrain from ....entity_object.conversion.converter_object import RawAPIObject from ....service.conversion import internal_name_lookups @@ -224,7 +224,7 @@ def unit_line_to_game_entity(unit_line: GenieUnitLineGroup) -> None: abilities_set.append(AoCAbilitySubprocessor.delete_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.despawn_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.idle_ability(unit_line)) - abilities_set.append(AoCAbilitySubprocessor.hitbox_ability(unit_line)) + abilities_set.append(AoCAbilitySubprocessor.collision_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.live_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.los_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.move_ability(unit_line)) @@ -436,7 +436,7 @@ def building_line_to_game_entity(building_line: GenieBuildingLineGroup) -> None: abilities_set.append(AoCAbilitySubprocessor.delete_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.despawn_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.idle_ability(building_line)) - abilities_set.append(AoCAbilitySubprocessor.hitbox_ability(building_line)) + abilities_set.append(AoCAbilitySubprocessor.collision_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.live_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.los_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.named_ability(building_line)) @@ -450,9 +450,8 @@ def building_line_to_game_entity(building_line: GenieBuildingLineGroup) -> None: if building_line.is_creatable(): abilities_set.append(AoCAbilitySubprocessor.constructable_ability(building_line)) - if building_line.is_passable() or\ - (isinstance(building_line, GenieStackBuildingGroup) and building_line.is_gate()): - abilities_set.append(AoCAbilitySubprocessor.passable_ability(building_line)) + if not building_line.is_passable(): + abilities_set.append(AoCAbilitySubprocessor.pathable_ability(building_line)) if building_line.has_foundation(): if building_line.get_class_id() == 49: diff --git a/openage/convert/processor/conversion/ror/nyan_subprocessor.py b/openage/convert/processor/conversion/ror/nyan_subprocessor.py index a06a86e1dc..5286c19f9e 100644 --- a/openage/convert/processor/conversion/ror/nyan_subprocessor.py +++ b/openage/convert/processor/conversion/ror/nyan_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-lines,too-many-locals,too-many-statements,too-many-branches # @@ -214,7 +214,7 @@ def unit_line_to_game_entity(unit_line): abilities_set.append(AoCAbilitySubprocessor.delete_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.despawn_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.idle_ability(unit_line)) - abilities_set.append(AoCAbilitySubprocessor.hitbox_ability(unit_line)) + abilities_set.append(AoCAbilitySubprocessor.collision_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.live_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.los_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.move_ability(unit_line)) @@ -406,7 +406,7 @@ def building_line_to_game_entity(building_line): abilities_set.append(AoCAbilitySubprocessor.delete_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.despawn_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.idle_ability(building_line)) - abilities_set.append(AoCAbilitySubprocessor.hitbox_ability(building_line)) + abilities_set.append(AoCAbilitySubprocessor.collision_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.live_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.los_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.named_ability(building_line)) @@ -527,7 +527,7 @@ def ambient_group_to_game_entity(ambient_group): if interaction_mode >= 0: abilities_set.append(AoCAbilitySubprocessor.death_ability(ambient_group)) - abilities_set.append(AoCAbilitySubprocessor.hitbox_ability(ambient_group)) + abilities_set.append(AoCAbilitySubprocessor.collision_ability(ambient_group)) abilities_set.append(AoCAbilitySubprocessor.idle_ability(ambient_group)) abilities_set.append(AoCAbilitySubprocessor.live_ability(ambient_group)) abilities_set.append(AoCAbilitySubprocessor.named_ability(ambient_group)) @@ -538,8 +538,8 @@ def ambient_group_to_game_entity(ambient_group): if interaction_mode >= 2: abilities_set.extend(AoCAbilitySubprocessor.selectable_ability(ambient_group)) - if ambient_group.is_passable(): - abilities_set.append(AoCAbilitySubprocessor.passable_ability(ambient_group)) + if not ambient_group.is_passable(): + abilities_set.append(AoCAbilitySubprocessor.pathable_ability(ambient_group)) if ambient_group.is_harvestable(): abilities_set.append(AoCAbilitySubprocessor.harvestable_ability(ambient_group)) diff --git a/openage/convert/processor/conversion/swgbcc/ability_subprocessor.py b/openage/convert/processor/conversion/swgbcc/ability_subprocessor.py index aacdf8df6c..07ca29004a 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-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 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 @@ -1225,16 +1225,16 @@ def harvestable_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref @staticmethod - def hitbox_ability(line: GenieGameEntityGroup) -> ForwardRef: + def collision_ability(line: GenieGameEntityGroup) -> ForwardRef: """ - Adds the Hitbox ability to a line. + Adds the Collision 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 """ - ability_forward_ref = AoCAbilitySubprocessor.hitbox_ability(line) + ability_forward_ref = AoCAbilitySubprocessor.collision_ability(line) # TODO: Implement diffing of civ lines diff --git a/openage/convert/processor/conversion/swgbcc/nyan_subprocessor.py b/openage/convert/processor/conversion/swgbcc/nyan_subprocessor.py index 9fd2c52242..b014629619 100644 --- a/openage/convert/processor/conversion/swgbcc/nyan_subprocessor.py +++ b/openage/convert/processor/conversion/swgbcc/nyan_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-lines,too-many-locals,too-many-statements,too-many-branches # @@ -14,7 +14,7 @@ from ....entity_object.conversion.aoc.genie_tech import UnitLineUpgrade from ....entity_object.conversion.aoc.genie_unit import GenieVillagerGroup, \ - GenieStackBuildingGroup, GenieGarrisonMode, GenieMonkGroup + GenieGarrisonMode, GenieMonkGroup from ....entity_object.conversion.converter_object import RawAPIObject from ....service.conversion import internal_name_lookups from ....value_object.conversion.forward_ref import ForwardRef @@ -222,7 +222,7 @@ def unit_line_to_game_entity(unit_line: GenieUnitLineGroup) -> None: abilities_set.append(AoCAbilitySubprocessor.delete_ability(unit_line)) abilities_set.append(AoCAbilitySubprocessor.despawn_ability(unit_line)) abilities_set.append(SWGBCCAbilitySubprocessor.idle_ability(unit_line)) - abilities_set.append(SWGBCCAbilitySubprocessor.hitbox_ability(unit_line)) + abilities_set.append(SWGBCCAbilitySubprocessor.collision_ability(unit_line)) abilities_set.append(SWGBCCAbilitySubprocessor.live_ability(unit_line)) abilities_set.append(SWGBCCAbilitySubprocessor.los_ability(unit_line)) abilities_set.append(SWGBCCAbilitySubprocessor.move_ability(unit_line)) @@ -432,7 +432,7 @@ def building_line_to_game_entity(building_line: GenieBuildingLineGroup) -> None: abilities_set.append(AoCAbilitySubprocessor.delete_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.despawn_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.idle_ability(building_line)) - abilities_set.append(AoCAbilitySubprocessor.hitbox_ability(building_line)) + abilities_set.append(AoCAbilitySubprocessor.collision_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.live_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.los_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.named_ability(building_line)) @@ -446,9 +446,8 @@ def building_line_to_game_entity(building_line: GenieBuildingLineGroup) -> None: # if building_line.is_creatable(): # abilities_set.append(SWGBCCAbilitySubprocessor.constructable_ability(building_line)) - if building_line.is_passable() or\ - (isinstance(building_line, GenieStackBuildingGroup) and building_line.is_gate()): - abilities_set.append(AoCAbilitySubprocessor.passable_ability(building_line)) + if not building_line.is_passable(): + abilities_set.append(AoCAbilitySubprocessor.pathable_ability(building_line)) if building_line.has_foundation(): if building_line.get_class_id() == 7: @@ -583,7 +582,7 @@ def ambient_group_to_game_entity(ambient_group: GenieAmbientGroup) -> None: if interaction_mode >= 0: abilities_set.append(AoCAbilitySubprocessor.death_ability(ambient_group)) - abilities_set.append(AoCAbilitySubprocessor.hitbox_ability(ambient_group)) + abilities_set.append(AoCAbilitySubprocessor.collision_ability(ambient_group)) abilities_set.append(AoCAbilitySubprocessor.idle_ability(ambient_group)) abilities_set.append(AoCAbilitySubprocessor.live_ability(ambient_group)) abilities_set.append(AoCAbilitySubprocessor.named_ability(ambient_group)) @@ -594,8 +593,8 @@ def ambient_group_to_game_entity(ambient_group: GenieAmbientGroup) -> None: if interaction_mode >= 2: abilities_set.extend(AoCAbilitySubprocessor.selectable_ability(ambient_group)) - if ambient_group.is_passable(): - abilities_set.append(AoCAbilitySubprocessor.passable_ability(ambient_group)) + if not ambient_group.is_passable(): + abilities_set.append(AoCAbilitySubprocessor.pathable_ability(ambient_group)) if ambient_group.is_harvestable(): abilities_set.append(SWGBCCAbilitySubprocessor.harvestable_ability(ambient_group)) From f10b1a3e4a49fd21caaa89031b5cc749710324a2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 May 2024 04:46:56 +0200 Subject: [PATCH 448/771] convert: Add Genie object for terrain restrictions. --- .../conversion/aoc/genie_terrain.py | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/openage/convert/entity_object/conversion/aoc/genie_terrain.py b/openage/convert/entity_object/conversion/aoc/genie_terrain.py index a361c754db..55c76f9f75 100644 --- a/openage/convert/entity_object/conversion/aoc/genie_terrain.py +++ b/openage/convert/entity_object/conversion/aoc/genie_terrain.py @@ -1,4 +1,4 @@ -# Copyright 2019-2022 the openage authors. See copying.md for legal info. +# Copyright 2019-2024 the openage authors. See copying.md for legal info. """ Contains structures and API-like objects for terrain from AoC. @@ -94,3 +94,42 @@ def get_terrain(self) -> GenieTerrainObject: def __repr__(self): return f"GenieTerrainGroup<{self.get_id()}>" + + +class GenieTerrainRestriction(ConverterObject): + """ + Terrain restriction definition from a .dat file. + """ + + __slots__ = ('data',) + + def __init__( + self, + restriction_id: int, + full_data_set: GenieObjectContainer, + members: dict[str, ValueMember] = None + ): + """ + Creates a new Genie terrain restriction object. + + :param restriction_id: The index of the terrain restriction in the .dat file. + :param full_data_set: GenieObjectContainer instance that + contains all relevant data for the conversion + process. + """ + super().__init__(restriction_id, members=members) + + self.data = full_data_set + + def is_accessible(self, terrain_index: int) -> bool: + """ + Checks if a terrain is accessible by this restriction. + + :param terrain_index: Index of the terrain. + """ + multiplier = self.members["accessible_dmgmultiplier"][terrain_index].value + + return multiplier > 0 + + def __repr__(self): + return f"GenieTerrainRestriction<{self.get_id()}>" From e8726b37db58ad5b9268b349a0e5c1037aa4c4de Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 May 2024 05:06:18 +0200 Subject: [PATCH 449/771] convert: Process restrictions to converter object. --- .../conversion/aoc/genie_object_container.py | 5 +-- .../conversion/aoc/ability_subprocessor.py | 4 +-- .../processor/conversion/aoc/processor.py | 33 +++++++++++++++---- .../processor/conversion/de1/processor.py | 3 +- .../processor/conversion/de2/processor.py | 3 +- .../processor/conversion/hd/processor.py | 3 +- .../processor/conversion/ror/processor.py | 3 +- .../processor/conversion/swgbcc/processor.py | 3 +- 8 files changed, 42 insertions(+), 15 deletions(-) diff --git a/openage/convert/entity_object/conversion/aoc/genie_object_container.py b/openage/convert/entity_object/conversion/aoc/genie_object_container.py index c4a04376b9..00ce6cd79e 100644 --- a/openage/convert/entity_object/conversion/aoc/genie_object_container.py +++ b/openage/convert/entity_object/conversion/aoc/genie_object_container.py @@ -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. # # pylint: disable=too-many-instance-attributes,too-few-public-methods @@ -28,7 +28,7 @@ AgeUpgrade, BuildingLineUpgrade, BuildingUnlock, CivBonus, GenieTechEffectBundleGroup, \ InitiatedTech, StatUpgrade, UnitLineUpgrade, UnitUnlock from openage.convert.entity_object.conversion.aoc.genie_terrain import GenieTerrainObject, \ - GenieTerrainGroup + GenieTerrainGroup, GenieTerrainRestriction from openage.convert.entity_object.conversion.aoc.genie_unit import GenieUnitObject, \ GenieAmbientGroup, GenieBuildingLineGroup, GenieMonkGroup, GenieUnitLineGroup, \ GenieUnitTaskGroup, GenieUnitTransformGroup, GenieVariantGroup, GenieVillagerGroup, \ @@ -75,6 +75,7 @@ def __init__(self): self.genie_graphics: dict[int, GenieGraphic] = {} self.genie_sounds: dict[int, GenieSound] = {} self.genie_terrains: dict[int, GenieTerrainObject] = {} + self.genie_terrain_restrictions: dict[int, GenieTerrainRestriction] = {} # Phase 2: API-like objects # ConverterObjectGroup types (things that will become diff --git a/openage/convert/processor/conversion/aoc/ability_subprocessor.py b/openage/convert/processor/conversion/aoc/ability_subprocessor.py index 867665b3a2..120541fe4c 100644 --- a/openage/convert/processor/conversion/aoc/ability_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/ability_subprocessor.py @@ -4807,8 +4807,8 @@ def pathable_ability(line: GenieGameEntityGroup) -> ForwardRef: # Costs path_costs = { - dataset.pregen_nyan_objects["util.path.types.Land"]: 255, # impassable - dataset.pregen_nyan_objects["util.path.types.Water"]: 255, # impassable + dataset.pregen_nyan_objects["util.path.types.Land"].get_nyan_object(): 255, # impassable + dataset.pregen_nyan_objects["util.path.types.Water"].get_nyan_object(): 255, # impassable } ability_raw_api_object.add_raw_member("path_costs", path_costs, diff --git a/openage/convert/processor/conversion/aoc/processor.py b/openage/convert/processor/conversion/aoc/processor.py index 6f03cc3421..718ab8af01 100644 --- a/openage/convert/processor/conversion/aoc/processor.py +++ b/openage/convert/processor/conversion/aoc/processor.py @@ -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. # # pylint: disable=too-many-lines,too-many-branches,too-many-statements # pylint: disable=too-many-locals,too-many-public-methods @@ -25,8 +25,8 @@ from ....entity_object.conversion.aoc.genie_tech import GenieTechObject from ....entity_object.conversion.aoc.genie_tech import StatUpgrade, InitiatedTech, \ BuildingUnlock -from ....entity_object.conversion.aoc.genie_terrain import GenieTerrainGroup -from ....entity_object.conversion.aoc.genie_terrain import GenieTerrainObject +from ....entity_object.conversion.aoc.genie_terrain import GenieTerrainGroup, \ + GenieTerrainObject, GenieTerrainRestriction from ....entity_object.conversion.aoc.genie_unit import GenieAmbientGroup, \ GenieGarrisonMode from ....entity_object.conversion.aoc.genie_unit import GenieStackBuildingGroup, \ @@ -134,6 +134,7 @@ def _pre_processor( cls.extract_genie_graphics(gamespec, dataset) cls.extract_genie_sounds(gamespec, dataset) cls.extract_genie_terrains(gamespec, dataset) + cls.extract_genie_restrictions(gamespec, dataset) return dataset @@ -478,15 +479,35 @@ def extract_genie_terrains(gamespec: ArrayMember, full_data_set: GenieObjectCont # call hierarchy: wrapper[0]->terrains raw_terrains = gamespec[0]["terrains"].value - index = 0 - for raw_terrain in raw_terrains: + for index, raw_terrain in enumerate(raw_terrains): terrain_index = index terrain_members = raw_terrain.value terrain = GenieTerrainObject(terrain_index, full_data_set, members=terrain_members) full_data_set.genie_terrains.update({terrain.get_id(): terrain}) - index += 1 + @staticmethod + def extract_genie_restrictions( + gamespec: ArrayMember, + full_data_set: GenieObjectContainer + ) -> None: + """ + Extract terrain restrictions from the game data. + + :param gamespec: Gamedata from empires.dat file. + :type gamespec: class: ...dataformat.value_members.ArrayMember + """ + # call hierarchy: wrapper[0]->terrains + raw_restrictions = gamespec[0]["terrain_restrictions"].value + + for index, raw_restriction in enumerate(raw_restrictions): + restriction_index = index + restriction_members = raw_restriction.value + + restriction = GenieTerrainRestriction(restriction_index, + full_data_set, + members=restriction_members) + full_data_set.genie_terrain_restrictions.update({restriction.get_id(): restriction}) @staticmethod def create_unit_lines(full_data_set: GenieObjectContainer) -> None: diff --git a/openage/convert/processor/conversion/de1/processor.py b/openage/convert/processor/conversion/de1/processor.py index c505c68467..cb3f54cc91 100644 --- a/openage/convert/processor/conversion/de1/processor.py +++ b/openage/convert/processor/conversion/de1/processor.py @@ -1,4 +1,4 @@ -# Copyright 2021-2023 the openage authors. See copying.md for legal info. +# Copyright 2021-2024 the openage authors. See copying.md for legal info. """ Convert data from DE1 to openage formats. @@ -107,6 +107,7 @@ def _pre_processor( cls.extract_genie_graphics(gamespec, dataset) RoRProcessor.extract_genie_sounds(gamespec, dataset) AoCProcessor.extract_genie_terrains(gamespec, dataset) + AoCProcessor.extract_genie_restrictions(gamespec, dataset) return dataset diff --git a/openage/convert/processor/conversion/de2/processor.py b/openage/convert/processor/conversion/de2/processor.py index eea3ebd346..d78fb218d9 100644 --- a/openage/convert/processor/conversion/de2/processor.py +++ b/openage/convert/processor/conversion/de2/processor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=line-too-long,too-many-lines,too-many-branches,too-many-statements """ @@ -113,6 +113,7 @@ def _pre_processor( cls.extract_genie_graphics(gamespec, dataset) AoCProcessor.extract_genie_sounds(gamespec, dataset) AoCProcessor.extract_genie_terrains(gamespec, dataset) + AoCProcessor.extract_genie_restrictions(gamespec, dataset) return dataset diff --git a/openage/convert/processor/conversion/hd/processor.py b/openage/convert/processor/conversion/hd/processor.py index 7379309f57..00835c7b1f 100644 --- a/openage/convert/processor/conversion/hd/processor.py +++ b/openage/convert/processor/conversion/hd/processor.py @@ -1,4 +1,4 @@ -# Copyright 2021-2023 the openage authors. See copying.md for legal info. +# Copyright 2021-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-few-public-methods @@ -107,6 +107,7 @@ def _pre_processor( AoCProcessor.extract_genie_graphics(gamespec, dataset) AoCProcessor.extract_genie_sounds(gamespec, dataset) AoCProcessor.extract_genie_terrains(gamespec, dataset) + AoCProcessor.extract_genie_restrictions(gamespec, dataset) return dataset diff --git a/openage/convert/processor/conversion/ror/processor.py b/openage/convert/processor/conversion/ror/processor.py index e056d934b6..d6220753fa 100644 --- a/openage/convert/processor/conversion/ror/processor.py +++ b/openage/convert/processor/conversion/ror/processor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=line-too-long,too-many-lines,too-many-branches,too-many-statements,too-many-locals """ @@ -116,6 +116,7 @@ def _pre_processor( AoCProcessor.extract_genie_graphics(gamespec, dataset) cls.extract_genie_sounds(gamespec, dataset) AoCProcessor.extract_genie_terrains(gamespec, dataset) + AoCProcessor.extract_genie_restrictions(gamespec, dataset) return dataset diff --git a/openage/convert/processor/conversion/swgbcc/processor.py b/openage/convert/processor/conversion/swgbcc/processor.py index 2fef37ffa2..8fbb9d97ae 100644 --- a/openage/convert/processor/conversion/swgbcc/processor.py +++ b/openage/convert/processor/conversion/swgbcc/processor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-lines,too-many-branches,too-many-statements,too-many-locals # @@ -126,6 +126,7 @@ def _pre_processor( AoCProcessor.extract_genie_graphics(gamespec, dataset) AoCProcessor.extract_genie_sounds(gamespec, dataset) AoCProcessor.extract_genie_terrains(gamespec, dataset) + AoCProcessor.extract_genie_restrictions(gamespec, dataset) return dataset From 0c53852c7d5a4cb4d7db0bd4153b047dc47acda1 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 May 2024 05:07:02 +0200 Subject: [PATCH 450/771] convert: Get path costs from terrain restrictions. --- .../conversion/aoc/nyan_subprocessor.py | 30 +++++++++++++++++++ .../conversion/de2/nyan_subprocessor.py | 30 +++++++++++++++++++ .../conversion/ror/nyan_subprocessor.py | 30 +++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/openage/convert/processor/conversion/aoc/nyan_subprocessor.py b/openage/convert/processor/conversion/aoc/nyan_subprocessor.py index 3402938bde..7afbd38134 100644 --- a/openage/convert/processor/conversion/aoc/nyan_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/nyan_subprocessor.py @@ -1033,6 +1033,36 @@ def terrain_group_to_terrain(terrain_group: GenieTerrainGroup) -> None: raw_api_object.add_raw_member("ambience", ambience, "engine.util.terrain.Terrain") + # ======================================================================= + # Path Costs + # ======================================================================= + path_costs = {} + restrictions = dataset.genie_terrain_restrictions + + # Land grid + path_type = dataset.pregen_nyan_objects["util.path.types.Land"].get_nyan_object() + land_restrictions = restrictions[0x07] + if land_restrictions.is_accessible(terrain_index): + path_costs[path_type] = 1 + + else: + path_costs[path_type] = 255 + + # Water grid + path_type = dataset.pregen_nyan_objects["util.path.types.Water"].get_nyan_object() + water_restrictions = restrictions[0x03] + if water_restrictions.is_accessible(terrain_index): + path_costs[path_type] = 1 + + else: + path_costs[path_type] = 255 + + # Air grid (default accessible) + path_type = dataset.pregen_nyan_objects["util.path.types.Air"].get_nyan_object() + path_costs[path_type] = 1 + + raw_api_object.add_raw_member("path_costs", path_costs, "engine.util.terrain.Terrain") + # ======================================================================= # Graphic # ======================================================================= diff --git a/openage/convert/processor/conversion/de2/nyan_subprocessor.py b/openage/convert/processor/conversion/de2/nyan_subprocessor.py index c6f779dc4b..4474751136 100644 --- a/openage/convert/processor/conversion/de2/nyan_subprocessor.py +++ b/openage/convert/processor/conversion/de2/nyan_subprocessor.py @@ -783,6 +783,36 @@ def terrain_group_to_terrain(terrain_group: GenieTerrainGroup) -> None: raw_api_object.add_raw_member("ambience", ambience, "engine.util.terrain.Terrain") + # ======================================================================= + # Path Costs + # ======================================================================= + path_costs = {} + restrictions = dataset.genie_terrain_restrictions + + # Land grid + path_type = dataset.pregen_nyan_objects["util.path.types.Land"].get_nyan_object() + land_restrictions = restrictions[0x07] + if land_restrictions.is_accessible(terrain_index): + path_costs[path_type] = 1 + + else: + path_costs[path_type] = 255 + + # Water grid + path_type = dataset.pregen_nyan_objects["util.path.types.Water"].get_nyan_object() + water_restrictions = restrictions[0x03] + if water_restrictions.is_accessible(terrain_index): + path_costs[path_type] = 1 + + else: + path_costs[path_type] = 255 + + # Air grid (default accessible) + path_type = dataset.pregen_nyan_objects["util.path.types.Air"].get_nyan_object() + path_costs[path_type] = 1 + + raw_api_object.add_raw_member("path_costs", path_costs, "engine.util.terrain.Terrain") + # ======================================================================= # Graphic # ======================================================================= diff --git a/openage/convert/processor/conversion/ror/nyan_subprocessor.py b/openage/convert/processor/conversion/ror/nyan_subprocessor.py index 5286c19f9e..c3f6b09706 100644 --- a/openage/convert/processor/conversion/ror/nyan_subprocessor.py +++ b/openage/convert/processor/conversion/ror/nyan_subprocessor.py @@ -813,6 +813,36 @@ def terrain_group_to_terrain(terrain_group): raw_api_object.add_raw_member("ambience", ambience, "engine.util.terrain.Terrain") + # ======================================================================= + # Path Costs + # ======================================================================= + path_costs = {} + restrictions = dataset.genie_terrain_restrictions + + # Land grid + path_type = dataset.pregen_nyan_objects["util.path.types.Land"].get_nyan_object() + land_restrictions = restrictions[0x07] + if land_restrictions.is_accessible(terrain_index): + path_costs[path_type] = 1 + + else: + path_costs[path_type] = 255 + + # Water grid + path_type = dataset.pregen_nyan_objects["util.path.types.Water"].get_nyan_object() + water_restrictions = restrictions[0x03] + if water_restrictions.is_accessible(terrain_index): + path_costs[path_type] = 1 + + else: + path_costs[path_type] = 255 + + # Air grid (default accessible) + path_type = dataset.pregen_nyan_objects["util.path.types.Air"].get_nyan_object() + path_costs[path_type] = 1 + + raw_api_object.add_raw_member("path_costs", path_costs, "engine.util.terrain.Terrain") + # ======================================================================= # Graphic # ======================================================================= From 9126f55bda0fad702cda89f4d1a8dae786688310 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 May 2024 05:38:12 +0200 Subject: [PATCH 451/771] convert: Add alias for pregen path types. --- .../convert/processor/conversion/aoc/modpack_subprocessor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openage/convert/processor/conversion/aoc/modpack_subprocessor.py b/openage/convert/processor/conversion/aoc/modpack_subprocessor.py index ed5d0a5727..4f7628751e 100644 --- a/openage/convert/processor/conversion/aoc/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/modpack_subprocessor.py @@ -323,6 +323,10 @@ def _set_static_aliases(modpack: Modpack, import_tree: ImportTree) -> None: "garrison_empty"), "empty_garrison_condition" ) + import_tree.add_alias( + (modpack.name, "data", "util", "path_type", "types"), + prefix + "path_type" + ) import_tree.add_alias( (modpack.name, "data", "util", "resource", "market_trading"), prefix + "market_trading" From e599b1fac465dc6b2917997d076fbed6446505ad Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 May 2024 06:20:34 +0200 Subject: [PATCH 452/771] convert: Bump required nyan API version. --- openage/convert/service/init/api_export_required.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openage/convert/service/init/api_export_required.py b/openage/convert/service/init/api_export_required.py index 105527e176..90b78309d7 100644 --- a/openage/convert/service/init/api_export_required.py +++ b/openage/convert/service/init/api_export_required.py @@ -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. """ Test whether the openage nyan API modpack is present. @@ -16,7 +16,7 @@ from openage.util.fslike.union import UnionPath -CURRENT_API_VERSION = "0.4.1" +CURRENT_API_VERSION = "0.5.0" def api_export_required(asset_dir: UnionPath) -> bool: From 125359fb72643526dc93eedc2545b506f434f4b5 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 21 Apr 2024 22:51:00 +0200 Subject: [PATCH 453/771] nyan: Add pathfinding types to UML. --- doc/nyan/aoe2_nyan_tree.uxf | 212 +++++++++++------------------------- 1 file changed, 64 insertions(+), 148 deletions(-) diff --git a/doc/nyan/aoe2_nyan_tree.uxf b/doc/nyan/aoe2_nyan_tree.uxf index 6c3bc18848..1de716591e 100644 --- a/doc/nyan/aoe2_nyan_tree.uxf +++ b/doc/nyan/aoe2_nyan_tree.uxf @@ -230,9 +230,9 @@ condition : set(LogicElement) UMLClass 1330 - 3730 + 3710 320 - 130 + 150 *Terrain* bg=pink @@ -242,13 +242,14 @@ name : TranslatedString types : set(TerrainType) terrain_graphic : Terrain sound : Sound -ambience : set(TerrainAmbient) +ambience : set(TerrainAmbient) +path_costs : dict(PathType, int) UMLClass - 1510 + 1530 3900 190 80 @@ -387,14 +388,15 @@ sounds : set(Sound) 4310 3840 180 - 80 + 100 *Move* bg=green -- speed : float -modes : set(MoveMode) +modes : set(MoveMode) +path_type : PathType @@ -557,7 +559,7 @@ changes : orderedset(Patch) Relation - 1540 + 1580 3850 30 70 @@ -582,10 +584,10 @@ changes : orderedset(Patch) 1470 3620 30 - 130 + 110 lt=- - 10.0;10.0;10.0;110.0 + 10.0;10.0;10.0;90.0 UMLClass @@ -1934,16 +1936,16 @@ foundation_terrain : Terrain UMLClass 5100 - 4310 + 4320 220 - 90 + 80 - *Passable* + *Pathable* bg=green -- hitbox : Hitbox -mode : PassableMode +path_costs : dict(PathType, int) @@ -2016,11 +2018,11 @@ long_description : TranslatedMarkupFile UMLClass 5100 - 4410 - 140 + 4420 + 220 80 - *Hitbox* + *Collision* bg=green -- @@ -3829,10 +3831,10 @@ bg=pink UMLClass - 1100 - 3980 + 1040 + 4100 270 - 120 + 130 *Tech* bg=pink @@ -3851,10 +3853,10 @@ updates : orderedset(Patch) 1230 3650 30 - 350 + 470 lt=<<- - 10.0;10.0;10.0;330.0 + 10.0;10.0;10.0;450.0 UMLClass @@ -4524,7 +4526,7 @@ ignore_containers : set(EntityContainer) Relation 5060 - 4440 + 4450 60 30 @@ -4612,7 +4614,7 @@ ignore_containers : set(EntityContainer) Relation 5060 - 4340 + 4350 60 30 @@ -5502,7 +5504,7 @@ blacklisted_entities : set(GameEntity) 230 30 - openage nyan data API v0.4.1 + openage nyan data API v0.5.0 @@ -7118,7 +7120,7 @@ bg=pink UMLClass - 1350 + 1400 3900 120 60 @@ -7131,7 +7133,7 @@ bg=pink Relation - 1400 + 1450 3850 30 70 @@ -7809,8 +7811,8 @@ bg=pink UMLClass - 5280 - 4410 + 5380 + 4420 140 100 @@ -7823,105 +7825,17 @@ radius_y : float radius_z : float - - Relation - - 5230 - 4440 - 70 - 30 - - lt=<. - 50.0;10.0;10.0;10.0 - - - UMLClass - - 5380 - 4310 - 320 - 90 - - *PassableMode* -bg=pink - --- -allowed_types : set(GameEntityType) -blacklisted_entities : set(GameEntity) - - Relation 5310 - 4340 + 4450 90 30 lt=<. 70.0;10.0;10.0;10.0 - - UMLClass - - 5600 - 4490 - 220 - 80 - - *Gate* -bg=pink - --- -stances : set (DiplomaticStance) - - - - UMLClass - - 5600 - 4420 - 110 - 60 - - -*Normal* -bg=pink - - - - Relation - - 5560 - 4390 - 30 - 150 - - lt=<<- - 10.0;10.0;10.0;130.0 - - - Relation - - 5560 - 4440 - 60 - 30 - - lt=- - 10.0;10.0;40.0;10.0 - - - Relation - - 5560 - 4510 - 60 - 30 - - lt=- - 10.0;10.0;40.0;10.0 - UMLClass @@ -8116,17 +8030,6 @@ stack_limit : int lt=<<- 10.0;10.0;10.0;270.0 - - Relation - - 1230 - 3920 - 140 - 30 - - lt=- - 10.0;10.0;120.0;10.0 - Relation @@ -8293,7 +8196,7 @@ game_entities : set(GameEntity) UMLClass 1070 - 3870 + 4000 120 60 @@ -8306,7 +8209,7 @@ bg=pink Relation 1180 - 3890 + 4020 80 30 @@ -8317,18 +8220,18 @@ bg=pink Relation 1120 - 3920 + 4050 30 - 80 + 70 lt=<. - 10.0;10.0;10.0;60.0 + 10.0;10.0;10.0;50.0 UMLClass 1090 - 3780 + 3910 100 60 @@ -8337,22 +8240,11 @@ bg=pink bg=pink - - Relation - - 1180 - 3800 - 80 - 30 - - lt=- - 10.0;10.0;60.0;10.0 - Relation 1130 - 3830 + 3960 30 60 @@ -8362,7 +8254,7 @@ bg=pink UMLClass - 1400 + 1410 3990 100 60 @@ -8375,7 +8267,7 @@ bg=pink Relation - 1430 + 1450 3950 30 60 @@ -10633,4 +10525,28 @@ bg=pink lt=- 10.0;10.0;30.0;10.0 + + UMLClass + + 4340 + 3980 + 120 + 60 + + +*PathType* +bg=pink + + + + Relation + + 4390 + 3930 + 30 + 70 + + lt=<. + 10.0;50.0;10.0;10.0 + From f1967c3c002d444510e50f54c9cdbb83419a9ec4 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 28 Jul 2024 04:03:55 +0200 Subject: [PATCH 454/771] nyan: Update SVG for nyan API v0.5.0. --- doc/nyan/aoe2_nyan_tree.svg | 3121 +++++++++++++++++------------------ 1 file changed, 1539 insertions(+), 1582 deletions(-) diff --git a/doc/nyan/aoe2_nyan_tree.svg b/doc/nyan/aoe2_nyan_tree.svg index 8a50988dbe..27360da9cf 100644 --- a/doc/nyan/aoe2_nyan_tree.svg +++ b/doc/nyan/aoe2_nyan_tree.svg @@ -9,13 +9,13 @@ >PathTypeNextCommandMoveNextCommandIdleCommandInQueueConditionnext : NodeCommandInQueueWaitAbilityconstruction_progress : set(Progress)TransformCarryRestockHarvestConstructare game entitiesProjectileBarracksSwordsmanRelicTreeTruePatchPropertyResetProgressPropertymultiplier : floatModifierPropertyNOTSUBSETMAXsize : intAnyAnyAnyTechTypethreshold : floatNyanPatchMeanDistributionTypeblacklisted_entities : set(GameEntity)NormalGatestances : set (DiplomaticStance)PassableModeallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Hitboxradius_x : floatradius_y : floatradius_z : floatProjectileHitTerrainstances : set(DiplomaticStance)SUBSETMINsize : intexclude : set(ResearchableTech)Creatablesexclude : set(CreatableGameEntity)ProductionModeProductionQueuesize : intproduction_modes : set(ProductionMode)container : EntityContainerNoStackLinearshift_x : intshift_y : intscale_factor : floatHyperbolicshift_x : intshift_y : intscale_factor : floatCalculationTypeStackedstack_limit : intcalculation_type : CalculationTypedistribution_type : DistributionTypeTerrainTypeMostHerdingLongestTimeInRangeClosestHerdingHerdableModeShadowTimeRelativeProgressIncreaseTimeRelativeProgressDecreaseTimeRelativeProgressIncreaseTimeRelativeProgressDecreaseAttributeChangePaymentModeResearchAttributeCostattributes : set(Attribute)researchables : set(ResearchableTech)CreationAttributeCostattributes : set(Attribute)creatables : set(CreatableGameEntity)payment_mode : PaymentModeUnconditionalUnconditionalTimeRelativeProgressChangeTimeRelativeAttributeChangePricePoolDynamicchange_value : floatmin_price : floatmax_price : floatPriceModeDepositResourcesOnProgressprogress_type : ProgressTyperesources : set(Resource)affected_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Attributename : TranslatedStringabbreviation : TranslatedStringOverlayTerrainterrain_overlay : TerrainTerrainRequirementallowed_types : set(TerrainType)blacklisted_terrains : set(Terrain)state_change : StateChangerAoE1TradeRouteexchange_resources : set(Resource)trade_amount : intProgressTypeTimeRelativeProgressChangetype : ProgressTypeFlatAttributeIncreaseFlatAttributeDecreaseTimeRelativeAttributeChangetype : AttributeChangeTypeTimeRelativeProgressChangetype : ProgressTypetotal_change_time : floatTimeRelativeAttributeIncreaseTimeRelativeAttributeDecreaseTimeRelativeAttributeChangetype : AttributeChangeTypetotal_change_time : floatignore_protection : set(ProtectingAttribute)progress : floatRefundOnConditioncondition : set(LogicElement)refund_amount : set(ResourceAmount)sounds : set(Sound)InverseLinearAdjacentTilesVariantnorth : optional(GameEntity)north_east : optional(GameEntity)east : optional(GameEntity)south_east : optional(GameEntity)south : optional(GameEntity)south_west : optional(GameEntity)west : optional(GameEntity)north_west : optional(GameEntity)Placetile_snap_distance : floatclearance_size_x : floatclearance_size_y : floatallow_rotation : boolmax_elevation_difference : intPlacementModeSendToContainerTypeLureTypeDiplomaticLineOfSightdiplomatic_stance : DiplomaticStanceNormalInContainerDiscreteEffectcontainers : set(EntityContainer)ability : ApplyDiscreteEffectInContainerContinuousEffectcontainers : set(EntityContainer)ability : ApplyContinuousEffectStateChangerenable_abilities : set(Ability)disable_abilities : set(Ability)enable_modifiers : set(Modifier)disable_modifiers : set(Modifier)transform_pool : optional(TransformPool) = Nonepriority : intopenage nyan data API v0.4.1openage nyan data API v0.5.0blacklisted_entities : set(GameEntity)Standardresource_spot : ResourceSpotAoE2ProjectileAmountprovider_abilities : set(ApplyDiscreteEffect)receiver_abilities : set(ApplyDiscreteEffect)change_types : set(AttributeChangeType)Orange elements:Effects/Resistances that canbe applied on other gameentitiesRevealline_of_sight : floataffected_types : set(GameEntityType)blacklisted_entities : set(GameEntity)ResearchTimeresearchables : set(ResearchableTech)StorageElementCapacitystorage_element : StorageElementDefinitionEntityContainerCapacitycontainer : EntityContainerHerdrange : floatstrength : intallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)condition : set(LogicElement)ResearchResourceCostresources : set(Resource)researchables : set(ResearchableTech)CreationResourceCostresources : set(Resource)creatables : set(CreatableGameEntity)CreationTimecreatables : set(CreatableGameEntity)resource_spot : ResourceSpotAbsoluteProjectileAmountamount : floatStrayMoveModeModifierScopeExpectedPositionCurrentPositionTargetModeSendToContainertype : SendToContainerTypesearch_range : floatignore_containers : set(EntityContainer)SendToContainertype : SendToContainerTypestorages : set(EntityContainer)Scopedstances : set(DiplomaticStance)scope : ModifierScopesubformations : set(Subformation)FlatAttributeIncreaseFlatAttributeDecreaseFlatAttributeIncreaseFlatAttributeDecreaseFlatAttributeIncreaseFlatAttributeDecreaseFlatAttributeIncreaseFlatAttributeDecreaseAoE2TradeRouteend_trade_post : GameEntityTechtypes : set(TechType)name : TranslatedStringdescription : TranslatedMarkupFilelong_description : TranslatedMarkupFileupdates : orderedset(Patch)FallbackLinearNoDropoffDropoffTypecost : CostEntityContainerallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)storage_element_defs : set(StorageElementDefinition)slots : intcarry_progress : set(Progress)string : textLuretype : LureTypestances : set(DiplomaticStance)DiplomaticStanceExitContainerallowed_containers : set(EntityContainer)EnterContainerallowed_containers : set(EntityContainer)allowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)max_range : intHuntConvertTypemax_range : intMakeHarvestableresource_spot : ResourceSpotresist_condition : set(LogicElement)MakeHarvestableresource_spot : ResourceSpotGatherauto_resume : boolresume_search_range : floattargets : set(ResourceSpot)gather_rate : ResourceRatecontainer : ResourceContainerMonkHealApplyContinuousEffecteffects : set(ContinuousEffect)application_delay : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)ApplyDiscreteEffectbatches : set(EffectBatch)reload_time : floatapplication_delay : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)FlatAttributeChangetype : AttributeChangeTypeblock_rate : AttributeRateContinuousResistancerate : floatDiscreteResistanceDiscreteEffectFlatAttributeChangetype : AttributeChangeTypemin_change_rate : optional(AttributeRate) = Nonemax_change_rate : optional(AttributeRate) = Nonechange_rate : AttributeRateignore_protection : set(ProtectingAttribute)ContinuousEffectAoE2Convertguaranteed_resist_rounds : intprotected_rounds : intprotection_round_recharge_time : floatAoE2Convertskip_guaranteed_rounds : intskip_protected_rounds : intConverttype : ConvertTypechance_resist : floatConverttype : ConvertTypemin_chance_success : optional(float) = Nonemax_chance_success : optional(float) = Nonechance_success : floatcost_fail : optional(Cost) = NoneAttributeChangeTypeFlatAttributeChangetype : AttributeChangeTypeblock_value : AttributeAmountFlatAttributeChangetype : AttributeChangeTypemin_change_value : optional(AttributeAmount) = Nonemax_change_value : optional(AttributeAmount) = Nonechange_value : AttributeAmountignore_protection : set(ProtectingAttribute)Effectors' sideResistors' sideproperties : dict(EffectProperty, EffectProperty) = {}Resistanceproperties : dict(ResistanceProperty, ResistanceProperty) = {}amount : intAccuracyaccuracy : floataccuracy_dispersion : floatdispersion_dropoff : DropOffTypetarget_types : set(GameEntityType)blacklisted_entities : set(GameEntity)protects : AttributeAttributeSettingattribute : Attributemin_value : intmax_value : intstarting_value : intsprite : fileCreatableGameEntitygame_entity : GameEntityvariants : set(Variant)cost : Costcreation_time : floatcreation_sounds : set(Sound)condition : set(LogicElement)placement_modes : set(PlacementMode)scope : LiteralScopeProjectilearc : intaccuracy : set(Accuracy)target_mode : TargetModeignored_types : set(GameEntityType)unignore_entities : set(GameEntity)change_progress : set(Progress)HitboxCollisionhitbox : HitboxNamedname : TranslatedStringdescription : TranslatedMarkupFilelong_description : TranslatedMarkupFileblacklisted_entities : set(GameEntity)Herdableadjacent_discover_range : floatmode : HerdableModeStopPassablePathablehitbox : Hitboxmode : PassableModepath_costs : dict(PathType, int)angle : intRestockauto_restock : booltarget : ResourceSpotrestock_time : floatmanual_cost : Costauto_cost : Costamount : intPassiveStandGroundAggressiveTradePosttrade_routes : set(TradeRoute)PatrolGameEntityStancesearch_range : floatability_preference : orderedset(Ability)type_preference : orderedset(GameEntityType)stances: set(GameEntityStance)SendBackToTaskallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)TransferStoragestorage_element : GameEntitysource_container : EntityContainertarget_container : EntityContainerGameEntityProgressgame_entity : GameEntitystatus : ProgressStatuspriority : intActiveTransformTotarget_state : StateChangertransform_time : floattransform_progress : set(Progress)Selectableselection_box : SelectionBoxRallyPointWhite elements:AoE2 specific objectsYellow elements:Modifiers (handled by engineimplementation)Green elements:Abilities (handled by engineimplementation)Pink elements:Basic nyan API objectsmin_elevation_difference : optional(float) = NoneFlyoverrelative_angle : floatflyover_types : set(GameEntityType)blacklisted_entities : set(GameEntity)sprite : fileVillager Gather abilities canoverride the graphics ofIdle,Move,Die and Despawnvia CarryProgress objects withAnimationOverridesAbilities andStorageElementscan use theseOverride typesto change anyother ability'sanimation.If min_projectiles is greater thanthe number of Projectiles inprojectiles, the last projectilein the orderedset should be usedIn AoE2 there is onlyone HarvestProgress Statein the interval [0,100],but AoM had more.Stores what happens aftera percentage ofconstruction, damage,transformation, etc. isreachedProgressproperties : dict(ProgressProperty, ProgressProperty) = {}type : ProgressTypeleft_boundary : floatright_boundary : floatetc.)RemoveStoragecontainer : EntityContainerstorage_elements : set(GameEntity)CollectStoragecontainer : EntityContainerstorage_elements : set(GameEntity)StorageElementDefinitionstorage_element : GameEntityelements_per_slot : intconflicts : set(StorageElementDefinition)state_change : optional(StateChanger) = NoneStoragecontainer : EntityContainerempty_condition : set(LogicElement)chance_share : floatResearchresearchables : set(ResearchableTech)container : ResourceContainerCreatecreatables : set(CreatableGameEntity)RelicBonusattribute : AttributeFeitoriaBonusFoodAmountWoodAmountStoneAmountGoldAmountrates : set(ResourceRate)Modifier should only be usedin cases where Patches don'twork. For example, if thebonus is a percentage valueor continuously stacks (likeresources from the Feitoria).Modifier objects can still bepatched.IdlePlayerSetupname : TranslatedStringdescription : TranslatedMarkupFilelong_description : TranslatedMarkupFileleader_names : set(TranslatedString)modifiers : set(Modifier)starting_resources : set(ResourceAmount)game_setup : orderedset(Patch)Despawnactivation_condition : set(LogicElement)despawn_condition : set(LogicElement)despawn_time : floatstate_change : optional(StateChanger) = NoneTauntactivation_message : textdisplay_message : TranslatedStringsound : Soundattributes : set(AttributeSetting)Harvestableresources : ResourceSpotharvest_progress : set(Progress)restock_progress : set(Progress)gatherer_limit : intharvestable_by_default : boolPassiveTransformTocondition : set(LogicElement)transform_time : floattarget_state : StateChangertransform_progress : set(Progress)resistances : set(Resistance)ShootProjectileprojectiles : orderedset(GameEntity)min_projectiles : intmax_projectiles : intmin_range : intmax_range : intreload_time : floatspawn_delay : floatprojectile_delay : floatrequire_turning : boolmanual_aiming_allowed : boolspawning_area_offset_x : floatspawning_area_offset_y : floatspawning_area_offset_z : floatspawning_area_width : floatspawning_area_height : floatspawning_area_randomness : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)rate : AttributeRateFormationformations : set(GameEntityFormation)Movespeed : floatmodes : set(MoveMode)path_type : PathTypeanimations : set(Animation)Abilityproperties : dict(AbilityProperty, AbilityProperty) = {}patches : orderedset(Patch)Patchproperties : dict(PatchProperty, PatchProperty) = {}patch : NyanPatchModifierproperties : dict(ModifierProperty, ModifierProperty) = {}sounds : orderedset(file)TerrainAmbientmax_density : intTerrainname : TranslatedStringtypes : set(TerrainType)terrain_graphic : Terrainsound : Soundambience : set(TerrainAmbient)path_costs : dict(PathType, int)ResearchableTechtech : Techcost : Costresearch_time : floatresearch_sounds : set(Sound)condition : set(LogicElement)TranslatedSoundtranslations : set(LanguageSoundPair)TranslatedMarkupFiletranslations : set(LanguageMarkupPair)TranslatedObjectTranslatedStringtranslations : set(LanguageTextPair)max_storage : intResourceSpotresource : Resourcemax_amount : intstarting_amount : intdecay_rate : floatDropSiteaccepts_from : set(ResourceContainer)GameEntitytypes : set(GameEntityType)abilities : set(Ability)modifiers : set(Modifier)variants : set(Variant)Object Date: Sun, 21 Apr 2024 23:22:06 +0200 Subject: [PATCH 455/771] nyan: Document pathfinding types in API reference. --- doc/nyan/api_reference/reference_ability.md | 72 ++++++++++++--------- doc/nyan/api_reference/reference_util.md | 56 ++++++---------- 2 files changed, 60 insertions(+), 68 deletions(-) diff --git a/doc/nyan/api_reference/reference_ability.md b/doc/nyan/api_reference/reference_ability.md index 098492089e..dfddc1c82f 100644 --- a/doc/nyan/api_reference/reference_ability.md +++ b/doc/nyan/api_reference/reference_ability.md @@ -259,6 +259,18 @@ Container the target game entity will be inserted into. A `Storage` ability with **storage_elements** Game entities that can be inserted into the container. The container must allow the `GameEntity` objects. +## ability.type.Collision + +```python +Collision(Ability): + hitbox : Hitbox +``` + +Adds collision behaviour to a game entity. + +**hitbox** +Defines the size (x, y, z) of the collision hitbox. + ## ability.type.Constructable ```python @@ -567,18 +579,6 @@ When other herdables are in this range around the herded game entity, they will **mode** Determines who gets ownership of the herdable game entity when multiple game entities using `Herd` are in range. -## ability.type.Hitbox - -```python -Hitbox(Ability): - hitbox : Hitbox -``` - -Adds a hitbox to a game entity that is used for collision with other game entities. - -**hitbox** -Defines the size (x, y, z) of the hitbox. - ## ability.type.Idle ```python @@ -628,8 +628,9 @@ Lock pools definitions. ```python Move(Ability): - speed : float - modes : set(MoveMode) + speed : float + modes : set(MoveMode) + path_type : children(PathType) ``` Allows a game entity to move around the map. @@ -638,7 +639,10 @@ Allows a game entity to move around the map. Speed of movement. **modes** -Type of movements that can be used. +Modes of movements that can be used. + +**path_type** +Path type determining which pathfinding grid is searched to find a path from the start to the goal location. ## ability.type.Named @@ -672,22 +676,6 @@ Temporarily replace the map terrain the game entity is positioned on with a spec **terrain_overlay** Terrain that is temporily replaces the existing map terrain. -## ability.type.Passable - -```python -Passable(Ability): - hitbox : Hitbox - mode : PassableMode -``` - -Deactivates a specified hitbox of the game entity for movement of other game entities. The hitbox is still relevant for the game entity's own movement. - -**hitbox** -Reference to the hitbox that should be deactivated. - -**mode** -Defines the game entities for which the hitbox is deactivated. - ## ability.type.PassiveTransformTo ```python @@ -712,6 +700,28 @@ State change activated after `transform_time` has passed. **transform_progress** Can alter the game entity while the transformation is in progress. The objects in the set must have progress type `Transform`. +## ability.type.Pathable + +```python +Pathable(Ability): + hitbox : Hitbox + path_costs : dict(children(PathType), int) +``` + +Lets a game entity influence the pathing costs on the (static) pathfinding grid. + +This ability should only be used for game entitie that never (or rarely) change positions as pathfinding grid recalculations are expensive. For dynamic pathfinding effects, using the `Collision` ability should be preferred. + +**hitbox** +Hitbox around the game entity that affects the underlying pathfinding grids. All grid cells that are covered by this hitbox ae influenced by the cost definitions in the `path_costs` attribute. + +**path_costs** +Costs of traversing the area defined by the `hitbox` attribute on the pathfinding grid. + +Keys are `PathType` objects that are associated with a pathfinding grid in the pathfinder. + +Values represent the pathing cost for the terrain on the pathfinding grid. Each value must be an integer between `1` and `255`. `1` defines the *minimum* possible cost and `254` represents the *maximum* possible cost. `255` signifies that the terrain is impassable for the specified path type. + ## ability.type.ProductionQueue ```python diff --git a/doc/nyan/api_reference/reference_util.md b/doc/nyan/api_reference/reference_util.md index 66fced3ff6..dd9646e862 100644 --- a/doc/nyan/api_reference/reference_util.md +++ b/doc/nyan/api_reference/reference_util.md @@ -1491,43 +1491,6 @@ Patrol(MoveMode): Lets player set two or more waypoints that the game entity will follow. Stances from `GameEntityStance` ability are considered during movement. -## util.passable_mode.PassableMode - -```python -PassableMode(Object): - allowed_types : set(children(GameEntityType)) - blacklisted_entities : set(GameEntity) -``` - -Generalization object for all passable modes. Define passability options for the `Passable` ability. - -**allowed_types** -Lists the game entities types which can pass the hitbox. - -**blacklisted_entities** -Used to blacklist game entities that have one of the types listed in `allowed_types`, but should not be covered by this `PassableMode` object. - -## util.passable_mode.type.Gate - -```python -Gate(PassableMode): - stances : set(children(DiplomaticStance)) -``` - -Lets all compatible game entities from players with the specified stances pass through the hitbox. Game entities of players with other stances can also pass through while any unit is passing through. - -**stances** -Stances of players whose game entities are always allowed to pass though the hitbox. - -## util.passable_mode.type.Normal - -```python -Normal(PassableMode): - pass -``` - -Lets all compatible game entities pass through the hitbox. - ## util.patch.NyanPatch ```python @@ -1578,6 +1541,15 @@ The patch is applied to all players that have the specified diplomatic stances. **stances** Diplomatic stances of the players the patch should apply to. +## util.path_type.PathType + +```python +PathType(Object): + pass +``` + +Path type that is associated with an internal pathfinding grid at runtime. + ## util.payment_mode.PaymentMode ```python @@ -2413,6 +2385,7 @@ Terrain(Object): terrain_graphic : Terrain sound : Sound ambience : set(TerrainAmbient) + path_costs : dict(children(PathType), int) ``` Terrains define the properties of the ground which the game entities are placed on. @@ -2432,6 +2405,15 @@ Ambient sound played when the camera of the player is looking onto the terrain. **ambience** Ambient objects placed on the terrain. +**path_costs** +Base costs of traversing the pathfinding grid on map areas where the terrain is placed. + +Keys are `PathType` objects that are associated with a pathfinding grid in the pathfinder. + +Values represent the pathing cost for the terrain on the pathfinding grid. Each value must be an integer between `1` and `255`. `1` defines the *minimum* possible cost and `254` represents the *maximum* possible cost. `255` signifies that the terrain is impassable for the specified path type. + +For `PathType` objects that exist in the modpack but are not keys in this dict, a default cost value of `1` is assumed. + ## util.terrain.TerrainAmbient ```python From bc123aa19efe83fb0bd728d303b7e480f4d3a0df Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 27 Apr 2024 22:59:41 +0200 Subject: [PATCH 456/771] nyan: Changelog for API version 0.5.0. --- doc/changelogs/nyan_api/v0.5.0.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 doc/changelogs/nyan_api/v0.5.0.md diff --git a/doc/changelogs/nyan_api/v0.5.0.md b/doc/changelogs/nyan_api/v0.5.0.md new file mode 100644 index 0000000000..127a2b4d37 --- /dev/null +++ b/doc/changelogs/nyan_api/v0.5.0.md @@ -0,0 +1,27 @@ +# [0.5.0] - 2024-07-29 +All notable changes for version [v0.5.0] are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Renamed +### Ability module +- Rename `Hitbox` to `Collision` + +## Added +### Ability module +- Add `Pathable(Ability)` object; defines pathing costs for the game entity when the ability is active +- Add `path_type : PathType` member to `Move` + +### Utility module +- Add `PathType(Object)` object; associates move abilities with pathfinding grids +- Add `path_costs : dict(children(PathType), int)` member to `Terrain`; defines pathing costs for the terrain + +### Removed +### Ability module +- Remove `Passable(Ability)` object; functionality superceded by `Pathable` + + +## Reference visualization + +* [Gamedata](https://github.com/SFTtech/openage/blob/f1967c3c002d444510e50f54c9cdbb83419a9ec4/doc/nyan/aoe2_nyan_tree.svg) From 07ac9673bbd67b26a1f548965cb82a3c0245ed8d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 27 Jul 2024 11:36:13 +0200 Subject: [PATCH 457/771] renderer: Add more comments to demo 6 (frustum culling). --- libopenage/renderer/demo/demo_6.cpp | 46 ++++++++++++++++++++++------- libopenage/renderer/demo/demo_6.h | 21 +++++++++++++ 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/libopenage/renderer/demo/demo_6.cpp b/libopenage/renderer/demo/demo_6.cpp index 038bb0240e..95d5021e50 100644 --- a/libopenage/renderer/demo/demo_6.cpp +++ b/libopenage/renderer/demo/demo_6.cpp @@ -43,6 +43,8 @@ void renderer_demo_6(const util::Path &path) { render_mgr.obj_3d_pass->add_renderables({renderables_3d}); render_mgr.frame_pass->add_renderables(std::move(renderables_frame)); + // Move the camera with the WASD keys + // This is where we will also update the frustum for the 2D objects render_mgr.window->add_key_callback([&](const QKeyEvent &ev) { if (ev.type() == QEvent::KeyPress) { auto key = ev.key(); @@ -50,13 +52,13 @@ void renderer_demo_6(const util::Path &path) { // move_frame moves the camera in the specified direction in the next drawn frame switch (key) { case Qt::Key_W: { // forward - render_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), 0.25f); + render_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), 0.5f); } break; case Qt::Key_A: { // left render_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), 0.25f); } break; case Qt::Key_S: { // back - render_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), 0.25f); + render_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), 0.5f); } break; case Qt::Key_D: { // right render_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), 0.25f); @@ -65,6 +67,7 @@ void renderer_demo_6(const util::Path &path) { break; } + // Update the camera uniform buffer auto new_cam_unifs = render_mgr.camera->get_uniform_buffer()->new_uniform_input( "view", render_mgr.camera->get_view_matrix(), @@ -72,16 +75,32 @@ void renderer_demo_6(const util::Path &path) { render_mgr.camera->get_projection_matrix()); render_mgr.camera->get_uniform_buffer()->update_uniforms(new_cam_unifs); + /* Generate the frustum for the 2D objects */ + + // Copy the camera used for drawing + // + // In this demo, we will manipulate the frustum camera + // to create a slightly smaller frustum for the 2D objects + // to show the effect of frustum culling. + // + // In the real renderer, the normal camera would be used. auto frustum_camera = *render_mgr.camera; - auto half_cam_size = util::Vector2s{render_mgr.camera->get_viewport_size()[0] * 0.7, - render_mgr.camera->get_viewport_size()[1] * 0.7}; - frustum_camera.resize(half_cam_size[0], half_cam_size[1]); + + // Downsize the frustum to 70% of the camera size + float frustum_factor = 0.7f; + auto frustum_cam_size = util::Vector2s{render_mgr.camera->get_viewport_size()[0] * frustum_factor, + render_mgr.camera->get_viewport_size()[1] * frustum_factor}; + frustum_camera.resize(frustum_cam_size[0], frustum_cam_size[1]); + + // Get a 2D frustum object auto frustum_2d = frustum_camera.get_frustum_2d(); frustum_2d.update(frustum_camera.get_viewport_size(), frustum_camera.get_view_matrix(), frustum_camera.get_projection_matrix(), frustum_camera.get_zoom()); + // Check if the 2D scene objects are in the frustum + // and update the renderables in the 2D render pass auto renderables_2d = render_mgr.create_2d_obj(); std::vector renderables_in_frustum{}; for (size_t i = 0; i < render_mgr.obj_2d_positions.size(); ++i) { @@ -91,14 +110,19 @@ void renderer_demo_6(const util::Path &path) { render_mgr.animation_2d_info.get_scalefactor(), render_mgr.animation_2d_info.get_max_bounds()); if (in_frustum) { + // Only add objects that are in the frustum renderables_in_frustum.push_back(renderables_2d.at(i)); } } + + // Clear the renderables in the 2D render pass + // and add ONLY the renderables that are inside the frustum render_mgr.obj_2d_pass->clear_renderables(); render_mgr.obj_2d_pass->add_renderables(std::move(renderables_in_frustum)); } }); + // Draw everything render_mgr.run(); } @@ -167,10 +191,6 @@ const std::vector RenderManagerDemo6::create_2d_obj() { } const renderer::Renderable RenderManagerDemo6::create_3d_obj() { - auto terrain_tex_info = this->terrain_3d_info.get_texture(0); - auto terrain_tex_data = resources::Texture2dData{*terrain_tex_info}; - this->obj_3d_texture = this->renderer->add_texture(terrain_tex_data); - // Create renderable for terrain auto terrain_unifs = this->obj_3d_shader->new_uniform_input( "tex", @@ -337,7 +357,7 @@ void RenderManagerDemo6::load_shaders() { } void RenderManagerDemo6::load_assets() { - // Load assets + // Load assets for 2D objects auto animation_2d_path = this->path / "assets" / "test" / "textures" / "test_tank.sprite"; this->animation_2d_info = resources::parser::parse_sprite_file(animation_2d_path); @@ -345,9 +365,13 @@ void RenderManagerDemo6::load_assets() { auto tex_data = resources::Texture2dData{*tex_info}; this->obj_2d_texture = this->renderer->add_texture(tex_data); - // Load assets + // Load assets for 3D objects auto terrain_path = this->path / "assets" / "test" / "textures" / "test_terrain.terrain"; this->terrain_3d_info = resources::parser::parse_terrain_file(terrain_path); + + auto terrain_tex_info = this->terrain_3d_info.get_texture(0); + auto terrain_tex_data = resources::Texture2dData{*terrain_tex_info}; + this->obj_3d_texture = this->renderer->add_texture(terrain_tex_data); } void RenderManagerDemo6::create_camera() { diff --git a/libopenage/renderer/demo/demo_6.h b/libopenage/renderer/demo/demo_6.h index 7c5d3052c5..c3e74fba44 100644 --- a/libopenage/renderer/demo/demo_6.h +++ b/libopenage/renderer/demo/demo_6.h @@ -45,26 +45,40 @@ namespace tests { void renderer_demo_6(const util::Path &path); +/** + * Render manager that handles drawing, object creation, etc. + */ class RenderManagerDemo6 { public: RenderManagerDemo6(const util::Path &path); void run(); + /* Create objects to render */ + + /// Create 2D objects (sprites) const std::vector create_2d_obj(); + /// Create 3D objects (terrain) const renderer::Renderable create_3d_obj(); + /// Create frames around 2D objects. These represents the boundaries of the objects + /// that are used by the frustum culling algorithm. const std::vector create_frame_obj(); + /// Qt application std::shared_ptr qtapp; + /// OpenGL window std::shared_ptr window; + /// Camera std::shared_ptr camera; + /// Render passes std::shared_ptr obj_2d_pass; std::shared_ptr obj_3d_pass; std::shared_ptr frame_pass; std::shared_ptr display_pass; + /// 2D object (sprite) positions const std::array obj_2d_positions = { coord::scene3{0, 0, 0}, coord::scene3{-4, -4, 0}, @@ -73,26 +87,33 @@ class RenderManagerDemo6 { coord::scene3{3, -2, 0}, }; + /// Animation and texture information resources::Animation2dInfo animation_2d_info; resources::TerrainInfo terrain_3d_info; private: + /// Setup everything necessary for rendering. void setup(); + /// Load shaders, assets, create camera, and render passes. void load_shaders(); void load_assets(); void create_camera(); void create_render_passes(); + /// Directory path util::Path path; + /// Renderer std::shared_ptr renderer; + /// Shaders std::shared_ptr obj_2d_shader; std::shared_ptr obj_3d_shader; std::shared_ptr frame_shader; std::shared_ptr display_shader; + /// Textures for the rendered objects (2D and 3D) std::shared_ptr obj_2d_texture; std::shared_ptr obj_3d_texture; }; From 7286cedcf15293c39d372dae7d83f4df2c5f7971 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 27 Jul 2024 11:50:33 +0200 Subject: [PATCH 458/771] renderer: Make demo 6 frame color configurable in shader. --- assets/test/shaders/demo_6_2d_frame.frag.glsl | 6 ++++-- libopenage/renderer/demo/demo_6.cpp | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/assets/test/shaders/demo_6_2d_frame.frag.glsl b/assets/test/shaders/demo_6_2d_frame.frag.glsl index c67ca484cc..747ed38e5a 100644 --- a/assets/test/shaders/demo_6_2d_frame.frag.glsl +++ b/assets/test/shaders/demo_6_2d_frame.frag.glsl @@ -1,7 +1,9 @@ #version 330 -out vec4 col; +out vec4 outcol; + +uniform vec4 incol; void main() { - col = vec4(1.0, 0.0, 0.0, 0.8); + outcol = incol; } diff --git a/libopenage/renderer/demo/demo_6.cpp b/libopenage/renderer/demo/demo_6.cpp index 95d5021e50..d59c39ade9 100644 --- a/libopenage/renderer/demo/demo_6.cpp +++ b/libopenage/renderer/demo/demo_6.cpp @@ -254,7 +254,9 @@ const std::vector RenderManagerDemo6::create_frame_obj() { "scale", scale, "frame_size", - frame_size); + frame_size, + "incol", + Eigen::Vector4f{0.0f, 0.0f, 1.0f, 1.0f}); Renderable frame_obj{ frame_unifs, frame_geometry, From dc76a400b0c1d633f6dd4951f9edd3e32f095350 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 27 Jul 2024 12:00:32 +0200 Subject: [PATCH 459/771] renderer: Show frustum in demo 6. --- .../shaders/demo_6_2d_frustum_frame.vert.glsl | 8 ++++ libopenage/renderer/demo/demo_6.cpp | 45 ++++++++++++++++--- libopenage/renderer/demo/demo_6.h | 1 + 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 assets/test/shaders/demo_6_2d_frustum_frame.vert.glsl diff --git a/assets/test/shaders/demo_6_2d_frustum_frame.vert.glsl b/assets/test/shaders/demo_6_2d_frustum_frame.vert.glsl new file mode 100644 index 0000000000..8c5556f713 --- /dev/null +++ b/assets/test/shaders/demo_6_2d_frustum_frame.vert.glsl @@ -0,0 +1,8 @@ +#version 330 + +layout(location=0) in vec2 v_position; + +void main() { + // flip the y coordinate in OpenGL + gl_Position = vec4(v_position.x, v_position.y, 0.0, 1.0); +} diff --git a/libopenage/renderer/demo/demo_6.cpp b/libopenage/renderer/demo/demo_6.cpp index d59c39ade9..a03039fce0 100644 --- a/libopenage/renderer/demo/demo_6.cpp +++ b/libopenage/renderer/demo/demo_6.cpp @@ -52,16 +52,16 @@ void renderer_demo_6(const util::Path &path) { // move_frame moves the camera in the specified direction in the next drawn frame switch (key) { case Qt::Key_W: { // forward - render_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), 0.5f); + render_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), 0.2f); } break; case Qt::Key_A: { // left - render_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), 0.25f); + render_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), 0.1f); } break; case Qt::Key_S: { // back - render_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), 0.5f); + render_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), 0.2f); } break; case Qt::Key_D: { // right - render_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), 0.25f); + render_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), 0.1f); } break; default: break; @@ -231,7 +231,7 @@ const renderer::Renderable RenderManagerDemo6::create_3d_obj() { const std::vector RenderManagerDemo6::create_frame_obj() { std::vector renderables; for (auto scene_pos : this->obj_2d_positions) { - // Create renderable for frame + // Create renderables for frame around sprites std::array frame_verts{-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f}; std::vector frame_vert_data(frame_verts.size() * sizeof(float)); std::memcpy(frame_vert_data.data(), frame_verts.data(), frame_vert_data.size()); @@ -267,6 +267,29 @@ const std::vector RenderManagerDemo6::create_frame_obj() { renderables.push_back(frame_obj); } + // Create renderable for frustum frame + std::array frame_verts{-0.7f, -0.7f, -0.7f, 0.7f, 0.7f, 0.7f, 0.7f, -0.7f}; + std::vector frame_vert_data(frame_verts.size() * sizeof(float)); + std::memcpy(frame_vert_data.data(), frame_verts.data(), frame_vert_data.size()); + auto frame_vert_info = resources::VertexInputInfo{ + {resources::vertex_input_t::V2F32}, + resources::vertex_layout_t::AOS, + resources::vertex_primitive_t::LINE_LOOP, + }; + auto frame_mesh = resources::MeshData{std::move(frame_vert_data), frame_vert_info}; + auto frame_geometry = this->renderer->add_mesh_geometry(frame_mesh); + + auto frame_unifs = this->frustum_shader->new_uniform_input( + "incol", + Eigen::Vector4f{1.0f, 0.0f, 0.0f, 1.0f}); + Renderable frame_obj{ + frame_unifs, + frame_geometry, + true, + true, + }; + renderables.push_back(frame_obj); + return renderables; } @@ -336,6 +359,17 @@ void RenderManagerDemo6::load_shaders() { frame_fshader_file.read()); frame_fshader_file.close(); + /* Shader for frustrum frame */ + auto frustum_vshader_file = (shaderdir / "demo_6_2d_frustum_frame.vert.glsl").open(); + auto frustum_vshader_src = resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::vertex, + frustum_vshader_file.read()); + frustum_vshader_file.close(); + + // Use the same fragment shader as the frame shader + auto frustum_fshader_src = frame_fshader_src; + /* Shader for rendering to the screen */ auto display_vshader_file = (shaderdir / "demo_6_display.vert.glsl").open(); auto display_vshader_src = resources::ShaderSource( @@ -355,6 +389,7 @@ void RenderManagerDemo6::load_shaders() { this->obj_3d_shader = this->renderer->add_shader({obj_vshader_src, obj_fshader_src}); this->obj_2d_shader = this->renderer->add_shader({obj_2d_vshader_src, obj_2d_fshader_src}); this->frame_shader = this->renderer->add_shader({frame_vshader_src, frame_fshader_src}); + this->frustum_shader = this->renderer->add_shader({frustum_vshader_src, frustum_fshader_src}); this->display_shader = this->renderer->add_shader({display_vshader_src, display_fshader_src}); } diff --git a/libopenage/renderer/demo/demo_6.h b/libopenage/renderer/demo/demo_6.h index c3e74fba44..c3c73b8a04 100644 --- a/libopenage/renderer/demo/demo_6.h +++ b/libopenage/renderer/demo/demo_6.h @@ -111,6 +111,7 @@ class RenderManagerDemo6 { std::shared_ptr obj_2d_shader; std::shared_ptr obj_3d_shader; std::shared_ptr frame_shader; + std::shared_ptr frustum_shader; std::shared_ptr display_shader; /// Textures for the rendered objects (2D and 3D) From 8399a24cfa8d6d742a0a90420a827785717952a5 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 27 Jul 2024 14:49:28 +0200 Subject: [PATCH 460/771] renderer: Fix calculating pixel size of 2D frustum boundaries. --- libopenage/renderer/camera/frustum_2d.cpp | 12 ++++++------ libopenage/renderer/camera/frustum_2d.h | 6 ++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/libopenage/renderer/camera/frustum_2d.cpp b/libopenage/renderer/camera/frustum_2d.cpp index 614b2b74a2..4729c2ac3e 100644 --- a/libopenage/renderer/camera/frustum_2d.cpp +++ b/libopenage/renderer/camera/frustum_2d.cpp @@ -16,7 +16,7 @@ void Frustum2d::update(const util::Vector2s &viewport_size, const Eigen::Matrix4f &view_matrix, const Eigen::Matrix4f &projection_matrix, const float zoom) { - this->inv_viewport_size = {1.0f / viewport_size[0], 1.0f / viewport_size[1]}; + this->pixel_size_ndc = {2.0f / viewport_size[0], 2.0f / viewport_size[1]}; this->inv_zoom_factor = 1.0f / zoom; // calculate the transformation matrix @@ -34,11 +34,11 @@ bool Frustum2d::in_frustum(const Eigen::Vector3f &scene_pos, float zoom_scale = scalefactor * this->inv_zoom_factor; - // Scale the boundaries by the zoom factor and the viewport size - float left_bound = boundaries[0] * zoom_scale * this->inv_viewport_size[0]; - float right_bound = boundaries[1] * zoom_scale * this->inv_viewport_size[0]; - float top_bound = boundaries[2] * zoom_scale * this->inv_viewport_size[1]; - float bottom_bound = boundaries[3] * zoom_scale * this->inv_viewport_size[1]; + // Scale the boundaries by the zoom factor and the pixel size + float left_bound = boundaries[0] * zoom_scale * this->pixel_size_ndc[0]; + float right_bound = boundaries[1] * zoom_scale * this->pixel_size_ndc[0]; + float top_bound = boundaries[2] * zoom_scale * this->pixel_size_ndc[1]; + float bottom_bound = boundaries[3] * zoom_scale * this->pixel_size_ndc[1]; // check if the object boundaries are inside the frustum if (x_ndc - left_bound >= 1.0f) { diff --git a/libopenage/renderer/camera/frustum_2d.h b/libopenage/renderer/camera/frustum_2d.h index 3403d72a43..b479fcbc18 100644 --- a/libopenage/renderer/camera/frustum_2d.h +++ b/libopenage/renderer/camera/frustum_2d.h @@ -68,9 +68,11 @@ class Frustum2d { Eigen::Matrix4f transform_matrix; /** - * Viewport size of the camera (width x height). + * Size of a pixel (width x height) in clip space. + * + * Uses normalized device coordinates (NDC) for the pixel size. */ - Eigen::Vector2f inv_viewport_size; + Eigen::Vector2f pixel_size_ndc; /** * Zoom factor of the camera. From ffd2bdce6dcb452a01dc5618665e763e3a58c38a Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 27 Jul 2024 14:50:16 +0200 Subject: [PATCH 461/771] renderer: Show different sprite angles in demo 6. --- libopenage/renderer/demo/demo_6.cpp | 9 ++++++--- libopenage/renderer/demo/demo_6.h | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/libopenage/renderer/demo/demo_6.cpp b/libopenage/renderer/demo/demo_6.cpp index a03039fce0..9844503e64 100644 --- a/libopenage/renderer/demo/demo_6.cpp +++ b/libopenage/renderer/demo/demo_6.cpp @@ -148,11 +148,13 @@ void RenderManagerDemo6::run() { const std::vector RenderManagerDemo6::create_2d_obj() { std::vector renderables; - for (auto scene_pos : this->obj_2d_positions) { + for (size_t i = 0; i < this->obj_2d_positions.size(); ++i) { // Create renderable for 2D animation auto scale = this->animation_2d_info.get_scalefactor(); - auto tex_id = this->animation_2d_info.get_layer(0).get_angle(0)->get_frame(0)->get_texture_idx(); - auto subtex_id = this->animation_2d_info.get_layer(0).get_angle(0)->get_frame(0)->get_subtexture_idx(); + auto angle = this->obj_2d_angles.at(i); + auto frame_info = this->animation_2d_info.get_layer(0).get_angle(angle)->get_frame(0); + auto tex_id = frame_info->get_texture_idx(); + auto subtex_id = frame_info->get_subtexture_idx(); auto subtex = this->animation_2d_info.get_texture(tex_id)->get_subtex_info(subtex_id); auto subtex_size = subtex.get_size(); Eigen::Vector2f subtex_size_vec{ @@ -163,6 +165,7 @@ const std::vector RenderManagerDemo6::create_2d_obj() { static_cast(anchor_params[0]), static_cast(anchor_params[1])}; auto tile_params = subtex.get_subtex_coords(); + auto scene_pos = this->obj_2d_positions.at(i); auto animation_2d_unifs = this->obj_2d_shader->new_uniform_input( "obj_world_position", scene_pos.to_world_space(), diff --git a/libopenage/renderer/demo/demo_6.h b/libopenage/renderer/demo/demo_6.h index c3c73b8a04..f86728819b 100644 --- a/libopenage/renderer/demo/demo_6.h +++ b/libopenage/renderer/demo/demo_6.h @@ -87,6 +87,9 @@ class RenderManagerDemo6 { coord::scene3{3, -2, 0}, }; + /// Rendered angles of the 2D objects + const std::array obj_2d_angles = {0, 1, 2, 3, 4}; + /// Animation and texture information resources::Animation2dInfo animation_2d_info; resources::TerrainInfo terrain_3d_info; From c6c29dbad29dd7a9d5087f0d0fc2ff97d595cbf2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 27 Jul 2024 15:09:42 +0200 Subject: [PATCH 462/771] doc: Document frustum culling. --- doc/code/renderer/level2.md | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/doc/code/renderer/level2.md b/doc/code/renderer/level2.md index cec5950bfd..cc927ef6a2 100644 --- a/doc/code/renderer/level2.md +++ b/doc/code/renderer/level2.md @@ -8,6 +8,7 @@ High-level renderer for transforming data from the gamestate to render objects f 2. [Stages](#stages) 1. [Updating Render Stages from the Gamestate](#updating-render-stages-from-the-gamestate) 3. [Camera](#camera) + 1. [Frustum Culling](#frustum-culling) ## Stages @@ -61,3 +62,40 @@ Zoom levels can also be adjusted with these methods: For displaying 3D objects, the `Camera` can also calculate a view matrix (`get_view_matrix()`) and projection matrix (`get_projection_matrix()`) that take current position and zoom level into account. Camera parameters may be used for raycasting operations, e.g. mouse picking/selection. Since the camera utilizes orthographic projection and a fied angle, the ray direction is exactly the same as the camera direction vector (accessible as `cam_direction`). To find the origin point of a ray for a pixel coordinate in the viewport, the `get_input_pos(..)` method can be used. This method calculates the position of the pixel coordinate on the othographic camera plane that represents the viewport. The result is the absolute position of the pixel coordinate inside the 3D scene. Ray origin point and direction can then be used to perform calculations for line-plane or line sphere intersections. + +### Frustum Culling + +Frustum culling is a technique used to discard objects that are outside the view frustum of the camera. +This can save a lot of computation time that would be spent on updating shaders and rendering objects +that are not visible in the camera's view. + +The openage renderer provides two frustum types: 2D and 3D frustums. 2D frustums are used +for sprite animations and other 2D objects, while 3D frustums are used for 3D objects. Both frustums +can be created from a camera object. + +```c++ +std::shared_ptr camera = std::make_shared(renderer, {800, 600}); + +Frustum2d frustum_2d = camera->get_frustum_2d(); +Frustum3d frustum_3d = camera->get_frustum_3d(); +``` + +`Frustum2d` and `Frustum3d` provide a method `is_visible(..)` that can be used to check if an object is +located inside the frustum. The required inputs differ depending on the frustum type. For 3D frustums, +only the 3D scene position is required: + +```c++ +bool is_visible = frustum_3d.is_visible({0.f, 0.f, 0.f}); +``` + +For 2D frustums, in addition to the 3D scene position, a model matrix, the animation's scalefactor +as well as the bounding box of the animation must be provided. + +```c++ +bool is_visible = frustum_2d.is_visible( + {0.f, 0.f, 0.f}, + model_matrix, // the model matrix of the animation + scalefactor, // how much the animation is scaled + {10, 20, 50, 10} // max distance from the center to the edges of the bounding box +); +``` From ae9dd8f4dbac31f8e1ef32c1e03bf9f1cf7a27d2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 27 Jul 2024 15:55:15 +0200 Subject: [PATCH 463/771] doc: Document demos in the renderer. --- doc/code/renderer/demos.md | 172 ++++++++++++++++++++++ doc/code/renderer/images/demo_0.png | Bin 0 -> 7996 bytes doc/code/renderer/images/demo_1.png | Bin 0 -> 112898 bytes doc/code/renderer/images/demo_2.png | Bin 0 -> 13191 bytes doc/code/renderer/images/demo_3.png | Bin 0 -> 233896 bytes doc/code/renderer/images/demo_4.png | Bin 0 -> 13191 bytes doc/code/renderer/images/demo_5.png | Bin 0 -> 82570 bytes doc/code/renderer/images/demo_6.png | Bin 0 -> 167586 bytes doc/code/renderer/images/stresstest_0.png | Bin 0 -> 268551 bytes doc/code/renderer/images/stresstest_1.png | Bin 0 -> 526770 bytes 10 files changed, 172 insertions(+) create mode 100644 doc/code/renderer/demos.md create mode 100644 doc/code/renderer/images/demo_0.png create mode 100644 doc/code/renderer/images/demo_1.png create mode 100644 doc/code/renderer/images/demo_2.png create mode 100644 doc/code/renderer/images/demo_3.png create mode 100644 doc/code/renderer/images/demo_4.png create mode 100644 doc/code/renderer/images/demo_5.png create mode 100644 doc/code/renderer/images/demo_6.png create mode 100644 doc/code/renderer/images/stresstest_0.png create mode 100644 doc/code/renderer/images/stresstest_1.png diff --git a/doc/code/renderer/demos.md b/doc/code/renderer/demos.md new file mode 100644 index 0000000000..3bb7b266f3 --- /dev/null +++ b/doc/code/renderer/demos.md @@ -0,0 +1,172 @@ +# Interactive Demos & Stresstests + +openage builds contain several *interactive* renderer tech demo entrypoints that show of specific features. +These demos are also useful for learning the renderer API and for testing new functionality. In addition to +the demos, there are also stresstests that are used to test the performance of the renderer. +The source code for renderer demos and stresstests is located in [`libopenage/renderer/demo`](/libopenage/renderer/demo/). + +This documents describes the purpose of each demo and contains instructions on how to interact with them. + +1. [Demos](#demos) + 1. [Demo 0](#demo-0) + +## Demos + +### Demo 0 + +This demo shows the creation of a minmal renderer setup and the rendering of a simple mesh. + +The demo initializes a GUI application, a window, a renderer object, and a render pass. +It then loads a shader program and creates a single mesh object which is then rendered to the screen +using the shader program. + +The demo mostly follows the steps described in the [Level 1 Renderer - Basic Usage](level1.md#basic-usage) +documentation. + +```bash +./bin/run test --demo renderer.tests.renderer_demo 0 +``` + +**Result:** + +![Demo 0](/doc/code/renderer/images/demo_0.png) + + +### Demo 1 + +This demo shows how simple *textured* meshes can be created and rendered. It also demonstrates +how to interact with the window and the renderer using window callbacks. + +```bash +./bin/run test --demo renderer.tests.renderer_demo 1 +``` + +**Controls:** + +- LMB: Click on the textured meshes to get a debug message with the object ID. + +**Result:** + +![Demo 1](/doc/code/renderer/images/demo_1.png) + + +### Demo 2 + +In this demo, we show how animation and texture metadata files are parsed and used to +load and render the correct textures and animations for a mesh. + +```bash +./bin/run test --demo renderer.tests.renderer_demo 2 +``` + +**Controls:** + +- LMB: Click on the sprite to get a debug message with the object ID. +- : Go back one frame in the animation. +- : Advance the animation by one frame. + +**Result:** + +![Demo 2](/doc/code/renderer/images/demo_2.png) + + +### Demo 3 + +This demo shows a minimal setup for the [Level 2 Renderer](level2.md) and how to render objects +with it. The demo also introduces the camera system and how to interact with it. + +```bash +./bin/run test --demo renderer.tests.renderer_demo 3 +``` + +**Controls:** + +- W, A, S, D: Move the camera in the scene. +- `Mouse Wheel`: Zoom in and out. + +**Result:** + +![Demo 3](/doc/code/renderer/images/demo_3.png) + + +### Demo 4 + +This demos shows how animation frame timing works and how to control the animation speed +with the engine's internal clock. + +```bash +./bin/run test --demo renderer.tests.renderer_demo 4 +``` + +**Controls:** + +- Space: Pause/resume the clock. +- +, -: Increase/decrease the simulation speed. +- Return: Toggle between real time and simulation time. + +**Result:** + +![Demo 4](/doc/code/renderer/images/demo_4.png) + + +### Demo 5 + +This demo shows how to create [uniform buffers](level1.md#uniform-buffers) and how to use them to pass data to shaders. +Additionally, uniform buffer usage for the camera system is demonstrated. + +```bash +./bin/run test --demo renderer.tests.renderer_demo 5 +``` + +**Controls:** + +- W, A, S, D: Move the camera in the scene. + +**Result:** + +![Demo 5](/doc/code/renderer/images/demo_5.png) + + +### Demo 6 + +This demo shows how to use [frustum culling](level2.md#frustum-culling) in the renderer. + +```bash +./bin/run test --demo renderer.tests.renderer_demo 6 +``` + +**Controls:** + +- W, A, S, D: Move the camera in the scene. + +**Result:** + +![Demo 6](/doc/code/renderer/images/demo_6.png) + + +## Stresstests + +### Stresstest 0 + +This stresstest tests the performance when rendering an increasingly larger number of objects. + +```bash +./bin/run test --demo renderer.tests.stresstest 0 +``` + +**Result:** + +![Stresstest 0](/doc/code/renderer/images/stresstest_0.png) + +### Stresstest 1 + +This stresstest tests the performance when [frustum culling](level2.md#frustum-culling) is enabled and an increasingly larger +number of objects is rendered on the screen. + +```bash +./bin/run test --demo renderer.tests.stresstest 1 +``` + +**Result:** + +![Stresstest 1](/doc/code/renderer/images/stresstest_1.png) diff --git a/doc/code/renderer/images/demo_0.png b/doc/code/renderer/images/demo_0.png new file mode 100644 index 0000000000000000000000000000000000000000..6bcd90b06af8c3836a92ccd018917f1ca4e73a8e GIT binary patch literal 7996 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�TW1?zg8X6iAwJp%in9|n;1d>BKCF%*siZ(P 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 aeme}<&wc6l$b9AgFpX}R>Xqp@MgJcT?|i)g literal 0 HcmV?d00001 diff --git a/doc/code/renderer/images/demo_2.png b/doc/code/renderer/images/demo_2.png new file mode 100644 index 0000000000000000000000000000000000000000..17a90580f9d3f3f878967c1478c82130d1196333 GIT binary patch literal 13191 zcmcI~bzD^2`u8RTR1ic(35g>i0+P~g;Lr{wjYx=;(#?PZ2NjSR8iqjyhL9YjVL(b6 z8M+3gySv^!p8J08`JH<|_Z)A`A2Vy#Uh%Bw`}EqOn(9haXBf@^06+z~e+LQxq@(~q zB0xa~_LT4x4gr9$I^>Rmwg=JT01;WeJmsk_wH5#AjzCM_JCKl=onO<_k-(Re$_wE5 zKYlbLs#w6jhmQIKw$kK;Rs~7m&@&}tDH8C{=a!vqyOiJ)E7L(SJTcgDw2n>iyGYs7 zu28eqZ@skvcEOB}F&!0w0Sv)J^rTD==xL1-PZOrXHg?{;GGkQ?pbC)p9jqYvb-qJ9 zeB#7;eyPXCyQKX_ihDaF1_11a7jDS|$`nEAEtGX_8Ku{9IXBHUQ%=7;EzkQh<;$%) zDUP=fbViqr-FFuEOsT^|mVHs>#$)?_yK~Q%B|1HUWdF_$rkOlL(kIFsqI+K^eGy+$ z8%(Hg5P?JhKyLlC9Kv7i6(<+xN!;X;D`ScHINimkw%hz<1LAjq;eKzy9W~7>&3eX(x zcm?C)wC6-kJC&g%B_*qAeoT4BA08eqy1&2gC)zHjmaCeJyhO507swctoK2#rmUV?D z-p9?uWA~N$ZxrriV=x;HwY9Z8$k)RXdl#gXsC}uosC`S$q`o5JS)RaGpi8)Xoctm|Eq8+#{R6PmSM+!Q_nVq!bDcnFXKz>|-;xw(1iXte44 z$Ot#ZONaC0+lFLG{AW^i$v5A;dE>+LV_3>JPEO8hT$$(7XnBOz#Z|n}Tff<03`w#T zW|05xL{*KQ)sWG znZr7Mk=Q`>7sNoSwZH#Kzj!$6^$%9c!`Y*o?cZr;6lI?dls7Omuw#E{HkzZ)D~V`%T|Yiwm@Wnp78M^u%!2FUMxS@8d~qI}R*eJBuq z!}LnVa10US0Z2ToWMq5W%`&lWZf=gm%XEUsEE~ogr^huEyS>NS^ynaaE%8J=sm#kG zmXAbXONUq#Tpxfbt?cg)-L%c4UdqOhm=R8q)eV#j0jv zCMa&*(j_N#ELUXB(7%!aS_l<~^I}HUMNPIlS&Hx8-(V$z zpUKotQPxh&g8p*)jx;|1an3b)^+< zW_r4-t1HDNF(Dzpu<(15X7qAR4PankplNKJv+vZ_*7l}5-`UkQXl%@+r?)rLZ?V&N zV`gUN*=KU10^X!;U2W}*YH-CX%*=g9YWA*9K0a?EBhSx6O(W8f)sp0sdwcqO545yC zN?S$lu3Qa*V);*;0M}(+4=d;&CQ`-*GLs; zjw4P7V5w%dvGz@yn??QGri0&RW;?wr}o)YK_kTia4BR>{ftpDn>aUWO_wm9X%$|j$VMKPmKmVq)}}Pz)2!bFDfc(?&L(<-ri0@K~Ymz_hKJ= zfed%y!i6g=EPC1bl*JAX4sloV7ldFF-kx9a_&iF_%op();(n(2yPU z6#N<*`rs0=mX_Ajb*mJnix*RMUx|r|n$OMk!w7q*?Q0=hDxiC@Nq%oJ*(x{W#Jc^s2KE)Tqhj6>dD741O@Lt zJpZ}nQn;m#ilX9YtZXtXMc_SJVr<{J4k8^Log^*~x!O)uvN+p>rp4*JtDDD2Z~$v5 zo~5*`>@-wU6ZZMDBANZ+#}{97a`4DuhljyTmtTL zu##$ru%D)2XA5Pe$W$Y~P8-BSn(`8+sHgxi(VV(SLBrh@P`7m;l5te&RfcZ5pVHx!sF;y1chnCL=8kDv0r5C9B^!Nl|gJ z0h!T>x38M5*fGOuvY^JwrCn0lR18U`Y17xQU%M1)c260Tj*gCkQfIZaw8Z(6=5BE6 z^~bIf!@M6^a_MX0U=ft7qii2kfJBJ-mZR4K6Sen%{#duhNJa3HZ_*zNI(*@B0 z##`S#+q$&YRu&g2FMZ3-=5dCdDFoHhntpP^_N8`SfIH>xV;i6QW?Uq>+c9#DcI`K zY46^#QIN^X%91@Ve)sO3P^4twLV7x9Yv8t>Tuhc|zzwX{Am@{r*Y}x?Oie>90D;TP z##C*YC?t};lm#?x3jO1|yK#L^f%iyhS}z7NGBP4|6IvX6eXEA4yyvJXDcRTAjy{Qx zO)2c$W}dP72y=CIew>(?I7^+vrl82`RBg`sl7_uyE~@q1ZuZHEgod27#G2gE>Q^Zd z5+fs;L-#v=^i9`xBlaVK4)&Kciaz4n$_EDrR0Ce*4`dD2o0Cw<$qbuYThXHzPq#p> zbY-;{WVDbh4c4sX@;q~LQhY6^Kwoh=f0V_4qw^B6_B19gDT%VlStg?kbN4$avI9D` zkR{t26_s2t_z7qQSIFJyIqrYIon`yHiyo*a%ADRvHa0PFlIcxO>=ZPhbwT7$kcP72 zGgIDgoMyX2#YI}0H=`eoj+WJvyRT}I)8kiKwYTR<3p{vN3(c$-r1!g+aU10H%M|0~ z@85q}yq;fGWk1de6%!OZKTLw^J1X?tSzBAXOe8_nzqG_RfBZd`2B- zw5c1zcyQ+%Pwjp)OUob_y@IU6|2o zX=oHF*eEjk`uiU|Qz}C(y6;LzOEWQ}?;hQf)?kzsQ%YW0S^^!=t)6AmH`na!>;}Gk zxu4O~ZKbr}uA*`_XLzhpk81bcTa83%%%t`Ky?YIL)FgyB>h!fjzHeh=$xF-2LyL!FrH`|-Fyy@5#(qeD3L4Gj&H^<)tJz>`VDQVgI_onPcGxejI<5|~ z)Q5?kWT;Ewj}w*Q2B7)9gwmj3dwTI$qc!bS7?qvnH94rGO3g3&HJ6@+>~}n5$3IH$ zk3hSXp|%x3w$m6I8opDj%Fb>DgNP;J+f5{VQ>0#`Q7-41n3#6y+}^=e1&j%TMPW5L z7x#U}TZ-y_!UpEXB>5=>JpSmt&64MlpF!J)$g<-XQi~a_-Ctnf*hrdUj*-E^f-5gx zya-YyBkaRm)2-0-MEg_pGfDD-^GbeCEC{G)5M8mAw;3C58tg2tqj@gJC`c!{Ewu9lrZ9d(EKBHza#WN zZ~m`{{@uF&Bl=+o`oClNpD^No0NMZk^uLMj@AmocH&ZTy8CvxG;{Ek)b}8s|i}oHw zT?GgPg&gb}W8Z8Mjxcr+w)(Q`94%aDF?SYrUiKE30wT&!xNsrCUmoD+(5?0uj~=3@2p3iu*Yrr zvBkr)R0sZfj{53lJX%v(@`k;h_uzgEvnQX1S8~(7Y}Vcw4YOk-&{ZkhtOT8Hzbq}t zU-?bJrb147HrV>`$gkv(cS<&9@rBr~bf~-*Le0|3KGIXgg&-!t&~@ZUS*l^+;57m{*;VAU5ofY=77)2@mQ4&SvJp7= zM=JgS+fCw{Of73i_~6AjeA4{N6;*dyL4SQ`YY;ro1KkuxRDu?UpG!lW@Hy|sEn!L-sns;UO2!(aB!4DY? zIlOyBQ##iu<+Qt6AhH%+Vw7-}86$@f$j!oXd46D(e_bWh?5#D6S?L^`IpzpcWf@U* ze=N*#PDe+!N&r!(6TL(Xu59KAKS=Gun#+SHEzbJ(EULXWe~&Ui=7s7PudI=n?ZB&Q zvX9-E)RvtM8M3rKB3$O#b&gK~-amx(cXIou&8kC3phq842v_9aZyB@Z9qm#Hjeo>% zjrUu_vZy2#6!B^43bhhe#`>~``{_}-8ow2G#0|F;8G~D7W*AYrWDtj zA9?=Y9NIX`;_#^`lO4~QHN5YmX(WBv05jrZVOmWg<0WHYDWXl)3 z@Se$v=NVI7{k7L2WAT@lZ(!OEuxhz!%`?s&&dDT!k9Ly}iq3(;^cVHeQV$q!>HSnW zy=9H7UsRPlxO^EpAK_@)!GiJ-N$aC)M8lKRgsD^cT*Em$^&KeXVsBX2UC8b*hht+c z1W(B>MmQ}>U?a_0wkm8!Gh;ceBhX*^>~zsphnnzDJ9{%ypu)mf^OO6lkC$xBpFfh- zs6Lv$I21~RU2UdL575eVlI+J$K&qw1g7vs07?Yp8hRE_X+jBpgRa~Dkjhr?(M~v1; z?|BM$REeGf#{X#)@XyZ*JGsjbRN{@UMy&5XiK89W9>ga?&{^FvOJA?^T)v6!*>4mO zdnx%Ea%=)`xVW3LGvc+F9J2ByY`pqfMCGQ=miMxHd@s9P)Q#-HocT(T=Cz{980Wza zF@N6I7(;HS#ekyu9u)+p3HK%FwnX%#UCYO1O7OHb_LqJ;>2k){cYgDZkb!c*u^U&&n0QB< zTQu!x7NSgD%wBR+Q?6+_Q6n?EBn)-*unJkso4ZZT>^<{6B`Z6sbm9Bh$o1py7O1jX zw%6)@9S`c+J7yEr_-Jhj1m`?2heTuy?~#dnW<`7-ukrnILqyTkV!>p<1h;5XXFNNP zjTHyMvatwcB`N2sz3cB!+yReB>^vhmxIwQott-V{fFACQ91JR&>pVFKLUM08VDq{;c))9a5(UN^6e&>Zg_u|z?6&MuIf?#^L%FL1j{HowNTg|hFY zbGZBY^Oqb|I1D+N8CYBS#-*w=s#o#uz2eunV2ZebCiy7_=ANwrR*zl$&-CiZcpTR6 zCXadDx*u>93ny`8Obd1BZ8kzD7LOdq#EX17{Fw9cYSPP-ueT$TI_HP1l#K?&+B~Eo zBr2KCLR5?XwljkpE4keygd;O!byg#~9}Pu@ZKz zBCjHVPusa=f68jv>1a>wS(iZ^e&Z?WuF2Hi zdo}V9cqa6hJkRf3)234ukr~{ABdvVPY$JWHR-_B1Dm<2yuFa(*sS_>QZlXNRoDDVK zR*rrOwG>N4CAN<>>_yG;t8yW|7JQ%@(3Fb!+P3I(@K-md)QrvM4dBRM!ceFUnoGYs&wN=H7G1@ zrg5Hpr(!8ww`bYPnz~pzB|l>>qn0g+yisMsm6J&fRG4sST={tweKOlJFOZmM$i2mF z_+dmMK0Oqbi6{x$2?`6#d4WGBLHmElZFf*>r6gN%Q?*m=m(6wgD7SBWm#Guz2Zo0c zb~zf*r#6pyn(iS)3Iya5H!Hq%I!Gr|-uuwZm8xv(HL6Z`*@^@Z|9hnNr&eSfJ9i2& z*lxuf9a>>yIdexU<34O0Ma9kSbT7F?;#?X?y;t^uS@Ks zA_W&nKq9r?_b;SpWkoTBHJ16<+_E=#cqKK|RxoAPa+Jxz+CW3=DZZ#<^aCWpz!Scl>!LN!3-{DY`03mOyczih%)5@*UaHnv6*|%rslmmj7yWC?#{ z*wSjGXpUpJ+q74)mo>7)r}jFP%-b8Lk&JaJh$PkcDRwmcBN@Q;XGy~EH2X(WzWXd7 zQZ%wO*?Z)mY@C@@WMib`*q{8!nvPIVI1)2QzozX(G=$uetNKhkJ7i=FHbFQ0Mu44)XF4wA6)8 zspj$%5IcN?tq@KN-cmkDCLJZ2~i#9^dB|&9KO&{4~-7mdcl9VX?hqM zC54=9ATo$NLFYX)W9lYM#B}Xy+8!Dmk)&v+QE7a8~ z>&b+g=V)0bY^$QTIz$9B7TVHA?6?LOUAu!FN|dvI1ektKT^iVB^qrFa=5JZbVoRki zmcldq_Nk!=!->i#woy>*Vd`zq8MW6ow{_Bd3APHxMG8&3pQkmYIV2q4M<0QjW`pxvtaes^EtynsqgyYLACXfiq#r-J^&j&W-q@EvkOGMM#| zT>?pVvma+vZJPikAL~}T2I8x2WA0ask{R_=e$#}rRWP%+j-7qODN$Sy4b4L>alPW)0*lNx5p`=>UD$v{y+?@)R`%ux7<}FNWNHD(8T0t4^kL%q`@A>Aw zgR3`WHf%16FxS%uTC_?57HsSvpoo5C*oT?52^5)cqgw?%!iUDw5;7?PVyKBn42Ti=Tsd=&6*-#+Kyr2d-0$>_r*6r4KZ) zTSF}@ANj7uCa)EDs=q7iRhQil*+Jix5*$B#5(jnk9}{S0kx{;lhEBQs8xfvJ|5;!} zUKEWeN;7m7>YtmwmoG7*p6QifX9s#<^ew{)8;5GoTWMr3;wh)CaKVERu^nk2_E_?| zDDhHwW1}nbn@bY(a(H;5k3|#|q2SBetel_^N&x8X2m}@{&TTtexUq=^Iur6R59xL( zmhw`}u+wn&aoe^ex9h=GG2_HUT<}qyLiLfD+)dE%TX0|__0j^6S5{a1yp*fv%IYfj zSg3F{{i!W;Mwfr;kn;RM*9Zwj9o^N7R@Y$v06M7Euqw)%+BS8m$yWA4Enz8Rxghqm z2ne!VJv-R&gLJu*M4A;DM*m@4>(*k+{Jn{YSQYYn`hNS@(}KwtFc9YZWzj%a94i3m zsB`qEcP%WmdxK7xWakhOG_qZBRP@MMvboN1q3nFmOjBkE&S>SPiAksL4oWRkMXwJd zGt#nUw>RIL&cY8uSiPdJK>|X@Lkx)Z{fv752za_$w@82tgzCa!!~m13(^`$L@{<0F zanj&2rIr~qt)krB#bYS5g8NyTSj+Uhh}xUW^N--Kxl0&+pwpfd}#HNJVgFVGTl)+)Zo{b^szRrln2 zL-he_M#b+#2VN~cj2-m}GmL>7Qxl@jtR%>7o~_-L^sbS|I*qPkM~|Yp=nm3d3Ep=x zM@nlk1ypl3Q={cabEvELD~ho>3Dah7nbtm9a^$AC z(?HEKB{85mrFw>HVPrdbMm2b_A$k2m6#zudPM#)&Y<~>P{uQe>3lE5D2o;jTIOF*O zjkwCqZqo#2m4IV|X!P+Nu`1h&dV8FWtdBRG(*dcDJ4dJ>z;0wis&7>cNG+_HLY#O{ zfjOJgWb7CtDRi44vb68}ote$z%-U2?!oak`NfXFE49EwhN+lRAvvJ z^-t5XKT3KH=EFIlM%I0E+Gzc2fXLQ(&eB-j+7pg;yBOb?ENMdB8W@o3#Dn*}nRt^u zOziZ&Wwz9#zp&G9S0O*F ziS~Xt1+nVqr&%BWkB|FPNyrD^Z-q4*@>L*4$>n|oIs;r;V2S1DYb<}d)qgVJFGBcl zYVN;~{{MvN-|BX4%nG0k61?}nxaENP50y<^LI<$I>Mjpo*s}43vMAXmE|mvyQ(C^_ zNx!ptV0Pb;U&MbU!j9;-rT(Ik{^ySPN4of@#nOLsz(3#eztsD0()6d(#@YVimR4Sa z1cYoc4?W0BvFXFt2O3RARH9()Qr>OI)1?u`sMoZ=(6&H>Zc%@4*) z!aGUi6C^2ou5Ng?H3_b)8MBx>#E6-JFW|$j5%tHpwpq4$wnYe{f1!|W^P&0TwbteM zxcUyw?!^b=;?t}Ym4t#`pgbey9;NtQnq6k^tu2i)T5(y=F1YN@VVB33&Z0GJ;}Dxw zC`c1f{PnRxo3Eqas|6W~qL32O<^4lO;;jb(Dnpytv`;&mZZOtW25=k&v8TUZxJ1Ji zl=&)!zq5r|7}beHP98VyBrEt7u)9tkaPgGWm4}ZxBPiRj@cjB04o;C@t(e*C0XO=W>^nw4ykaP9{wY4~$b5?SbfwP}byB}iD$?G3Sw!Byg? zt@S7Cx9pE`Gd`VQfeiqvC`lqvEGvAxxZ0GlL;ePtJi_vu^Y#JrD_z#2hnO8+jeb)q zuy1w?zqRi}6Nl9!U;{ha4Bes%tidu8#>+G9@)Lf4|G@%u9cTiz|dLeh!@8l|) G1^z#*=R$D+ literal 0 HcmV?d00001 diff --git a/doc/code/renderer/images/demo_3.png b/doc/code/renderer/images/demo_3.png new file mode 100644 index 0000000000000000000000000000000000000000..27af7e8dfb1b1090eada2a177a319518e7022b5b GIT binary patch literal 233896 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

eqfc)lIsVWK&QNo=5{a3TwJ>eDIH4 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

=602YB{H~9e|y3=Uf9E95@>KQ3w^+7Gl7J2rgq~ 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 We{PpR!2A!!t*7?xfE=We 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 literal 0 HcmV?d00001 From b9a7c4cb1d279dca06476e779d53f5874f92feb9 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 28 Jul 2024 17:52:04 +0200 Subject: [PATCH 464/771] convert: Fix conversion for DE2 update 118476. --- .../conversion/de2/nyan_subprocessor.py | 3 + .../processor/conversion/de2/processor.py | 5 +- .../conversion/de2/tech_subprocessor.py | 6 +- .../de2/upgrade_resource_subprocessor.py | 102 +++++++++++++++++- .../conversion/de2/internal_nyan_names.py | 35 +++++- .../read/media/datfile/lookup_dicts.py | 2 + .../value_object/read/media/datfile/unit.py | 5 +- 7 files changed, 151 insertions(+), 7 deletions(-) diff --git a/openage/convert/processor/conversion/de2/nyan_subprocessor.py b/openage/convert/processor/conversion/de2/nyan_subprocessor.py index 4474751136..b688cc3e7c 100644 --- a/openage/convert/processor/conversion/de2/nyan_subprocessor.py +++ b/openage/convert/processor/conversion/de2/nyan_subprocessor.py @@ -446,6 +446,9 @@ def building_line_to_game_entity(building_line: GenieBuildingLineGroup) -> None: abilities_set.append(AoCAbilitySubprocessor.terrain_requirement_ability(building_line)) abilities_set.append(AoCAbilitySubprocessor.visibility_ability(building_line)) + if building_line.get_head_unit()["speed"].value > 0: + abilities_set.append(AoCAbilitySubprocessor.move_ability(building_line)) + # Config abilities if building_line.is_creatable(): abilities_set.append(AoCAbilitySubprocessor.constructable_ability(building_line)) diff --git a/openage/convert/processor/conversion/de2/processor.py b/openage/convert/processor/conversion/de2/processor.py index d78fb218d9..19001b9087 100644 --- a/openage/convert/processor/conversion/de2/processor.py +++ b/openage/convert/processor/conversion/de2/processor.py @@ -312,7 +312,10 @@ def create_extra_building_lines(full_data_set: GenieObjectContainer) -> None: process. :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer """ - extra_units = (1734,) # Folwark + extra_units = ( + 1734, # Folwark + 1808, # Mule Cart + ) for unit_id in extra_units: building_line = GenieBuildingLineGroup(unit_id, full_data_set) diff --git a/openage/convert/processor/conversion/de2/tech_subprocessor.py b/openage/convert/processor/conversion/de2/tech_subprocessor.py index 2f2aaf9e76..c9c3a71c17 100644 --- a/openage/convert/processor/conversion/de2/tech_subprocessor.py +++ b/openage/convert/processor/conversion/de2/tech_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals,too-many-branches @@ -155,7 +155,11 @@ class DE2TechSubprocessor: 267: DE2UpgradeResourceSubprocessor.forager_wood_gather_upgrade, 268: DE2UpgradeResourceSubprocessor.resource_decay_upgrade, 269: DE2UpgradeResourceSubprocessor.tech_reward_upgrade, + 272: DE2UpgradeResourceSubprocessor.cliff_defense_upgrade, + 273: DE2UpgradeResourceSubprocessor.elevation_defense_upgrade, 274: DE2UpgradeResourceSubprocessor.chieftains_upgrade, + 280: DE2UpgradeResourceSubprocessor.conversion_range_upgrade, + 282: DE2UpgradeResourceSubprocessor.unknown_recharge_rate_upgrade, } @classmethod diff --git a/openage/convert/processor/conversion/de2/upgrade_resource_subprocessor.py b/openage/convert/processor/conversion/de2/upgrade_resource_subprocessor.py index a394061fa1..9abf477831 100644 --- a/openage/convert/processor/conversion/de2/upgrade_resource_subprocessor.py +++ b/openage/convert/processor/conversion/de2/upgrade_resource_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 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 # @@ -123,6 +123,31 @@ def cliff_attack_upgrade( return patches + @staticmethod + def cliff_defense_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the cliff defense multiplier effect (ID: 272). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: MemberOperator + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + @staticmethod def conversion_min_adjustment_upgrade( converter_group: ConverterObjectGroup, @@ -258,6 +283,31 @@ def conversion_building_chance_upgrade( return patches + @staticmethod + def conversion_range_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the conversion range modifer (ID: 280). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: MemberOperator + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + @staticmethod def cuman_tc_upgrade( converter_group: ConverterObjectGroup, @@ -441,6 +491,31 @@ def elevation_attack_upgrade( return patches + @staticmethod + def elevation_defense_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the elevation defense multiplier effect (ID: 273). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: MemberOperator + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + @staticmethod def feitoria_gold_upgrade( converter_group: ConverterObjectGroup, @@ -851,6 +926,31 @@ def trade_food_bonus_upgrade( return patches + @staticmethod + def unknown_recharge_rate_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the unknown recharge rate bonus effect (ID: 282). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: MemberOperator + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + @staticmethod def workshop_food_gen_upgrade( converter_group: ConverterObjectGroup, diff --git a/openage/convert/value_object/conversion/de2/internal_nyan_names.py b/openage/convert/value_object/conversion/de2/internal_nyan_names.py index 6d1fe7f380..30f9826ba3 100644 --- a/openage/convert/value_object/conversion/de2/internal_nyan_names.py +++ b/openage/convert/value_object/conversion/de2/internal_nyan_names.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=line-too-long @@ -51,6 +51,13 @@ 1790: ("Centurion", "centurion"), 1795: ("Dromon", "dromon"), + # TMR + 1800: ("CompositeBowman", "composite_bowman"), + 1803: ("Monaspa", "monaspa"), + 1811: ("WarriorPriest", "warrior_priest"), + 1813: ("Savar", "savar"), + 1817: ("QizilbashWarrior", "qizilbash_warrior"), + # TODO: These are upgrades 1737: ("EliteUrumiSwordsman", "elite_urumi_swordsman"), 1743: ("EliteChakramThrower", "elite_chakram_thrower"), @@ -60,6 +67,9 @@ 1761: ("EliteRRatha", "elite_rratha"), 1792: ("EliteCenturion", "elite_centurion"), 1793: ("Legionary", "legionary"), + + 1802: ("EliteCompositeBowman", "elite_composite_bowman"), + 1805: ("EliteMonaspa", "elite_monaspa"), } # key: head unit id; value: (nyan object name, filename prefix) @@ -75,6 +85,9 @@ # DOI 1754: ("Caravanserai", "caravanserai"), + + # TMR + 1808: ("MuleCart", "mule_cart"), } # key: (head) unit id; value: (nyan object name, filename prefix) @@ -91,6 +104,8 @@ # key: head unit id; value: (nyan object name, filename prefix) # contains only new techs of DE2 TECH_GROUP_LOOKUPS = { + 46: ("Devotion", "devotion"), + 488: ("Kamandaran", "kamandaran"), 678: ("EliteKonnik", "elite_konnik"), 680: ("EliteKeshik", "elite_keshik"), @@ -139,11 +154,23 @@ 840: ("EliteGhulam", "elite_ghulam"), 843: ("EliteSHrivamshaRider", "elite_shrivamsha_rider"), + 875: ("Gambesons", "gambesons"), + # ROR 882: ("EliteCenturion", "elite_centurion"), 883: ("Ballistas", "ballistas"), 884: ("Comitatensis", "comitatensis"), 885: ("Legionary", "legionary"), + + # TMR + 918: ("EliteCompositeBowman", "elite_composite_bowman"), + 920: ("EliteMonaspa", "elite_monaspa"), + 921: ("Fereters", "fereters"), + 922: ("CilicianFleet", "cilician_fleet"), + 923: ("SvanTowers", "svan_towers"), + 924: ("AsnuariCavalry", "asnuari_cavalry"), + 929: ("FortifiedChurch", "fortified_church"), + 967: ("EliteQizilbashWarrior", "elite_qizilbash_warrior"), } # key: civ index; value: (nyan object name, filename prefix) @@ -170,6 +197,10 @@ # ROR 43: ("Romans", "romans"), + + # TMR + 44: ("Armenians", "armenians"), + 45: ("Georgians", "georgians"), } # key: civ index; value: (civ ids, nyan object name, filename prefix) @@ -177,7 +208,7 @@ GRAPHICS_SET_LOOKUPS = { 0: ((0, 1, 2, 13, 14, 36), "WesternEuropean", "western_european"), 4: ((7, 37), "Byzantine", "byzantine"), - 6: ((19, 24, 43), "Mediterranean", "mediterranean"), + 6: ((19, 24, 43, 44, 45), "Mediterranean", "mediterranean"), 7: ((20, 40, 41, 42), "Indian", "indian"), 8: ((22, 23, 32, 35, 38, 39), "EasternEuropean", "eastern_european"), 11: ((33, 34), "CentralAsian", "central_asian"), diff --git a/openage/convert/value_object/read/media/datfile/lookup_dicts.py b/openage/convert/value_object/read/media/datfile/lookup_dicts.py index 80b0dd5955..7f52816dff 100644 --- a/openage/convert/value_object/read/media/datfile/lookup_dicts.py +++ b/openage/convert/value_object/read/media/datfile/lookup_dicts.py @@ -431,6 +431,7 @@ 149: "SHEAR", 150: "REGENERATION", 151: "FEITORIA", + 153: "RESOURCE_FOLLOW", 154: "LOOT", # Chieftains tech; looting on killing villagers, monks, trade carts 155: "BOOST_MOVE_AND_ATTACK", 768: "UNKNOWN_768", @@ -752,6 +753,7 @@ 0x04: "CAVALRY", 0x07: "SWGB_NO_JEDI", 0x08: "MONK", + 0x09: "DE2_FORTIFIED_CHURCH", 0x0b: "NOCAVALRY", 0x0f: "ALL", 0x10: "SWGB_LIVESTOCK", diff --git a/openage/convert/value_object/read/media/datfile/unit.py b/openage/convert/value_object/read/media/datfile/unit.py index c0cdc29172..ae0e7c5f99 100644 --- a/openage/convert/value_object/read/media/datfile/unit.py +++ b/openage/convert/value_object/read/media/datfile/unit.py @@ -1,4 +1,4 @@ -# Copyright 2013-2023 the openage authors. See copying.md for legal info. +# Copyright 2013-2024 the openage authors. See copying.md for legal info. # TODO pylint: disable=C,R,too-many-lines from __future__ import annotations @@ -695,7 +695,8 @@ def get_data_format_members( if game_version.edition.game_id == "AOE2DE": data_format.extend([ - (READ_GEN, "drop_sites", StorageType.ARRAY_ID, "int16_t[3]"), + (READ, "drop_sites_count", StorageType.INT_MEMBER, "int16_t"), + (READ_GEN, "drop_sites", StorageType.ARRAY_ID, "int16_t[drop_sites_count]"), ]) else: data_format.extend([ From 0e6fe2d7bf332773f3e92a6eb3649ab9f3d4fb3b Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 28 Jul 2024 18:06:06 +0200 Subject: [PATCH 465/771] convert: Update DE2 modpack version. --- .../convert/processor/conversion/de2/modpack_subprocessor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openage/convert/processor/conversion/de2/modpack_subprocessor.py b/openage/convert/processor/conversion/de2/modpack_subprocessor.py index 810ff96752..5af6fdf8b5 100644 --- a/openage/convert/processor/conversion/de2/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/de2/modpack_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-few-public-methods @@ -40,7 +40,7 @@ def _get_aoe2_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("de2_base", "0.5.1", versionstr="1.0c", repo="openage") + mod_def.set_info("de2_base", "0.6.0", versionstr="Update 118476+", repo="openage") mod_def.add_include("data/**") From 3f32291697372e145c49c2ea40f3b3d1c19d4dd8 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 28 Jul 2024 18:06:23 +0200 Subject: [PATCH 466/771] convert: Remove old changelog code. --- openage/convert/service/init/CMakeLists.txt | 1 - .../service/init/api_export_required.py | 2 +- openage/convert/service/init/changelog.py | 69 ------------------- openage/convert/tool/driver.py | 10 +-- openage/testing/testlist.py | 1 - 5 files changed, 6 insertions(+), 77 deletions(-) delete mode 100644 openage/convert/service/init/changelog.py diff --git a/openage/convert/service/init/CMakeLists.txt b/openage/convert/service/init/CMakeLists.txt index 3c7e404c69..aa588dcb29 100644 --- a/openage/convert/service/init/CMakeLists.txt +++ b/openage/convert/service/init/CMakeLists.txt @@ -1,7 +1,6 @@ add_py_modules( __init__.py api_export_required.py - changelog.py conversion_required.py modpack_search.py mount_asset_dirs.py diff --git a/openage/convert/service/init/api_export_required.py b/openage/convert/service/init/api_export_required.py index 90b78309d7..8bb3a51850 100644 --- a/openage/convert/service/init/api_export_required.py +++ b/openage/convert/service/init/api_export_required.py @@ -21,7 +21,7 @@ def api_export_required(asset_dir: UnionPath) -> bool: """ - Returns true if the openage nyan API modpack cannot be found. + Returns true if the openage nyan API modpack cannot be found or is outdated. TODO: Remove once the API modpack is generated by default. diff --git a/openage/convert/service/init/changelog.py b/openage/convert/service/init/changelog.py deleted file mode 100644 index 0c63aac864..0000000000 --- a/openage/convert/service/init/changelog.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2015-2022 the openage authors. See copying.md for legal info. - -""" -Asset version change log - -used to determine whether assets that were converted by an earlier version of -openage are still up to date. -""" -from __future__ import annotations -import typing -from ....log import warn -from ....testing.testing import TestError - -# filename where to store the versioning information -ASSET_VERSION_FILENAME = "asset_version" - -# filename where to store the gamespec version hash -GAMESPEC_VERSION_FILENAME = "gamespec_version" - -# available components for reconversion -COMPONENTS = { - "graphics", - "sounds", - "metadata", - "interface", -} - -# each line represents changes to the assets. -# the last line is the most recent change. -CHANGES = ( - {"graphics", "sounds"}, - {"sounds"}, - {"graphics"}, - {"interface"}, - {"interface"}, - {"metadata"}, - {"metadata"}, - {"graphics"}, -) - -# the current version number equals the number of changes -ASSET_VERSION = len(CHANGES) - 1 - - -def changes(asset_version: int) -> set: - """ - return all changed components since the passed version number. - """ - if asset_version >= len(CHANGES): - warn("asset version from the future: %d", asset_version) - warn("current version is: %d", ASSET_VERSION) - warn("leaving assets as they are.") - return set() - - changed_components = set() - - # TODO: Reimplement with proper detection based on file hashing - - return changed_components - - -def test() -> typing.NoReturn: - """ - verify only allowed versions are stored in the changes - """ - for entry in CHANGES: - if entry > COMPONENTS: - invalid = entry - COMPONENTS - raise TestError(f"'{invalid}': invalid changelog entry") diff --git a/openage/convert/tool/driver.py b/openage/convert/tool/driver.py index ba974f42e8..cdd1ed04a3 100644 --- a/openage/convert/tool/driver.py +++ b/openage/convert/tool/driver.py @@ -16,7 +16,6 @@ from ..service.debug_info import debug_gamedata_format from ..service.debug_info import debug_string_resources, \ debug_registered_graphics, debug_modpack, debug_execution_time -from ..service.init.changelog import (ASSET_VERSION) from ..service.read.gamedata import get_gamespec from ..service.read.palette import get_palettes from ..service.read.register_media import get_existing_graphics @@ -40,8 +39,6 @@ def convert(args: Namespace) -> None: # clean args (set by convert_metadata for convert_media) del args.palettes - info(f"asset conversion complete; asset version: {ASSET_VERSION}", ) - def convert_metadata(args: Namespace) -> None: """ @@ -105,8 +102,11 @@ def convert_metadata(args: Namespace) -> None: ModpackExporter.export(modpack, args) debug_modpack(args.debugdir, args.debug_info, modpack) - export_end = timeit.default_timer() - info("Finished modpack export (%.2f seconds)", export_end - export_start) + export_end = timeit.default_timer() + info("Finished export of modpack '%s' v%s (%.2f seconds)", + modpack.info.packagename, + modpack.info.version, + export_end - export_start) stages_time = { "read": read_end - read_start, diff --git a/openage/testing/testlist.py b/openage/testing/testlist.py index 37f323e8a8..8227537697 100644 --- a/openage/testing/testlist.py +++ b/openage/testing/testlist.py @@ -26,7 +26,6 @@ def tests_py(): yield "openage.assets.test" yield ("openage.cabextract.test.test", "test CAB archive extraction", lambda env: env["has_assets"]) - yield "openage.convert.service.init.changelog.test" yield "openage.cppinterface.exctranslate_tests.cpp_to_py" yield ("openage.cppinterface.exctranslate_tests.cpp_to_py_bounce", "translates the exception back and forth a few times") From 6d15023893177621941c910724571aad774f2e49 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 28 Jul 2024 18:18:23 +0200 Subject: [PATCH 467/771] main: Make modpack selection more reliable. --- openage/convert/service/init/modpack_search.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openage/convert/service/init/modpack_search.py b/openage/convert/service/init/modpack_search.py index ea8bf87c24..8d89b73da1 100644 --- a/openage/convert/service/init/modpack_search.py +++ b/openage/convert/service/init/modpack_search.py @@ -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. """ Search for and enumerate openage modpacks. @@ -86,17 +86,21 @@ def query_modpack(proposals: set[str]) -> str: Query interactively for a modpack from a selection of proposals. """ print("\nPlease select a modpack before starting.") - print("Insert the index of one of the proposals (Default = 0):") + print("Enter the index of one of the proposals (Default = 0):") proposals = sorted(proposals) for index, proposal in enumerate(proposals): print(f"({index}) {proposal}") user_selection = input("> ") - if user_selection.isdecimal() and int(user_selection) < len(proposals): - selection = proposals[int(user_selection)] + if user_selection == "": + selection = proposals[0] else: - selection = proposals[0] + while not (user_selection.isdecimal() and int(user_selection) < len(proposals)): + print(f"'{user_selection}' is not a valid index. Please try again.") + user_selection = input("> ") + + selection = proposals[int(user_selection)] return selection From 2d38e8a177df37fca8ff42d6ee8f6e5eff965a9f Mon Sep 17 00:00:00 2001 From: fabiobarkoski Date: Thu, 25 Jul 2024 13:16:53 -0300 Subject: [PATCH 468/771] docs: Add note about linuxmint issue on ubuntu dependencies --- doc/build_instructions/ubuntu.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/build_instructions/ubuntu.md b/doc/build_instructions/ubuntu.md index 08fc635abb..d46ebd159f 100644 --- a/doc/build_instructions/ubuntu.md +++ b/doc/build_instructions/ubuntu.md @@ -15,3 +15,6 @@ from pip: ``` pip3 install cython --break-system-packages ``` + +# Linux Mint Issue +Linux Mint has a [problem with `toml11`](https://github.com/SFTtech/openage/issues/1601), since CMake can't find it. To solve this, download the [toml11.zip](https://github.com/SFTtech/openage/files/13401192/toml11.zip), after, put the files in the `/usr/lib/x86_64-linux-gnu/cmake/toml11` path. (if the `toml11` directory doesn't exist, create it) From 2fd354decaa726dd7f7c6a4b44809f105abc9cec Mon Sep 17 00:00:00 2001 From: Christoph Heine <6852422+heinezen@users.noreply.github.com> Date: Wed, 31 Jul 2024 08:32:24 +0200 Subject: [PATCH 469/771] doc: Correct usage of `--palettes-path` argument --- doc/convert/convert_single_file.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/convert/convert_single_file.md b/doc/convert/convert_single_file.md index f8c7f16d31..630dbb869f 100644 --- a/doc/convert/convert_single_file.md +++ b/doc/convert/convert_single_file.md @@ -7,27 +7,27 @@ The invocation could be: SLPs in DRS archives (for older versions of Age of Empires 1, Age of Empires 2 and SWGB): ``` -python3 -m openage convert-file --palettes-path ~/games/aoe2/Data/interfac.drs --drs ~/games/aoe2/Data/graphics.drs 326.slp /tmp/rofl.png +python3 -m openage convert-file --palettes-path ~/games/aoe2/Data/ --drs ~/games/aoe2/Data/graphics.drs 326.slp /tmp/rofl.png ``` Standalone SLPs (Age of Empires 1: DE and Age of Empires 2: HD): ``` -python3 -m openage convert-file --palette-file ~/games/aoe2hd/Data/50500.bina 326.slp /tmp/rofl.png +python3 -m openage convert-file --palettes-path ~/games/aoede/Assets/Palettes 326.slp /tmp/rofl.png ``` Standalone SLDs (Age of Empires 2: DE): ``` -python3 -m openage convert-file --player-palette-file ~/games/aoe2de/Data/playercolor_blue.pal --palette-file ~/games/aoe2de/Data/b_west.pal u_elite_eagle.sld /tmp/rofl.png +python3 -m openage convert-file --palettes-path ~/games/aoe2de/Data/ u_elite_eagle.sld /tmp/rofl.png ``` Standalone SMXs (Age of Empires 2: DE): ``` -python3 -m openage convert-file --player-palette-file ~/games/aoe2de/Data/playercolor_blue.pal --palette-file ~/games/aoe2de/Data/b_west.pal u_elite_eagle.smx /tmp/rofl.png +python3 -m openage convert-file --palettes-path ~/games/aoe2de/Data/ u_elite_eagle.smx /tmp/rofl.png ``` Standalone SMPs (Age of Empires 2: DE): ``` -python3 -m openage convert-file --player-palette-file ~/games/aoe2de/Data/playercolor_blue.pal --palette-file ~/games/aoe2de/Data/b_west.pal u_elite_eagle.smp /tmp/rofl.png +python3 -m openage convert-file --palettes-path ~/games/aoe2de/resources/_common/palettes u_elite_eagle.smp /tmp/rofl.png ``` WAVs in DRS archives (for older versions of Age of Empires 1, Age of Empires 2 and SWGB): From 612544aa27842df033cd181eee8fd91efeaa50a8 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 28 Jul 2024 19:05:10 +0200 Subject: [PATCH 470/771] renderer: Reduce calls to shared_from_this. --- libopenage/renderer/opengl/shader_program.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/renderer/opengl/shader_program.cpp b/libopenage/renderer/opengl/shader_program.cpp index cf59be1edb..5725b9b088 100644 --- a/libopenage/renderer/opengl/shader_program.cpp +++ b/libopenage/renderer/opengl/shader_program.cpp @@ -324,12 +324,12 @@ void GlShaderProgram::use() { bool GlShaderProgram::in_use() const { - return this->context->get_current_program().lock() == this->shared_from_this(); + return this->context->get_current_program().lock().get() == this; } void GlShaderProgram::update_uniforms(std::shared_ptr const &unif_in) { - ENSURE(unif_in->get_program() == this->shared_from_this(), "Uniform input passed to different shader than it was created with."); + ENSURE(unif_in->get_program().get() == this, "Uniform input passed to different shader than it was created with."); // TODO: use glProgramUniform when we're on OpenGL 4.1 // then we don't need to "use" and then call glUniform* From a23b5daa7758bc463b8d52e5deeb715e911ba932 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 29 Jul 2024 04:06:11 +0200 Subject: [PATCH 471/771] renderer: Access uniform vector without bounds check. --- libopenage/renderer/opengl/shader_program.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libopenage/renderer/opengl/shader_program.cpp b/libopenage/renderer/opengl/shader_program.cpp index 5725b9b088..f17c295c26 100644 --- a/libopenage/renderer/opengl/shader_program.cpp +++ b/libopenage/renderer/opengl/shader_program.cpp @@ -341,7 +341,7 @@ void GlShaderProgram::update_uniforms(std::shared_ptr const &uni uint8_t const *data = unif_in->update_data.data(); for (auto const &pair : unif_in->update_offs) { uint8_t const *ptr = data + pair.second; - const auto &unif = this->uniforms.at(pair.first); + const auto &unif = this->uniforms[pair.first]; auto loc = unif.location; switch (unif.type) { @@ -497,7 +497,10 @@ void GlShaderProgram::set_unif(std::shared_ptr const &in, ENSURE(unif_id < this->uniforms.size(), "Tried to set uniform '" << unif_id << "' that does not exist in the shader program."); - auto const &unif_info = this->uniforms.at(unif_id); + ENSURE(unif_id < this->uniforms.size(), + "Tried to set uniform with invalid ID " << unif_id); + + auto const &unif_info = this->uniforms[unif_id]; ENSURE(type == unif_info.type, "Tried to set uniform '" << unif_id << "' to a value of the wrong type."); From 776920d5889cfae6a5cac85aec91a9d78181ca96 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 29 Jul 2024 06:40:28 +0200 Subject: [PATCH 472/771] renderer: Vectorize tex unit bindings in shader program. --- libopenage/renderer/opengl/shader_data.h | 10 ++++ libopenage/renderer/opengl/shader_program.cpp | 48 ++++++++++++------- libopenage/renderer/opengl/shader_program.h | 4 +- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/libopenage/renderer/opengl/shader_data.h b/libopenage/renderer/opengl/shader_data.h index 456c60d6cf..4320dedf28 100644 --- a/libopenage/renderer/opengl/shader_data.h +++ b/libopenage/renderer/opengl/shader_data.h @@ -88,4 +88,14 @@ struct GlVertexAttrib { GLint size; }; +/** + * Represents a texture unit binding in a shader program. + */ +struct TexunitBinding { + /// Texture bound to the texture unit. + GLuint tex = 0; + /// true if the texture unit is currently bound to a texture. + bool used = false; +}; + } // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/shader_program.cpp b/libopenage/renderer/opengl/shader_program.cpp index f17c295c26..a68ae32d3b 100644 --- a/libopenage/renderer/opengl/shader_program.cpp +++ b/libopenage/renderer/opengl/shader_program.cpp @@ -179,7 +179,7 @@ GlShaderProgram::GlShaderProgram(const std::shared_ptr &context, block_binding})); } - GLuint tex_unit = 0; + GLuint tex_unit_id = 0; // Extract information about uniforms in the default block. @@ -211,19 +211,22 @@ GlShaderProgram::GlShaderProgram(const std::shared_ptr &context, unif_id)); if (type == GL_SAMPLER_2D) { - ENSURE(tex_unit < caps.max_texture_slots, + ENSURE(tex_unit_id < caps.max_texture_slots, "Tried to create an OpenGL shader that uses more texture sampler uniforms " << "than there are texture unit slots (" << caps.max_texture_slots << " available)."); - this->texunits_per_unifs.insert(std::make_pair(unif_id, tex_unit)); + this->texunits_per_unifs.insert(std::make_pair(unif_id, tex_unit_id)); - tex_unit += 1; + tex_unit_id += 1; } // Increment uniform ID unif_id += 1; } + // Initialize the texture unit bindings + this->textures_per_texunits.resize(this->texunits_per_unifs.size()); + // Extract vertex attribute descriptions. for (GLuint i_attrib = 0; i_attrib < attrib_count; ++i_attrib) { GLint size; @@ -304,21 +307,28 @@ void GlShaderProgram::use() { std::static_pointer_cast( this->shared_from_this())); - for (auto const &pair : this->textures_per_texunits) { + for (size_t i = 0; i < this->textures_per_texunits.size(); ++i) { + auto &tex_unit = this->textures_per_texunits[i]; + if (not tex_unit.used) { + continue; + } + + if (not glIsTexture(tex_unit.tex)) { + // By the time we use the shader again, the texture may have been deleted + // but if it's fixed afterwards using update_uniforms, the render state + // will still be fine + // We can free the texture unit in this case + tex_unit.used = false; + continue; + } + // We have to bind the texture to their texture units here because // the texture unit bindings are global to the context. Each time // the shader switches, it is possible that some other shader overwrote // these, and since we want the uniform values to persist across update_uniforms // calls, we have to set them more often than just on update_uniforms. - glActiveTexture(GL_TEXTURE0 + pair.first); - glBindTexture(GL_TEXTURE_2D, pair.second); - - // By the time we call bind, the texture may have been deleted, but if it's fixed - // afterwards using update_uniforms, the render state will still be fine, so we can ignore - // this error. - // TODO this will swallow actual errors elsewhere, and should be avoided. how? - // probably by holding the texture object as shared_ptr as long as it is bound - glGetError(); + glActiveTexture(GL_TEXTURE0 + i); + glBindTexture(GL_TEXTURE_2D, tex_unit.tex); } } @@ -395,13 +405,15 @@ void GlShaderProgram::update_uniforms(std::shared_ptr const &uni glUniformMatrix4fv(loc, 1, GLboolean(false), reinterpret_cast(ptr)); break; case GL_SAMPLER_2D: { - GLuint tex_unit = this->texunits_per_unifs.at(pair.first); + GLuint tex_unit_id = this->texunits_per_unifs.at(pair.first); GLuint tex = *reinterpret_cast(ptr); - glActiveTexture(GL_TEXTURE0 + tex_unit); + glActiveTexture(GL_TEXTURE0 + tex_unit_id); glBindTexture(GL_TEXTURE_2D, tex); // TODO: maybe call this at a more appropriate position - glUniform1i(loc, tex_unit); - this->textures_per_texunits[tex_unit] = tex; + glUniform1i(loc, tex_unit_id); + auto &tex_unit = this->textures_per_texunits[tex_unit_id]; + tex_unit.tex = tex; + tex_unit.used = true; break; } default: diff --git a/libopenage/renderer/opengl/shader_program.h b/libopenage/renderer/opengl/shader_program.h index a49c8214a9..e20123bf7c 100644 --- a/libopenage/renderer/opengl/shader_program.h +++ b/libopenage/renderer/opengl/shader_program.h @@ -158,8 +158,8 @@ class GlShaderProgram final : public ShaderProgram /// Maps sampler uniform names to their assigned texture units. std::unordered_map texunits_per_unifs; - /// Maps texture units to the texture handles that are currently bound to them. - std::unordered_map textures_per_texunits; + /// Store which texture handles are currently bound to the shader's texture units. + std::vector textures_per_texunits; /// Whether this program has been validated. bool validated; From 2de33df70841b1598c0068433357bdffb287cb0e Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 30 Jul 2024 03:53:29 +0200 Subject: [PATCH 473/771] renderer: Query max uniform locations. --- libopenage/renderer/opengl/context.cpp | 4 +++- libopenage/renderer/opengl/context.h | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/libopenage/renderer/opengl/context.cpp b/libopenage/renderer/opengl/context.cpp index e02992d25e..59f82339aa 100644 --- a/libopenage/renderer/opengl/context.cpp +++ b/libopenage/renderer/opengl/context.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include #include @@ -74,6 +74,8 @@ gl_context_spec GlContext::find_spec() { caps.max_texture_slots = temp; glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &temp); caps.max_vertex_attributes = temp; + glGetIntegerv(GL_MAX_UNIFORM_LOCATIONS, &temp); + caps.max_uniform_locations = temp; glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &temp); caps.max_uniform_buffer_bindings = temp; diff --git a/libopenage/renderer/opengl/context.h b/libopenage/renderer/opengl/context.h index 1b9cd936ff..d0a3f4352a 100644 --- a/libopenage/renderer/opengl/context.h +++ b/libopenage/renderer/opengl/context.h @@ -24,6 +24,8 @@ struct gl_context_spec { size_t max_texture_slots; /// The maximum size of a single dimension of a texture. size_t max_texture_size; + /// The maximum number of uniform locations per shader. + size_t max_uniform_locations; /// The maximum number of binding points for uniform blocks /// in a single shader. size_t max_uniform_buffer_bindings; From 4a089e1e60f8cfb28e7d83a84f4f6b7ba36810fb Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 30 Jul 2024 05:14:02 +0200 Subject: [PATCH 474/771] renderer: Vectorize storage of uniform inputs. --- libopenage/renderer/opengl/shader_program.cpp | 66 +++++++------------ libopenage/renderer/opengl/shader_program.h | 29 ++++++++ libopenage/renderer/opengl/uniform_input.cpp | 23 ++++++- libopenage/renderer/opengl/uniform_input.h | 20 ++++-- 4 files changed, 86 insertions(+), 52 deletions(-) diff --git a/libopenage/renderer/opengl/shader_program.cpp b/libopenage/renderer/opengl/shader_program.cpp index a68ae32d3b..eaa71e91ff 100644 --- a/libopenage/renderer/opengl/shader_program.cpp +++ b/libopenage/renderer/opengl/shader_program.cpp @@ -348,10 +348,17 @@ void GlShaderProgram::update_uniforms(std::shared_ptr const &uni this->use(); } + auto &update_offs = unif_in->update_offs; + auto &uniforms = this->uniforms; uint8_t const *data = unif_in->update_data.data(); - for (auto const &pair : unif_in->update_offs) { - uint8_t const *ptr = data + pair.second; - const auto &unif = this->uniforms[pair.first]; + for (uniform_id_t i = 0; i < this->uniforms.size(); ++i) { + if (not update_offs[i].used) { + // Uniform value has not been set + continue; + } + + uint8_t const *ptr = data + update_offs[i].offset; + const auto &unif = uniforms[i]; auto loc = unif.location; switch (unif.type) { @@ -405,7 +412,7 @@ void GlShaderProgram::update_uniforms(std::shared_ptr const &uni glUniformMatrix4fv(loc, 1, GLboolean(false), reinterpret_cast(ptr)); break; case GL_SAMPLER_2D: { - GLuint tex_unit_id = this->texunits_per_unifs.at(pair.first); + GLuint tex_unit_id = this->texunits_per_unifs.at(i); GLuint tex = *reinterpret_cast(ptr); glActiveTexture(GL_TEXTURE0 + tex_unit_id); glBindTexture(GL_TEXTURE_2D, tex); @@ -446,6 +453,14 @@ uniform_id_t GlShaderProgram::get_uniform_id(const char *name) { return this->uniforms_by_name.at(name); } +const std::vector &GlShaderProgram::get_uniforms() const { + return this->uniforms; +} + +const std::unordered_map &GlShaderProgram::get_uniform_blocks() const { + return this->uniform_blocks; +} + bool GlShaderProgram::has_uniform(const char *name) { return this->uniforms_by_name.contains(name); } @@ -471,33 +486,11 @@ void GlShaderProgram::set_unif(std::shared_ptr const &in, const char *unif, void const *val, GLenum type) { - auto unif_in = std::dynamic_pointer_cast(in); - auto uniform_id = this->uniforms_by_name.find(unif); ENSURE(uniform_id != std::end(this->uniforms_by_name), "Tried to set uniform '" << unif << "' that does not exist in the shader program."); - auto const &unif_info = this->uniforms.at(uniform_id->second); - - ENSURE(type == unif_info.type, - "Tried to set uniform '" << unif << "' to a value of the wrong type."); - - size_t size = get_uniform_type_size(type); - - auto update_off = unif_in->update_offs.find(uniform_id->second); - if (update_off != std::end(unif_in->update_offs)) [[likely]] { // always used after the uniform value is written once - // already wrote to this uniform since last upload - size_t off = update_off->second; - memcpy(unif_in->update_data.data() + off, val, size); - } - else { - // first time writing to this uniform since last upload, so - // extend the buffer before storing the uniform value - size_t prev_size = unif_in->update_data.size(); - unif_in->update_data.resize(prev_size + size); - memcpy(unif_in->update_data.data() + prev_size, val, size); - unif_in->update_offs.emplace(uniform_id->second, prev_size); - } + this->set_unif(in, uniform_id->second, val, type); } void GlShaderProgram::set_unif(std::shared_ptr const &in, @@ -516,22 +509,11 @@ void GlShaderProgram::set_unif(std::shared_ptr const &in, ENSURE(type == unif_info.type, "Tried to set uniform '" << unif_id << "' to a value of the wrong type."); + auto &update_off = unif_in->update_offs[unif_id]; + auto offset = update_off.offset; size_t size = get_uniform_type_size(type); - - auto update_off = unif_in->update_offs.find(unif_id); - if (update_off != std::end(unif_in->update_offs)) [[likely]] { // always used after the uniform value is written once - // already wrote to this uniform since last upload - size_t off = update_off->second; - memcpy(unif_in->update_data.data() + off, val, size); - } - else { - // first time writing to this uniform since last upload, so - // extend the buffer before storing the uniform value - size_t prev_size = unif_in->update_data.size(); - unif_in->update_data.resize(prev_size + size); - memcpy(unif_in->update_data.data() + prev_size, val, size); - unif_in->update_offs.emplace(unif_id, prev_size); - } + memcpy(unif_in->update_data.data() + offset, val, size); + update_off.used = true; } void GlShaderProgram::set_i32(std::shared_ptr const &in, const char *unif, int32_t val) { diff --git a/libopenage/renderer/opengl/shader_program.h b/libopenage/renderer/opengl/shader_program.h index e20123bf7c..7419ad5928 100644 --- a/libopenage/renderer/opengl/shader_program.h +++ b/libopenage/renderer/opengl/shader_program.h @@ -64,8 +64,37 @@ class GlShaderProgram final : public ShaderProgram */ const GlUniformBlock &get_uniform_block(const char *block_name) const; + /** + * Get the uniform ID for the given uniform name. + * + * @param name Name of the uniform in the shader code. + * + * @return ID of the uniform. + */ uniform_id_t get_uniform_id(const char *name) override; + /** + * Get the uniforms in the default block of the shader program. + * This does not include uniforms in blocks. + * + * @return Uniforms in the shader program. + */ + const std::vector &get_uniforms() const; + + /** + * Get the map of uniform blocks in the shader program. + * + * @return Uniform blocks in the shader program. + */ + const std::unordered_map &get_uniform_blocks() const; + + /** + * Check whether the shader program contains a uniform variable with the given name. + * + * @param name Name of the uniform in the shader code. + * + * @return true if the shader program contains the uniform, false otherwise. + */ bool has_uniform(const char *name) override; /** diff --git a/libopenage/renderer/opengl/uniform_input.cpp b/libopenage/renderer/opengl/uniform_input.cpp index d8c7eeaba8..a598d5062f 100644 --- a/libopenage/renderer/opengl/uniform_input.cpp +++ b/libopenage/renderer/opengl/uniform_input.cpp @@ -1,11 +1,28 @@ -// Copyright 2019-2023 the openage authors. See copying.md for legal info. +// Copyright 2019-2024 the openage authors. See copying.md for legal info. #include "uniform_input.h" +#include "renderer/opengl/shader_program.h" +#include "renderer/opengl/util.h" + + namespace openage::renderer::opengl { -GlUniformInput::GlUniformInput(std::shared_ptr const &prog) : - UniformInput{prog} {} +GlUniformInput::GlUniformInput(const std::shared_ptr &prog) : + UniformInput{prog} { + auto glprog = std::dynamic_pointer_cast(prog); + + // Calculate the byte-wise offsets of all uniforms. + size_t offset = 0; + this->update_offs.reserve(glprog->get_uniforms().size()); + for (auto &uniform : glprog->get_uniforms()) { + this->update_offs.push_back({offset, false}); + offset += get_uniform_type_size(uniform.type); + } + + // Resize the update data buffer to the total size of all uniforms. + this->update_data.resize(offset); +} GlUniformBufferInput::GlUniformBufferInput(std::shared_ptr const &buffer) : UniformBufferInput{buffer} {} diff --git a/libopenage/renderer/opengl/uniform_input.h b/libopenage/renderer/opengl/uniform_input.h index 1bb54f3cd4..86cfa67d2f 100644 --- a/libopenage/renderer/opengl/uniform_input.h +++ b/libopenage/renderer/opengl/uniform_input.h @@ -23,18 +23,24 @@ class GlShaderProgram; * Describes OpenGL-specific uniform valuations. */ class GlUniformInput final : public UniformInput { +private: + struct GlUniformOffset { + // Offset in the update_data buffer. + size_t offset; + /// Dtermine whether the uniform value has been set. + bool used; + }; + public: - GlUniformInput(std::shared_ptr const &); + GlUniformInput(const std::shared_ptr &prog); /** - * We store uniform updates lazily. They are only actually uploaded to GPU - * when a draw call is made. + * Store offsets of uniforms in the update_data buffer and + * whether the uniform value has been set. * - * \p update_offs maps the uniform IDs to where their - * value is in \p update_data in terms of a byte-wise offset. This is only a partial - * valuation, so not all uniforms have to be present here. + * Index in the vector corresponds to the uniform ID in the shader. */ - std::unordered_map update_offs; + std::vector update_offs; /** * Buffer containing untyped uniform update data. From cb32f457edc4f27846704adfb4d99dd79a2172d3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 30 Jul 2024 06:26:23 +0200 Subject: [PATCH 475/771] renderer: Vectorize texture unit assignment to uniforms. --- libopenage/renderer/opengl/shader_data.h | 18 +++++----- libopenage/renderer/opengl/shader_program.cpp | 36 ++++++++++--------- libopenage/renderer/opengl/shader_program.h | 5 +-- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/libopenage/renderer/opengl/shader_data.h b/libopenage/renderer/opengl/shader_data.h index 4320dedf28..7b987fb6a7 100644 --- a/libopenage/renderer/opengl/shader_data.h +++ b/libopenage/renderer/opengl/shader_data.h @@ -2,6 +2,7 @@ #pragma once +#include #include #include @@ -22,6 +23,13 @@ struct GlUniform { * NOT the same as the uniform index. */ GLuint location; + + /** + * Only used for sampler uniforms. + * + * Texture unit to which the sampler is bound. + */ + std::optional tex_unit; }; /** @@ -88,14 +96,4 @@ struct GlVertexAttrib { GLint size; }; -/** - * Represents a texture unit binding in a shader program. - */ -struct TexunitBinding { - /// Texture bound to the texture unit. - GLuint tex = 0; - /// true if the texture unit is currently bound to a texture. - bool used = false; -}; - } // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/shader_program.cpp b/libopenage/renderer/opengl/shader_program.cpp index eaa71e91ff..4e07d8078a 100644 --- a/libopenage/renderer/opengl/shader_program.cpp +++ b/libopenage/renderer/opengl/shader_program.cpp @@ -204,7 +204,7 @@ GlShaderProgram::GlShaderProgram(const std::shared_ptr &context, GLuint loc = glGetUniformLocation(handle, name.data()); - this->uniforms.push_back({type, loc}); + this->uniforms.push_back({type, loc, std::nullopt}); this->uniforms_by_name.insert(std::make_pair( name.data(), @@ -215,7 +215,7 @@ GlShaderProgram::GlShaderProgram(const std::shared_ptr &context, "Tried to create an OpenGL shader that uses more texture sampler uniforms " << "than there are texture unit slots (" << caps.max_texture_slots << " available)."); - this->texunits_per_unifs.insert(std::make_pair(unif_id, tex_unit_id)); + this->uniforms[unif_id].tex_unit = tex_unit_id; tex_unit_id += 1; } @@ -224,8 +224,9 @@ GlShaderProgram::GlShaderProgram(const std::shared_ptr &context, unif_id += 1; } - // Initialize the texture unit bindings - this->textures_per_texunits.resize(this->texunits_per_unifs.size()); + // Resize the texture unit bindings + // to number of texture units used by the shader + this->textures_per_texunits.resize(tex_unit_id); // Extract vertex attribute descriptions. for (GLuint i_attrib = 0; i_attrib < attrib_count; ++i_attrib) { @@ -309,16 +310,16 @@ void GlShaderProgram::use() { for (size_t i = 0; i < this->textures_per_texunits.size(); ++i) { auto &tex_unit = this->textures_per_texunits[i]; - if (not tex_unit.used) { + if (not tex_unit) { continue; } - if (not glIsTexture(tex_unit.tex)) { + if (not glIsTexture(*tex_unit)) { // By the time we use the shader again, the texture may have been deleted // but if it's fixed afterwards using update_uniforms, the render state // will still be fine // We can free the texture unit in this case - tex_unit.used = false; + tex_unit = std::nullopt; continue; } @@ -328,7 +329,7 @@ void GlShaderProgram::use() { // these, and since we want the uniform values to persist across update_uniforms // calls, we have to set them more often than just on update_uniforms. glActiveTexture(GL_TEXTURE0 + i); - glBindTexture(GL_TEXTURE_2D, tex_unit.tex); + glBindTexture(GL_TEXTURE_2D, *tex_unit); } } @@ -348,16 +349,17 @@ void GlShaderProgram::update_uniforms(std::shared_ptr const &uni this->use(); } - auto &update_offs = unif_in->update_offs; - auto &uniforms = this->uniforms; + const auto &update_offs = unif_in->update_offs; + const auto &uniforms = this->uniforms; uint8_t const *data = unif_in->update_data.data(); for (uniform_id_t i = 0; i < this->uniforms.size(); ++i) { - if (not update_offs[i].used) { + const auto &update_off = update_offs[i]; + if (not update_off.used) { // Uniform value has not been set continue; } - uint8_t const *ptr = data + update_offs[i].offset; + uint8_t const *ptr = data + update_off.offset; const auto &unif = uniforms[i]; auto loc = unif.location; @@ -412,15 +414,17 @@ void GlShaderProgram::update_uniforms(std::shared_ptr const &uni glUniformMatrix4fv(loc, 1, GLboolean(false), reinterpret_cast(ptr)); break; case GL_SAMPLER_2D: { - GLuint tex_unit_id = this->texunits_per_unifs.at(i); + ENSURE(unif.tex_unit, + "Tried to access texture unit for uniform that has no texture unit assigned."); + GLuint tex_unit_id = *unif.tex_unit; + GLuint tex = *reinterpret_cast(ptr); glActiveTexture(GL_TEXTURE0 + tex_unit_id); glBindTexture(GL_TEXTURE_2D, tex); // TODO: maybe call this at a more appropriate position glUniform1i(loc, tex_unit_id); - auto &tex_unit = this->textures_per_texunits[tex_unit_id]; - tex_unit.tex = tex; - tex_unit.used = true; + auto &tex_value = *this->textures_per_texunits[tex_unit_id]; + tex_value = tex; break; } default: diff --git a/libopenage/renderer/opengl/shader_program.h b/libopenage/renderer/opengl/shader_program.h index 7419ad5928..ee7f8e5607 100644 --- a/libopenage/renderer/opengl/shader_program.h +++ b/libopenage/renderer/opengl/shader_program.h @@ -184,11 +184,8 @@ class GlShaderProgram final : public ShaderProgram /// Maps per-vertex attribute names to their descriptions. std::unordered_map attribs; - /// Maps sampler uniform names to their assigned texture units. - std::unordered_map texunits_per_unifs; - /// Store which texture handles are currently bound to the shader's texture units. - std::vector textures_per_texunits; + std::vector> textures_per_texunits; /// Whether this program has been validated. bool validated; From 09a696390114a6f7de089beccc3118f85f71b684 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 1 Aug 2024 22:28:58 +0200 Subject: [PATCH 476/771] renderer: Store used uniform IDs for uniform input. --- libopenage/renderer/opengl/lookup.h | 2 +- libopenage/renderer/opengl/shader_program.cpp | 24 ++++++++++++------- libopenage/renderer/opengl/uniform_input.cpp | 3 +++ libopenage/renderer/opengl/uniform_input.h | 5 ++++ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/libopenage/renderer/opengl/lookup.h b/libopenage/renderer/opengl/lookup.h index a137101482..fd4fcc53fd 100644 --- a/libopenage/renderer/opengl/lookup.h +++ b/libopenage/renderer/opengl/lookup.h @@ -137,7 +137,7 @@ static constexpr auto GL_PRIMITIVE = datastructure::create_const_map const &uni } const auto &update_offs = unif_in->update_offs; + const auto &used_uniforms = unif_in->used_uniforms; const auto &uniforms = this->uniforms; uint8_t const *data = unif_in->update_data.data(); - for (uniform_id_t i = 0; i < this->uniforms.size(); ++i) { - const auto &update_off = update_offs[i]; - if (not update_off.used) { - // Uniform value has not been set - continue; - } + size_t unif_count = used_uniforms.size(); + for (size_t i = 0; i < unif_count; ++i) { + uniform_id_t unif_id = used_uniforms[i]; + + const auto &update_off = update_offs[unif_id]; uint8_t const *ptr = data + update_off.offset; - const auto &unif = uniforms[i]; + + const auto &unif = uniforms[unif_id]; auto loc = unif.location; switch (unif.type) { @@ -517,7 +518,14 @@ void GlShaderProgram::set_unif(std::shared_ptr const &in, auto offset = update_off.offset; size_t size = get_uniform_type_size(type); memcpy(unif_in->update_data.data() + offset, val, size); - update_off.used = true; + if (not update_off.used) [[unlikely]] { // only true if the uniform value was not set before + auto lower_bound = std::lower_bound( + std::begin(unif_in->used_uniforms), + std::end(unif_in->used_uniforms), + unif_id); + unif_in->used_uniforms.insert(lower_bound, unif_id); + update_off.used = true; + } } void GlShaderProgram::set_i32(std::shared_ptr const &in, const char *unif, int32_t val) { diff --git a/libopenage/renderer/opengl/uniform_input.cpp b/libopenage/renderer/opengl/uniform_input.cpp index a598d5062f..c2073fbf3f 100644 --- a/libopenage/renderer/opengl/uniform_input.cpp +++ b/libopenage/renderer/opengl/uniform_input.cpp @@ -12,6 +12,9 @@ GlUniformInput::GlUniformInput(const std::shared_ptr &prog) : UniformInput{prog} { auto glprog = std::dynamic_pointer_cast(prog); + // Reserve space for the used uniforms. + this->used_uniforms.reserve(glprog->get_uniforms().size()); + // Calculate the byte-wise offsets of all uniforms. size_t offset = 0; this->update_offs.reserve(glprog->get_uniforms().size()); diff --git a/libopenage/renderer/opengl/uniform_input.h b/libopenage/renderer/opengl/uniform_input.h index 86cfa67d2f..8ca3ff02dd 100644 --- a/libopenage/renderer/opengl/uniform_input.h +++ b/libopenage/renderer/opengl/uniform_input.h @@ -34,6 +34,11 @@ class GlUniformInput final : public UniformInput { public: GlUniformInput(const std::shared_ptr &prog); + /** + * Store the IDs of the uniforms from the shader set by this uniform input. + */ + std::vector used_uniforms; + /** * Store offsets of uniforms in the update_data buffer and * whether the uniform value has been set. From b8a12ff08799835ea81be3047728558476b6fe2a Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 1 Aug 2024 23:05:52 +0200 Subject: [PATCH 477/771] renderer: Evaluate OpenGL uniform size at compile time. --- libopenage/renderer/opengl/lookup.h | 4 ++ libopenage/renderer/opengl/shader_program.cpp | 69 ++++++++++--------- libopenage/renderer/opengl/shader_program.h | 18 +++-- libopenage/renderer/opengl/uniform_buffer.cpp | 4 +- libopenage/renderer/opengl/uniform_input.cpp | 3 +- libopenage/renderer/opengl/util.cpp | 63 +---------------- libopenage/renderer/opengl/util.h | 10 ++- 7 files changed, 67 insertions(+), 104 deletions(-) diff --git a/libopenage/renderer/opengl/lookup.h b/libopenage/renderer/opengl/lookup.h index fd4fcc53fd..9521369ef5 100644 --- a/libopenage/renderer/opengl/lookup.h +++ b/libopenage/renderer/opengl/lookup.h @@ -33,6 +33,10 @@ static constexpr auto GL_UNIFORM_TYPE_SIZE = datastructure::create_const_map const &in, const char *unif, void const *val, + size_t size, GLenum type) { auto uniform_id = this->uniforms_by_name.find(unif); ENSURE(uniform_id != std::end(this->uniforms_by_name), "Tried to set uniform '" << unif << "' that does not exist in the shader program."); - this->set_unif(in, uniform_id->second, val, type); + this->set_unif(in, uniform_id->second, val, size, type); } void GlShaderProgram::set_unif(std::shared_ptr const &in, const uniform_id_t &unif_id, void const *val, + size_t size, GLenum type) { auto unif_in = std::dynamic_pointer_cast(in); @@ -516,7 +518,6 @@ void GlShaderProgram::set_unif(std::shared_ptr const &in, auto &update_off = unif_in->update_offs[unif_id]; auto offset = update_off.offset; - size_t size = get_uniform_type_size(type); memcpy(unif_in->update_data.data() + offset, val, size); if (not update_off.used) [[unlikely]] { // only true if the uniform value was not set before auto lower_bound = std::lower_bound( @@ -529,137 +530,137 @@ void GlShaderProgram::set_unif(std::shared_ptr const &in, } void GlShaderProgram::set_i32(std::shared_ptr const &in, const char *unif, int32_t val) { - this->set_unif(in, unif, &val, GL_INT); + this->set_unif(in, unif, &val, get_uniform_type_size(GL_INT), GL_INT); } void GlShaderProgram::set_u32(std::shared_ptr const &in, const char *unif, uint32_t val) { - this->set_unif(in, unif, &val, GL_UNSIGNED_INT); + this->set_unif(in, unif, &val, get_uniform_type_size(GL_UNSIGNED_INT), GL_UNSIGNED_INT); } void GlShaderProgram::set_f32(std::shared_ptr const &in, const char *unif, float val) { - this->set_unif(in, unif, &val, GL_FLOAT); + this->set_unif(in, unif, &val, get_uniform_type_size(GL_FLOAT), GL_FLOAT); } void GlShaderProgram::set_f64(std::shared_ptr const &in, const char *unif, double val) { // TODO requires extension - this->set_unif(in, unif, &val, GL_DOUBLE); + this->set_unif(in, unif, &val, get_uniform_type_size(GL_DOUBLE), GL_DOUBLE); } void GlShaderProgram::set_bool(std::shared_ptr const &in, const char *unif, bool val) { - this->set_unif(in, unif, &val, GL_BOOL); + this->set_unif(in, unif, &val, get_uniform_type_size(GL_BOOL), GL_BOOL); } void GlShaderProgram::set_v2f32(std::shared_ptr const &in, const char *unif, Eigen::Vector2f const &val) { - this->set_unif(in, unif, &val, GL_FLOAT_VEC2); + this->set_unif(in, unif, &val, get_uniform_type_size(GL_FLOAT_VEC2), GL_FLOAT_VEC2); } void GlShaderProgram::set_v3f32(std::shared_ptr const &in, const char *unif, Eigen::Vector3f const &val) { - this->set_unif(in, unif, &val, GL_FLOAT_VEC3); + this->set_unif(in, unif, &val, get_uniform_type_size(GL_FLOAT_VEC3), GL_FLOAT_VEC3); } void GlShaderProgram::set_v4f32(std::shared_ptr const &in, const char *unif, Eigen::Vector4f const &val) { - this->set_unif(in, unif, &val, GL_FLOAT_VEC4); + this->set_unif(in, unif, &val, get_uniform_type_size(GL_FLOAT_VEC4), GL_FLOAT_VEC4); } void GlShaderProgram::set_v2i32(std::shared_ptr const &in, const char *unif, Eigen::Vector2i const &val) { - this->set_unif(in, unif, &val, GL_INT_VEC2); + this->set_unif(in, unif, &val, get_uniform_type_size(GL_INT_VEC2), GL_INT_VEC2); } void GlShaderProgram::set_v3i32(std::shared_ptr const &in, const char *unif, Eigen::Vector3i const &val) { - this->set_unif(in, unif, &val, GL_INT_VEC3); + this->set_unif(in, unif, &val, get_uniform_type_size(GL_INT_VEC3), GL_INT_VEC3); } void GlShaderProgram::set_v4i32(std::shared_ptr const &in, const char *unif, Eigen::Vector4i const &val) { - this->set_unif(in, unif, &val, GL_INT_VEC4); + this->set_unif(in, unif, &val, get_uniform_type_size(GL_INT_VEC4), GL_INT_VEC4); } void GlShaderProgram::set_v2ui32(std::shared_ptr const &in, const char *unif, Eigen::Vector2 const &val) { - this->set_unif(in, unif, &val, GL_UNSIGNED_INT_VEC2); + this->set_unif(in, unif, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC2), GL_UNSIGNED_INT_VEC2); } void GlShaderProgram::set_v3ui32(std::shared_ptr const &in, const char *unif, Eigen::Vector3 const &val) { - this->set_unif(in, unif, &val, GL_UNSIGNED_INT_VEC3); + this->set_unif(in, unif, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC3), GL_UNSIGNED_INT_VEC3); } void GlShaderProgram::set_v4ui32(std::shared_ptr const &in, const char *unif, Eigen::Vector4 const &val) { - this->set_unif(in, unif, &val, GL_UNSIGNED_INT_VEC4); + this->set_unif(in, unif, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC4), GL_UNSIGNED_INT_VEC4); } void GlShaderProgram::set_m4f32(std::shared_ptr const &in, const char *unif, Eigen::Matrix4f const &val) { - this->set_unif(in, unif, val.data(), GL_FLOAT_MAT4); + this->set_unif(in, unif, val.data(), get_uniform_type_size(GL_FLOAT_MAT4), GL_FLOAT_MAT4); } void GlShaderProgram::set_tex(std::shared_ptr const &in, const char *unif, std::shared_ptr const &val) { auto tex = std::dynamic_pointer_cast(val); GLuint handle = tex->get_handle(); - this->set_unif(in, unif, &handle, GL_SAMPLER_2D); + this->set_unif(in, unif, &handle, get_uniform_type_size(GL_SAMPLER_2D), GL_SAMPLER_2D); } void GlShaderProgram::set_i32(std::shared_ptr const &in, const uniform_id_t &id, int32_t val) { - this->set_unif(in, id, &val, GL_INT); + this->set_unif(in, id, &val, get_uniform_type_size(GL_INT), GL_INT); } void GlShaderProgram::set_u32(std::shared_ptr const &in, const uniform_id_t &id, uint32_t val) { - this->set_unif(in, id, &val, GL_UNSIGNED_INT); + this->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT), GL_UNSIGNED_INT); } void GlShaderProgram::set_f32(std::shared_ptr const &in, const uniform_id_t &id, float val) { - this->set_unif(in, id, &val, GL_FLOAT); + this->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT), GL_FLOAT); } void GlShaderProgram::set_f64(std::shared_ptr const &in, const uniform_id_t &id, double val) { // TODO requires extension - this->set_unif(in, id, &val, GL_DOUBLE); + this->set_unif(in, id, &val, get_uniform_type_size(GL_DOUBLE), GL_DOUBLE); } void GlShaderProgram::set_bool(std::shared_ptr const &in, const uniform_id_t &id, bool val) { - this->set_unif(in, id, &val, GL_BOOL); + this->set_unif(in, id, &val, get_uniform_type_size(GL_BOOL), GL_BOOL); } void GlShaderProgram::set_v2f32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector2f const &val) { - this->set_unif(in, id, &val, GL_FLOAT_VEC2); + this->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT_VEC2), GL_FLOAT_VEC2); } void GlShaderProgram::set_v3f32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector3f const &val) { - this->set_unif(in, id, &val, GL_FLOAT_VEC3); + this->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT_VEC3), GL_FLOAT_VEC3); } void GlShaderProgram::set_v4f32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector4f const &val) { - this->set_unif(in, id, &val, GL_FLOAT_VEC4); + this->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT_VEC4), GL_FLOAT_VEC4); } void GlShaderProgram::set_v2i32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector2i const &val) { - this->set_unif(in, id, &val, GL_INT_VEC2); + this->set_unif(in, id, &val, get_uniform_type_size(GL_INT_VEC2), GL_INT_VEC2); } void GlShaderProgram::set_v3i32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector3i const &val) { - this->set_unif(in, id, &val, GL_INT_VEC3); + this->set_unif(in, id, &val, get_uniform_type_size(GL_INT_VEC3), GL_INT_VEC3); } void GlShaderProgram::set_v4i32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector4i const &val) { - this->set_unif(in, id, &val, GL_INT_VEC4); + this->set_unif(in, id, &val, get_uniform_type_size(GL_INT_VEC4), GL_INT_VEC4); } void GlShaderProgram::set_v2ui32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector2 const &val) { - this->set_unif(in, id, &val, GL_UNSIGNED_INT_VEC2); + this->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC2), GL_UNSIGNED_INT_VEC2); } void GlShaderProgram::set_v3ui32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector3 const &val) { - this->set_unif(in, id, &val, GL_UNSIGNED_INT_VEC3); + this->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC3), GL_UNSIGNED_INT_VEC3); } void GlShaderProgram::set_v4ui32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector4 const &val) { - this->set_unif(in, id, &val, GL_UNSIGNED_INT_VEC4); + this->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC4), GL_UNSIGNED_INT_VEC4); } void GlShaderProgram::set_m4f32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Matrix4f const &val) { - this->set_unif(in, id, val.data(), GL_FLOAT_MAT4); + this->set_unif(in, id, val.data(), get_uniform_type_size(GL_FLOAT_MAT4), GL_FLOAT_MAT4); } void GlShaderProgram::set_tex(std::shared_ptr const &in, const uniform_id_t &id, std::shared_ptr const &val) { auto tex = std::dynamic_pointer_cast(val); GLuint handle = tex->get_handle(); - this->set_unif(in, id, &handle, GL_SAMPLER_2D); + this->set_unif(in, id, &handle, get_uniform_type_size(GL_SAMPLER_2D), GL_SAMPLER_2D); } } // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/shader_program.h b/libopenage/renderer/opengl/shader_program.h index ee7f8e5607..58553b03ed 100644 --- a/libopenage/renderer/opengl/shader_program.h +++ b/libopenage/renderer/opengl/shader_program.h @@ -154,22 +154,32 @@ class GlShaderProgram final : public ShaderProgram * If performance is important, use the alternative \p set_unif(..) implementation * that works on IDs instead. * - * @param unif_in Uniform input. + * @param in Uniform input. * @param name Name of the uniform. * @param value Value to set. + * @param size Size of the value (in bytes). * @param type Type of the value. */ - void set_unif(std::shared_ptr const &, const char *, void const *, GLenum); + void set_unif(std::shared_ptr const &in, + const char *name, + void const *value, + size_t size, + GLenum type); /** * Set the uniform value via uniform ID from a uniform input. * - * @param unif_in Uniform input. + * @param in Uniform input. * @param id ID of the uniform. * @param value Value to set. + * @param size Size of the value (in bytes). * @param type Type of the value. */ - void set_unif(std::shared_ptr const &, const uniform_id_t &, void const *, GLenum); + void set_unif(std::shared_ptr const &, + const uniform_id_t &unif_id, + void const *value, + size_t size, + GLenum type); /// Uniforms in the shader program. Contains only /// uniforms in the default block, i.e. not within named blocks. diff --git a/libopenage/renderer/opengl/uniform_buffer.cpp b/libopenage/renderer/opengl/uniform_buffer.cpp index b84b17e6e1..661f9e3173 100644 --- a/libopenage/renderer/opengl/uniform_buffer.cpp +++ b/libopenage/renderer/opengl/uniform_buffer.cpp @@ -92,7 +92,9 @@ void GlUniformBuffer::set_unif(std::shared_ptr const &in, co ENSURE(type == unif_data.type, "Tried to set uniform " << unif << " to a value of the wrong type."); - size_t size = get_uniform_type_size(type); + size_t size = GL_UNIFORM_TYPE_SIZE.get(type); + ENSURE(size == unif_data.size, + "Tried to set uniform " << unif << " to a value of the wrong size."); auto update_off = unif_in->update_offs.find(unif); if (update_off != std::end(unif_in->update_offs)) [[likely]] { // always used after the uniform value is written once diff --git a/libopenage/renderer/opengl/uniform_input.cpp b/libopenage/renderer/opengl/uniform_input.cpp index c2073fbf3f..111b33b879 100644 --- a/libopenage/renderer/opengl/uniform_input.cpp +++ b/libopenage/renderer/opengl/uniform_input.cpp @@ -2,6 +2,7 @@ #include "uniform_input.h" +#include "renderer/opengl/lookup.h" #include "renderer/opengl/shader_program.h" #include "renderer/opengl/util.h" @@ -20,7 +21,7 @@ GlUniformInput::GlUniformInput(const std::shared_ptr &prog) : this->update_offs.reserve(glprog->get_uniforms().size()); for (auto &uniform : glprog->get_uniforms()) { this->update_offs.push_back({offset, false}); - offset += get_uniform_type_size(uniform.type); + offset += GL_UNIFORM_TYPE_SIZE.get(uniform.type); } // Resize the update data buffer to the total size of all uniforms. diff --git a/libopenage/renderer/opengl/util.cpp b/libopenage/renderer/opengl/util.cpp index 7a8a6e2f91..81185b6d97 100644 --- a/libopenage/renderer/opengl/util.cpp +++ b/libopenage/renderer/opengl/util.cpp @@ -1,68 +1,7 @@ -// 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 "util.h" -#include "error/error.h" - - namespace openage::renderer::opengl { -size_t get_uniform_type_size(GLenum type) { - switch (type) { - case GL_BOOL: - return 1; - break; - case GL_BOOL_VEC2: - return 2; - break; - case GL_BOOL_VEC3: - return 3; - break; - case GL_BOOL_VEC4: - case GL_FLOAT: - case GL_INT: - case GL_UNSIGNED_INT: - case GL_SAMPLER_1D: - case GL_SAMPLER_2D: - case GL_SAMPLER_2D_ARRAY: - case GL_SAMPLER_3D: - case GL_SAMPLER_CUBE: - return 4; - break; - - case GL_FLOAT_VEC2: - case GL_INT_VEC2: - case GL_UNSIGNED_INT_VEC2: - return 8; - break; - - case GL_FLOAT_VEC3: - case GL_INT_VEC3: - case GL_UNSIGNED_INT_VEC3: - return 12; - break; - - case GL_FLOAT_VEC4: - case GL_INT_VEC4: - case GL_UNSIGNED_INT_VEC4: - case GL_FLOAT_MAT2: - return 16; - break; - - case GL_FLOAT_MAT3: - return 36; - break; - - case GL_FLOAT_MAT4: - return 64; - break; - - default: - throw Error(MSG(err) << "Unknown GL uniform type: " << type); - break; - } - - return 0; -} - } // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/util.h b/libopenage/renderer/opengl/util.h index 3ad5b4626e..5ed01ed43c 100644 --- a/libopenage/renderer/opengl/util.h +++ b/libopenage/renderer/opengl/util.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 @@ -6,16 +6,22 @@ #include +#include "renderer/opengl/lookup.h" + namespace openage::renderer::opengl { /** * Get the sizes of a uniform value with a given uniform type. * + * Guaranteed to be a evaluated at compile time. + * * @param type Uniform type. * * @return Size of uniform value (in bytes). */ -size_t get_uniform_type_size(GLenum type); +consteval size_t get_uniform_type_size(GLenum type) { + return GL_UNIFORM_TYPE_SIZE.get(type); +} } // namespace openage::renderer::opengl From 3791e50dd32d63bcc998d389044c38a12152203a Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 2 Aug 2024 01:40:21 +0200 Subject: [PATCH 478/771] renderer: Replace shared_ptr creation with passing mutable reference. --- libopenage/renderer/opengl/shader_program.cpp | 80 +++++++++---------- libopenage/renderer/opengl/shader_program.h | 70 ++++++++-------- libopenage/renderer/shader_program.h | 66 +++++++-------- libopenage/renderer/uniform_input.cpp | 70 ++++++++-------- libopenage/renderer/uniform_input.h | 3 +- 5 files changed, 144 insertions(+), 145 deletions(-) diff --git a/libopenage/renderer/opengl/shader_program.cpp b/libopenage/renderer/opengl/shader_program.cpp index 66f7cd74a4..563c8a1221 100644 --- a/libopenage/renderer/opengl/shader_program.cpp +++ b/libopenage/renderer/opengl/shader_program.cpp @@ -487,7 +487,7 @@ void GlShaderProgram::bind_uniform_buffer(const char *block_name, std::shared_pt glUniformBlockBinding(*this->handle, block.index, block.binding_point); } -void GlShaderProgram::set_unif(std::shared_ptr const &in, +void GlShaderProgram::set_unif(UniformInput &in, const char *unif, void const *val, size_t size, @@ -499,12 +499,12 @@ void GlShaderProgram::set_unif(std::shared_ptr const &in, this->set_unif(in, uniform_id->second, val, size, type); } -void GlShaderProgram::set_unif(std::shared_ptr const &in, +void GlShaderProgram::set_unif(UniformInput &in, const uniform_id_t &unif_id, void const *val, size_t size, GLenum type) { - auto unif_in = std::dynamic_pointer_cast(in); + auto &unif_in = dynamic_cast(in); ENSURE(unif_id < this->uniforms.size(), "Tried to set uniform '" << unif_id << "' that does not exist in the shader program."); @@ -516,148 +516,148 @@ void GlShaderProgram::set_unif(std::shared_ptr const &in, ENSURE(type == unif_info.type, "Tried to set uniform '" << unif_id << "' to a value of the wrong type."); - auto &update_off = unif_in->update_offs[unif_id]; + auto &update_off = unif_in.update_offs[unif_id]; auto offset = update_off.offset; - memcpy(unif_in->update_data.data() + offset, val, size); + memcpy(unif_in.update_data.data() + offset, val, size); if (not update_off.used) [[unlikely]] { // only true if the uniform value was not set before auto lower_bound = std::lower_bound( - std::begin(unif_in->used_uniforms), - std::end(unif_in->used_uniforms), + std::begin(unif_in.used_uniforms), + std::end(unif_in.used_uniforms), unif_id); - unif_in->used_uniforms.insert(lower_bound, unif_id); + unif_in.used_uniforms.insert(lower_bound, unif_id); update_off.used = true; } } -void GlShaderProgram::set_i32(std::shared_ptr const &in, const char *unif, int32_t val) { +void GlShaderProgram::set_i32(UniformInput &in, const char *unif, int32_t val) { this->set_unif(in, unif, &val, get_uniform_type_size(GL_INT), GL_INT); } -void GlShaderProgram::set_u32(std::shared_ptr const &in, const char *unif, uint32_t val) { +void GlShaderProgram::set_u32(UniformInput &in, const char *unif, uint32_t val) { this->set_unif(in, unif, &val, get_uniform_type_size(GL_UNSIGNED_INT), GL_UNSIGNED_INT); } -void GlShaderProgram::set_f32(std::shared_ptr const &in, const char *unif, float val) { +void GlShaderProgram::set_f32(UniformInput &in, const char *unif, float val) { this->set_unif(in, unif, &val, get_uniform_type_size(GL_FLOAT), GL_FLOAT); } -void GlShaderProgram::set_f64(std::shared_ptr const &in, const char *unif, double val) { +void GlShaderProgram::set_f64(UniformInput &in, const char *unif, double val) { // TODO requires extension this->set_unif(in, unif, &val, get_uniform_type_size(GL_DOUBLE), GL_DOUBLE); } -void GlShaderProgram::set_bool(std::shared_ptr const &in, const char *unif, bool val) { +void GlShaderProgram::set_bool(UniformInput &in, const char *unif, bool val) { this->set_unif(in, unif, &val, get_uniform_type_size(GL_BOOL), GL_BOOL); } -void GlShaderProgram::set_v2f32(std::shared_ptr const &in, const char *unif, Eigen::Vector2f const &val) { +void GlShaderProgram::set_v2f32(UniformInput &in, const char *unif, Eigen::Vector2f const &val) { this->set_unif(in, unif, &val, get_uniform_type_size(GL_FLOAT_VEC2), GL_FLOAT_VEC2); } -void GlShaderProgram::set_v3f32(std::shared_ptr const &in, const char *unif, Eigen::Vector3f const &val) { +void GlShaderProgram::set_v3f32(UniformInput &in, const char *unif, Eigen::Vector3f const &val) { this->set_unif(in, unif, &val, get_uniform_type_size(GL_FLOAT_VEC3), GL_FLOAT_VEC3); } -void GlShaderProgram::set_v4f32(std::shared_ptr const &in, const char *unif, Eigen::Vector4f const &val) { +void GlShaderProgram::set_v4f32(UniformInput &in, const char *unif, Eigen::Vector4f const &val) { this->set_unif(in, unif, &val, get_uniform_type_size(GL_FLOAT_VEC4), GL_FLOAT_VEC4); } -void GlShaderProgram::set_v2i32(std::shared_ptr const &in, const char *unif, Eigen::Vector2i const &val) { +void GlShaderProgram::set_v2i32(UniformInput &in, const char *unif, Eigen::Vector2i const &val) { this->set_unif(in, unif, &val, get_uniform_type_size(GL_INT_VEC2), GL_INT_VEC2); } -void GlShaderProgram::set_v3i32(std::shared_ptr const &in, const char *unif, Eigen::Vector3i const &val) { +void GlShaderProgram::set_v3i32(UniformInput &in, const char *unif, Eigen::Vector3i const &val) { this->set_unif(in, unif, &val, get_uniform_type_size(GL_INT_VEC3), GL_INT_VEC3); } -void GlShaderProgram::set_v4i32(std::shared_ptr const &in, const char *unif, Eigen::Vector4i const &val) { +void GlShaderProgram::set_v4i32(UniformInput &in, const char *unif, Eigen::Vector4i const &val) { this->set_unif(in, unif, &val, get_uniform_type_size(GL_INT_VEC4), GL_INT_VEC4); } -void GlShaderProgram::set_v2ui32(std::shared_ptr const &in, const char *unif, Eigen::Vector2 const &val) { +void GlShaderProgram::set_v2ui32(UniformInput &in, const char *unif, Eigen::Vector2 const &val) { this->set_unif(in, unif, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC2), GL_UNSIGNED_INT_VEC2); } -void GlShaderProgram::set_v3ui32(std::shared_ptr const &in, const char *unif, Eigen::Vector3 const &val) { +void GlShaderProgram::set_v3ui32(UniformInput &in, const char *unif, Eigen::Vector3 const &val) { this->set_unif(in, unif, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC3), GL_UNSIGNED_INT_VEC3); } -void GlShaderProgram::set_v4ui32(std::shared_ptr const &in, const char *unif, Eigen::Vector4 const &val) { +void GlShaderProgram::set_v4ui32(UniformInput &in, const char *unif, Eigen::Vector4 const &val) { this->set_unif(in, unif, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC4), GL_UNSIGNED_INT_VEC4); } -void GlShaderProgram::set_m4f32(std::shared_ptr const &in, const char *unif, Eigen::Matrix4f const &val) { +void GlShaderProgram::set_m4f32(UniformInput &in, const char *unif, Eigen::Matrix4f const &val) { this->set_unif(in, unif, val.data(), get_uniform_type_size(GL_FLOAT_MAT4), GL_FLOAT_MAT4); } -void GlShaderProgram::set_tex(std::shared_ptr const &in, const char *unif, std::shared_ptr const &val) { +void GlShaderProgram::set_tex(UniformInput &in, const char *unif, std::shared_ptr const &val) { auto tex = std::dynamic_pointer_cast(val); GLuint handle = tex->get_handle(); this->set_unif(in, unif, &handle, get_uniform_type_size(GL_SAMPLER_2D), GL_SAMPLER_2D); } -void GlShaderProgram::set_i32(std::shared_ptr const &in, const uniform_id_t &id, int32_t val) { +void GlShaderProgram::set_i32(UniformInput &in, const uniform_id_t &id, int32_t val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_INT), GL_INT); } -void GlShaderProgram::set_u32(std::shared_ptr const &in, const uniform_id_t &id, uint32_t val) { +void GlShaderProgram::set_u32(UniformInput &in, const uniform_id_t &id, uint32_t val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT), GL_UNSIGNED_INT); } -void GlShaderProgram::set_f32(std::shared_ptr const &in, const uniform_id_t &id, float val) { +void GlShaderProgram::set_f32(UniformInput &in, const uniform_id_t &id, float val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT), GL_FLOAT); } -void GlShaderProgram::set_f64(std::shared_ptr const &in, const uniform_id_t &id, double val) { +void GlShaderProgram::set_f64(UniformInput &in, const uniform_id_t &id, double val) { // TODO requires extension this->set_unif(in, id, &val, get_uniform_type_size(GL_DOUBLE), GL_DOUBLE); } -void GlShaderProgram::set_bool(std::shared_ptr const &in, const uniform_id_t &id, bool val) { +void GlShaderProgram::set_bool(UniformInput &in, const uniform_id_t &id, bool val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_BOOL), GL_BOOL); } -void GlShaderProgram::set_v2f32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector2f const &val) { +void GlShaderProgram::set_v2f32(UniformInput &in, const uniform_id_t &id, Eigen::Vector2f const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT_VEC2), GL_FLOAT_VEC2); } -void GlShaderProgram::set_v3f32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector3f const &val) { +void GlShaderProgram::set_v3f32(UniformInput &in, const uniform_id_t &id, Eigen::Vector3f const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT_VEC3), GL_FLOAT_VEC3); } -void GlShaderProgram::set_v4f32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector4f const &val) { +void GlShaderProgram::set_v4f32(UniformInput &in, const uniform_id_t &id, Eigen::Vector4f const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT_VEC4), GL_FLOAT_VEC4); } -void GlShaderProgram::set_v2i32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector2i const &val) { +void GlShaderProgram::set_v2i32(UniformInput &in, const uniform_id_t &id, Eigen::Vector2i const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_INT_VEC2), GL_INT_VEC2); } -void GlShaderProgram::set_v3i32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector3i const &val) { +void GlShaderProgram::set_v3i32(UniformInput &in, const uniform_id_t &id, Eigen::Vector3i const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_INT_VEC3), GL_INT_VEC3); } -void GlShaderProgram::set_v4i32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector4i const &val) { +void GlShaderProgram::set_v4i32(UniformInput &in, const uniform_id_t &id, Eigen::Vector4i const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_INT_VEC4), GL_INT_VEC4); } -void GlShaderProgram::set_v2ui32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector2 const &val) { +void GlShaderProgram::set_v2ui32(UniformInput &in, const uniform_id_t &id, Eigen::Vector2 const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC2), GL_UNSIGNED_INT_VEC2); } -void GlShaderProgram::set_v3ui32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector3 const &val) { +void GlShaderProgram::set_v3ui32(UniformInput &in, const uniform_id_t &id, Eigen::Vector3 const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC3), GL_UNSIGNED_INT_VEC3); } -void GlShaderProgram::set_v4ui32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector4 const &val) { +void GlShaderProgram::set_v4ui32(UniformInput &in, const uniform_id_t &id, Eigen::Vector4 const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC4), GL_UNSIGNED_INT_VEC4); } -void GlShaderProgram::set_m4f32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Matrix4f const &val) { +void GlShaderProgram::set_m4f32(UniformInput &in, const uniform_id_t &id, Eigen::Matrix4f const &val) { this->set_unif(in, id, val.data(), get_uniform_type_size(GL_FLOAT_MAT4), GL_FLOAT_MAT4); } -void GlShaderProgram::set_tex(std::shared_ptr const &in, const uniform_id_t &id, std::shared_ptr const &val) { +void GlShaderProgram::set_tex(UniformInput &in, const uniform_id_t &id, std::shared_ptr const &val) { auto tex = std::dynamic_pointer_cast(val); GLuint handle = tex->get_handle(); this->set_unif(in, id, &handle, get_uniform_type_size(GL_SAMPLER_2D), GL_SAMPLER_2D); diff --git a/libopenage/renderer/opengl/shader_program.h b/libopenage/renderer/opengl/shader_program.h index 58553b03ed..4e9a6b94cf 100644 --- a/libopenage/renderer/opengl/shader_program.h +++ b/libopenage/renderer/opengl/shader_program.h @@ -111,39 +111,39 @@ class GlShaderProgram final : public ShaderProgram protected: std::shared_ptr new_unif_in() override; - void set_i32(std::shared_ptr const &, const char *, int32_t) override; - void set_u32(std::shared_ptr const &, const char *, uint32_t) override; - void set_f32(std::shared_ptr const &, const char *, float) override; - void set_f64(std::shared_ptr const &, const char *, double) override; - void set_bool(std::shared_ptr const &, const char *, bool) override; - void set_v2f32(std::shared_ptr const &, const char *, Eigen::Vector2f const &) override; - void set_v3f32(std::shared_ptr const &, const char *, Eigen::Vector3f const &) override; - void set_v4f32(std::shared_ptr const &, const char *, Eigen::Vector4f const &) override; - void set_v2i32(std::shared_ptr const &, const char *, Eigen::Vector2i const &) override; - void set_v3i32(std::shared_ptr const &, const char *, Eigen::Vector3i const &) override; - void set_v4i32(std::shared_ptr const &, const char *, Eigen::Vector4i const &) override; - void set_v2ui32(std::shared_ptr const &, const char *, Eigen::Vector2 const &) override; - void set_v3ui32(std::shared_ptr const &, const char *, Eigen::Vector3 const &) override; - void set_v4ui32(std::shared_ptr const &, const char *, Eigen::Vector4 const &) override; - void set_m4f32(std::shared_ptr const &, const char *, Eigen::Matrix4f const &) override; - void set_tex(std::shared_ptr const &, const char *, std::shared_ptr const &) override; - - void set_i32(std::shared_ptr const &, const uniform_id_t &, int32_t) override; - void set_u32(std::shared_ptr const &, const uniform_id_t &, uint32_t) override; - void set_f32(std::shared_ptr const &, const uniform_id_t &, float) override; - void set_f64(std::shared_ptr const &, const uniform_id_t &, double) override; - void set_bool(std::shared_ptr const &, const uniform_id_t &, bool) override; - void set_v2f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector2f const &) override; - void set_v3f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector3f const &) override; - void set_v4f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector4f const &) override; - void set_v2i32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector2i const &) override; - void set_v3i32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector3i const &) override; - void set_v4i32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector4i const &) override; - void set_v2ui32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector2 const &) override; - void set_v3ui32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector3 const &) override; - void set_v4ui32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector4 const &) override; - void set_m4f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Matrix4f const &) override; - void set_tex(std::shared_ptr const &, const uniform_id_t &, std::shared_ptr const &) override; + void set_i32(UniformInput &in, const char *, int32_t) override; + void set_u32(UniformInput &in, const char *, uint32_t) override; + void set_f32(UniformInput &in, const char *, float) override; + void set_f64(UniformInput &in, const char *, double) override; + void set_bool(UniformInput &in, const char *, bool) override; + void set_v2f32(UniformInput &in, const char *, Eigen::Vector2f const &) override; + void set_v3f32(UniformInput &in, const char *, Eigen::Vector3f const &) override; + void set_v4f32(UniformInput &in, const char *, Eigen::Vector4f const &) override; + void set_v2i32(UniformInput &in, const char *, Eigen::Vector2i const &) override; + void set_v3i32(UniformInput &in, const char *, Eigen::Vector3i const &) override; + void set_v4i32(UniformInput &in, const char *, Eigen::Vector4i const &) override; + void set_v2ui32(UniformInput &in, const char *, Eigen::Vector2 const &) override; + void set_v3ui32(UniformInput &in, const char *, Eigen::Vector3 const &) override; + void set_v4ui32(UniformInput &in, const char *, Eigen::Vector4 const &) override; + void set_m4f32(UniformInput &in, const char *, Eigen::Matrix4f const &) override; + void set_tex(UniformInput &in, const char *, std::shared_ptr const &) override; + + void set_i32(UniformInput &in, const uniform_id_t &, int32_t) override; + void set_u32(UniformInput &in, const uniform_id_t &, uint32_t) override; + void set_f32(UniformInput &in, const uniform_id_t &, float) override; + void set_f64(UniformInput &in, const uniform_id_t &, double) override; + void set_bool(UniformInput &in, const uniform_id_t &, bool) override; + void set_v2f32(UniformInput &in, const uniform_id_t &, Eigen::Vector2f const &) override; + void set_v3f32(UniformInput &in, const uniform_id_t &, Eigen::Vector3f const &) override; + void set_v4f32(UniformInput &in, const uniform_id_t &, Eigen::Vector4f const &) override; + void set_v2i32(UniformInput &in, const uniform_id_t &, Eigen::Vector2i const &) override; + void set_v3i32(UniformInput &in, const uniform_id_t &, Eigen::Vector3i const &) override; + void set_v4i32(UniformInput &in, const uniform_id_t &, Eigen::Vector4i const &) override; + void set_v2ui32(UniformInput &in, const uniform_id_t &, Eigen::Vector2 const &) override; + void set_v3ui32(UniformInput &in, const uniform_id_t &, Eigen::Vector3 const &) override; + void set_v4ui32(UniformInput &in, const uniform_id_t &, Eigen::Vector4 const &) override; + void set_m4f32(UniformInput &in, const uniform_id_t &, Eigen::Matrix4f const &) override; + void set_tex(UniformInput &in, const uniform_id_t &, std::shared_ptr const &) override; private: /** @@ -160,7 +160,7 @@ class GlShaderProgram final : public ShaderProgram * @param size Size of the value (in bytes). * @param type Type of the value. */ - void set_unif(std::shared_ptr const &in, + void set_unif(UniformInput &in, const char *name, void const *value, size_t size, @@ -175,7 +175,7 @@ class GlShaderProgram final : public ShaderProgram * @param size Size of the value (in bytes). * @param type Type of the value. */ - void set_unif(std::shared_ptr const &, + void set_unif(UniformInput &in, const uniform_id_t &unif_id, void const *value, size_t size, diff --git a/libopenage/renderer/shader_program.h b/libopenage/renderer/shader_program.h index beb365b2a4..86f3067cb6 100644 --- a/libopenage/renderer/shader_program.h +++ b/libopenage/renderer/shader_program.h @@ -94,39 +94,39 @@ class ShaderProgram : public std::enable_shared_from_this { /** * Set a uniform input variable in the actual shader program. */ - virtual void set_i32(std::shared_ptr const &, const char *, int32_t) = 0; - virtual void set_u32(std::shared_ptr const &, const char *, uint32_t) = 0; - virtual void set_f32(std::shared_ptr const &, const char *, float) = 0; - virtual void set_f64(std::shared_ptr const &, const char *, double) = 0; - virtual void set_bool(std::shared_ptr const &, const char *, bool) = 0; - virtual void set_v2f32(std::shared_ptr const &, const char *, Eigen::Vector2f const &) = 0; - virtual void set_v3f32(std::shared_ptr const &, const char *, Eigen::Vector3f const &) = 0; - virtual void set_v4f32(std::shared_ptr const &, const char *, Eigen::Vector4f const &) = 0; - virtual void set_v2i32(std::shared_ptr const &, const char *, Eigen::Vector2i const &) = 0; - virtual void set_v3i32(std::shared_ptr const &, const char *, Eigen::Vector3i const &) = 0; - virtual void set_v4i32(std::shared_ptr const &, const char *, Eigen::Vector4i const &) = 0; - virtual void set_v2ui32(std::shared_ptr const &, const char *, Eigen::Vector2 const &) = 0; - virtual void set_v3ui32(std::shared_ptr const &, const char *, Eigen::Vector3 const &) = 0; - virtual void set_v4ui32(std::shared_ptr const &, const char *, Eigen::Vector4 const &) = 0; - virtual void set_m4f32(std::shared_ptr const &, const char *, Eigen::Matrix4f const &) = 0; - virtual void set_tex(std::shared_ptr const &, const char *, std::shared_ptr const &) = 0; - - virtual void set_i32(std::shared_ptr const &, const uniform_id_t &, int32_t) = 0; - virtual void set_u32(std::shared_ptr const &, const uniform_id_t &, uint32_t) = 0; - virtual void set_f32(std::shared_ptr const &, const uniform_id_t &, float) = 0; - virtual void set_f64(std::shared_ptr const &, const uniform_id_t &, double) = 0; - virtual void set_bool(std::shared_ptr const &, const uniform_id_t &, bool) = 0; - virtual void set_v2f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector2f const &) = 0; - virtual void set_v3f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector3f const &) = 0; - virtual void set_v4f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector4f const &) = 0; - virtual void set_v2i32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector2i const &) = 0; - virtual void set_v3i32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector3i const &) = 0; - virtual void set_v4i32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector4i const &) = 0; - virtual void set_v2ui32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector2 const &) = 0; - virtual void set_v3ui32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector3 const &) = 0; - virtual void set_v4ui32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector4 const &) = 0; - virtual void set_m4f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Matrix4f const &) = 0; - virtual void set_tex(std::shared_ptr const &, const uniform_id_t &, std::shared_ptr const &) = 0; + virtual void set_i32(UniformInput &in, const char *, int32_t) = 0; + virtual void set_u32(UniformInput &in, const char *, uint32_t) = 0; + virtual void set_f32(UniformInput &in, const char *, float) = 0; + virtual void set_f64(UniformInput &in, const char *, double) = 0; + virtual void set_bool(UniformInput &in, const char *, bool) = 0; + virtual void set_v2f32(UniformInput &in, const char *, Eigen::Vector2f const &) = 0; + virtual void set_v3f32(UniformInput &in, const char *, Eigen::Vector3f const &) = 0; + virtual void set_v4f32(UniformInput &in, const char *, Eigen::Vector4f const &) = 0; + virtual void set_v2i32(UniformInput &in, const char *, Eigen::Vector2i const &) = 0; + virtual void set_v3i32(UniformInput &in, const char *, Eigen::Vector3i const &) = 0; + virtual void set_v4i32(UniformInput &in, const char *, Eigen::Vector4i const &) = 0; + virtual void set_v2ui32(UniformInput &in, const char *, Eigen::Vector2 const &) = 0; + virtual void set_v3ui32(UniformInput &in, const char *, Eigen::Vector3 const &) = 0; + virtual void set_v4ui32(UniformInput &in, const char *, Eigen::Vector4 const &) = 0; + virtual void set_m4f32(UniformInput &in, const char *, Eigen::Matrix4f const &) = 0; + virtual void set_tex(UniformInput &in, const char *, std::shared_ptr const &) = 0; + + virtual void set_i32(UniformInput &in, const uniform_id_t &, int32_t) = 0; + virtual void set_u32(UniformInput &in, const uniform_id_t &, uint32_t) = 0; + virtual void set_f32(UniformInput &in, const uniform_id_t &, float) = 0; + virtual void set_f64(UniformInput &in, const uniform_id_t &, double) = 0; + virtual void set_bool(UniformInput &in, const uniform_id_t &, bool) = 0; + virtual void set_v2f32(UniformInput &in, const uniform_id_t &, Eigen::Vector2f const &) = 0; + virtual void set_v3f32(UniformInput &in, const uniform_id_t &, Eigen::Vector3f const &) = 0; + virtual void set_v4f32(UniformInput &in, const uniform_id_t &, Eigen::Vector4f const &) = 0; + virtual void set_v2i32(UniformInput &in, const uniform_id_t &, Eigen::Vector2i const &) = 0; + virtual void set_v3i32(UniformInput &in, const uniform_id_t &, Eigen::Vector3i const &) = 0; + virtual void set_v4i32(UniformInput &in, const uniform_id_t &, Eigen::Vector4i const &) = 0; + virtual void set_v2ui32(UniformInput &in, const uniform_id_t &, Eigen::Vector2 const &) = 0; + virtual void set_v3ui32(UniformInput &in, const uniform_id_t &, Eigen::Vector3 const &) = 0; + virtual void set_v4ui32(UniformInput &in, const uniform_id_t &, Eigen::Vector4 const &) = 0; + virtual void set_m4f32(UniformInput &in, const uniform_id_t &, Eigen::Matrix4f const &) = 0; + virtual void set_tex(UniformInput &in, const uniform_id_t &, std::shared_ptr const &) = 0; }; } // namespace renderer diff --git a/libopenage/renderer/uniform_input.cpp b/libopenage/renderer/uniform_input.cpp index 8ad865ca04..6ff14c2d36 100644 --- a/libopenage/renderer/uniform_input.cpp +++ b/libopenage/renderer/uniform_input.cpp @@ -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. #include "uniform_input.h" @@ -14,140 +14,140 @@ UniformInput::UniformInput(std::shared_ptr const &prog) : void UniformInput::update() {} void UniformInput::update(const char *unif, int32_t val) { - this->program->set_i32(this->shared_from_this(), unif, val); + this->program->set_i32(*this, unif, val); } void UniformInput::update(const char *unif, uint32_t val) { - this->program->set_u32(this->shared_from_this(), unif, val); + this->program->set_u32(*this, unif, val); } void UniformInput::update(const char *unif, float val) { - this->program->set_f32(this->shared_from_this(), unif, val); + this->program->set_f32(*this, unif, val); } void UniformInput::update(const char *unif, double val) { - this->program->set_f64(this->shared_from_this(), unif, val); + this->program->set_f64(*this, unif, val); } void UniformInput::update(const char *unif, bool val) { - this->program->set_bool(this->shared_from_this(), unif, val); + this->program->set_bool(*this, unif, val); } void UniformInput::update(const char *unif, Eigen::Vector2f const &val) { - this->program->set_v2f32(this->shared_from_this(), unif, val); + this->program->set_v2f32(*this, unif, val); } void UniformInput::update(const char *unif, Eigen::Vector3f const &val) { - this->program->set_v3f32(this->shared_from_this(), unif, val); + this->program->set_v3f32(*this, unif, val); } void UniformInput::update(const char *unif, Eigen::Vector4f const &val) { - this->program->set_v4f32(this->shared_from_this(), unif, val); + this->program->set_v4f32(*this, unif, val); } void UniformInput::update(const char *unif, Eigen::Vector2i const &val) { - this->program->set_v2i32(this->shared_from_this(), unif, val); + this->program->set_v2i32(*this, unif, val); } void UniformInput::update(const char *unif, Eigen::Vector3i const &val) { - this->program->set_v3i32(this->shared_from_this(), unif, val); + this->program->set_v3i32(*this, unif, val); } void UniformInput::update(const char *unif, Eigen::Vector4i const &val) { - this->program->set_v4i32(this->shared_from_this(), unif, val); + this->program->set_v4i32(*this, unif, val); } void UniformInput::update(const char *unif, Eigen::Vector2 const &val) { - this->program->set_v2ui32(this->shared_from_this(), unif, val); + this->program->set_v2ui32(*this, unif, val); } void UniformInput::update(const char *unif, Eigen::Vector3 const &val) { - this->program->set_v3ui32(this->shared_from_this(), unif, val); + this->program->set_v3ui32(*this, unif, val); } void UniformInput::update(const char *unif, Eigen::Vector4 const &val) { - this->program->set_v4ui32(this->shared_from_this(), unif, val); + this->program->set_v4ui32(*this, unif, val); } void UniformInput::update(const char *unif, std::shared_ptr const &val) { - this->program->set_tex(this->shared_from_this(), unif, val); + this->program->set_tex(*this, unif, val); } void UniformInput::update(const char *unif, std::shared_ptr &val) { - this->program->set_tex(this->shared_from_this(), unif, val); + this->program->set_tex(*this, unif, val); } void UniformInput::update(const char *unif, Eigen::Matrix4f const &val) { - this->program->set_m4f32(this->shared_from_this(), unif, val); + this->program->set_m4f32(*this, unif, val); } void UniformInput::update(const uniform_id_t &id, int32_t val) { - this->program->set_i32(this->shared_from_this(), id, val); + this->program->set_i32(*this, id, val); } void UniformInput::update(const uniform_id_t &id, uint32_t val) { - this->program->set_u32(this->shared_from_this(), id, val); + this->program->set_u32(*this, id, val); } void UniformInput::update(const uniform_id_t &id, float val) { - this->program->set_f32(this->shared_from_this(), id, val); + this->program->set_f32(*this, id, val); } void UniformInput::update(const uniform_id_t &id, double val) { - this->program->set_f64(this->shared_from_this(), id, val); + this->program->set_f64(*this, id, val); } void UniformInput::update(const uniform_id_t &id, bool val) { - this->program->set_bool(this->shared_from_this(), id, val); + this->program->set_bool(*this, id, val); } void UniformInput::update(const uniform_id_t &id, Eigen::Vector2f const &val) { - this->program->set_v2f32(this->shared_from_this(), id, val); + this->program->set_v2f32(*this, id, val); } void UniformInput::update(const uniform_id_t &id, Eigen::Vector3f const &val) { - this->program->set_v3f32(this->shared_from_this(), id, val); + this->program->set_v3f32(*this, id, val); } void UniformInput::update(const uniform_id_t &id, Eigen::Vector4f const &val) { - this->program->set_v4f32(this->shared_from_this(), id, val); + this->program->set_v4f32(*this, id, val); } void UniformInput::update(const uniform_id_t &id, Eigen::Vector2i const &val) { - this->program->set_v2i32(this->shared_from_this(), id, val); + this->program->set_v2i32(*this, id, val); } void UniformInput::update(const uniform_id_t &id, Eigen::Vector3i const &val) { - this->program->set_v3i32(this->shared_from_this(), id, val); + this->program->set_v3i32(*this, id, val); } void UniformInput::update(const uniform_id_t &id, Eigen::Vector4i const &val) { - this->program->set_v4i32(this->shared_from_this(), id, val); + this->program->set_v4i32(*this, id, val); } void UniformInput::update(const uniform_id_t &id, Eigen::Vector2 const &val) { - this->program->set_v2ui32(this->shared_from_this(), id, val); + this->program->set_v2ui32(*this, id, val); } void UniformInput::update(const uniform_id_t &id, Eigen::Vector3 const &val) { - this->program->set_v3ui32(this->shared_from_this(), id, val); + this->program->set_v3ui32(*this, id, val); } void UniformInput::update(const uniform_id_t &id, Eigen::Vector4 const &val) { - this->program->set_v4ui32(this->shared_from_this(), id, val); + this->program->set_v4ui32(*this, id, val); } void UniformInput::update(const uniform_id_t &id, std::shared_ptr const &val) { - this->program->set_tex(this->shared_from_this(), id, val); + this->program->set_tex(*this, id, val); } void UniformInput::update(const uniform_id_t &id, std::shared_ptr &val) { - this->program->set_tex(this->shared_from_this(), id, val); + this->program->set_tex(*this, id, val); } void UniformInput::update(const uniform_id_t &id, Eigen::Matrix4f const &val) { - this->program->set_m4f32(this->shared_from_this(), id, val); + this->program->set_m4f32(*this, id, val); } diff --git a/libopenage/renderer/uniform_input.h b/libopenage/renderer/uniform_input.h index 9e66b64712..7942ee9eca 100644 --- a/libopenage/renderer/uniform_input.h +++ b/libopenage/renderer/uniform_input.h @@ -54,8 +54,7 @@ class DataInput { * Abstract base for uniform input. Besides the uniform values, it stores information about * which shader program the input was created for. */ -class UniformInput : public DataInput - , public std::enable_shared_from_this { +class UniformInput : public DataInput { protected: /** * Create a new uniform input for a given shader program. From e57f8650ab52cc12583a0c30314361776b2ed287 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 2 Aug 2024 01:51:14 +0200 Subject: [PATCH 479/771] renderer: Pass uniform ID by value to avoid dereference. --- libopenage/renderer/opengl/shader_program.cpp | 34 +++++++++--------- libopenage/renderer/opengl/shader_program.h | 36 +++++++++---------- libopenage/renderer/shader_program.h | 32 ++++++++--------- libopenage/renderer/uniform_input.cpp | 34 +++++++++--------- libopenage/renderer/uniform_input.h | 34 +++++++++--------- 5 files changed, 85 insertions(+), 85 deletions(-) diff --git a/libopenage/renderer/opengl/shader_program.cpp b/libopenage/renderer/opengl/shader_program.cpp index 563c8a1221..da24ac1596 100644 --- a/libopenage/renderer/opengl/shader_program.cpp +++ b/libopenage/renderer/opengl/shader_program.cpp @@ -500,7 +500,7 @@ void GlShaderProgram::set_unif(UniformInput &in, } void GlShaderProgram::set_unif(UniformInput &in, - const uniform_id_t &unif_id, + uniform_id_t unif_id, void const *val, size_t size, GLenum type) { @@ -596,68 +596,68 @@ void GlShaderProgram::set_tex(UniformInput &in, const char *unif, std::shared_pt this->set_unif(in, unif, &handle, get_uniform_type_size(GL_SAMPLER_2D), GL_SAMPLER_2D); } -void GlShaderProgram::set_i32(UniformInput &in, const uniform_id_t &id, int32_t val) { +void GlShaderProgram::set_i32(UniformInput &in, uniform_id_t id, int32_t val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_INT), GL_INT); } -void GlShaderProgram::set_u32(UniformInput &in, const uniform_id_t &id, uint32_t val) { +void GlShaderProgram::set_u32(UniformInput &in, uniform_id_t id, uint32_t val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT), GL_UNSIGNED_INT); } -void GlShaderProgram::set_f32(UniformInput &in, const uniform_id_t &id, float val) { +void GlShaderProgram::set_f32(UniformInput &in, uniform_id_t id, float val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT), GL_FLOAT); } -void GlShaderProgram::set_f64(UniformInput &in, const uniform_id_t &id, double val) { +void GlShaderProgram::set_f64(UniformInput &in, uniform_id_t id, double val) { // TODO requires extension this->set_unif(in, id, &val, get_uniform_type_size(GL_DOUBLE), GL_DOUBLE); } -void GlShaderProgram::set_bool(UniformInput &in, const uniform_id_t &id, bool val) { +void GlShaderProgram::set_bool(UniformInput &in, uniform_id_t id, bool val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_BOOL), GL_BOOL); } -void GlShaderProgram::set_v2f32(UniformInput &in, const uniform_id_t &id, Eigen::Vector2f const &val) { +void GlShaderProgram::set_v2f32(UniformInput &in, uniform_id_t id, Eigen::Vector2f const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT_VEC2), GL_FLOAT_VEC2); } -void GlShaderProgram::set_v3f32(UniformInput &in, const uniform_id_t &id, Eigen::Vector3f const &val) { +void GlShaderProgram::set_v3f32(UniformInput &in, uniform_id_t id, Eigen::Vector3f const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT_VEC3), GL_FLOAT_VEC3); } -void GlShaderProgram::set_v4f32(UniformInput &in, const uniform_id_t &id, Eigen::Vector4f const &val) { +void GlShaderProgram::set_v4f32(UniformInput &in, uniform_id_t id, Eigen::Vector4f const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT_VEC4), GL_FLOAT_VEC4); } -void GlShaderProgram::set_v2i32(UniformInput &in, const uniform_id_t &id, Eigen::Vector2i const &val) { +void GlShaderProgram::set_v2i32(UniformInput &in, uniform_id_t id, Eigen::Vector2i const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_INT_VEC2), GL_INT_VEC2); } -void GlShaderProgram::set_v3i32(UniformInput &in, const uniform_id_t &id, Eigen::Vector3i const &val) { +void GlShaderProgram::set_v3i32(UniformInput &in, uniform_id_t id, Eigen::Vector3i const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_INT_VEC3), GL_INT_VEC3); } -void GlShaderProgram::set_v4i32(UniformInput &in, const uniform_id_t &id, Eigen::Vector4i const &val) { +void GlShaderProgram::set_v4i32(UniformInput &in, uniform_id_t id, Eigen::Vector4i const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_INT_VEC4), GL_INT_VEC4); } -void GlShaderProgram::set_v2ui32(UniformInput &in, const uniform_id_t &id, Eigen::Vector2 const &val) { +void GlShaderProgram::set_v2ui32(UniformInput &in, uniform_id_t id, Eigen::Vector2 const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC2), GL_UNSIGNED_INT_VEC2); } -void GlShaderProgram::set_v3ui32(UniformInput &in, const uniform_id_t &id, Eigen::Vector3 const &val) { +void GlShaderProgram::set_v3ui32(UniformInput &in, uniform_id_t id, Eigen::Vector3 const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC3), GL_UNSIGNED_INT_VEC3); } -void GlShaderProgram::set_v4ui32(UniformInput &in, const uniform_id_t &id, Eigen::Vector4 const &val) { +void GlShaderProgram::set_v4ui32(UniformInput &in, uniform_id_t id, Eigen::Vector4 const &val) { this->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC4), GL_UNSIGNED_INT_VEC4); } -void GlShaderProgram::set_m4f32(UniformInput &in, const uniform_id_t &id, Eigen::Matrix4f const &val) { +void GlShaderProgram::set_m4f32(UniformInput &in, uniform_id_t id, Eigen::Matrix4f const &val) { this->set_unif(in, id, val.data(), get_uniform_type_size(GL_FLOAT_MAT4), GL_FLOAT_MAT4); } -void GlShaderProgram::set_tex(UniformInput &in, const uniform_id_t &id, std::shared_ptr const &val) { +void GlShaderProgram::set_tex(UniformInput &in, uniform_id_t id, std::shared_ptr const &val) { auto tex = std::dynamic_pointer_cast(val); GLuint handle = tex->get_handle(); this->set_unif(in, id, &handle, get_uniform_type_size(GL_SAMPLER_2D), GL_SAMPLER_2D); diff --git a/libopenage/renderer/opengl/shader_program.h b/libopenage/renderer/opengl/shader_program.h index 4e9a6b94cf..5d31f2a071 100644 --- a/libopenage/renderer/opengl/shader_program.h +++ b/libopenage/renderer/opengl/shader_program.h @@ -128,22 +128,22 @@ class GlShaderProgram final : public ShaderProgram void set_m4f32(UniformInput &in, const char *, Eigen::Matrix4f const &) override; void set_tex(UniformInput &in, const char *, std::shared_ptr const &) override; - void set_i32(UniformInput &in, const uniform_id_t &, int32_t) override; - void set_u32(UniformInput &in, const uniform_id_t &, uint32_t) override; - void set_f32(UniformInput &in, const uniform_id_t &, float) override; - void set_f64(UniformInput &in, const uniform_id_t &, double) override; - void set_bool(UniformInput &in, const uniform_id_t &, bool) override; - void set_v2f32(UniformInput &in, const uniform_id_t &, Eigen::Vector2f const &) override; - void set_v3f32(UniformInput &in, const uniform_id_t &, Eigen::Vector3f const &) override; - void set_v4f32(UniformInput &in, const uniform_id_t &, Eigen::Vector4f const &) override; - void set_v2i32(UniformInput &in, const uniform_id_t &, Eigen::Vector2i const &) override; - void set_v3i32(UniformInput &in, const uniform_id_t &, Eigen::Vector3i const &) override; - void set_v4i32(UniformInput &in, const uniform_id_t &, Eigen::Vector4i const &) override; - void set_v2ui32(UniformInput &in, const uniform_id_t &, Eigen::Vector2 const &) override; - void set_v3ui32(UniformInput &in, const uniform_id_t &, Eigen::Vector3 const &) override; - void set_v4ui32(UniformInput &in, const uniform_id_t &, Eigen::Vector4 const &) override; - void set_m4f32(UniformInput &in, const uniform_id_t &, Eigen::Matrix4f const &) override; - void set_tex(UniformInput &in, const uniform_id_t &, std::shared_ptr const &) override; + void set_i32(UniformInput &in, uniform_id_t id, int32_t) override; + void set_u32(UniformInput &in, uniform_id_t id, uint32_t) override; + void set_f32(UniformInput &in, uniform_id_t id, float) override; + void set_f64(UniformInput &in, uniform_id_t id, double) override; + void set_bool(UniformInput &in, uniform_id_t id, bool) override; + void set_v2f32(UniformInput &in, uniform_id_t id, Eigen::Vector2f const &) override; + void set_v3f32(UniformInput &in, uniform_id_t id, Eigen::Vector3f const &) override; + void set_v4f32(UniformInput &in, uniform_id_t id, Eigen::Vector4f const &) override; + void set_v2i32(UniformInput &in, uniform_id_t id, Eigen::Vector2i const &) override; + void set_v3i32(UniformInput &in, uniform_id_t id, Eigen::Vector3i const &) override; + void set_v4i32(UniformInput &in, uniform_id_t id, Eigen::Vector4i const &) override; + void set_v2ui32(UniformInput &in, uniform_id_t id, Eigen::Vector2 const &) override; + void set_v3ui32(UniformInput &in, uniform_id_t id, Eigen::Vector3 const &) override; + void set_v4ui32(UniformInput &in, uniform_id_t id, Eigen::Vector4 const &) override; + void set_m4f32(UniformInput &in, uniform_id_t id, Eigen::Matrix4f const &) override; + void set_tex(UniformInput &in, uniform_id_t id, std::shared_ptr const &) override; private: /** @@ -170,13 +170,13 @@ class GlShaderProgram final : public ShaderProgram * Set the uniform value via uniform ID from a uniform input. * * @param in Uniform input. - * @param id ID of the uniform. + * @param unif_id ID of the uniform. * @param value Value to set. * @param size Size of the value (in bytes). * @param type Type of the value. */ void set_unif(UniformInput &in, - const uniform_id_t &unif_id, + uniform_id_t unif_id, void const *value, size_t size, GLenum type); diff --git a/libopenage/renderer/shader_program.h b/libopenage/renderer/shader_program.h index 86f3067cb6..a0d27dc5b0 100644 --- a/libopenage/renderer/shader_program.h +++ b/libopenage/renderer/shader_program.h @@ -111,22 +111,22 @@ class ShaderProgram : public std::enable_shared_from_this { virtual void set_m4f32(UniformInput &in, const char *, Eigen::Matrix4f const &) = 0; virtual void set_tex(UniformInput &in, const char *, std::shared_ptr const &) = 0; - virtual void set_i32(UniformInput &in, const uniform_id_t &, int32_t) = 0; - virtual void set_u32(UniformInput &in, const uniform_id_t &, uint32_t) = 0; - virtual void set_f32(UniformInput &in, const uniform_id_t &, float) = 0; - virtual void set_f64(UniformInput &in, const uniform_id_t &, double) = 0; - virtual void set_bool(UniformInput &in, const uniform_id_t &, bool) = 0; - virtual void set_v2f32(UniformInput &in, const uniform_id_t &, Eigen::Vector2f const &) = 0; - virtual void set_v3f32(UniformInput &in, const uniform_id_t &, Eigen::Vector3f const &) = 0; - virtual void set_v4f32(UniformInput &in, const uniform_id_t &, Eigen::Vector4f const &) = 0; - virtual void set_v2i32(UniformInput &in, const uniform_id_t &, Eigen::Vector2i const &) = 0; - virtual void set_v3i32(UniformInput &in, const uniform_id_t &, Eigen::Vector3i const &) = 0; - virtual void set_v4i32(UniformInput &in, const uniform_id_t &, Eigen::Vector4i const &) = 0; - virtual void set_v2ui32(UniformInput &in, const uniform_id_t &, Eigen::Vector2 const &) = 0; - virtual void set_v3ui32(UniformInput &in, const uniform_id_t &, Eigen::Vector3 const &) = 0; - virtual void set_v4ui32(UniformInput &in, const uniform_id_t &, Eigen::Vector4 const &) = 0; - virtual void set_m4f32(UniformInput &in, const uniform_id_t &, Eigen::Matrix4f const &) = 0; - virtual void set_tex(UniformInput &in, const uniform_id_t &, std::shared_ptr const &) = 0; + virtual void set_i32(UniformInput &in, uniform_id_t id, int32_t) = 0; + virtual void set_u32(UniformInput &in, uniform_id_t id, uint32_t) = 0; + virtual void set_f32(UniformInput &in, uniform_id_t id, float) = 0; + virtual void set_f64(UniformInput &in, uniform_id_t id, double) = 0; + virtual void set_bool(UniformInput &in, uniform_id_t id, bool) = 0; + virtual void set_v2f32(UniformInput &in, uniform_id_t id, Eigen::Vector2f const &) = 0; + virtual void set_v3f32(UniformInput &in, uniform_id_t id, Eigen::Vector3f const &) = 0; + virtual void set_v4f32(UniformInput &in, uniform_id_t id, Eigen::Vector4f const &) = 0; + virtual void set_v2i32(UniformInput &in, uniform_id_t id, Eigen::Vector2i const &) = 0; + virtual void set_v3i32(UniformInput &in, uniform_id_t id, Eigen::Vector3i const &) = 0; + virtual void set_v4i32(UniformInput &in, uniform_id_t id, Eigen::Vector4i const &) = 0; + virtual void set_v2ui32(UniformInput &in, uniform_id_t id, Eigen::Vector2 const &) = 0; + virtual void set_v3ui32(UniformInput &in, uniform_id_t id, Eigen::Vector3 const &) = 0; + virtual void set_v4ui32(UniformInput &in, uniform_id_t id, Eigen::Vector4 const &) = 0; + virtual void set_m4f32(UniformInput &in, uniform_id_t id, Eigen::Matrix4f const &) = 0; + virtual void set_tex(UniformInput &in, uniform_id_t id, std::shared_ptr const &) = 0; }; } // namespace renderer diff --git a/libopenage/renderer/uniform_input.cpp b/libopenage/renderer/uniform_input.cpp index 6ff14c2d36..ee42202faf 100644 --- a/libopenage/renderer/uniform_input.cpp +++ b/libopenage/renderer/uniform_input.cpp @@ -82,71 +82,71 @@ void UniformInput::update(const char *unif, Eigen::Matrix4f const &val) { } -void UniformInput::update(const uniform_id_t &id, int32_t val) { +void UniformInput::update(uniform_id_t id, int32_t val) { this->program->set_i32(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, uint32_t val) { +void UniformInput::update(uniform_id_t id, uint32_t val) { this->program->set_u32(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, float val) { +void UniformInput::update(uniform_id_t id, float val) { this->program->set_f32(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, double val) { +void UniformInput::update(uniform_id_t id, double val) { this->program->set_f64(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, bool val) { +void UniformInput::update(uniform_id_t id, bool val) { this->program->set_bool(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, Eigen::Vector2f const &val) { +void UniformInput::update(uniform_id_t id, Eigen::Vector2f const &val) { this->program->set_v2f32(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, Eigen::Vector3f const &val) { +void UniformInput::update(uniform_id_t id, Eigen::Vector3f const &val) { this->program->set_v3f32(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, Eigen::Vector4f const &val) { +void UniformInput::update(uniform_id_t id, Eigen::Vector4f const &val) { this->program->set_v4f32(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, Eigen::Vector2i const &val) { +void UniformInput::update(uniform_id_t id, Eigen::Vector2i const &val) { this->program->set_v2i32(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, Eigen::Vector3i const &val) { +void UniformInput::update(uniform_id_t id, Eigen::Vector3i const &val) { this->program->set_v3i32(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, Eigen::Vector4i const &val) { +void UniformInput::update(uniform_id_t id, Eigen::Vector4i const &val) { this->program->set_v4i32(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, Eigen::Vector2 const &val) { +void UniformInput::update(uniform_id_t id, Eigen::Vector2 const &val) { this->program->set_v2ui32(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, Eigen::Vector3 const &val) { +void UniformInput::update(uniform_id_t id, Eigen::Vector3 const &val) { this->program->set_v3ui32(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, Eigen::Vector4 const &val) { +void UniformInput::update(uniform_id_t id, Eigen::Vector4 const &val) { this->program->set_v4ui32(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, std::shared_ptr const &val) { +void UniformInput::update(uniform_id_t id, std::shared_ptr const &val) { this->program->set_tex(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, std::shared_ptr &val) { +void UniformInput::update(uniform_id_t id, std::shared_ptr &val) { this->program->set_tex(*this, id, val); } -void UniformInput::update(const uniform_id_t &id, Eigen::Matrix4f const &val) { +void UniformInput::update(uniform_id_t id, Eigen::Matrix4f const &val) { this->program->set_m4f32(*this, id, val); } diff --git a/libopenage/renderer/uniform_input.h b/libopenage/renderer/uniform_input.h index 7942ee9eca..6f2536459b 100644 --- a/libopenage/renderer/uniform_input.h +++ b/libopenage/renderer/uniform_input.h @@ -85,23 +85,23 @@ class UniformInput : public DataInput { void update(const char *unif, std::shared_ptr &val) override; void update(const char *unif, Eigen::Matrix4f const &val) override; - void update(const uniform_id_t &id, int32_t val); - void update(const uniform_id_t &id, uint32_t val); - void update(const uniform_id_t &id, float val); - void update(const uniform_id_t &id, double val); - void update(const uniform_id_t &id, bool val); - void update(const uniform_id_t &id, Eigen::Vector2f const &val); - void update(const uniform_id_t &id, Eigen::Vector3f const &val); - void update(const uniform_id_t &id, Eigen::Vector4f const &val); - void update(const uniform_id_t &id, Eigen::Vector2i const &val); - void update(const uniform_id_t &id, Eigen::Vector3i const &val); - void update(const uniform_id_t &id, Eigen::Vector4i const &val); - void update(const uniform_id_t &id, Eigen::Vector2 const &val); - void update(const uniform_id_t &id, Eigen::Vector3 const &val); - void update(const uniform_id_t &id, Eigen::Vector4 const &val); - void update(const uniform_id_t &id, std::shared_ptr const &val); - void update(const uniform_id_t &id, std::shared_ptr &val); - void update(const uniform_id_t &id, Eigen::Matrix4f const &val); + void update(uniform_id_t id, int32_t val); + void update(uniform_id_t id, uint32_t val); + void update(uniform_id_t id, float val); + void update(uniform_id_t id, double val); + void update(uniform_id_t id, bool val); + void update(uniform_id_t id, Eigen::Vector2f const &val); + void update(uniform_id_t id, Eigen::Vector3f const &val); + void update(uniform_id_t id, Eigen::Vector4f const &val); + void update(uniform_id_t id, Eigen::Vector2i const &val); + void update(uniform_id_t id, Eigen::Vector3i const &val); + void update(uniform_id_t id, Eigen::Vector4i const &val); + void update(uniform_id_t id, Eigen::Vector2 const &val); + void update(uniform_id_t id, Eigen::Vector3 const &val); + void update(uniform_id_t id, Eigen::Vector4 const &val); + void update(uniform_id_t id, std::shared_ptr const &val); + void update(uniform_id_t id, std::shared_ptr &val); + void update(uniform_id_t id, Eigen::Matrix4f const &val); /** * Catch-all template in order to handle unsupported types and avoid infinite recursion. From 2e277bafe9e7fb12294ecc5e4413ea69af6a8fcd Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 3 Aug 2024 20:46:34 +0200 Subject: [PATCH 480/771] cfg: Add modpack metadata to the converter cfg files. --- cfg/converter/games/game_editions.toml | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/cfg/converter/games/game_editions.toml b/cfg/converter/games/game_editions.toml index 7b0331e81e..06837bc96e 100644 --- a/cfg/converter/games/game_editions.toml +++ b/cfg/converter/games/game_editions.toml @@ -37,6 +37,11 @@ expansions = [] "C:/Program Files (x86)/Microsoft Games/Age of Empires II", ] + [AOC.targetmods.aoe2_base] + version = "0.5.1" + versionstr = "1.0c" + min_api_version = "0.5.0" + [AOCDEMO] name = "Age of Empires 2: The Conqueror's Trial Version" @@ -57,6 +62,12 @@ expansions = [] terrain = ["data/Terrain.drs"] blend = ["data/blendomatic.dat"] + [AOCDEMO.targetmods.trial_base] + version = "0.5.1" + versionstr = "Trial" + min_api_version = "0.5.0" + + [AOK] name = "Age of Empires 2: Age of Kings" game_edition_id = "AOK" @@ -131,6 +142,11 @@ expansions = [] "C:/Program Files (x86)/Steam/steamapps/common/AoEDE", ] + [AOE1DE.targetmods.de1_base] + version = "0.5.1" + versionstr = "1.0a" + min_api_version = "0.5.0" + [ROR] name = "Age of Empires 1: Rise of Rome" @@ -166,6 +182,11 @@ expansions = [] "C:/Program Files (x86)/Microsoft Games/Age of Empires", ] + [ROR.targetmods.aoe1_base] + version = "0.5.1" + versionstr = "1.0a" + min_api_version = "0.5.0" + [HDEDITION] name = "Age of Empires 2: HD Edition" @@ -205,6 +226,11 @@ expansions = [] "C:/Program Files (x86)/Steam/steamapps/common/Age2HD", ] + [HDEDITION.targetmods.hd_base] + version = "0.5.1" + versionstr = "5.8" + min_api_version = "0.5.0" + [AOE2DE] name = "Age of Empires 2: Definitive Edition" @@ -249,6 +275,11 @@ expansions = [] "C:/Program Files (x86)/Steam/steamapps/common/AoE2DE", ] + [AOE2DE.targetmods.de2_base] + version = "0.6.0" + versionstr = "Update 118476+" + min_api_version = "0.5.0" + [SWGB] name = "Star Wars: Galactic Battlegrounds" @@ -285,3 +316,8 @@ expansions = ["SWGB_CC"] "C:/Program Files (x86)/Steam/steamapps/common/STAR WARS - Galactic Battlegrounds Saga", "C:/Program Files/Steam/steamapps/common/STAR WARS - Galactic Battlegrounds Saga", ] + + [SWGB.targetmods.swgb_base] + version = "0.5.1" + versionstr = "1.1-gog4" + min_api_version = "0.5.0" From b923bd52fd22f6ba60bd9362bc50acf70bf9fef2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 3 Aug 2024 21:09:12 +0200 Subject: [PATCH 481/771] convert: Log pickle cache filename before reading it. --- openage/convert/service/read/gamedata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openage/convert/service/read/gamedata.py b/openage/convert/service/read/gamedata.py index 522d959b8e..575f51ad67 100644 --- a/openage/convert/service/read/gamedata.py +++ b/openage/convert/service/read/gamedata.py @@ -73,8 +73,8 @@ def load_gamespec( # pickle.load() can fail in many ways, we need to catch all. # pylint: disable=broad-except try: - gamespec = pickle.load(cachefile) info("using cached wrapper: %s", cachefile_name) + gamespec = pickle.load(cachefile) return gamespec except Exception: warn("could not use cached wrapper:") From c232c5b527914b188a802777cb17748dd2ddadcd Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 3 Aug 2024 21:37:50 +0200 Subject: [PATCH 482/771] convert: Fix time logging of modpack export. --- .../processor/conversion/aoc/modpack_subprocessor.py | 5 ++++- openage/convert/tool/driver.py | 9 +++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/openage/convert/processor/conversion/aoc/modpack_subprocessor.py b/openage/convert/processor/conversion/aoc/modpack_subprocessor.py index 4f7628751e..b842fc74c2 100644 --- a/openage/convert/processor/conversion/aoc/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/modpack_subprocessor.py @@ -44,7 +44,10 @@ def _get_aoe2_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("aoe2_base", "0.5.1", versionstr="1.0c", repo="openage") + targetmod_info = full_data_set.game_version.edition.target_modpacks["aoe2_base"] + version = targetmod_info["version"] + versionstr = targetmod_info["versionstr"] + mod_def.set_info("aoe2_base", version, versionstr=versionstr, repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/tool/driver.py b/openage/convert/tool/driver.py index cdd1ed04a3..888a5039e1 100644 --- a/openage/convert/tool/driver.py +++ b/openage/convert/tool/driver.py @@ -10,6 +10,8 @@ import typing import timeit +from openage.convert.service import export + from ...log import info, dbg from ..processor.export.modpack_exporter import ModpackExporter @@ -99,14 +101,17 @@ def convert_metadata(args: Namespace) -> None: export_start = timeit.default_timer() for modpack in modpacks: + mod_export_start = timeit.default_timer() ModpackExporter.export(modpack, args) debug_modpack(args.debugdir, args.debug_info, modpack) - export_end = timeit.default_timer() + mod_export_end = timeit.default_timer() info("Finished export of modpack '%s' v%s (%.2f seconds)", modpack.info.packagename, modpack.info.version, - export_end - export_start) + mod_export_end - mod_export_start) + + export_end = timeit.default_timer() stages_time = { "read": read_end - read_start, From 001bd357eb73bed6b83d461021bfcfa18d241454 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 3 Aug 2024 21:41:33 +0200 Subject: [PATCH 483/771] convert: Read modpack version and versionstr from cfg file. --- cfg/converter/games/game_editions.toml | 2 + cfg/converter/games/game_expansions.toml | 45 ++++++++++--------- .../aoc_demo/modpack_subprocessor.py | 7 ++- .../conversion/de1/modpack_subprocessor.py | 7 ++- .../conversion/de2/modpack_subprocessor.py | 5 ++- .../conversion/hd/modpack_subprocessor.py | 7 ++- .../conversion/ror/modpack_subprocessor.py | 7 ++- .../conversion/swgbcc/modpack_subprocessor.py | 7 ++- .../convert/service/init/version_detect.py | 4 +- openage/convert/service/read/gamedata.py | 2 +- openage/convert/tool/driver.py | 2 - .../convert/value_object/init/game_version.py | 4 +- openage/game/main.py | 8 +++- 13 files changed, 68 insertions(+), 39 deletions(-) diff --git a/cfg/converter/games/game_editions.toml b/cfg/converter/games/game_editions.toml index 06837bc96e..24628e8273 100644 --- a/cfg/converter/games/game_editions.toml +++ b/cfg/converter/games/game_editions.toml @@ -101,6 +101,8 @@ expansions = [] "C:/Program Files (x86)/Microsoft Games/Age of Empires II", ] + [AOK.targetmods] + [AOE1DE] name = "Age of Empires 1: Definitive Edition (Steam)" diff --git a/cfg/converter/games/game_expansions.toml b/cfg/converter/games/game_expansions.toml index b3dd159c12..5aca31f780 100644 --- a/cfg/converter/games/game_expansions.toml +++ b/cfg/converter/games/game_expansions.toml @@ -3,31 +3,36 @@ file_version = "1.0" [AFRI_KING] -name = "African Kingdoms (HD)" +name = "African Kingdoms (HD)" game_edition_id = "AFRI_KING" -subfolder = "hd_ak" -support = "nope" -targetmod = [ "aoe2-ak", "aoe2-ak-graphics" ] +subfolder = "hd_ak" +support = "nope" +targetmod = ["aoe2-ak", "aoe2-ak-graphics"] [AFRI_KING.mediapaths] - graphics = [ "resources/_common/slp/" ] - sounds = [ "resources/_common/sound/" ] - interface = [ "resources/_common/drs/interface/" ] - terrain = [ "resources/_common/terrain/" ] + graphics = ["resources/_common/slp/"] + sounds = ["resources/_common/sound/"] + interface = ["resources/_common/drs/interface/"] + terrain = ["resources/_common/terrain/"] + + [AFRI_KING.targetmods] + [SWGB_CC] -name = "Clone Campaigns" +name = "Clone Campaigns" game_edition_id = "SWGB_CC" -subfolder = "swgb_cc" -support = "yes" -targetmod = [ "swgb-cc", "swgb-cc-graphics" ] +subfolder = "swgb_cc" +support = "yes" +targetmod = ["swgb-cc", "swgb-cc-graphics"] [SWGB_CC.mediapaths] - datfile = [ "Game/Data/genie_x1.dat" ] - gamedata = [ "Game/Data/genie_x1.dat" ] - graphics = [ "Game/Data/graphics_x1.drs" ] - language = [ "Game/language_x1.dll" ] - palettes = [ "Game/Data/interfac_x1.drs" ] - sounds = [ "Game/Data/sounds_x1.drs" ] - interface = [ "Game/Data/interfac_x1.drs" ] - terrain = [ "Game/Data/terrain_x1.drs" ] + datfile = ["Game/Data/genie_x1.dat"] + gamedata = ["Game/Data/genie_x1.dat"] + graphics = ["Game/Data/graphics_x1.drs"] + language = ["Game/language_x1.dll"] + palettes = ["Game/Data/interfac_x1.drs"] + sounds = ["Game/Data/sounds_x1.drs"] + interface = ["Game/Data/interfac_x1.drs"] + terrain = ["Game/Data/terrain_x1.drs"] + + [SWGB_CC.targetmods] diff --git a/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py b/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py index 8ae5c83705..0759ed9127 100644 --- a/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py @@ -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. # # pylint: disable=too-few-public-methods @@ -40,7 +40,10 @@ def _get_demo_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("trial_base", "0.5.1", versionstr="Trial", repo="openage") + targetmod_info = full_data_set.game_version.edition.target_modpacks["trial_base"] + version = targetmod_info["version"] + versionstr = targetmod_info["versionstr"] + mod_def.set_info("trial_base", version, versionstr=versionstr, repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/de1/modpack_subprocessor.py b/openage/convert/processor/conversion/de1/modpack_subprocessor.py index 1a431e81d2..55c143ee83 100644 --- a/openage/convert/processor/conversion/de1/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/de1/modpack_subprocessor.py @@ -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. # # pylint: disable=too-few-public-methods @@ -40,7 +40,10 @@ def _get_aoe1_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("de1_base", "0.5.1", versionstr="1.0a", repo="openage") + targetmod_info = full_data_set.game_version.edition.target_modpacks["de1_base"] + version = targetmod_info["version"] + versionstr = targetmod_info["versionstr"] + mod_def.set_info("de1_base", version, versionstr=versionstr, repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/de2/modpack_subprocessor.py b/openage/convert/processor/conversion/de2/modpack_subprocessor.py index 5af6fdf8b5..ab09c233a6 100644 --- a/openage/convert/processor/conversion/de2/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/de2/modpack_subprocessor.py @@ -40,7 +40,10 @@ def _get_aoe2_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("de2_base", "0.6.0", versionstr="Update 118476+", repo="openage") + targetmod_info = full_data_set.game_version.edition.target_modpacks["de2_base"] + version = targetmod_info["version"] + versionstr = targetmod_info["versionstr"] + mod_def.set_info("de2_base", version, versionstr=versionstr, repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/hd/modpack_subprocessor.py b/openage/convert/processor/conversion/hd/modpack_subprocessor.py index 8be52ac744..536c451ae1 100644 --- a/openage/convert/processor/conversion/hd/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/hd/modpack_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2021-2023 the openage authors. See copying.md for legal info. +# Copyright 2021-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-few-public-methods @@ -40,7 +40,10 @@ def _get_aoe2_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("hd_base", "0.5.1", versionstr="5.8", repo="openage") + targetmod_info = full_data_set.game_version.edition.target_modpacks["hd_base"] + version = targetmod_info["version"] + versionstr = targetmod_info["versionstr"] + mod_def.set_info("hd_base", version, versionstr=versionstr, repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/ror/modpack_subprocessor.py b/openage/convert/processor/conversion/ror/modpack_subprocessor.py index ff7746e861..b0022c601e 100644 --- a/openage/convert/processor/conversion/ror/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/ror/modpack_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-few-public-methods @@ -41,7 +41,10 @@ def _get_aoe1_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("aoe1_base", "0.5.1", versionstr="1.0a", repo="openage") + targetmod_info = full_data_set.game_version.edition.target_modpacks["aoe1_base"] + version = targetmod_info["version"] + versionstr = targetmod_info["versionstr"] + mod_def.set_info("aoe1_base", version, versionstr=versionstr, repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/swgbcc/modpack_subprocessor.py b/openage/convert/processor/conversion/swgbcc/modpack_subprocessor.py index 5a69a47516..262926b919 100644 --- a/openage/convert/processor/conversion/swgbcc/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/swgbcc/modpack_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-few-public-methods @@ -40,7 +40,10 @@ def _get_swgb_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("swgb_base", "0.5.1", versionstr="1.1-gog4", repo="openage") + targetmod_info = full_data_set.game_version.edition.target_modpacks["swgb_base"] + version = targetmod_info["version"] + versionstr = targetmod_info["versionstr"] + mod_def.set_info("swgb_base", version, versionstr=versionstr, repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/service/init/version_detect.py b/openage/convert/service/init/version_detect.py index 792ce9708b..58cec8925a 100644 --- a/openage/convert/service/init/version_detect.py +++ b/openage/convert/service/init/version_detect.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-arguments,too-many-locals,too-many-branches """ @@ -180,7 +180,7 @@ def create_game_obj( game_name = game_info['name'] game_id = game_info['game_edition_id'] support = game_info['support'] - modpacks = game_info['targetmod'] + modpacks = game_info['targetmods'] if not expansion: expansions = game_info['expansions'] diff --git a/openage/convert/service/read/gamedata.py b/openage/convert/service/read/gamedata.py index 575f51ad67..0d50c81d03 100644 --- a/openage/convert/service/read/gamedata.py +++ b/openage/convert/service/read/gamedata.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. """ Module for reading .dat files. diff --git a/openage/convert/tool/driver.py b/openage/convert/tool/driver.py index 888a5039e1..0c422cdb80 100644 --- a/openage/convert/tool/driver.py +++ b/openage/convert/tool/driver.py @@ -10,8 +10,6 @@ import typing import timeit -from openage.convert.service import export - from ...log import info, dbg from ..processor.export.modpack_exporter import ModpackExporter diff --git a/openage/convert/value_object/init/game_version.py b/openage/convert/value_object/init/game_version.py index e4364c3dde..2bffcbd3ed 100644 --- a/openage/convert/value_object/init/game_version.py +++ b/openage/convert/value_object/init/game_version.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-arguments @@ -35,7 +35,7 @@ def __init__( support: Support, game_version_info: list[tuple[list[str], dict[str, str]]], media_paths: list[tuple[str, list[str]]], - modpacks: list[str], + modpacks: dict[str, dict[str, str]], **flags ): """ diff --git a/openage/game/main.py b/openage/game/main.py index 15fb0a3962..5a5cbf1292 100644 --- a/openage/game/main.py +++ b/openage/game/main.py @@ -1,4 +1,4 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals @@ -79,8 +79,14 @@ def main(args, error): # ensure that the assets have been converted if conversion_required(asset_path): + # TODO: Run conversion for mods specified in args.modpacks convert_assets(asset_path, args) + # else + # for modpack in args.modpacks: + # if check_modpack_outdated(): + # convert_assets(asset_path, args) + # modpacks mods = [] for modpack in args.modpacks: From ebace8e11c2a13c56f7a9be0f2bfc7e4461f6c95 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 4 Aug 2024 00:26:29 +0200 Subject: [PATCH 484/771] util: OS agnostic newline string splitter. --- libopenage/util/strings.cpp | 31 ++++++++++++++++++++----------- libopenage/util/strings.h | 5 +++++ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/libopenage/util/strings.cpp b/libopenage/util/strings.cpp index 4a9ce1436e..9243992db5 100644 --- a/libopenage/util/strings.cpp +++ b/libopenage/util/strings.cpp @@ -1,17 +1,17 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #include "strings.h" -#include #include +#include #include #include #include #include -#include "config.h" #include "../error/error.h" #include "compiler.h" +#include "config.h" namespace openage::util { @@ -72,12 +72,10 @@ size_t rstrip(char *s) { size_t strippedlen = strlen(s); while (strippedlen > 0) { - if (s[strippedlen - 1] == '\n' || - s[strippedlen - 1] == ' ' || - s[strippedlen - 1] == '\t') { - + if (s[strippedlen - 1] == '\n' || s[strippedlen - 1] == ' ' || s[strippedlen - 1] == '\t') { strippedlen -= 1; - } else { + } + else { break; } } @@ -141,7 +139,6 @@ std::vector split(const std::string &txt, char delimiter) { std::vector split_escape(const std::string &txt, char delim, size_t size_hint) { - // output vector std::vector items; if (size_hint) [[likely]] { @@ -161,7 +158,6 @@ std::vector split_escape(const std::string &txt, char delim, size_t // copy characters to buf, and a buf is emitted as a token // when the delimiter or end is reached. while (true) { - // end of input string if (*r == '\0') { items.emplace_back(std::begin(buf), std::end(buf)); @@ -210,4 +206,17 @@ std::vector split_escape(const std::string &txt, char delim, size_t return items; } -} // openage::util +std::vector split_newline(const std::string &txt) { + auto lines = split(txt, '\n'); + + // remove the '\r' from the end of each line + for (auto &line : lines) { + if (not line.empty() and line.back() == '\r') { + line.pop_back(); + } + } + + return lines; +} + +} // namespace openage::util diff --git a/libopenage/util/strings.h b/libopenage/util/strings.h index 4ab90de510..ad04e5267c 100644 --- a/libopenage/util/strings.h +++ b/libopenage/util/strings.h @@ -116,5 +116,10 @@ std::vector split_escape(const std::string &txt, char delim, size_t size_hint = 0); +/** + * Newline splitter that works with both \n and \r\n. + */ +std::vector split_newline(const std::string &txt); + } // namespace util } // namespace openage From 03a671cae7596867793cf0b3dcfc7722492a6cce Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 4 Aug 2024 00:52:57 +0200 Subject: [PATCH 485/771] renderer: Use safe newline splitter for file parsers. --- .../resources/parser/parse_blendmask.cpp | 18 ++++++++--------- .../resources/parser/parse_blendtable.cpp | 20 +++++++++---------- .../resources/parser/parse_palette.cpp | 18 ++++++++--------- .../resources/parser/parse_sprite.cpp | 16 +++++++-------- .../resources/parser/parse_terrain.cpp | 18 ++++++++--------- .../resources/parser/parse_texture.cpp | 16 +++++++-------- 6 files changed, 53 insertions(+), 53 deletions(-) diff --git a/libopenage/renderer/resources/parser/parse_blendmask.cpp b/libopenage/renderer/resources/parser/parse_blendmask.cpp index d7f6e4aa74..ddfbd22a2d 100644 --- a/libopenage/renderer/resources/parser/parse_blendmask.cpp +++ b/libopenage/renderer/resources/parser/parse_blendmask.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 "parse_blendmask.h" @@ -58,16 +58,16 @@ blending_mask parse_mask(const std::vector &args) { return mask; } -BlendPatternInfo parse_blendmask_file(const util::Path &file, +BlendPatternInfo parse_blendmask_file(const util::Path &path, const std::shared_ptr &cache) { - if (not file.is_file()) [[unlikely]] { + if (not path.is_file()) [[unlikely]] { throw Error(MSG(err) << "Reading .blmask file '" - << file.get_name() + << path.get_name() << "' failed. Reason: File not found"); } - auto content = file.open(); - auto lines = content.get_lines(); + auto file = path.open(); + auto lines = util::split_newline(file.read()); float scalefactor = 1.0; std::vector textures; @@ -79,7 +79,7 @@ BlendPatternInfo parse_blendmask_file(const util::Path &file, if (version_no != 2) { throw Error(MSG(err) << "Reading .blmask file '" - << file.get_name() + << path.get_name() << "' failed. Reason: Version " << version_no << " not supported"); } @@ -104,7 +104,7 @@ BlendPatternInfo parse_blendmask_file(const util::Path &file, // TODO: Avoid double lookup with keywordfuncs.find(args[0]) if (not keywordfuncs.contains(args[0])) [[unlikely]] { throw Error(MSG(err) << "Reading .blmask file '" - << file.get_name() + << path.get_name() << "' failed. Reason: Keyword " << args[0] << " is not defined"); } @@ -128,7 +128,7 @@ BlendPatternInfo parse_blendmask_file(const util::Path &file, // Parse textures std::vector> texture_infos; for (auto texture : textures) { - util::Path texturepath = (file.get_parent() / texture.path); + util::Path texturepath = (path.get_parent() / texture.path); if (cache && cache->check_texture_cache(texturepath)) { // already loaded diff --git a/libopenage/renderer/resources/parser/parse_blendtable.cpp b/libopenage/renderer/resources/parser/parse_blendtable.cpp index c37579ce36..648e83b9ed 100644 --- a/libopenage/renderer/resources/parser/parse_blendtable.cpp +++ b/libopenage/renderer/resources/parser/parse_blendtable.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 "parse_blendtable.h" @@ -67,16 +67,16 @@ PatternData parse_pattern(const std::vector &args) { return pattern; } -BlendTableInfo parse_blendtable_file(const util::Path &file, +BlendTableInfo parse_blendtable_file(const util::Path &path, const std::shared_ptr &cache) { - if (not file.is_file()) [[unlikely]] { + if (not path.is_file()) [[unlikely]] { throw Error(MSG(err) << "Reading .bltable file '" - << file.get_name() + << path.get_name() << "' failed. Reason: File not found"); } - auto content = file.open(); - auto lines = content.get_lines(); + auto file = path.open(); + auto lines = util::split_newline(file.read()); std::vector blendtable; std::vector patterns; @@ -87,7 +87,7 @@ BlendTableInfo parse_blendtable_file(const util::Path &file, if (version_no != 1) { throw Error(MSG(err) << "Reading .bltable file '" - << file.get_name() + << path.get_name() << "' failed. Reason: Version " << version_no << " not supported"); } @@ -110,7 +110,7 @@ BlendTableInfo parse_blendtable_file(const util::Path &file, // TODO: Avoid double lookup with keywordfuncs.find(args[0]) if (not keywordfuncs.contains(args[0])) [[unlikely]] { throw Error(MSG(err) << "Reading .bltable file '" - << file.get_name() + << path.get_name() << "' failed. Reason: Keyword " << args[0] << " is not defined"); } @@ -125,7 +125,7 @@ BlendTableInfo parse_blendtable_file(const util::Path &file, i += 1; if (i >= lines.size()) { throw Error(MSG(err) << "Reading .bltable file '" - << file.get_name() + << path.get_name() << "' failed. Reason: Matrix for keyword " << args[0] << " is malformed"); } @@ -142,7 +142,7 @@ BlendTableInfo parse_blendtable_file(const util::Path &file, std::vector> pattern_infos; for (auto pattern : patterns) { - util::Path maskpath = (file.get_parent() / pattern.path); + util::Path maskpath = (path.get_parent() / pattern.path); if (cache && cache->check_blpattern_cache(maskpath)) { // already loaded diff --git a/libopenage/renderer/resources/parser/parse_palette.cpp b/libopenage/renderer/resources/parser/parse_palette.cpp index 344466dd86..c2e0ce8e20 100644 --- a/libopenage/renderer/resources/parser/parse_palette.cpp +++ b/libopenage/renderer/resources/parser/parse_palette.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 "parse_palette.h" @@ -63,15 +63,15 @@ std::vector parse_colours(const std::vector &lines) { return entries; } -PaletteInfo parse_palette_file(const util::Path &file) { - if (not file.is_file()) [[unlikely]] { +PaletteInfo parse_palette_file(const util::Path &path) { + if (not path.is_file()) [[unlikely]] { throw Error(MSG(err) << "Reading .opal file '" - << file.get_name() + << path.get_name() << "' failed. Reason: File not found"); } - auto content = file.open(); - auto lines = content.get_lines(); + auto file = path.open(); + auto lines = util::split_newline(file.read()); size_t entries = 0; std::vector colours; @@ -82,7 +82,7 @@ PaletteInfo parse_palette_file(const util::Path &file) { if (version_no != 1) { throw Error(MSG(err) << "Reading .opal file '" - << file.get_name() + << path.get_name() << "' failed. Reason: Version " << version_no << " not supported"); } @@ -105,7 +105,7 @@ PaletteInfo parse_palette_file(const util::Path &file) { // TODO: Avoid double lookup with keywordfuncs.find(args[0]) if (not keywordfuncs.contains(args[0])) [[unlikely]] { throw Error(MSG(err) << "Reading .opal file '" - << file.get_name() + << path.get_name() << "' failed. Reason: Keyword " << args[0] << " is not defined"); } @@ -120,7 +120,7 @@ PaletteInfo parse_palette_file(const util::Path &file) { i += 1; if (i >= lines.size()) { throw Error(MSG(err) << "Reading .opal file '" - << file.get_name() + << path.get_name() << "' failed. Reason: Matrix for keyword " << args[0] << " is malformed"); } diff --git a/libopenage/renderer/resources/parser/parse_sprite.cpp b/libopenage/renderer/resources/parser/parse_sprite.cpp index 0974f98342..fae428729f 100644 --- a/libopenage/renderer/resources/parser/parse_sprite.cpp +++ b/libopenage/renderer/resources/parser/parse_sprite.cpp @@ -134,16 +134,16 @@ FrameData parse_frame(const std::vector &args) { return frame; } -Animation2dInfo parse_sprite_file(const util::Path &file, +Animation2dInfo parse_sprite_file(const util::Path &path, const std::shared_ptr &cache) { - if (not file.is_file()) [[unlikely]] { + if (not path.is_file()) [[unlikely]] { throw Error(MSG(err) << "Reading .sprite file '" - << file.get_name() + << path.get_name() << "' failed. Reason: File not found"); } - auto content = file.open(); - auto lines = content.get_lines(); + auto file = path.open(); + auto lines = util::split_newline(file.read()); float scalefactor = 1.0; std::vector textures; @@ -162,7 +162,7 @@ Animation2dInfo parse_sprite_file(const util::Path &file, if (version_no != 2) { throw Error(MSG(err) << "Reading .sprite file '" - << file.get_name() + << path.get_name() << "' failed. Reason: Version " << version_no << " not supported"); } @@ -204,7 +204,7 @@ Animation2dInfo parse_sprite_file(const util::Path &file, // TODO: Avoid double lookup with keywordfuncs.find(args[0]) if (not keywordfuncs.contains(args[0])) [[unlikely]] { throw Error(MSG(err) << "Reading .sprite file '" - << file.get_name() + << path.get_name() << "' failed. Reason: Keyword " << args[0] << " is not defined"); } @@ -317,7 +317,7 @@ Animation2dInfo parse_sprite_file(const util::Path &file, // Parse textures std::vector> texture_infos; for (auto texture : textures) { - util::Path texturepath = (file.get_parent() / texture.path); + util::Path texturepath = (path.get_parent() / texture.path); if (cache && cache->check_texture_cache(texturepath)) { // already loaded diff --git a/libopenage/renderer/resources/parser/parse_terrain.cpp b/libopenage/renderer/resources/parser/parse_terrain.cpp index 26dc77f10b..9db68c098f 100644 --- a/libopenage/renderer/resources/parser/parse_terrain.cpp +++ b/libopenage/renderer/resources/parser/parse_terrain.cpp @@ -139,16 +139,16 @@ TerrainFrameData parse_terrain_frame(const std::vector &args) { return frame; } -TerrainInfo parse_terrain_file(const util::Path &file, +TerrainInfo parse_terrain_file(const util::Path &path, const std::shared_ptr &cache) { - if (not file.is_file()) [[unlikely]] { + if (not path.is_file()) [[unlikely]] { throw Error(MSG(err) << "Reading .terrain file '" - << file.get_name() + << path.get_name() << "' failed. Reason: File not found"); } - auto content = file.open(); - auto lines = content.get_lines(); + auto file = path.open(); + auto lines = util::split_newline(file.read()); float scalefactor = 1.0; std::vector textures; @@ -167,7 +167,7 @@ TerrainInfo parse_terrain_file(const util::Path &file, if (version_no != 2) { throw Error(MSG(err) << "Reading .terrain file '" - << file.get_name() + << path.get_name() << "' failed. Reason: Version " << version_no << " not supported"); } @@ -209,7 +209,7 @@ TerrainInfo parse_terrain_file(const util::Path &file, // TODO: Avoid double lookup with keywordfuncs.find(args[0]) if (not keywordfuncs.contains(args[0])) [[unlikely]] { throw Error(MSG(err) << "Reading .terrain file '" - << file.get_name() + << path.get_name() << "' failed. Reason: Keyword " << args[0] << " is not defined"); } @@ -269,7 +269,7 @@ TerrainInfo parse_terrain_file(const util::Path &file, // Parse textures std::vector> texture_infos; for (auto texture : textures) { - util::Path texturepath = (file.get_parent() / texture.path); + util::Path texturepath = (path.get_parent() / texture.path); if (cache && cache->check_texture_cache(texturepath)) { // already loaded @@ -287,7 +287,7 @@ TerrainInfo parse_terrain_file(const util::Path &file, std::shared_ptr blendtable_info; if (blendtable) { - util::Path tablepath = (file.get_parent() / blendtable.value().path); + util::Path tablepath = (path.get_parent() / blendtable.value().path); if (cache && cache->check_bltable_cache(tablepath)) { // already loaded diff --git a/libopenage/renderer/resources/parser/parse_texture.cpp b/libopenage/renderer/resources/parser/parse_texture.cpp index c3d90fa30a..527707d49c 100644 --- a/libopenage/renderer/resources/parser/parse_texture.cpp +++ b/libopenage/renderer/resources/parser/parse_texture.cpp @@ -144,15 +144,15 @@ SubtextureData parse_subtex(const std::vector &args) { return subtex; } -Texture2dInfo parse_texture_file(const util::Path &file) { - if (not file.is_file()) [[unlikely]] { +Texture2dInfo parse_texture_file(const util::Path &path) { + if (not path.is_file()) [[unlikely]] { throw Error(MSG(err) << "Reading .texture file '" - << file.get_name() + << path.get_name() << "' failed. Reason: File not found"); } - auto content = file.open(); - auto lines = content.get_lines(); + auto file = path.open(); + auto lines = util::split_newline(file.read()); std::string imagefile; SizeData size; @@ -165,7 +165,7 @@ Texture2dInfo parse_texture_file(const util::Path &file) { if (version_no != 1) { throw Error(MSG(err) << "Reading .texture file '" - << file.get_name() + << path.get_name() << "' failed. Reason: Version " << version_no << " not supported"); } @@ -193,7 +193,7 @@ Texture2dInfo parse_texture_file(const util::Path &file) { // TODO: Avoid double lookup with keywordfuncs.find(args[0]) if (not keywordfuncs.contains(args[0])) [[unlikely]] { throw Error(MSG(err) << "Reading .texture file '" - << file.get_name() + << path.get_name() << "' failed. Reason: Keyword " << args[0] << " is not defined"); } @@ -213,7 +213,7 @@ Texture2dInfo parse_texture_file(const util::Path &file) { size.height); } - auto imagepath = file.get_parent() / imagefile; + auto imagepath = path.get_parent() / imagefile; auto align = guess_row_alignment(size.width); return Texture2dInfo(size.width, size.height, pxformat.format, imagepath, align, std::move(subinfos)); From 565151616c3261730331fff31510f62dfd45d061 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 4 Aug 2024 00:59:02 +0200 Subject: [PATCH 486/771] renderer: Remove handling of CR in parsers. --- libopenage/renderer/resources/parser/common.cpp | 8 +------- libopenage/renderer/resources/parser/parse_sprite.cpp | 4 ++-- libopenage/renderer/resources/parser/parse_terrain.cpp | 4 ++-- libopenage/renderer/resources/parser/parse_texture.cpp | 9 ++------- 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/libopenage/renderer/resources/parser/common.cpp b/libopenage/renderer/resources/parser/common.cpp index 483c2cb0dd..c270ebf891 100644 --- a/libopenage/renderer/resources/parser/common.cpp +++ b/libopenage/renderer/resources/parser/common.cpp @@ -20,13 +20,7 @@ TextureData parse_texture(const std::vector &args) { texture.texture_id = std::stoul(args[1]); // Call substr() to get rid of the quotes - // If the line ends in a carriage return, remove it as well - if (args[2][args[2].size() - 1] == '\r') { - texture.path = args[2].substr(1, args[2].size() - 3); - } - else { - texture.path = args[2].substr(1, args[2].size() - 2); - } + texture.path = args[2].substr(1, args[2].size() - 2); return texture; } diff --git a/libopenage/renderer/resources/parser/parse_sprite.cpp b/libopenage/renderer/resources/parser/parse_sprite.cpp index fae428729f..4d783c5ba4 100644 --- a/libopenage/renderer/resources/parser/parse_sprite.cpp +++ b/libopenage/renderer/resources/parser/parse_sprite.cpp @@ -195,8 +195,8 @@ Animation2dInfo parse_sprite_file(const util::Path &path, })}; for (auto line : lines) { - // Skip empty lines, lines with carriage returns, and comments - if (line.empty() || line.substr(0, 1) == "#" || line[0] == '\r') { + // Skip empty lines and comments + if (line.empty() || line.substr(0, 1) == "#") { continue; } std::vector args{util::split(line, ' ')}; diff --git a/libopenage/renderer/resources/parser/parse_terrain.cpp b/libopenage/renderer/resources/parser/parse_terrain.cpp index 9db68c098f..d46c9bc146 100644 --- a/libopenage/renderer/resources/parser/parse_terrain.cpp +++ b/libopenage/renderer/resources/parser/parse_terrain.cpp @@ -200,8 +200,8 @@ TerrainInfo parse_terrain_file(const util::Path &path, })}; for (auto line : lines) { - // Skip empty lines, lines with carriage returns, and comments - if (line.empty() || line.substr(0, 1) == "#" || line[0] == '\r') { + // Skip empty lines and comments + if (line.empty() || line.substr(0, 1) == "#") { continue; } std::vector args{util::split(line, ' ')}; diff --git a/libopenage/renderer/resources/parser/parse_texture.cpp b/libopenage/renderer/resources/parser/parse_texture.cpp index 527707d49c..52d3958731 100644 --- a/libopenage/renderer/resources/parser/parse_texture.cpp +++ b/libopenage/renderer/resources/parser/parse_texture.cpp @@ -56,11 +56,6 @@ std::string parse_imagefile(const std::vector &args) { // it should result in an error if wrongly used here. // Call substr() to get rid of the quotes - // If the line ends in a carriage return, remove it as well - if (args[1][args[1].size() - 1] == '\r') { - return args[1].substr(1, args[1].size() - 3); - } - return args[1].substr(1, args[1].size() - 2); } @@ -184,8 +179,8 @@ Texture2dInfo parse_texture_file(const util::Path &path) { })}; for (auto line : lines) { - // Skip empty lines, lines with carriage returns, and comments - if (line.empty() || line.substr(0, 1) == "#" || line[0] == '\r') { + // Skip empty lines and comments + if (line.empty() || line.substr(0, 1) == "#") { continue; } std::vector args{util::split(line, ' ')}; From 810672f8bc42748a3782a069daaff4822b882f92 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 5 Aug 2024 00:56:56 +0200 Subject: [PATCH 487/771] util: Helper class for handling semantic versioning. --- openage/util/CMakeLists.txt | 1 + openage/util/version.py | 84 +++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 openage/util/version.py diff --git a/openage/util/CMakeLists.txt b/openage/util/CMakeLists.txt index 79e7309cb3..cc31682da6 100644 --- a/openage/util/CMakeLists.txt +++ b/openage/util/CMakeLists.txt @@ -16,6 +16,7 @@ add_py_modules( struct.py system.py threading.py + version.py ) add_subdirectory(filelike) diff --git a/openage/util/version.py b/openage/util/version.py new file mode 100644 index 0000000000..712b04ec79 --- /dev/null +++ b/openage/util/version.py @@ -0,0 +1,84 @@ +# Copyright 2024-2024 the openage authors. See copying.md for legal info. + +""" +Handling of version information for openage. +""" +from __future__ import annotations + +import re + +SEMVER_REGEX = re.compile( + (r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)" + r"(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)" + r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" + r"(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$")) + + +class SemanticVersion: + """ + Semantic versioning information. + """ + + def __init__(self, version: str) -> None: + """ + Create a new semantic version object from a version string. + + :param version: The version string to parse. + """ + match = SEMVER_REGEX.match(version) + if not match: + raise ValueError(f"Invalid semantic version: {version}") + + self.major = int(match.group("major")) + self.minor = int(match.group("minor")) + self.patch = int(match.group("patch")) + self.prerelease = match.group("prerelease") + 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 + + return False + + def __le__(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 + + return False + + def __eq__(self, other: SemanticVersion) -> bool: + return (self.major == other.major and + self.minor == other.minor and + self.patch == other.patch) + + def __ne__(self, other: SemanticVersion) -> bool: + return not self.__eq__(other) + + def __gt__(self, other: SemanticVersion) -> bool: + return not self.__le__(other) + + def __ge__(self, other: SemanticVersion) -> bool: + return not self.__lt__(other) + + def __str__(self) -> str: + version = f"{self.major}.{self.minor}.{self.patch}" + if self.prerelease: + version += f"-{self.prerelease}" + if self.buildmetadata: + version += f"+{self.buildmetadata}" + + return version + + def __repr__(self) -> str: + return f"SemanticVersion('{str(self)}')" From 85d159f8c5d3122e2286c48f744be89f9adf4a00 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 5 Aug 2024 02:52:15 +0200 Subject: [PATCH 488/771] convert: Detect outdated modpacks. --- openage/convert/main.py | 25 +++-- openage/convert/service/init/CMakeLists.txt | 1 + openage/convert/service/init/changelog.py | 44 +++++++++ .../service/init/conversion_required.py | 10 +- .../convert/service/init/modpack_search.py | 22 +++-- .../convert/tool/subtool/acquire_sourcedir.py | 94 ++++--------------- openage/game/main.py | 26 ++--- openage/main/main.py | 30 +++--- 8 files changed, 128 insertions(+), 124 deletions(-) create mode 100644 openage/convert/service/init/changelog.py diff --git a/openage/convert/main.py b/openage/convert/main.py index f89356402f..369af4945e 100644 --- a/openage/convert/main.py +++ b/openage/convert/main.py @@ -15,11 +15,12 @@ from ..util.fslike.wrapper import (DirectoryCreator, Synchronizer as AccessSynchronizer) from .service.debug_info import debug_cli_args, debug_game_version, debug_mounts -from .service.init.conversion_required import conversion_required from .service.init.mount_asset_dirs import mount_asset_dirs from .service.init.version_detect import create_version_objects +from .service.init.changelog import check_updates from .tool.interactive import interactive_browser -from .tool.subtool.acquire_sourcedir import acquire_conversion_source_dir, wanna_convert +from .tool.subtool.acquire_sourcedir import acquire_conversion_source_dir, wanna_convert, \ + wanna_check_updates from .tool.subtool.version_select import get_game_version if typing.TYPE_CHECKING: @@ -238,6 +239,16 @@ def init_subparser(cli: ArgumentParser): "--export-api", action='store_true', help="Export the openage nyan API definition as a modpack") + cli.add_argument( + "--check-updates", action='store_true', + help="Check if the assets are up to date" + ) + + cli.add_argument( + "--no-prompts", action='store_false', dest='show_prompts', + help="Disable user prompts" + ) + def main(args, error): """ CLI entry point """ @@ -268,11 +279,13 @@ def main(args, error): from ..assets import get_asset_path outdir = get_asset_path(args.output_dir) - if args.force or wanna_convert() or conversion_required(outdir): + if args.force or (args.show_prompts and wanna_convert()): convert_assets(outdir, args, srcdir) - else: - print("assets are up to date; no conversion is required.") - print("override with --force.") + if args.check_updates or (args.show_prompts and wanna_check_updates()): + # check if the assets are up to date + modpack_dir = outdir / "converted" + game_info_dir = args.cfg_dir / "converter" / "games" + check_updates(modpack_dir, game_info_dir) return 0 diff --git a/openage/convert/service/init/CMakeLists.txt b/openage/convert/service/init/CMakeLists.txt index aa588dcb29..3c7e404c69 100644 --- a/openage/convert/service/init/CMakeLists.txt +++ b/openage/convert/service/init/CMakeLists.txt @@ -1,6 +1,7 @@ add_py_modules( __init__.py api_export_required.py + changelog.py conversion_required.py modpack_search.py mount_asset_dirs.py diff --git a/openage/convert/service/init/changelog.py b/openage/convert/service/init/changelog.py new file mode 100644 index 0000000000..2cb4a244d6 --- /dev/null +++ b/openage/convert/service/init/changelog.py @@ -0,0 +1,44 @@ +# Copyright 2024-2024 the openage authors. See copying.md for legal info. + +""" +Check for updates in the openage converter modpacks. +""" +from __future__ import annotations +import typing + +from itertools import chain + +from openage.util.version import SemanticVersion + +from ....log import info + +from .modpack_search import enumerate_modpacks +from ..init.version_detect import create_version_objects + + +if typing.TYPE_CHECKING: + from openage.util.fslike.union import UnionPath + + +def check_updates(modpack_dir: UnionPath, game_info_dir: UnionPath): + """ + Check if there are updates available for the openage converter modpacks. + + :param modpack_dir: The directory containing the modpacks. + :param game_info_dir: The directory containing the game information. + """ + available_modpacks = enumerate_modpacks(modpack_dir, exclude={"engine"}) + + game_editions, game_expansions = create_version_objects(game_info_dir) + for game_def in chain(game_editions, game_expansions): + for targetmod_name, targetmod_def in game_def.target_modpacks.items(): + if targetmod_name in available_modpacks: + converter_version = SemanticVersion(targetmod_def["version"]) + modpack_version = SemanticVersion(available_modpacks[targetmod_name]) + if converter_version > modpack_version: + info(("Modpack %s v%s is outdated: " + "newer version v%s is available"), + targetmod_name, modpack_version, converter_version) + + else: + info("Modpack %s v%s is up-to-date", targetmod_name, modpack_version) diff --git a/openage/convert/service/init/conversion_required.py b/openage/convert/service/init/conversion_required.py index 1c13a33142..7c7e53c640 100644 --- a/openage/convert/service/init/conversion_required.py +++ b/openage/convert/service/init/conversion_required.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. """ Test whether there already are converted modpacks present. @@ -21,7 +21,6 @@ def conversion_required(asset_dir: UnionPath) -> bool: Asset conversions are required if: - the modpack folder does not exist - no modpacks inside the modpack folder exist - - the converted assets are outdated :param asset_dir: The asset directory to check. :type asset_dir: UnionPath @@ -30,15 +29,14 @@ def conversion_required(asset_dir: UnionPath) -> bool: try: # TODO: Do not use hardcoded path to "converted" directory. # The mod directory should be its own configurable path - modpacks = enumerate_modpacks(asset_dir / "converted") + modpacks = enumerate_modpacks(asset_dir / "converted", exclude={"engine"}) except FileNotFoundError: # modpack folder not created yet modpacks = set() - if not modpacks or \ - len(modpacks) == 1 and "engine" in modpacks: # engine is not usable on its own - info("No converted assets have been found") + if not modpacks: + info("No modpacks have been found") return True # TODO: Check if modpack version of converted assets are up to date diff --git a/openage/convert/service/init/modpack_search.py b/openage/convert/service/init/modpack_search.py index 8d89b73da1..aa6a814004 100644 --- a/openage/convert/service/init/modpack_search.py +++ b/openage/convert/service/init/modpack_search.py @@ -14,27 +14,35 @@ from openage.util.fslike.union import UnionPath -def enumerate_modpacks(modpacks_dir: UnionPath) -> set[str]: +def enumerate_modpacks(modpacks_dir: UnionPath, exclude: set[str] = None) -> dict[str, str]: """ Enumerate openage modpacks in a directory. :param asset_dir: The asset directory to search in. :type asset_dir: UnionPath - :returns: A list of modpack names that were found. - :rtype: set[str] + :param exclude: Modpack names to exclude from the enumeration. + :type exclude: set[str] + :returns: Modpacks that were found. Names are keys, versions are values. + :rtype: dict[str, str] """ if not modpacks_dir.exists(): info("openage modpack directory has not been created yet") raise FileNotFoundError("openage modpack directory not found") - modpacks: set[str] = set() + modpacks: dict[str] = {} for check_dir in modpacks_dir.iterdir(): if check_dir.is_dir(): try: modpack_info = get_modpack_info(check_dir) modpack_name = modpack_info["info"]["name"] - info("Found modpack %s", modpack_name) - modpacks.add(modpack_name) + modpack_version = modpack_info["info"]["version"] + info("Found modpack %s v%s", modpack_name, modpack_version) + + if exclude is not None and modpack_name in exclude: + dbg("Excluding modpack %s from enumeration", modpack_name) + continue + + modpacks.update({modpack_name: modpack_version}) except (FileNotFoundError, TypeError, toml.TomlDecodeError): dbg("No modpack found in directory: %s", check_dir) @@ -81,7 +89,7 @@ def get_modpack_info(modpack_dir: UnionPath) -> dict[str, typing.Any]: raise err -def query_modpack(proposals: set[str]) -> str: +def query_modpack(proposals: list[str]) -> str: """ Query interactively for a modpack from a selection of proposals. """ diff --git a/openage/convert/tool/subtool/acquire_sourcedir.py b/openage/convert/tool/subtool/acquire_sourcedir.py index 5d9368e526..fd070b7825 100644 --- a/openage/convert/tool/subtool/acquire_sourcedir.py +++ b/openage/convert/tool/subtool/acquire_sourcedir.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-branches @@ -21,7 +21,6 @@ from urllib.request import urlopen from ....log import warn, info, dbg -from ....util.files import which from ....util.fslike.directory import CaseIgnoringDirectory, Directory if typing.TYPE_CHECKING: @@ -48,13 +47,15 @@ def expand_relative_path(path: str) -> AnyStr: return os.path.realpath(os.path.expandvars(os.path.expanduser(path))) -def wanna_convert() -> bool: +def prompt(msg: str, answer: typing.Union[bool, None] = None) -> bool: """ - Ask the user if assets should be converted. + Ask the user a yes/no question. + + :param msg: Message to display. + :param answer: Pre-determined answer (optional). """ - answer = None while answer is None: - print(" Do you want to convert assets? [Y/n]") + print(f" {msg} [Y/n]") user_selection = input("> ") if user_selection.lower() in {"yes", "y", ""}: @@ -66,88 +67,25 @@ def wanna_convert() -> bool: return answer -def wanna_download_trial() -> bool: +def wanna_convert(answer: typing.Union[bool, None] = None) -> bool: """ - Ask the user if the AoC trial should be downloaded. + Ask the user if assets should be converted. """ - answer = None - while answer is None: - print(" Do you want to download the AoC trial version? [Y/n]") - - user_selection = input("> ") - if user_selection.lower() in {"yes", "y", ""}: - answer = True - - elif user_selection.lower() in {"no", "n"}: - answer = False + return prompt("Do you want to convert assets?", answer=answer) - return answer - -def wanna_use_wine() -> bool: +def wanna_check_updates(answer: typing.Union[bool, None] = None) -> bool: """ - Ask the user if wine should be used. - Wine is not used if user has no wine installed. + Ask the user if they want to check for updates. """ - - # TODO: a possibility to call different wine binaries - # (e.g. wine-devel from wine upstream debian repos) - - if not which("wine"): - return False - - answer = None - long_prompt = True - while answer is None: - if long_prompt: - print( - " Should we call wine to determine an AOE installation? [Y/n]") - long_prompt = False - else: - # TODO: text-adventure here - print(" Don't know what you want. Use wine? [Y/n]") - - user_selection = input("> ") - if user_selection.lower() in {"yes", "y", ""}: - answer = True - - elif user_selection.lower() in {"no", "n"}: - answer = False - - return answer + return prompt("Do you want to check for updates?", answer=answer) -def set_custom_wineprefix() -> None: +def wanna_download_trial(answer: typing.Union[bool, None] = None) -> bool: """ - Allow the customization of the WINEPREFIX environment variable. + Ask the user if the AoC trial should be downloaded. """ - - print("The WINEPREFIX is a separate 'container' for windows " - "software installations.") - - current_wineprefix = os.environ.get("WINEPREFIX") - if current_wineprefix: - print(f"Currently: WINEPREFIX='{current_wineprefix}'") - - print("Enter a custom value or leave empty to keep it as-is:") - while True: - new_wineprefix = input("WINEPREFIX=") - - if not new_wineprefix: - break - - new_wineprefix = expand_relative_path(new_wineprefix) - - # test if it probably is a wineprefix - if (Path(new_wineprefix) / "drive_c").is_dir(): # pylint: disable=no-member - break - - print("This does not appear to be a valid WINEPREFIX.") - print("Enter a valid one, or leave it empty to skip.") - - # store the updated env variable for the wine subprocess - if new_wineprefix: - os.environ["WINEPREFIX"] = new_wineprefix + return prompt("Do you want to download the AoC trial version?", answer=answer) def query_source_dir(proposals: set[str]) -> AnyStr: diff --git a/openage/game/main.py b/openage/game/main.py index 5a5cbf1292..d20502c06b 100644 --- a/openage/game/main.py +++ b/openage/game/main.py @@ -8,6 +8,7 @@ from __future__ import annotations import typing + from ..log import info if typing.TYPE_CHECKING: @@ -27,7 +28,7 @@ def init_subparser(cli: ArgumentParser) -> None: help="run without displaying graphics") cli.add_argument( - "--modpacks", nargs="+", required=True, + "--modpacks", nargs="+", required=True, type=str, help="list of modpacks to load") @@ -43,9 +44,9 @@ def main(args, error): from .main_cpp import run_game from .. import config from ..assets import get_asset_path - from ..convert.main import conversion_required, convert_assets from ..convert.tool.api_export import export_api from ..convert.service.init.api_export_required import api_export_required + from ..convert.service.init.modpack_search import enumerate_modpacks from ..cppinterface.setup import setup as cpp_interface_setup from ..cvar.location import get_config_path from ..util.fslike.union import Union @@ -77,22 +78,15 @@ def main(args, error): converted_path.mkdirs() export_api(converted_path) - # ensure that the assets have been converted - if conversion_required(asset_path): - # TODO: Run conversion for mods specified in args.modpacks - convert_assets(asset_path, args) - - # else - # for modpack in args.modpacks: - # if check_modpack_outdated(): - # convert_assets(asset_path, args) - - # modpacks - mods = [] + # ensure that modpacks are available + modpack_dir = asset_path / "converted" + available_modpacks = enumerate_modpacks(modpack_dir, exclude={"engine"}) for modpack in args.modpacks: - mods.append(modpack.encode("utf-8")) + if modpack not in available_modpacks: + raise FileNotFoundError(f"Modpack '{modpack}' not found in {modpack_dir}") - args.modpacks = mods + # encode modpacks as bytes for the C++ interface + args.modpacks = [modpack.encode('utf-8') for modpack in args.modpacks] # start the game, continue in main_cpp.pyx! return run_game(args, root) diff --git a/openage/main/main.py b/openage/main/main.py index 685e5b6bc9..740bb270f6 100644 --- a/openage/main/main.py +++ b/openage/main/main.py @@ -1,4 +1,4 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. """ Main engine entry point for openage. @@ -25,7 +25,7 @@ def init_subparser(cli: ArgumentParser): help="run without displaying graphics") cli.add_argument( - "--modpacks", nargs="+", + "--modpacks", nargs="+", type=str, help="list of modpacks to load") @@ -42,8 +42,9 @@ def main(args, error): from .main_cpp import run_game from .. import config from ..assets import get_asset_path - from ..convert.main import conversion_required, convert_assets + from ..convert.main import convert_assets from ..convert.service.init.api_export_required import api_export_required + from ..convert.service.init.modpack_search import enumerate_modpacks, query_modpack from ..convert.tool.api_export import export_api from ..convert.tool.subtool.acquire_sourcedir import wanna_convert from ..cppinterface.setup import setup as cpp_interface_setup @@ -77,22 +78,29 @@ def main(args, error): converted_path.mkdirs() export_api(converted_path) - # ensure that the assets have been converted - if wanna_convert() or conversion_required(asset_path): + # check if modpacks need to be converted + if wanna_convert(): convert_assets(asset_path, args) + available_modpacks = enumerate_modpacks(asset_path / "converted", exclude={"engine"}) + if len(available_modpacks) == 0: + info("No modpacks have been found") + if not args.modpacks: + info("Starting bare 'engine' mode") + args.modpacks = ["engine"] + # pass modpacks to engine if args.modpacks: - mods = [] + # ensure that specified modpacks are available for modpack in args.modpacks: - mods.append(modpack.encode("utf-8")) + if modpack not in available_modpacks: + raise FileNotFoundError( + f"Modpack '{modpack}' not found in {asset_path / 'converted'}") - args.modpacks = mods + args.modpacks = [modpack.encode("utf-8") for modpack in args.modpacks] else: - from ..convert.service.init.modpack_search import enumerate_modpacks, query_modpack - avail_modpacks = enumerate_modpacks(asset_path / "converted") - args.modpacks = [query_modpack(avail_modpacks).encode("utf-8")] + args.modpacks = [query_modpack(list(available_modpacks.keys())).encode("utf-8")] # start the game, continue in main_cpp.pyx! return run_game(args, root) From 4ed575f5d26213ba21bbac869f82ba2ffb1eb29e Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 5 Aug 2024 03:02:14 +0200 Subject: [PATCH 489/771] convert: Fix pylint issues. --- openage/convert/tool/driver.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/openage/convert/tool/driver.py b/openage/convert/tool/driver.py index 0c422cdb80..61d53b0082 100644 --- a/openage/convert/tool/driver.py +++ b/openage/convert/tool/driver.py @@ -63,9 +63,11 @@ def convert_metadata(args: Namespace) -> None: if gamedata_path.exists(): gamedata_path.removerecursive() - read_start = timeit.default_timer() + # Record time taken for each stage + stages_time = {} # Read .dat + stage_start = timeit.default_timer() debug_gamedata_format(args.debugdir, args.debug_info, args.game_version) gamespec = get_gamespec(args.srcdir, args.game_version, not args.flag("no_pickle_cache")) @@ -84,20 +86,23 @@ def convert_metadata(args: Namespace) -> None: existing_graphics = get_existing_graphics(args) debug_registered_graphics(args.debugdir, args.debug_info, existing_graphics) - read_end = timeit.default_timer() - info("Finished metadata read (%.2f seconds)", read_end - read_start) + stage_end = timeit.default_timer() + info("Finished metadata read (%.2f seconds)", stage_end - stage_start) + stages_time.update({"read": stage_end - stage_start}) - conversion_start = timeit.default_timer() - # Convert + # nyan conversion + stage_start = timeit.default_timer() modpacks = args.converter.convert(gamespec, args, string_resources, existing_graphics) - conversion_end = timeit.default_timer() - info("Finished data conversion (%.2f seconds)", conversion_end - conversion_start) + stage_end = timeit.default_timer() + info("Finished data conversion (%.2f seconds)", stage_end - stage_start) + stages_time.update({"convert": stage_end - stage_start}) - export_start = timeit.default_timer() + # Export modpacks + stage_start = timeit.default_timer() for modpack in modpacks: mod_export_start = timeit.default_timer() ModpackExporter.export(modpack, args) @@ -109,13 +114,10 @@ def convert_metadata(args: Namespace) -> None: modpack.info.version, mod_export_end - mod_export_start) - export_end = timeit.default_timer() + stage_end = timeit.default_timer() + info("Finished export (%.2f seconds)", stage_end - stage_start) + stages_time.update({"export": stage_end - stage_start}) - stages_time = { - "read": read_end - read_start, - "convert": conversion_end - conversion_start, - "export": export_end - export_start, - } debug_execution_time(args.debugdir, args.debug_info, stages_time) # TODO: player palettes From 1f341e6e721ac88a4ede31fbc54a5609c7c59ed3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 5 Aug 2024 03:25:03 +0200 Subject: [PATCH 490/771] convert: Use modpacks from previous calls instead of reading them again. --- openage/convert/main.py | 8 ++++++-- openage/convert/service/init/changelog.py | 8 +++----- openage/game/main.py | 10 ++++++++++ openage/main/main.py | 4 ++++ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/openage/convert/main.py b/openage/convert/main.py index 369af4945e..cefa06d44e 100644 --- a/openage/convert/main.py +++ b/openage/convert/main.py @@ -15,9 +15,10 @@ from ..util.fslike.wrapper import (DirectoryCreator, Synchronizer as AccessSynchronizer) from .service.debug_info import debug_cli_args, debug_game_version, debug_mounts +from .service.init.changelog import check_updates +from .service.init.modpack_search import enumerate_modpacks from .service.init.mount_asset_dirs import mount_asset_dirs from .service.init.version_detect import create_version_objects -from .service.init.changelog import check_updates from .tool.interactive import interactive_browser from .tool.subtool.acquire_sourcedir import acquire_conversion_source_dir, wanna_convert, \ wanna_check_updates @@ -285,7 +286,10 @@ def main(args, error): if args.check_updates or (args.show_prompts and wanna_check_updates()): # check if the assets are up to date modpack_dir = outdir / "converted" + available_modpacks = enumerate_modpacks(modpack_dir, exclude={"engine"}) + game_info_dir = args.cfg_dir / "converter" / "games" - check_updates(modpack_dir, game_info_dir) + + check_updates(available_modpacks, game_info_dir) return 0 diff --git a/openage/convert/service/init/changelog.py b/openage/convert/service/init/changelog.py index 2cb4a244d6..8215e164da 100644 --- a/openage/convert/service/init/changelog.py +++ b/openage/convert/service/init/changelog.py @@ -12,7 +12,6 @@ from ....log import info -from .modpack_search import enumerate_modpacks from ..init.version_detect import create_version_objects @@ -20,15 +19,14 @@ from openage.util.fslike.union import UnionPath -def check_updates(modpack_dir: UnionPath, game_info_dir: UnionPath): +def check_updates(available_modpacks: dict[str, str], game_info_dir: UnionPath): """ Check if there are updates available for the openage converter modpacks. - :param modpack_dir: The directory containing the modpacks. + :param available_modpacks: Available modpacks and their versions. Modpack names are keys, + versions are values. :param game_info_dir: The directory containing the game information. """ - available_modpacks = enumerate_modpacks(modpack_dir, exclude={"engine"}) - game_editions, game_expansions = create_version_objects(game_info_dir) for game_def in chain(game_editions, game_expansions): for targetmod_name, targetmod_def in game_def.target_modpacks.items(): diff --git a/openage/game/main.py b/openage/game/main.py index d20502c06b..0fdf007259 100644 --- a/openage/game/main.py +++ b/openage/game/main.py @@ -31,6 +31,11 @@ def init_subparser(cli: ArgumentParser) -> None: "--modpacks", nargs="+", required=True, type=str, help="list of modpacks to load") + cli.add_argument( + "--check-updates", action='store_true', + help="Check if the assets are up to date" + ) + def main(args, error): """ @@ -46,6 +51,7 @@ def main(args, error): from ..assets import get_asset_path from ..convert.tool.api_export import export_api from ..convert.service.init.api_export_required import api_export_required + from ..convert.service.init.changelog import check_updates from ..convert.service.init.modpack_search import enumerate_modpacks from ..cppinterface.setup import setup as cpp_interface_setup from ..cvar.location import get_config_path @@ -85,6 +91,10 @@ def main(args, error): if modpack not in available_modpacks: raise FileNotFoundError(f"Modpack '{modpack}' not found in {modpack_dir}") + # check if the converted modpacks are up to date + if args.check_updates: + check_updates(available_modpacks, args.cfg_dir / "converter" / "games") + # encode modpacks as bytes for the C++ interface args.modpacks = [modpack.encode('utf-8') for modpack in args.modpacks] diff --git a/openage/main/main.py b/openage/main/main.py index 740bb270f6..fbeb527ee4 100644 --- a/openage/main/main.py +++ b/openage/main/main.py @@ -44,6 +44,7 @@ def main(args, error): from ..assets import get_asset_path from ..convert.main import convert_assets from ..convert.service.init.api_export_required import api_export_required + from ..convert.service.init.changelog import check_updates from ..convert.service.init.modpack_search import enumerate_modpacks, query_modpack from ..convert.tool.api_export import export_api from ..convert.tool.subtool.acquire_sourcedir import wanna_convert @@ -89,6 +90,9 @@ def main(args, error): info("Starting bare 'engine' mode") args.modpacks = ["engine"] + # check if the converted modpacks are up to date + check_updates(available_modpacks, args.cfg_dir / "converter" / "games") + # pass modpacks to engine if args.modpacks: # ensure that specified modpacks are available From e5b09ea4975c64e2ce208205272cfafc31a3a540 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 5 Aug 2024 03:29:42 +0200 Subject: [PATCH 491/771] convert: Remove unused module. --- openage/convert/service/init/CMakeLists.txt | 1 - .../service/init/conversion_required.py | 51 ------------------- 2 files changed, 52 deletions(-) delete mode 100644 openage/convert/service/init/conversion_required.py diff --git a/openage/convert/service/init/CMakeLists.txt b/openage/convert/service/init/CMakeLists.txt index 3c7e404c69..1d09b906d9 100644 --- a/openage/convert/service/init/CMakeLists.txt +++ b/openage/convert/service/init/CMakeLists.txt @@ -2,7 +2,6 @@ add_py_modules( __init__.py api_export_required.py changelog.py - conversion_required.py modpack_search.py mount_asset_dirs.py version_detect.py diff --git a/openage/convert/service/init/conversion_required.py b/openage/convert/service/init/conversion_required.py deleted file mode 100644 index 7c7e53c640..0000000000 --- a/openage/convert/service/init/conversion_required.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2020-2024 the openage authors. See copying.md for legal info. - -""" -Test whether there already are converted modpacks present. -""" -from __future__ import annotations -import typing - -from ....log import info - -from .modpack_search import enumerate_modpacks - -if typing.TYPE_CHECKING: - from openage.util.fslike.union import UnionPath - - -def conversion_required(asset_dir: UnionPath) -> bool: - """ - Check if an asset conversion is required to run the game. - - Asset conversions are required if: - - the modpack folder does not exist - - no modpacks inside the modpack folder exist - - :param asset_dir: The asset directory to check. - :type asset_dir: UnionPath - :return: True if an asset conversion is required, else False. - """ - try: - # TODO: Do not use hardcoded path to "converted" directory. - # The mod directory should be its own configurable path - modpacks = enumerate_modpacks(asset_dir / "converted", exclude={"engine"}) - - except FileNotFoundError: - # modpack folder not created yet - modpacks = set() - - if not modpacks: - info("No modpacks have been found") - return True - - # TODO: Check if modpack version of converted assets are up to date - # if not changes: - # dbg("Converted assets are up to date") - # return False - - # if asset_version >= 0 and asset_version != changelog.ASSET_VERSION: - # info("Found converted assets with version %d, " - # "but need version %d", asset_version, changelog.ASSET_VERSION) - - return False From 9d8641ec5877fb288e985c66dff3e7502c679ecf Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 5 Aug 2024 03:53:08 +0200 Subject: [PATCH 492/771] convert: Make update check more reliable. --- openage/convert/service/init/changelog.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openage/convert/service/init/changelog.py b/openage/convert/service/init/changelog.py index 8215e164da..20877664e4 100644 --- a/openage/convert/service/init/changelog.py +++ b/openage/convert/service/init/changelog.py @@ -32,7 +32,14 @@ def check_updates(available_modpacks: dict[str, str], game_info_dir: UnionPath): for targetmod_name, targetmod_def in game_def.target_modpacks.items(): if targetmod_name in available_modpacks: converter_version = SemanticVersion(targetmod_def["version"]) - modpack_version = SemanticVersion(available_modpacks[targetmod_name]) + try: + modpack_version = SemanticVersion(available_modpacks[targetmod_name]) + + except ValueError: + # Some really old converted modpacks don't use semantic versioning + # these should always be updated + modpack_version = SemanticVersion("0.0.0") + if converter_version > modpack_version: info(("Modpack %s v%s is outdated: " "newer version v%s is available"), From c8df5249fcfe112893744d3b1e500824aa548127 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 6 Aug 2024 23:45:46 +0200 Subject: [PATCH 493/771] util: Add safe split on \r\n in File::get_lines. --- libopenage/util/file.cpp | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/libopenage/util/file.cpp b/libopenage/util/file.cpp index 0acea80695..5aa630d4cc 100644 --- a/libopenage/util/file.cpp +++ b/libopenage/util/file.cpp @@ -1,4 +1,4 @@ -// Copyright 2013-2019 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #include "file.h" @@ -9,11 +9,13 @@ #include #include -#include "path.h" -#include "filelike/native.h" -#include "filelike/python.h" -#include "../error/error.h" -#include "../log/log.h" +#include "error/error.h" +#include "log/log.h" + +#include "util/filelike/native.h" +#include "util/filelike/python.h" +#include "util/path.h" +#include "util/strings.h" namespace openage::util { @@ -23,8 +25,7 @@ File::File() = default; // yes. i'm sorry. but cython can't enum class yet. -File::File(const std::string &path, int mode) - : +File::File(const std::string &path, int mode) : File{path, static_cast(mode)} {} @@ -33,8 +34,7 @@ File::File(const std::string &path, mode_t mode) { } -File::File(std::shared_ptr filelike) - : +File::File(std::shared_ptr filelike) : filelike{filelike} {} @@ -102,13 +102,7 @@ std::vector File::get_lines() { // TODO: relay the get_lines to the underlaying filelike // which may do a better job in getting the lines. // instead, we read everything and then split up into lines. - std::string line; - std::vector result{}; - std::istringstream content{this->read()}; - - while (std::getline(content, line)) { - result.push_back(line); - } + std::vector result = util::split_newline(this->read()); return result; } @@ -119,7 +113,7 @@ std::shared_ptr File::get_fileobj() const { } -std::ostream &operator <<(std::ostream &stream, const File &file) { +std::ostream &operator<<(std::ostream &stream, const File &file) { stream << "File("; file.filelike->repr(stream); stream << ")"; @@ -128,4 +122,4 @@ std::ostream &operator <<(std::ostream &stream, const File &file) { } -} // openage::util +} // namespace openage::util From 90de3a377a12e4d592743e6a8492d12e2fcb3276 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 7 Aug 2024 00:02:46 +0200 Subject: [PATCH 494/771] renderer: Use File::get_lines in resource parsers. --- libopenage/renderer/resources/parser/parse_blendmask.cpp | 2 +- libopenage/renderer/resources/parser/parse_blendtable.cpp | 2 +- libopenage/renderer/resources/parser/parse_palette.cpp | 2 +- libopenage/renderer/resources/parser/parse_sprite.cpp | 2 +- libopenage/renderer/resources/parser/parse_terrain.cpp | 2 +- libopenage/renderer/resources/parser/parse_texture.cpp | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libopenage/renderer/resources/parser/parse_blendmask.cpp b/libopenage/renderer/resources/parser/parse_blendmask.cpp index ddfbd22a2d..31a46cddc4 100644 --- a/libopenage/renderer/resources/parser/parse_blendmask.cpp +++ b/libopenage/renderer/resources/parser/parse_blendmask.cpp @@ -67,7 +67,7 @@ BlendPatternInfo parse_blendmask_file(const util::Path &path, } auto file = path.open(); - auto lines = util::split_newline(file.read()); + auto lines = file.get_lines(); float scalefactor = 1.0; std::vector textures; diff --git a/libopenage/renderer/resources/parser/parse_blendtable.cpp b/libopenage/renderer/resources/parser/parse_blendtable.cpp index 648e83b9ed..e564085820 100644 --- a/libopenage/renderer/resources/parser/parse_blendtable.cpp +++ b/libopenage/renderer/resources/parser/parse_blendtable.cpp @@ -76,7 +76,7 @@ BlendTableInfo parse_blendtable_file(const util::Path &path, } auto file = path.open(); - auto lines = util::split_newline(file.read()); + auto lines = file.get_lines(); std::vector blendtable; std::vector patterns; diff --git a/libopenage/renderer/resources/parser/parse_palette.cpp b/libopenage/renderer/resources/parser/parse_palette.cpp index c2e0ce8e20..abea683f6a 100644 --- a/libopenage/renderer/resources/parser/parse_palette.cpp +++ b/libopenage/renderer/resources/parser/parse_palette.cpp @@ -71,7 +71,7 @@ PaletteInfo parse_palette_file(const util::Path &path) { } auto file = path.open(); - auto lines = util::split_newline(file.read()); + auto lines = file.get_lines(); size_t entries = 0; std::vector colours; diff --git a/libopenage/renderer/resources/parser/parse_sprite.cpp b/libopenage/renderer/resources/parser/parse_sprite.cpp index 4d783c5ba4..01534ad755 100644 --- a/libopenage/renderer/resources/parser/parse_sprite.cpp +++ b/libopenage/renderer/resources/parser/parse_sprite.cpp @@ -143,7 +143,7 @@ Animation2dInfo parse_sprite_file(const util::Path &path, } auto file = path.open(); - auto lines = util::split_newline(file.read()); + auto lines = file.get_lines(); float scalefactor = 1.0; std::vector textures; diff --git a/libopenage/renderer/resources/parser/parse_terrain.cpp b/libopenage/renderer/resources/parser/parse_terrain.cpp index d46c9bc146..9b17f4608e 100644 --- a/libopenage/renderer/resources/parser/parse_terrain.cpp +++ b/libopenage/renderer/resources/parser/parse_terrain.cpp @@ -148,7 +148,7 @@ TerrainInfo parse_terrain_file(const util::Path &path, } auto file = path.open(); - auto lines = util::split_newline(file.read()); + auto lines = file.get_lines(); float scalefactor = 1.0; std::vector textures; diff --git a/libopenage/renderer/resources/parser/parse_texture.cpp b/libopenage/renderer/resources/parser/parse_texture.cpp index 52d3958731..6f1ee622d3 100644 --- a/libopenage/renderer/resources/parser/parse_texture.cpp +++ b/libopenage/renderer/resources/parser/parse_texture.cpp @@ -147,7 +147,7 @@ Texture2dInfo parse_texture_file(const util::Path &path) { } auto file = path.open(); - auto lines = util::split_newline(file.read()); + auto lines = file.get_lines(); std::string imagefile; SizeData size; From f4ff6cdf3445f9af05bde2d740d1c649d6593c2c Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 7 Aug 2024 00:33:21 +0200 Subject: [PATCH 495/771] renderer: Fix reading binary numbers in blendmask parser. --- libopenage/renderer/resources/parser/parse_blendmask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/renderer/resources/parser/parse_blendmask.cpp b/libopenage/renderer/resources/parser/parse_blendmask.cpp index 31a46cddc4..b5b9338b72 100644 --- a/libopenage/renderer/resources/parser/parse_blendmask.cpp +++ b/libopenage/renderer/resources/parser/parse_blendmask.cpp @@ -40,7 +40,7 @@ blending_mask parse_mask(const std::vector &args) { if (args[0].starts_with("0b")) { // discard prefix because std::stoul doesn't understand binary prefixes std::string strip = args[0].substr(2, args[2].size() - 1); - dir = std::stoul(args[0], nullptr, 2); + dir = std::stoul(strip, nullptr, 2); } else { dir = std::stoul(args[0]); From cb5a754a61fbf9be2ca90d0ddd9a81b04cdeb619 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 May 2024 18:59:36 +0200 Subject: [PATCH 496/771] gamestate: Map class to store terrain and pathfinder. --- libopenage/gamestate/CMakeLists.txt | 1 + libopenage/gamestate/map.cpp | 35 ++++++++++++++ libopenage/gamestate/map.h | 74 +++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 libopenage/gamestate/map.cpp create mode 100644 libopenage/gamestate/map.h diff --git a/libopenage/gamestate/CMakeLists.txt b/libopenage/gamestate/CMakeLists.txt index a94cd93514..3f0b57bf6b 100644 --- a/libopenage/gamestate/CMakeLists.txt +++ b/libopenage/gamestate/CMakeLists.txt @@ -5,6 +5,7 @@ add_sources(libopenage game_state.cpp game.cpp manager.cpp + map.cpp player.cpp simulation.cpp terrain_chunk.cpp diff --git a/libopenage/gamestate/map.cpp b/libopenage/gamestate/map.cpp new file mode 100644 index 0000000000..9561e599ac --- /dev/null +++ b/libopenage/gamestate/map.cpp @@ -0,0 +1,35 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "map.h" + +#include "gamestate/terrain.h" +#include "pathfinding/pathfinder.h" + + +namespace openage::gamestate { +Map::Map(const std::shared_ptr &terrain) : + terrain{terrain}, + pathfinder{std::make_shared()} { + // TODO: Initialize pathfinder with terrain path costs +} + +Map::Map(const std::shared_ptr &terrain, + const std::shared_ptr &pathfinder) : + terrain{terrain}, + pathfinder{pathfinder} { + // TODO: Initialize pathfinder with terrain path costs +} + +const util::Vector2s &Map::get_size() const { + return this->terrain->get_size(); +} + +const std::shared_ptr &Map::get_terrain() const { + return this->terrain; +} + +const std::shared_ptr &Map::get_pathfinder() const { + return this->pathfinder; +} + +} // namespace openage::gamestate diff --git a/libopenage/gamestate/map.h b/libopenage/gamestate/map.h new file mode 100644 index 0000000000..78a79e9d67 --- /dev/null +++ b/libopenage/gamestate/map.h @@ -0,0 +1,74 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include "util/vector.h" + + +namespace openage { +namespace path { +class Pathfinder; +} // namespace path + +namespace gamestate { +class Terrain; + +class Map { +public: + /** + * Create a new map from existing terrain. + * + * Initializes the pathfinder with the terrain path costs. + * + * @param terrain Terrain object. + */ + Map(const std::shared_ptr &terrain); + + /** + * Create a new map from existing terrain and pathfinder. + * + * @param terrain Terrain. + * @param pathfinder Pathfinder. + */ + Map(const std::shared_ptr &terrain, + const std::shared_ptr &pathfinder); + + ~Map() = default; + + /** + * Get the size of the map. + * + * @return Map size (width x height). + */ + const util::Vector2s &get_size() const; + + /** + * Get the terrain of the map. + * + * @return Terrain. + */ + const std::shared_ptr &get_terrain() const; + + /** + * Get the pathfinder for the map. + * + * @return Pathfinder. + */ + const std::shared_ptr &get_pathfinder() const; + +private: + /** + * Terrain. + */ + std::shared_ptr terrain; + + /** + * Pathfinder. + */ + std::shared_ptr pathfinder; +}; + +} // namespace gamestate +} // namespace openage From 7584db49829b4a2f4b8195f676c3b0c88804c8ed Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 May 2024 19:00:16 +0200 Subject: [PATCH 497/771] gamestate: Verify terrain chunk size. --- libopenage/gamestate/terrain.cpp | 67 +++++++++++++++++++++++++++++++- libopenage/gamestate/terrain.h | 50 ++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 4 deletions(-) diff --git a/libopenage/gamestate/terrain.cpp b/libopenage/gamestate/terrain.cpp index 9e40230ffe..b29a2fe541 100644 --- a/libopenage/gamestate/terrain.cpp +++ b/libopenage/gamestate/terrain.cpp @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #include "terrain.h" @@ -6,6 +6,8 @@ #include #include +#include "error/error.h" + #include "gamestate/terrain_chunk.h" #include "renderer/render_factory.h" @@ -18,6 +20,59 @@ Terrain::Terrain() : // TODO: Get actual size of terrain. } +Terrain::Terrain(const util::Vector2s &size, + const std::vector> &&chunks) : + size{size}, + chunks{std::move(chunks)} { + // Check if chunk is correct + coord::tile_delta current_offset{0, 0}; + util::Vector2s all_chunks_size{0, 0}; + if (this->chunks.size() > 0) { + all_chunks_size = this->chunks[0]->get_size(); + } + + for (const auto &chunk : this->chunks) { + auto chunk_size = chunk->get_size(); + current_offset.ne += chunk_size[0]; + + // Wrap around to the next row + if (current_offset.ne == size[0]) { + current_offset.ne = 0; + current_offset.se += chunk_size[1]; + } + + // Check terrain boundaries + if (current_offset.ne > size[0]) { + throw error::Error{ERR << "Width of chunk " << chunk->get_offset() + << " exceeds terrain width: " << chunk_size[0] + << " (max width: " << size[0] << ")"}; + } + else if (current_offset.se > size[1]) { + throw error::Error{ERR << "Height of chunk " << chunk->get_offset() + << " exceeds terrain height: " << chunk_size[1] + << " (max height: " << size[1] << ")"}; + } + // Check if chunk size is correct + else if (chunk_size != chunk_size) [[unlikely]] { + throw error::Error{ERR << "Chunk size of chunk " << chunk->get_offset() + << " is not equal to the first chunk size: " << chunk_size + << " (expected: " << all_chunks_size << ")"}; + } + + // Check chunk delta + auto chunk_offset = chunk->get_offset(); + if (current_offset != chunk_offset) [[unlikely]] { + throw error::Error{ERR << "Chunk offset of chunk " << chunk->get_offset() + << " does not match position in vector: " << chunk_offset + << " (expected: " << current_offset << ")"}; + } + } +} + +const util::Vector2s &Terrain::get_size() const { + return this->size; +} + void Terrain::add_chunk(const std::shared_ptr &chunk) { this->chunks.push_back(chunk); } @@ -26,6 +81,16 @@ const std::vector> &Terrain::get_chunks() const { return this->chunks; } +const std::shared_ptr &Terrain::get_chunk(size_t idx) const { + return this->chunks.at(idx); +} + +const std::shared_ptr &Terrain::get_chunk(const coord::chunk &pos) const { + size_t index = pos.ne + pos.se * this->size[0]; + + return this->get_chunk(index); +} + void Terrain::attach_renderer(const std::shared_ptr &render_factory) { for (auto &chunk : this->get_chunks()) { auto render_entity = render_factory->add_terrain_render_entity(chunk->get_size(), diff --git a/libopenage/gamestate/terrain.h b/libopenage/gamestate/terrain.h index 97ffcbdffd..0dbf6ba7dd 100644 --- a/libopenage/gamestate/terrain.h +++ b/libopenage/gamestate/terrain.h @@ -6,8 +6,10 @@ #include #include +#include "coord/chunk.h" #include "util/vector.h" + namespace openage { namespace renderer { class RenderFactory; @@ -27,6 +29,13 @@ class Terrain { Terrain(); ~Terrain() = default; + /** + * Get the size of the terrain (in tiles). + * + * @return Terrain tile size (width x height). + */ + const util::Vector2s &get_size() const; + /** * Add a chunk to the terrain. * @@ -41,6 +50,24 @@ class Terrain { */ const std::vector> &get_chunks() const; + /** + * Get a specific chunk of the terrain. + * + * @param idx Index of the chunk. + * + * @return Terrain chunk. + */ + const std::shared_ptr &get_chunk(size_t idx) const; + + /** + * Get a specific chunk of the terrain. + * + * @param pos Position of the chunk in the terrain. + * + * @return Terrain chunk. + */ + const std::shared_ptr &get_chunk(const coord::chunk &pos) const; + /** * Attach a renderer which enables graphical display. * @@ -54,16 +81,33 @@ class Terrain { */ void attach_renderer(const std::shared_ptr &render_factory); +protected: + /** + * Create a new terrain. + * + * Chunks must conform to these constraints: + * - All chunks that are not last in a row OR columns must have the same size (width x height). + * - The last chunk in a row may have a different width. + * - The last chunk in a column may have a different height. + * + * @param size Total size of the terrain in tiles (width x height). + * @param chunks Terrain chunks. + */ + Terrain(const util::Vector2s &size, + const std::vector> &&chunks); + private: /** - * Total size of the map - * origin is the left corner - * x = top left edge; y = top right edge + * Total size of the terrain in tiles (width x height). + * + * Origin is the top left corner (left corner with camera projection). */ util::Vector2s size; /** * Subdivision of the main terrain entity. + * + * Ordered in rows from left to right, top to bottom. */ std::vector> chunks; }; From 1ddc053ff716813ef9e8788f3607efe34b7123c3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 May 2024 19:13:04 +0200 Subject: [PATCH 498/771] gamestate: Get path costs from nyan API. --- libopenage/gamestate/api/terrain.cpp | 16 +++++++++++++++- libopenage/gamestate/api/terrain.h | 9 +++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/libopenage/gamestate/api/terrain.cpp b/libopenage/gamestate/api/terrain.cpp index e69edee5fc..180d82425f 100644 --- a/libopenage/gamestate/api/terrain.cpp +++ b/libopenage/gamestate/api/terrain.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 "terrain.h" @@ -21,4 +21,18 @@ const std::string APITerrain::get_terrain_path(const nyan::Object &terrain) { return resolve_file_path(terrain, terrain_path); } +const std::unordered_map APITerrain::get_path_costs(const nyan::Object &terrain) { + std::unordered_map result; + + 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()); + + result.emplace(key->get_name(), value->get()); + } + + return result; +} + } // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/terrain.h b/libopenage/gamestate/api/terrain.h index 0046a47bdc..d049bce279 100644 --- a/libopenage/gamestate/api/terrain.h +++ b/libopenage/gamestate/api/terrain.h @@ -30,6 +30,15 @@ class APITerrain { * @return Relative path to the terrain file. */ static const std::string get_terrain_path(const nyan::Object &terrain); + + /** + * Get the path costs of a terrain. + * + * @param terrain \p Terrain nyan object (type == \p engine.util.terrain.Terrain). + * + * @return Path costs for the cost fields of the pathfinder. + */ + static const std::unordered_map get_path_costs(const nyan::Object &terrain); }; } // namespace openage::gamestate::api From c6e6c8bedf65b3e36847cb6903685d2fd51e5296 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 12 May 2024 23:50:10 +0200 Subject: [PATCH 499/771] gamestate: Generate pathfinding grids from nyan terrain defs. --- libopenage/gamestate/map.cpp | 58 ++++++++++++++++++++++---- libopenage/gamestate/map.h | 9 ---- libopenage/gamestate/terrain.cpp | 26 +++++++++--- libopenage/gamestate/terrain.h | 15 +++++++ libopenage/gamestate/terrain_chunk.cpp | 12 ++++++ libopenage/gamestate/terrain_chunk.h | 25 +++++++++++ 6 files changed, 123 insertions(+), 22 deletions(-) diff --git a/libopenage/gamestate/map.cpp b/libopenage/gamestate/map.cpp index 9561e599ac..b7a9fa6bb5 100644 --- a/libopenage/gamestate/map.cpp +++ b/libopenage/gamestate/map.cpp @@ -2,22 +2,66 @@ #include "map.h" +#include + +#include "gamestate/api/terrain.h" #include "gamestate/terrain.h" +#include "gamestate/terrain_chunk.h" +#include "pathfinding/cost_field.h" +#include "pathfinding/grid.h" #include "pathfinding/pathfinder.h" +#include "pathfinding/sector.h" namespace openage::gamestate { Map::Map(const std::shared_ptr &terrain) : terrain{terrain}, pathfinder{std::make_shared()} { - // TODO: Initialize pathfinder with terrain path costs -} + // Get grid types + // TODO: This should probably not be dependent on the existing tiles, but + // on all defined nyan PathType objects + std::unordered_map path_types; + size_t grid_idx = 0; + auto chunk_size = this->terrain->get_chunk(0)->get_size(); + auto side_length = std::max(chunk_size[0], chunk_size[0]); + util::Vector2s grid_size{this->terrain->get_row_size(), this->terrain->get_column_size()}; + for (const auto &chunk : this->terrain->get_chunks()) { + for (const auto &tile : chunk->get_tiles()) { + if (tile.terrain != std::nullopt) { + auto path_costs = api::APITerrain::get_path_costs(*tile.terrain); -Map::Map(const std::shared_ptr &terrain, - const std::shared_ptr &pathfinder) : - terrain{terrain}, - pathfinder{pathfinder} { - // TODO: Initialize pathfinder with terrain path costs + for (const auto &path_cost : path_costs) { + if (not path_types.contains(path_cost.first)) { + auto grid = std::make_shared(grid_idx, grid_size, side_length); + this->pathfinder->add_grid(grid); + + path_types.emplace(path_cost.first, grid_idx); + grid_idx += 1; + } + } + } + } + } + + // Set path costs + for (size_t chunk_idx = 0; chunk_idx < this->terrain->get_chunks().size(); ++chunk_idx) { + auto chunk_terrain = this->terrain->get_chunk(chunk_idx); + for (size_t tile_idx = 0; tile_idx < chunk_terrain->get_tiles().size(); ++tile_idx) { + auto tile = chunk_terrain->get_tile(tile_idx); + if (tile.terrain != std::nullopt) { + auto path_costs = api::APITerrain::get_path_costs(*tile.terrain); + + for (const auto &path_cost : path_costs) { + auto grid_id = path_types.at(path_cost.first); + auto grid = this->pathfinder->get_grid(grid_id); + + auto sector = grid->get_sector(chunk_idx); + auto cost_field = sector->get_cost_field(); + cost_field->set_cost(tile_idx, path_cost.second); + } + } + } + } } const util::Vector2s &Map::get_size() const { diff --git a/libopenage/gamestate/map.h b/libopenage/gamestate/map.h index 78a79e9d67..b8037c13d5 100644 --- a/libopenage/gamestate/map.h +++ b/libopenage/gamestate/map.h @@ -26,15 +26,6 @@ class Map { */ Map(const std::shared_ptr &terrain); - /** - * Create a new map from existing terrain and pathfinder. - * - * @param terrain Terrain. - * @param pathfinder Pathfinder. - */ - Map(const std::shared_ptr &terrain, - const std::shared_ptr &pathfinder); - ~Map() = default; /** diff --git a/libopenage/gamestate/terrain.cpp b/libopenage/gamestate/terrain.cpp index b29a2fe541..8e52aad2c5 100644 --- a/libopenage/gamestate/terrain.cpp +++ b/libopenage/gamestate/terrain.cpp @@ -40,6 +40,16 @@ Terrain::Terrain(const util::Vector2s &size, current_offset.ne = 0; current_offset.se += chunk_size[1]; } + // Check if chunk size is correct + else if (chunk_size != chunk_size) [[unlikely]] { + throw error::Error{ERR << "Chunk size of chunk " << chunk->get_offset() + << " is not equal to the first chunk size: " << chunk_size + << " (expected: " << all_chunks_size << ")"}; + } + else if (chunk_size[0] != chunk_size[1]) { + throw error::Error{ERR << "Chunk size of chunk " << chunk->get_offset() + << " is not square: " << chunk_size}; + } // Check terrain boundaries if (current_offset.ne > size[0]) { @@ -52,12 +62,6 @@ Terrain::Terrain(const util::Vector2s &size, << " exceeds terrain height: " << chunk_size[1] << " (max height: " << size[1] << ")"}; } - // Check if chunk size is correct - else if (chunk_size != chunk_size) [[unlikely]] { - throw error::Error{ERR << "Chunk size of chunk " << chunk->get_offset() - << " is not equal to the first chunk size: " << chunk_size - << " (expected: " << all_chunks_size << ")"}; - } // Check chunk delta auto chunk_offset = chunk->get_offset(); @@ -73,6 +77,16 @@ const util::Vector2s &Terrain::get_size() const { return this->size; } +size_t Terrain::get_row_size() const { + auto chunk_size = this->chunks[0]->get_size(); + return this->size[0] / chunk_size[0]; +} + +size_t Terrain::get_column_size() const { + auto chunk_size = this->chunks[0]->get_size(); + return this->size[1] / chunk_size[1]; +} + void Terrain::add_chunk(const std::shared_ptr &chunk) { this->chunks.push_back(chunk); } diff --git a/libopenage/gamestate/terrain.h b/libopenage/gamestate/terrain.h index 0dbf6ba7dd..8e5b03159c 100644 --- a/libopenage/gamestate/terrain.h +++ b/libopenage/gamestate/terrain.h @@ -36,6 +36,20 @@ class Terrain { */ const util::Vector2s &get_size() const; + /** + * Get the size of a row in the terrain. + * + * @return Row size (width). + */ + size_t get_row_size() const; + + /** + * Get the size of a column in the terrain. + * + * @return Column size (height). + */ + size_t get_column_size() const; + /** * Add a chunk to the terrain. * @@ -87,6 +101,7 @@ class Terrain { * * Chunks must conform to these constraints: * - All chunks that are not last in a row OR columns must have the same size (width x height). + * - All chunks that are not last in a row OR columns must be square (width == height). * - The last chunk in a row may have a different width. * - The last chunk in a column may have a different height. * diff --git a/libopenage/gamestate/terrain_chunk.cpp b/libopenage/gamestate/terrain_chunk.cpp index e988260333..d863db9f67 100644 --- a/libopenage/gamestate/terrain_chunk.cpp +++ b/libopenage/gamestate/terrain_chunk.cpp @@ -30,6 +30,18 @@ const coord::tile_delta &TerrainChunk::get_offset() const { return this->offset; } +const std::vector &TerrainChunk::get_tiles() const { + return this->tiles; +} + +const TerrainTile &TerrainChunk::get_tile(size_t idx) const { + return this->tiles.at(idx); +} + +const TerrainTile &TerrainChunk::get_tile(const coord::tile &pos) const { + return this->tiles.at(pos.ne + pos.se * this->size[0]); +} + void TerrainChunk::render_update(const time::time_t &time) { if (this->render_entity != nullptr) { // TODO: Update individual tiles instead of the whole chunk diff --git a/libopenage/gamestate/terrain_chunk.h b/libopenage/gamestate/terrain_chunk.h index 684d3af357..574e63da4d 100644 --- a/libopenage/gamestate/terrain_chunk.h +++ b/libopenage/gamestate/terrain_chunk.h @@ -48,6 +48,31 @@ class TerrainChunk { */ const coord::tile_delta &get_offset() const; + /** + * Get the tiles of this terrain chunk. + * + * @return Terrain tiles. + */ + const std::vector &get_tiles() const; + + /** + * Get the tile at the given index. + * + * @param idx Index of the tile. + * + * @return Terrain tile. + */ + const TerrainTile &get_tile(size_t idx) const; + + /** + * Get the tile at the given position. + * + * @param pos Position of the tile. + * + * @return Terrain tile. + */ + const TerrainTile &get_tile(const coord::tile &pos) const; + /** * Update the render entity. * From 3d24ca4e1ab071c21c93d45562f99564ac487fa9 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 13 May 2024 00:45:27 +0200 Subject: [PATCH 500/771] gamestate: Fix nyan object value pointer cast. --- libopenage/gamestate/api/terrain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/gamestate/api/terrain.cpp b/libopenage/gamestate/api/terrain.cpp index 180d82425f..ad5e300b47 100644 --- a/libopenage/gamestate/api/terrain.cpp +++ b/libopenage/gamestate/api/terrain.cpp @@ -26,7 +26,7 @@ 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 key = std::dynamic_pointer_cast(pair.first.get_ptr()); auto value = std::dynamic_pointer_cast(pair.second.get_ptr()); result.emplace(key->get_name(), value->get()); From cdf83d848dba0006e523be2e5735244124bb4d28 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 13 May 2024 00:46:12 +0200 Subject: [PATCH 501/771] gamestate: Add map to state. --- libopenage/gamestate/game.cpp | 16 +++++++--------- libopenage/gamestate/game_state.cpp | 10 +++++----- libopenage/gamestate/game_state.h | 18 +++++++++--------- libopenage/gamestate/terrain_factory.cpp | 5 +++-- libopenage/gamestate/terrain_factory.h | 3 ++- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/libopenage/gamestate/game.cpp b/libopenage/gamestate/game.cpp index 8e3497bfbc..db50f8cc72 100644 --- a/libopenage/gamestate/game.cpp +++ b/libopenage/gamestate/game.cpp @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #include "game.h" @@ -13,6 +13,7 @@ #include "assets/modpack.h" #include "gamestate/entity_factory.h" #include "gamestate/game_state.h" +#include "gamestate/map.h" #include "gamestate/terrain.h" #include "gamestate/terrain_factory.h" #include "gamestate/universe.h" @@ -53,7 +54,7 @@ const std::shared_ptr &Game::get_state() const { void Game::attach_renderer(const std::shared_ptr &render_factory) { this->universe->attach_renderer(render_factory); - this->state->get_terrain()->attach_renderer(render_factory); + this->state->get_map()->get_terrain()->attach_renderer(render_factory); } void Game::load_data(const std::shared_ptr &mod_manager) { @@ -127,8 +128,6 @@ void Game::load_path(const util::Path &base_dir, } void Game::generate_terrain(const std::shared_ptr &terrain_factory) { - auto terrain = terrain_factory->add_terrain(); - auto chunk0 = terrain_factory->add_chunk(this->state, util::Vector2s{10, 10}, coord::tile_delta{0, 0}); @@ -141,12 +140,11 @@ void Game::generate_terrain(const std::shared_ptr &terrain_facto auto chunk3 = terrain_factory->add_chunk(this->state, util::Vector2s{10, 10}, coord::tile_delta{10, 10}); - terrain->add_chunk(chunk0); - terrain->add_chunk(chunk1); - terrain->add_chunk(chunk2); - terrain->add_chunk(chunk3); - this->state->set_terrain(terrain); + auto terrain = terrain_factory->add_terrain({20, 20}, {chunk0, chunk1, chunk2, chunk3}); + + auto map = std::make_shared(terrain); + this->state->set_map(map); } } // namespace openage::gamestate diff --git a/libopenage/gamestate/game_state.cpp b/libopenage/gamestate/game_state.cpp index 9de7c69bff..4bf0aaa348 100644 --- a/libopenage/gamestate/game_state.cpp +++ b/libopenage/gamestate/game_state.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 "game_state.h" @@ -37,8 +37,8 @@ void GameState::add_player(const std::shared_ptr &player) { this->players[player->get_id()] = player; } -void GameState::set_terrain(const std::shared_ptr &terrain) { - this->terrain = terrain; +void GameState::set_map(const std::shared_ptr &map) { + this->map = map; } const std::shared_ptr &GameState::get_game_entity(entity_id_t id) const { @@ -59,8 +59,8 @@ const std::shared_ptr &GameState::get_player(player_id_t id) const { return this->players.at(id); } -const std::shared_ptr &GameState::get_terrain() const { - return this->terrain; +const std::shared_ptr &GameState::get_map() const { + return this->map; } const std::shared_ptr &GameState::get_mod_manager() const { diff --git a/libopenage/gamestate/game_state.h b/libopenage/gamestate/game_state.h index bf1531bd55..98cdbc187a 100644 --- a/libopenage/gamestate/game_state.h +++ b/libopenage/gamestate/game_state.h @@ -26,8 +26,8 @@ class EventLoop; namespace gamestate { class GameEntity; +class Map; class Player; -class Terrain; /** * State of the game. @@ -70,11 +70,11 @@ class GameState : public openage::event::State { void add_player(const std::shared_ptr &player); /** - * Set the terrain of the current game. + * Set the map of the current game. * - * @param terrain Terrain object. + * @param terrain Map object. */ - void set_terrain(const std::shared_ptr &terrain); + void set_map(const std::shared_ptr &map); /** * Get a game entity by its ID. @@ -102,11 +102,11 @@ class GameState : public openage::event::State { const std::shared_ptr &get_player(player_id_t id) const; /** - * Get the terrain of the current game. + * Get the map of the current game. * - * @return Terrain object. + * @return Map object. */ - const std::shared_ptr &get_terrain() const; + const std::shared_ptr &get_map() const; /** * TODO: Only for testing. @@ -131,9 +131,9 @@ class GameState : public openage::event::State { std::unordered_map> players; /** - * Terrain of the current game. + * Map of the current game. */ - std::shared_ptr terrain; + std::shared_ptr map; /** * TODO: Only for testing diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp index 17b5d452cb..55c70b34cf 100644 --- a/libopenage/gamestate/terrain_factory.cpp +++ b/libopenage/gamestate/terrain_factory.cpp @@ -170,9 +170,10 @@ static const std::vector> layout_chunks{ }; -std::shared_ptr TerrainFactory::add_terrain() { +std::shared_ptr TerrainFactory::add_terrain(const util::Vector2s &size, + std::vector> &&chunks) { // TODO: Replace this with a proper terrain generator. - auto terrain = std::make_shared(); + auto terrain = std::make_shared(size, std::move(chunks)); return terrain; } diff --git a/libopenage/gamestate/terrain_factory.h b/libopenage/gamestate/terrain_factory.h index ab419acc30..f5ce40b283 100644 --- a/libopenage/gamestate/terrain_factory.h +++ b/libopenage/gamestate/terrain_factory.h @@ -36,7 +36,8 @@ class TerrainFactory { * * @return New terrain object. */ - std::shared_ptr add_terrain(); + std::shared_ptr add_terrain(const util::Vector2s &size, + std::vector> &&chunks); /** * Create a new empty terrain chunk. From 94e1afb2c799873b9827f21bbc3db99f351ad27d Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 13 May 2024 00:46:33 +0200 Subject: [PATCH 502/771] gamestate: Fix access of Terrain constructor. --- libopenage/gamestate/terrain.cpp | 19 ++++++++++--------- libopenage/gamestate/terrain.h | 31 +++++++++++++++---------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/libopenage/gamestate/terrain.cpp b/libopenage/gamestate/terrain.cpp index 8e52aad2c5..8625c0b727 100644 --- a/libopenage/gamestate/terrain.cpp +++ b/libopenage/gamestate/terrain.cpp @@ -21,7 +21,7 @@ Terrain::Terrain() : } Terrain::Terrain(const util::Vector2s &size, - const std::vector> &&chunks) : + std::vector> &&chunks) : size{size}, chunks{std::move(chunks)} { // Check if chunk is correct @@ -33,6 +33,15 @@ Terrain::Terrain(const util::Vector2s &size, for (const auto &chunk : this->chunks) { auto chunk_size = chunk->get_size(); + + // Check chunk delta + auto chunk_offset = chunk->get_offset(); + if (current_offset != chunk_offset) [[unlikely]] { + throw error::Error{ERR << "Chunk offset of chunk " << chunk->get_offset() + << " does not match position in vector: " << chunk_offset + << " (expected: " << current_offset << ")"}; + } + current_offset.ne += chunk_size[0]; // Wrap around to the next row @@ -62,14 +71,6 @@ Terrain::Terrain(const util::Vector2s &size, << " exceeds terrain height: " << chunk_size[1] << " (max height: " << size[1] << ")"}; } - - // Check chunk delta - auto chunk_offset = chunk->get_offset(); - if (current_offset != chunk_offset) [[unlikely]] { - throw error::Error{ERR << "Chunk offset of chunk " << chunk->get_offset() - << " does not match position in vector: " << chunk_offset - << " (expected: " << current_offset << ")"}; - } } } diff --git a/libopenage/gamestate/terrain.h b/libopenage/gamestate/terrain.h index 8e5b03159c..1a5db1bf95 100644 --- a/libopenage/gamestate/terrain.h +++ b/libopenage/gamestate/terrain.h @@ -29,6 +29,21 @@ class Terrain { Terrain(); ~Terrain() = default; + /** + * Create a new terrain. + * + * Chunks must conform to these constraints: + * - All chunks that are not last in a row OR columns must have the same size (width x height). + * - All chunks that are not last in a row OR columns must be square (width == height). + * - The last chunk in a row may have a different width. + * - The last chunk in a column may have a different height. + * + * @param size Total size of the terrain in tiles (width x height). + * @param chunks Terrain chunks. + */ + Terrain(const util::Vector2s &size, + std::vector> &&chunks); + /** * Get the size of the terrain (in tiles). * @@ -95,22 +110,6 @@ class Terrain { */ void attach_renderer(const std::shared_ptr &render_factory); -protected: - /** - * Create a new terrain. - * - * Chunks must conform to these constraints: - * - All chunks that are not last in a row OR columns must have the same size (width x height). - * - All chunks that are not last in a row OR columns must be square (width == height). - * - The last chunk in a row may have a different width. - * - The last chunk in a column may have a different height. - * - * @param size Total size of the terrain in tiles (width x height). - * @param chunks Terrain chunks. - */ - Terrain(const util::Vector2s &size, - const std::vector> &&chunks); - private: /** * Total size of the terrain in tiles (width x height). From 6d2a2006e7d97279eb28a12f9f5024a8d1f42e9b Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 20 May 2024 03:44:59 +0200 Subject: [PATCH 503/771] gamestate: Connect portals in pathfinder grid. --- libopenage/gamestate/map.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libopenage/gamestate/map.cpp b/libopenage/gamestate/map.cpp index b7a9fa6bb5..a21a96a01f 100644 --- a/libopenage/gamestate/map.cpp +++ b/libopenage/gamestate/map.cpp @@ -62,6 +62,12 @@ Map::Map(const std::shared_ptr &terrain) : } } } + + // Connect sectors with portals + for (const auto &path_type : path_types) { + auto grid = this->pathfinder->get_grid(path_type.second); + grid->init_portals(); + } } const util::Vector2s &Map::get_size() const { From ae363878fbf669f9b98c6be423865e2d62de190f Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 20 May 2024 04:38:24 +0200 Subject: [PATCH 504/771] gamestate: Record mapping of nyan path grid -> grid ID. --- libopenage/gamestate/map.cpp | 18 +++++++++++------- libopenage/gamestate/map.h | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/libopenage/gamestate/map.cpp b/libopenage/gamestate/map.cpp index a21a96a01f..dbe460ca36 100644 --- a/libopenage/gamestate/map.cpp +++ b/libopenage/gamestate/map.cpp @@ -2,7 +2,7 @@ #include "map.h" -#include +#include #include "gamestate/api/terrain.h" #include "gamestate/terrain.h" @@ -16,11 +16,11 @@ namespace openage::gamestate { Map::Map(const std::shared_ptr &terrain) : terrain{terrain}, - pathfinder{std::make_shared()} { + pathfinder{std::make_shared()}, + grid_lookup{} { // Get grid types // TODO: This should probably not be dependent on the existing tiles, but // on all defined nyan PathType objects - std::unordered_map path_types; size_t grid_idx = 0; auto chunk_size = this->terrain->get_chunk(0)->get_size(); auto side_length = std::max(chunk_size[0], chunk_size[0]); @@ -31,11 +31,11 @@ Map::Map(const std::shared_ptr &terrain) : auto path_costs = api::APITerrain::get_path_costs(*tile.terrain); for (const auto &path_cost : path_costs) { - if (not path_types.contains(path_cost.first)) { + if (not this->grid_lookup.contains(path_cost.first)) { auto grid = std::make_shared(grid_idx, grid_size, side_length); this->pathfinder->add_grid(grid); - path_types.emplace(path_cost.first, grid_idx); + this->grid_lookup.emplace(path_cost.first, grid_idx); grid_idx += 1; } } @@ -52,7 +52,7 @@ Map::Map(const std::shared_ptr &terrain) : auto path_costs = api::APITerrain::get_path_costs(*tile.terrain); for (const auto &path_cost : path_costs) { - auto grid_id = path_types.at(path_cost.first); + auto grid_id = this->grid_lookup.at(path_cost.first); auto grid = this->pathfinder->get_grid(grid_id); auto sector = grid->get_sector(chunk_idx); @@ -64,7 +64,7 @@ Map::Map(const std::shared_ptr &terrain) : } // Connect sectors with portals - for (const auto &path_type : path_types) { + for (const auto &path_type : this->grid_lookup) { auto grid = this->pathfinder->get_grid(path_type.second); grid->init_portals(); } @@ -82,4 +82,8 @@ const std::shared_ptr &Map::get_pathfinder() const { return this->pathfinder; } +path::grid_id_t Map::get_grid_id(const nyan::Object &path_grid) const { + return this->grid_lookup.at(path_grid.get_name()); +} + } // namespace openage::gamestate diff --git a/libopenage/gamestate/map.h b/libopenage/gamestate/map.h index b8037c13d5..d6ac1188c1 100644 --- a/libopenage/gamestate/map.h +++ b/libopenage/gamestate/map.h @@ -3,7 +3,11 @@ #pragma once #include +#include +#include + +#include "pathfinding/types.h" #include "util/vector.h" @@ -49,6 +53,15 @@ class Map { */ const std::shared_ptr &get_pathfinder() const; + /** + * Get the grid ID associated with a nyan path grid object. + * + * @param path_grid Path grid object. + * + * @return Grid ID. + */ + path::grid_id_t get_grid_id(const nyan::Object &path_grid) const; + private: /** * Terrain. @@ -59,6 +72,11 @@ class Map { * Pathfinder. */ std::shared_ptr pathfinder; + + /** + * Lookup table for mapping path grid objects in nyan to grid indices. + */ + std::unordered_map grid_lookup; }; } // namespace gamestate From 672efb1a4a4ea1b4370cf9dd1ad08f147891f1d4 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 20 May 2024 17:51:33 +0200 Subject: [PATCH 505/771] gamestate: Use pathfinder in Move system. --- libopenage/gamestate/map.cpp | 4 +- libopenage/gamestate/map.h | 4 +- libopenage/gamestate/system/activity.cpp | 14 +-- libopenage/gamestate/system/activity.h | 8 +- libopenage/gamestate/system/move.cpp | 121 +++++++++++++++++------ libopenage/gamestate/system/move.h | 5 + 6 files changed, 113 insertions(+), 43 deletions(-) diff --git a/libopenage/gamestate/map.cpp b/libopenage/gamestate/map.cpp index dbe460ca36..beb02b5e8a 100644 --- a/libopenage/gamestate/map.cpp +++ b/libopenage/gamestate/map.cpp @@ -82,8 +82,8 @@ const std::shared_ptr &Map::get_pathfinder() const { return this->pathfinder; } -path::grid_id_t Map::get_grid_id(const nyan::Object &path_grid) const { - return this->grid_lookup.at(path_grid.get_name()); +path::grid_id_t Map::get_grid_id(const nyan::fqon_t &path_grid) const { + return this->grid_lookup.at(path_grid); } } // namespace openage::gamestate diff --git a/libopenage/gamestate/map.h b/libopenage/gamestate/map.h index d6ac1188c1..4033373023 100644 --- a/libopenage/gamestate/map.h +++ b/libopenage/gamestate/map.h @@ -56,11 +56,11 @@ class Map { /** * Get the grid ID associated with a nyan path grid object. * - * @param path_grid Path grid object. + * @param path_grid Path grid object fqon. * * @return Grid ID. */ - path::grid_id_t get_grid_id(const nyan::Object &path_grid) const; + path::grid_id_t get_grid_id(const nyan::fqon_t &path_grid) const; private: /** diff --git a/libopenage/gamestate/system/activity.cpp b/libopenage/gamestate/system/activity.cpp index 49371fba30..7ef1d574b4 100644 --- a/libopenage/gamestate/system/activity.cpp +++ b/libopenage/gamestate/system/activity.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 "activity.h" @@ -79,7 +79,7 @@ void Activity::advance(const time::time_t &start_time, case activity::node_t::TASK_SYSTEM: { auto node = std::static_pointer_cast(current_node); auto task = node->get_system_id(); - event_wait_time = Activity::handle_subsystem(entity, start_time, task); + event_wait_time = Activity::handle_subsystem(start_time, entity, state, task); auto next_id = node->get_next(); current_node = node->next(next_id); } break; @@ -120,18 +120,20 @@ void Activity::advance(const time::time_t &start_time, activity_component->set_node(start_time, current_node); } -const time::time_t Activity::handle_subsystem(const std::shared_ptr &entity, - const time::time_t &start_time, +const time::time_t Activity::handle_subsystem(const time::time_t &start_time, + const std::shared_ptr &entity, + const std::shared_ptr &state, system_id_t system_id) { switch (system_id) { case system_id_t::IDLE: return Idle::idle(entity, start_time); break; case system_id_t::MOVE_COMMAND: - return Move::move_command(entity, start_time); + return Move::move_command(entity, state, start_time); break; case system_id_t::MOVE_DEFAULT: - return Move::move_default(entity, {1, 1, 1}, start_time); + // TODO: replace destination value with a parameter + return Move::move_default(entity, state, {1, 1, 1}, 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 a5c38e6f49..11dcce68ca 100644 --- a/libopenage/gamestate/system/activity.h +++ b/libopenage/gamestate/system/activity.h @@ -40,14 +40,16 @@ class Activity { /** * Run a built-in engine subsystem. * - * @param entity Game entity. * @param start_time Start time of change. + * @param entity Game entity. + * @param state Game state. * @param system_id ID of the subsystem to run. * * @return Runtime of the change in simulation time. */ - static const time::time_t handle_subsystem(const std::shared_ptr &entity, - const time::time_t &start_time, + static const time::time_t handle_subsystem(const time::time_t &start_time, + const std::shared_ptr &entity, + const std::shared_ptr &state, system_id_t system_id); }; diff --git a/libopenage/gamestate/system/move.cpp b/libopenage/gamestate/system/move.cpp index 3ebd02085b..9607658ca2 100644 --- a/libopenage/gamestate/system/move.cpp +++ b/libopenage/gamestate/system/move.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 "move.h" @@ -24,11 +24,52 @@ #include "gamestate/component/internal/position.h" #include "gamestate/component/types.h" #include "gamestate/game_entity.h" +#include "gamestate/game_state.h" +#include "gamestate/map.h" +#include "pathfinding/path.h" +#include "pathfinding/pathfinder.h" #include "util/fixed_point.h" namespace openage::gamestate::system { + + +std::vector find_path(const std::shared_ptr &pathfinder, + path::grid_id_t grid_id, + const coord::phys3 &start, + const coord::phys3 &end) { + auto start_tile = start.to_tile(); + auto end_tile = end.to_tile(); + + // Search for a path between the start and end tiles + path::PathRequest request{ + grid_id, + start_tile, + end_tile, + }; + auto tile_path = pathfinder->get_path(request); + + // Get the waypoints of the path + std::vector path; + path.reserve(tile_path.waypoints.size()); + + // Start poition is first waypoint + path.push_back(start); + + // Pathfinder waypoints contain start and end tile; we can ignore them + for (size_t i = 1; i < tile_path.waypoints.size() - 1; ++i) { + auto tile = tile_path.waypoints.at(i); + path.push_back(tile.to_phys3()); + } + + // End position is last waypoint + path.push_back(end); + + return path; +} + const time::time_t Move::move_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)); @@ -40,11 +81,12 @@ const time::time_t Move::move_command(const std::shared_ptrget_target(), start_time); + return Move::move_default(entity, state, command->get_target(), start_time); } const time::time_t Move::move_default(const std::shared_ptr &entity, + const std::shared_ptr &state, const coord::phys3 &destination, const time::time_t &start_time) { if (not entity->has_component(component::component_t::MOVE)) [[unlikely]] { @@ -61,6 +103,7 @@ const time::time_t Move::move_default(const std::shared_ptrget_component(component::component_t::MOVE)); auto move_ability = move_component->get_ability(); auto move_speed = move_ability.get("Move.speed"); + auto move_path_grid = move_ability.get("Move.path_type"); auto pos_component = std::dynamic_pointer_cast( entity->get_component(component::component_t::POSITION)); @@ -70,38 +113,56 @@ const time::time_t Move::move_default(const std::shared_ptris_infinite_positive()) { - auto angle_diff = new_angle - current_angle; - if (angle_diff < 0) { - // get the positive difference - angle_diff = angle_diff * -1; - } - if (angle_diff > 180) { - // always use the smaller angle - angle_diff = angle_diff - 360; - angle_diff = angle_diff * -1; + // Find path + auto map = state->get_map(); + auto pathfinder = map->get_pathfinder(); + auto grid_id = map->get_grid_id(move_path_grid->get_name()); + auto waypoints = find_path(pathfinder, grid_id, current_pos, destination); + + // use waypoints for movement + double total_time = 0; + pos_component->set_position(start_time, current_pos); + auto prev_angle = current_angle; + for (size_t i = 1; i < waypoints.size(); ++i) { + auto prev_waypoint = waypoints[i - 1]; + auto cur_waypoint = waypoints[i]; + + auto path_vector = cur_waypoint - prev_waypoint; + auto path_angle = path_vector.to_angle(); + + // rotation + if (not turn_speed->is_infinite_positive()) { + auto angle_diff = path_angle - current_angle; + if (angle_diff < 0) { + // get the positive difference + angle_diff = angle_diff * -1; + } + if (angle_diff > 180) { + // always use the smaller angle + angle_diff = angle_diff - 360; + angle_diff = angle_diff * -1; + } + + // Set an intermediate position keyframe to halt the game entity + // until the rotation is done + double turn_time = angle_diff.to_double() / turn_speed->get(); + total_time += turn_time; + pos_component->set_position(start_time + total_time, prev_waypoint); } + pos_component->set_angle(start_time + total_time, path_angle); - turn_time = angle_diff.to_double() / turn_speed->get(); - } - pos_component->set_angle(start_time + turn_time, new_angle); + // movement + double move_time = 0; + if (not move_speed->is_infinite_positive()) { + auto distance = path_vector.length(); + move_time = distance / move_speed->get(); + } + total_time += move_time; - // movement - double move_time = 0; - if (not move_speed->is_infinite_positive()) { - auto distance = path.length(); - move_time = distance / move_speed->get(); + pos_component->set_position(start_time + total_time, cur_waypoint); } - pos_component->set_position(start_time, current_pos); - pos_component->set_position(start_time + turn_time + move_time, destination); - + // properties auto ability = move_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); @@ -113,7 +174,7 @@ const time::time_t Move::move_default(const std::shared_ptr &entity, + const std::shared_ptr &state, const time::time_t &start_time); /** * Move a game entity to a destination. * * @param entity Game entity. + * @param state Game state. * @param destination Destination coordinates. * @param start_time Start time of change. * * @return Runtime of the change in simulation time. */ static const time::time_t move_default(const std::shared_ptr &entity, + const std::shared_ptr &state, const coord::phys3 &destination, const time::time_t &start_time); }; From a2ed206989f14e88d9c204a2350daf70dfc7a26d Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 20 May 2024 22:59:56 +0200 Subject: [PATCH 506/771] path: Block diagonal flow direction if adjactent horizontal/vertical cells are blocked. --- libopenage/pathfinding/flow_field.cpp | 70 +++++++++++++++++---------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 0aa8daf84b..d8674a87f4 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -2,6 +2,8 @@ #include "flow_field.h" +#include + #include "error/error.h" #include "log/log.h" @@ -76,61 +78,77 @@ void FlowField::build(const std::shared_ptr &integration_field this->cells[idx] = this->cells[idx] | FLOW_WAVEFRONT_BLOCKED_MASK; } + // Store which of the non-diagonal directions are unreachable. + std::bitset<4> directions_unreachable; + // Find the neighbor with the smallest cost. flow_dir_t direction = static_cast(this->cells[idx] & FLOW_DIR_MASK); auto smallest_cost = INTEGRATED_COST_UNREACHABLE; if (y > 0) { auto cost = integrate_cells[idx - this->size].cost; - if (cost < smallest_cost) { - smallest_cost = cost; - direction = flow_dir_t::NORTH; + if (cost == INTEGRATED_COST_UNREACHABLE) { + directions_unreachable[0] = true; } - } - if (x < this->size - 1 && y > 0) { - auto cost = integrate_cells[idx - this->size + 1].cost; - if (cost < smallest_cost) { + else if (cost < smallest_cost) { smallest_cost = cost; - direction = flow_dir_t::NORTH_EAST; + direction = flow_dir_t::NORTH; } } if (x < this->size - 1) { auto cost = integrate_cells[idx + 1].cost; - if (cost < smallest_cost) { - smallest_cost = cost; - direction = flow_dir_t::EAST; + if (cost == INTEGRATED_COST_UNREACHABLE) { + directions_unreachable[1] = true; } - } - if (x < this->size - 1 && y < this->size - 1) { - auto cost = integrate_cells[idx + this->size + 1].cost; - if (cost < smallest_cost) { + else if (cost < smallest_cost) { smallest_cost = cost; - direction = flow_dir_t::SOUTH_EAST; + direction = flow_dir_t::EAST; } } if (y < this->size - 1) { auto cost = integrate_cells[idx + this->size].cost; - if (cost < smallest_cost) { - smallest_cost = cost; - direction = flow_dir_t::SOUTH; + if (cost == INTEGRATED_COST_UNREACHABLE) { + directions_unreachable[2] = true; } - } - if (x > 0 && y < this->size - 1) { - auto cost = integrate_cells[idx + this->size - 1].cost; - if (cost < smallest_cost) { + else if (cost < smallest_cost) { smallest_cost = cost; - direction = flow_dir_t::SOUTH_WEST; + direction = flow_dir_t::SOUTH; } } if (x > 0) { auto cost = integrate_cells[idx - 1].cost; - if (cost < smallest_cost) { + if (cost == INTEGRATED_COST_UNREACHABLE) { + directions_unreachable[3] = true; + } + else if (cost < smallest_cost) { smallest_cost = cost; direction = flow_dir_t::WEST; } } + + if (x < this->size - 1 && y > 0) { + auto cost = integrate_cells[idx - this->size + 1].cost; + if (cost < smallest_cost and not(directions_unreachable[0] and directions_unreachable[1])) { + smallest_cost = cost; + direction = flow_dir_t::NORTH_EAST; + } + } + if (x < this->size - 1 && y < this->size - 1) { + auto cost = integrate_cells[idx + this->size + 1].cost; + if (cost < smallest_cost and not(directions_unreachable[1] and directions_unreachable[2])) { + smallest_cost = cost; + direction = flow_dir_t::SOUTH_EAST; + } + } + if (x > 0 && y < this->size - 1) { + auto cost = integrate_cells[idx + this->size - 1].cost; + if (cost < smallest_cost and not(directions_unreachable[2] and directions_unreachable[3])) { + smallest_cost = cost; + direction = flow_dir_t::SOUTH_WEST; + } + } if (x > 0 && y > 0) { auto cost = integrate_cells[idx - this->size - 1].cost; - if (cost < smallest_cost) { + if (cost < smallest_cost and not(directions_unreachable[3] and directions_unreachable[0])) { smallest_cost = cost; direction = flow_dir_t::NORTH_WEST; } From 68443c66ac52b6ad6b5be800b7e3c15b92948c37 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 21 May 2024 00:46:12 +0200 Subject: [PATCH 507/771] path: Only use reachable start/target portal nodes for high-level path. --- libopenage/pathfinding/flow_field.cpp | 20 +++--- libopenage/pathfinding/pathfinder.cpp | 98 +++++++++++++++++++++------ libopenage/pathfinding/pathfinder.h | 7 +- 3 files changed, 94 insertions(+), 31 deletions(-) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index d8674a87f4..7198ae5fbe 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -125,30 +125,34 @@ void FlowField::build(const std::shared_ptr &integration_field } } - if (x < this->size - 1 && y > 0) { + if (x < this->size - 1 and y > 0 + and not(directions_unreachable[0] and directions_unreachable[1])) { auto cost = integrate_cells[idx - this->size + 1].cost; - if (cost < smallest_cost and not(directions_unreachable[0] and directions_unreachable[1])) { + if (cost < smallest_cost) { smallest_cost = cost; direction = flow_dir_t::NORTH_EAST; } } - if (x < this->size - 1 && y < this->size - 1) { + if (x < this->size - 1 and y < this->size - 1 + and not(directions_unreachable[1] and directions_unreachable[2])) { auto cost = integrate_cells[idx + this->size + 1].cost; - if (cost < smallest_cost and not(directions_unreachable[1] and directions_unreachable[2])) { + if (cost < smallest_cost) { smallest_cost = cost; direction = flow_dir_t::SOUTH_EAST; } } - if (x > 0 && y < this->size - 1) { + if (x > 0 and y < this->size - 1 + and not(directions_unreachable[2] and directions_unreachable[3])) { auto cost = integrate_cells[idx + this->size - 1].cost; - if (cost < smallest_cost and not(directions_unreachable[2] and directions_unreachable[3])) { + if (cost < smallest_cost) { smallest_cost = cost; direction = flow_dir_t::SOUTH_WEST; } } - if (x > 0 && y > 0) { + if (x > 0 and y > 0 + and not(directions_unreachable[3] and directions_unreachable[0])) { auto cost = integrate_cells[idx - this->size - 1].cost; - if (cost < smallest_cost and not(directions_unreachable[3] and directions_unreachable[0])) { + if (cost < smallest_cost) { smallest_cost = cost; direction = flow_dir_t::NORTH_WEST; } diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 7e09278f7c..ca830ca075 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -5,6 +5,7 @@ #include "coord/phys.h" #include "pathfinding/flow_field.h" #include "pathfinding/grid.h" +#include "pathfinding/integration_field.h" #include "pathfinding/integrator.h" #include "pathfinding/portal.h" #include "pathfinding/sector.h" @@ -18,31 +19,84 @@ Pathfinder::Pathfinder() : } const Path Pathfinder::get_path(const PathRequest &request) { - // High-level pathfinding - // Find the portals to use to get from the start to the target - auto portal_path = this->portal_a_star(request); - - // Low-level pathfinding - // Find the path within the sectors auto grid = this->grids.at(request.grid_id); auto sector_size = grid->get_sector_size(); + auto start_sector_x = request.start.ne / sector_size; + auto start_sector_y = request.start.se / sector_size; + auto start_sector = grid->get_sector(start_sector_x, start_sector_y); + auto target_sector_x = request.target.ne / sector_size; auto target_sector_y = request.target.se / sector_size; auto target_sector = grid->get_sector(target_sector_x, target_sector_y); + // Integrate the target field coord::tile_t target_x = request.target.ne % sector_size; coord::tile_t target_y = request.target.se % sector_size; auto target = coord::tile{target_x, target_y}; + auto target_integration_field = this->integrator->integrate(target_sector->get_cost_field(), target); + + if (target_sector == start_sector) { + auto start_x = request.start.ne % sector_size; + auto start_y = request.start.se % sector_size; + + if (target_integration_field->get_cell(coord::tile{start_x, start_y}).cost != INTEGRATED_COST_UNREACHABLE) { + // Exit early if the start and target are in the same sector + // and are reachable from within the same sector + auto flow_field = this->integrator->build(target_integration_field); + auto flow_field_waypoints = this->get_waypoints({std::make_pair(target_sector->get_id(), flow_field)}, request); + + std::vector waypoints{request.start}; + waypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end()); + return Path{request.grid_id, waypoints}; + } + } + + // Check which portals are reachable from the target field + std::unordered_set target_portal_ids; + for (auto &portal : target_sector->get_portals()) { + auto center_cell = portal->get_entry_center(target_sector->get_id()); + + if (target_integration_field->get_cell(center_cell).cost != INTEGRATED_COST_UNREACHABLE) { + target_portal_ids.insert(portal->get_id()); + } + } + + // Check which portals are reachable from the start field + coord::tile_t start_x = request.start.ne % sector_size; + coord::tile_t start_y = request.start.se % sector_size; + auto start = coord::tile{start_x, start_y}; + auto start_integration_field = this->integrator->integrate(start_sector->get_cost_field(), start); + + std::unordered_set start_portal_ids; + for (auto &portal : start_sector->get_portals()) { + auto center_cell = portal->get_entry_center(start_sector->get_id()); + + if (start_integration_field->get_cell(center_cell).cost != INTEGRATED_COST_UNREACHABLE) { + start_portal_ids.insert(portal->get_id()); + } + } + + // ASDF + + // High-level pathfinding + // Find the portals to use to get from the start to the target + auto portal_path = this->portal_a_star(request, target_portal_ids, start_portal_ids); + + // Low-level pathfinding + // Find the path within the sectors - auto sector_fields = this->integrator->build(target_sector->get_cost_field(), target); - auto prev_integration_field = sector_fields.first; + // Build flow field for the target sector + auto prev_integration_field = target_integration_field; + auto prev_flow_field = this->integrator->build(prev_integration_field); auto prev_sector_id = target_sector->get_id(); + Integrator::build_return_t sector_fields{prev_integration_field, prev_flow_field}; + std::vector>> flow_fields; flow_fields.reserve(portal_path.size() + 1); - flow_fields.push_back(std::make_pair(target_sector->get_id(), sector_fields.second)); + for (auto &portal : portal_path) { auto prev_sector = grid->get_sector(prev_sector_id); auto next_sector_id = portal->get_exit_sector(prev_sector_id); @@ -77,7 +131,9 @@ void Pathfinder::add_grid(const std::shared_ptr &grid) { this->grids[grid->get_id()] = grid; } -const std::vector> Pathfinder::portal_a_star(const PathRequest &request) const { +const std::vector> Pathfinder::portal_a_star(const PathRequest &request, + const std::unordered_set &target_portal_ids, + const std::unordered_set &start_portal_ids) const { std::vector> result; auto grid = this->grids.at(request.grid_id); @@ -87,15 +143,6 @@ const std::vector> Pathfinder::portal_a_star(const PathR auto start_sector_y = request.start.se / sector_size; auto start_sector = grid->get_sector(start_sector_x, start_sector_y); - auto target_sector_x = request.target.ne / sector_size; - auto target_sector_y = request.target.se / sector_size; - auto target_sector = grid->get_sector(target_sector_x, target_sector_y); - - if (start_sector == target_sector) { - // exit early if the start and target are in the same sector - return result; - } - // path node storage, always provides cheapest next node. heap_t node_candidates; @@ -106,8 +153,13 @@ const std::vector> Pathfinder::portal_a_star(const PathR // TODO: Determine this cost for each portal // const int distance_cost = 1; - // start nodes: all portals in the start sector + // create start nodes for (auto &portal : start_sector->get_portals()) { + if (not start_portal_ids.contains(portal->get_id())) { + // only consider portals that are reachable from the start cell + continue; + } + auto portal_node = std::make_shared(portal, start_sector->get_id(), nullptr); auto sector_pos = grid->get_sector(portal->get_exit_sector(start_sector->get_id()))->get_position(); @@ -133,8 +185,10 @@ const std::vector> Pathfinder::portal_a_star(const PathR current_node->was_best = true; - // check if the current node is the target - if (current_node->portal->get_exit_sector(current_node->entry_sector) == target_sector->get_id()) { + // check if the current node is a portal in the target sector that can + // be reached from the target cell + auto exit_portal_id = current_node->portal->get_id(); + if (target_portal_ids.contains(exit_portal_id)) { auto backtrace = current_node->generate_backtrace(); for (auto &node : backtrace) { result.push_back(node->portal); diff --git a/libopenage/pathfinding/pathfinder.h b/libopenage/pathfinding/pathfinder.h index a4df0350d8..0d4d3dfa7c 100644 --- a/libopenage/pathfinding/pathfinder.h +++ b/libopenage/pathfinding/pathfinder.h @@ -4,6 +4,7 @@ #include #include +#include #include "coord/tile.h" #include "datastructure/pairing_heap.h" @@ -66,10 +67,14 @@ class Pathfinder { * High-level pathfinder. Uses A* to find the path through the portals of sectors. * * @param request Pathfinding request. + * @param target_portal_ids IDs of portals that can be reached from the target cell. + * @param start_portal_ids IDs of portals that can be reached from the start cell. * * @return Portals to traverse in order to reach the target. */ - const std::vector> portal_a_star(const PathRequest &request) const; + const std::vector> portal_a_star(const PathRequest &request, + const std::unordered_set &target_portal_ids, + const std::unordered_set &start_portal_ids) const; /** * Low-level pathfinder. Uses flow fields to find the path through the sectors. From 596aecb831c4250718ce695525af0b21f1efb581 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 21 May 2024 01:56:31 +0200 Subject: [PATCH 508/771] path: Store whether a path has been found. --- libopenage/gamestate/system/move.cpp | 5 ++++ libopenage/pathfinding/path.h | 17 +++++++----- libopenage/pathfinding/pathfinder.cpp | 37 ++++++++++++++++++++------- libopenage/pathfinding/pathfinder.h | 8 +++--- libopenage/pathfinding/types.h | 10 ++++++++ 5 files changed, 58 insertions(+), 19 deletions(-) diff --git a/libopenage/gamestate/system/move.cpp b/libopenage/gamestate/system/move.cpp index 9607658ca2..158beea6ac 100644 --- a/libopenage/gamestate/system/move.cpp +++ b/libopenage/gamestate/system/move.cpp @@ -49,6 +49,11 @@ std::vector find_path(const std::shared_ptr &pat }; auto tile_path = pathfinder->get_path(request); + if (tile_path.status != path::PathResult::FOUND) { + // No path found + return {}; + } + // Get the waypoints of the path std::vector path; path.reserve(tile_path.waypoints.size()); diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/path.h index 8d64f5914f..d1c61ac325 100644 --- a/libopenage/pathfinding/path.h +++ b/libopenage/pathfinding/path.h @@ -5,6 +5,7 @@ #include #include "coord/tile.h" +#include "pathfinding/types.h" namespace openage::path { @@ -13,11 +14,11 @@ namespace openage::path { * Path request for the pathfinder. */ struct PathRequest { - // ID of the grid to use for pathfinding. + /// ID of the grid to use for pathfinding. size_t grid_id; - // Start position of the path. + /// Start position of the path. coord::tile start; - // Target position of the path. + /// Target position of the path. coord::tile target; }; @@ -25,11 +26,13 @@ struct PathRequest { * Path found by the pathfinder. */ struct Path { - // ID of the grid to used for pathfinding. + /// ID of the grid to used for pathfinding. size_t grid_id; - // Waypoints of the path. - // First waypoint is the start position of the path request. - // Last waypoint is the target position of the path request. + /// Status + PathResult status; + /// Waypoints of the path. + /// First waypoint is the start position of the path request. + /// Last waypoint is the target position of the path request. std::vector waypoints; }; diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index ca830ca075..e16528afcf 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -48,7 +48,10 @@ const Path Pathfinder::get_path(const PathRequest &request) { std::vector waypoints{request.start}; waypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end()); - return Path{request.grid_id, waypoints}; + + log::log(INFO << "Path found (start = " << request.start << "; target = " << request.target << ")"); + log::log(DBG << "Path is within the same sector."); + return Path{request.grid_id, PathResult::FOUND, waypoints}; } } @@ -77,11 +80,18 @@ const Path Pathfinder::get_path(const PathRequest &request) { } } - // ASDF + if (target_portal_ids.empty() or start_portal_ids.empty()) { + // Exit early if no portals are reachable from the start or target + log::log(INFO << "Path not found (start = " << request.start << "; target = " << request.target << ")"); + log::log(DBG << "No portals are reachable from the start or target."); + return Path{request.grid_id, PathResult::NOT_FOUND, {}}; + } // High-level pathfinding // Find the portals to use to get from the start to the target - auto portal_path = this->portal_a_star(request, target_portal_ids, start_portal_ids); + auto portal_result = this->portal_a_star(request, target_portal_ids, start_portal_ids); + auto portal_status = portal_result.first; + auto portal_path = portal_result.second; // Low-level pathfinding // Find the path within the sectors @@ -120,7 +130,13 @@ const Path Pathfinder::get_path(const PathRequest &request) { auto flow_field_waypoints = this->get_waypoints(flow_fields, request); waypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end()); - return Path{request.grid_id, waypoints}; + if (portal_status == PathResult::NOT_FOUND) { + log::log(INFO << "Path not found (start = " << request.start << "; target = " << request.target << ")"); + } + else { + log::log(INFO << "Path found (start = " << request.start << "; target = " << request.target << ")"); + } + return Path{request.grid_id, portal_status, waypoints}; } const std::shared_ptr &Pathfinder::get_grid(grid_id_t id) const { @@ -131,9 +147,9 @@ void Pathfinder::add_grid(const std::shared_ptr &grid) { this->grids[grid->get_id()] = grid; } -const std::vector> Pathfinder::portal_a_star(const PathRequest &request, - const std::unordered_set &target_portal_ids, - const std::unordered_set &start_portal_ids) const { +const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &request, + const std::unordered_set &target_portal_ids, + const std::unordered_set &start_portal_ids) const { std::vector> result; auto grid = this->grids.at(request.grid_id); @@ -193,7 +209,8 @@ const std::vector> Pathfinder::portal_a_star(const PathR for (auto &node : backtrace) { result.push_back(node->portal); } - return result; + log::log(INFO << "Portal path found with " << result.size() << " portal traversals."); + return std::make_pair(PathResult::FOUND, result); } // check if the current node is the closest to the target @@ -248,7 +265,9 @@ const std::vector> Pathfinder::portal_a_star(const PathR result.push_back(node->portal); } - return result; + log::log(INFO << "Portal path not found."); + log::log(DBG << "Closest portal: " << closest_node->portal->get_id()); + return std::make_pair(PathResult::NOT_FOUND, result); } const std::vector Pathfinder::get_waypoints(const std::vector>> &flow_fields, diff --git a/libopenage/pathfinding/pathfinder.h b/libopenage/pathfinding/pathfinder.h index 0d4d3dfa7c..706e222318 100644 --- a/libopenage/pathfinding/pathfinder.h +++ b/libopenage/pathfinding/pathfinder.h @@ -63,6 +63,8 @@ class Pathfinder { const Path get_path(const PathRequest &request); private: + using portal_star_t = std::pair>>; + /** * High-level pathfinder. Uses A* to find the path through the portals of sectors. * @@ -72,9 +74,9 @@ class Pathfinder { * * @return Portals to traverse in order to reach the target. */ - const std::vector> portal_a_star(const PathRequest &request, - const std::unordered_set &target_portal_ids, - const std::unordered_set &start_portal_ids) const; + const portal_star_t portal_a_star(const PathRequest &request, + const std::unordered_set &target_portal_ids, + const std::unordered_set &start_portal_ids) const; /** * Low-level pathfinder. Uses flow fields to find the path through the sectors. diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index 11190f6551..b90e932323 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -8,6 +8,16 @@ namespace openage::path { +/** + * Path result type. + */ +enum class PathResult { + /// Path was found. + FOUND, + /// Path was not found. + NOT_FOUND, +}; + /** * Movement cost in the cost field. * From 564497e9e28f4067f4129955dec549e16df2a67c Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 21 May 2024 02:12:49 +0200 Subject: [PATCH 509/771] path: Prevent duplicate waypoints at beginning of path. --- libopenage/pathfinding/pathfinder.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index e16528afcf..48eeb7d83e 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -46,7 +46,10 @@ const Path Pathfinder::get_path(const PathRequest &request) { auto flow_field = this->integrator->build(target_integration_field); auto flow_field_waypoints = this->get_waypoints({std::make_pair(target_sector->get_id(), flow_field)}, request); - std::vector waypoints{request.start}; + std::vector waypoints{}; + if (flow_field_waypoints.at(0) != request.start) { + waypoints.push_back(request.start); + } waypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end()); log::log(INFO << "Path found (start = " << request.start << "; target = " << request.target << ")"); @@ -126,8 +129,11 @@ const Path Pathfinder::get_path(const PathRequest &request) { std::reverse(flow_fields.begin(), flow_fields.end()); // traverse the flow fields to get the waypoints - std::vector waypoints{request.start}; auto flow_field_waypoints = this->get_waypoints(flow_fields, request); + std::vector waypoints{}; + if (flow_field_waypoints.at(0) != request.start) { + waypoints.push_back(request.start); + } waypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end()); if (portal_status == PathResult::NOT_FOUND) { From f239a6f9950b17db6630757ba6eb420a02329316 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 21 May 2024 22:32:23 +0200 Subject: [PATCH 510/771] path: LOS integration with portal. --- libopenage/pathfinding/integration_field.cpp | 37 ++++++++++++++++++++ libopenage/pathfinding/integration_field.h | 17 +++++++++ 2 files changed, 54 insertions(+) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index bc2745879f..ff10e0e507 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -33,6 +33,43 @@ const integrated_t &IntegrationField::get_cell(size_t idx) const { return this->cells.at(idx); } +std::vector IntegrationField::integrate_los(const std::shared_ptr &cost_field, + sector_id_t other_sector_id, + const std::shared_ptr &portal) { + ENSURE(cost_field->get_size() == this->get_size(), + "cost field size " + << cost_field->get_size() << "x" << cost_field->get_size() + << " does not match integration field size " + << this->get_size() << "x" << this->get_size()); + + // Integrate the cost of the cells on the exit side (this) of the portal + std::vector start_cells; + auto exit_start = portal->get_exit_start(other_sector_id); + auto exit_end = portal->get_exit_end(other_sector_id); + auto entry_start = portal->get_entry_start(other_sector_id); + auto entry_end = portal->get_entry_end(other_sector_id); + + auto x_diff = exit_start.ne - entry_start.ne; + auto y_diff = exit_start.se - entry_start.se; + + for (auto y = exit_start.se; y <= exit_end.se; ++y) { + for (auto x = exit_start.ne; x <= exit_end.ne; ++x) { + // every portal cell is a target cell + auto target_idx = x + y * this->size; + + auto source_idx = x - x_diff + (y - y_diff) * this->size; + + // Set the cost of all target cells to the start value + this->cells[target_idx].cost = INTEGRATED_COST_START; + this->cells[target_idx].flags = this->cells[source_idx].flags; + + start_cells.push_back(target_idx); + } + } + + // TODO: Call main LOS integration function +} + std::vector IntegrationField::integrate_los(const std::shared_ptr &cost_field, const coord::tile &target) { ENSURE(cost_field->get_size() == this->get_size(), diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 183fb9cff6..c7c2bf5283 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -69,6 +69,23 @@ class IntegrationField { std::vector integrate_los(const std::shared_ptr &cost_field, const coord::tile &target); + /** + * Calculate the line-of-sight integration flags starting from a portal to another + * integration field. + * + * Returns a list of cells that are flagged as "wavefront blocked". These cells + * can be used as a starting point for the cost integration. + * + * @param cost_field Cost field to integrate. + * @param other_sector_id Sector ID of the other integration field. + * @param portal Portal connecting the two fields. + * + * @return Cells flagged as "wavefront blocked". + */ + std::vector integrate_los(const std::shared_ptr &cost_field, + sector_id_t other_sector_id, + const std::shared_ptr &portal); + /** * Calculate the cost integration field starting from a target cell. * From 0763deb77f279e3b1cdab428f26920f35a635273 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 21 May 2024 23:21:21 +0200 Subject: [PATCH 511/771] path: Change all relative coordinates to delta types. Prevents accidental confusion between relative and absolute coordinates on grid. --- libopenage/coord/chunk.cpp | 16 +++++- libopenage/coord/chunk.h | 4 ++ libopenage/pathfinding/cost_field.cpp | 4 +- libopenage/pathfinding/cost_field.h | 10 ++-- libopenage/pathfinding/demo/demo_0.cpp | 49 ++++++++-------- libopenage/pathfinding/demo/demo_1.cpp | 2 +- libopenage/pathfinding/flow_field.cpp | 4 +- libopenage/pathfinding/flow_field.h | 10 ++-- libopenage/pathfinding/integration_field.cpp | 22 +++---- libopenage/pathfinding/integration_field.h | 24 ++++---- libopenage/pathfinding/integrator.cpp | 4 +- libopenage/pathfinding/integrator.h | 6 +- libopenage/pathfinding/pathfinder.cpp | 30 +++++----- libopenage/pathfinding/pathfinder.h | 11 ++-- libopenage/pathfinding/portal.cpp | 24 ++++---- libopenage/pathfinding/portal.h | 60 +++++++++++--------- libopenage/pathfinding/sector.cpp | 24 ++++---- libopenage/pathfinding/sector.h | 4 +- libopenage/pathfinding/tests.cpp | 4 +- 19 files changed, 169 insertions(+), 143 deletions(-) diff --git a/libopenage/coord/chunk.cpp b/libopenage/coord/chunk.cpp index fd061586b7..19cb606295 100644 --- a/libopenage/coord/chunk.cpp +++ b/libopenage/coord/chunk.cpp @@ -1,8 +1,20 @@ -// Copyright 2016-2018 the openage authors. See copying.md for legal info. +// Copyright 2016-2024 the openage authors. See copying.md for legal info. #include "chunk.h" +#include "coord/tile.h" + + namespace openage { namespace coord { -}} // namespace openage::coord +tile_delta chunk_delta::to_tile(size_t tiles_per_chunk) const { + return tile_delta{this->ne * tiles_per_chunk, this->se * tiles_per_chunk}; +} + +tile chunk::to_tile(size_t tiles_per_chunk) const { + return tile{this->ne * tiles_per_chunk, this->se * tiles_per_chunk}; +} + +} // namespace coord +} // namespace openage diff --git a/libopenage/coord/chunk.h b/libopenage/coord/chunk.h index a894cc56c1..0b5a1d50c0 100644 --- a/libopenage/coord/chunk.h +++ b/libopenage/coord/chunk.h @@ -12,10 +12,14 @@ namespace coord { struct chunk_delta : CoordNeSeRelative { using CoordNeSeRelative::CoordNeSeRelative; + + tile_delta to_tile(size_t tiles_per_chunk) const; }; struct chunk : CoordNeSeAbsolute { using CoordNeSeAbsolute::CoordNeSeAbsolute; + + tile to_tile(size_t tiles_per_chunk) const; }; struct chunk3_delta : CoordNeSeUpRelative { diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index 248a260d8e..e85fecc3ed 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -21,7 +21,7 @@ size_t CostField::get_size() const { return this->size; } -cost_t CostField::get_cost(const coord::tile &pos) const { +cost_t CostField::get_cost(const coord::tile_delta &pos) const { return this->cells.at(pos.ne + pos.se * this->size); } @@ -29,7 +29,7 @@ cost_t CostField::get_cost(size_t idx) const { return this->cells.at(idx); } -void CostField::set_cost(const coord::tile &pos, cost_t cost) { +void CostField::set_cost(const coord::tile_delta &pos, cost_t cost) { this->cells[pos.ne + pos.se * this->size] = cost; } diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index ffb27bdec8..8b805c524a 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -10,7 +10,7 @@ namespace openage { namespace coord { -struct tile; +struct tile_delta; } // namespace coord namespace path { @@ -37,10 +37,10 @@ class CostField { /** * Get the cost at a specified position. * - * @param pos Coordinates of the cell. + * @param pos Coordinates of the cell (relative to field origin). * @return Cost at the specified position. */ - cost_t get_cost(const coord::tile &pos) const; + cost_t get_cost(const coord::tile_delta &pos) const; /** * Get the cost at a specified position. @@ -53,10 +53,10 @@ class CostField { /** * Set the cost at a specified position. * - * @param pos Coordinates of the cell. + * @param pos Coordinates of the cell (relative to field origin). * @param cost Cost to set. */ - void set_cost(const coord::tile &pos, cost_t cost); + void set_cost(const coord::tile_delta &pos, cost_t cost); /** * Set the cost at a specified position. diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index bcf4cca945..d90d5ef39d 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -33,15 +33,15 @@ void path_demo_0(const util::Path &path) { // Cost field with some obstacles auto cost_field = std::make_shared(field_length); - cost_field->set_cost(coord::tile{0, 0}, COST_IMPASSABLE); - cost_field->set_cost(coord::tile{1, 0}, 254); - cost_field->set_cost(coord::tile{4, 3}, 128); - cost_field->set_cost(coord::tile{5, 3}, 128); - cost_field->set_cost(coord::tile{6, 3}, 128); - cost_field->set_cost(coord::tile{4, 4}, 128); - cost_field->set_cost(coord::tile{5, 4}, 128); - cost_field->set_cost(coord::tile{6, 4}, 128); - cost_field->set_cost(coord::tile{1, 7}, COST_IMPASSABLE); + cost_field->set_cost(coord::tile_delta{0, 0}, COST_IMPASSABLE); + cost_field->set_cost(coord::tile_delta{1, 0}, 254); + cost_field->set_cost(coord::tile_delta{4, 3}, 128); + cost_field->set_cost(coord::tile_delta{5, 3}, 128); + cost_field->set_cost(coord::tile_delta{6, 3}, 128); + cost_field->set_cost(coord::tile_delta{4, 4}, 128); + cost_field->set_cost(coord::tile_delta{5, 4}, 128); + cost_field->set_cost(coord::tile_delta{6, 4}, 128); + cost_field->set_cost(coord::tile_delta{1, 7}, COST_IMPASSABLE); log::log(INFO << "Created cost field"); // Create an integration field from the cost field @@ -49,7 +49,7 @@ void path_demo_0(const util::Path &path) { log::log(INFO << "Created integration field"); // Set cell (7, 7) to be the initial target cell - auto wavefront_blocked = integration_field->integrate_los(cost_field, coord::tile{7, 7}); + auto wavefront_blocked = integration_field->integrate_los(cost_field, coord::tile_delta{7, 7}); integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); log::log(INFO << "Calculated integration field for target cell (7, 7)"); @@ -92,7 +92,8 @@ void path_demo_0(const util::Path &path) { // Recalculate the integration field and the flow field integration_field->reset(); - auto wavefront_blocked = integration_field->integrate_los(cost_field, coord::tile{grid_x, grid_y}); + auto wavefront_blocked = integration_field->integrate_los(cost_field, + coord::tile_delta{grid_x, grid_y}); integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); log::log(INFO << "Calculated integration field for target cell (" << grid_x << ", " << grid_y << ")"); @@ -275,7 +276,7 @@ void RenderManager0::show_vectors(const std::shared_ptr &field) this->vector_pass->clear_renderables(); for (size_t y = 0; y < field->get_size(); ++y) { for (size_t x = 0; x < field->get_size(); ++x) { - auto cell = field->get_cell(coord::tile(x, y)); + auto cell = field->get_cell(coord::tile_delta(x, y)); if (cell & FLOW_PATHABLE_MASK and not(cell & FLOW_LOS_MASK)) { Eigen::Affine3f arrow_model = Eigen::Affine3f::Identity(); arrow_model.translate(Eigen::Vector3f(y + 0.5, 0, -1.0f * x - 0.5)); @@ -566,19 +567,19 @@ renderer::resources::MeshData RenderManager0::get_cost_field_mesh(const std::sha // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cost(coord::tile((i - 1) / resolution, (j - 1) / resolution)); + auto cost = field->get_cost(coord::tile_delta((i - 1) / resolution, (j - 1) / resolution)); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cost(coord::tile((i - 1) / resolution, j / resolution)); + auto cost = field->get_cost(coord::tile_delta((i - 1) / resolution, j / resolution)); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cost(coord::tile(i / resolution, j / resolution)); + auto cost = field->get_cost(coord::tile_delta(i / resolution, j / resolution)); surround.push_back(cost); } if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cost(coord::tile(i / resolution, (j - 1) / resolution)); + auto cost = field->get_cost(coord::tile_delta(i / resolution, (j - 1) / resolution)); surround.push_back(cost); } // use the cost of the most expensive surrounding tile @@ -652,19 +653,19 @@ renderer::resources::MeshData RenderManager0::get_integration_field_mesh(const s // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile((i - 1) / resolution, (j - 1) / resolution)).cost; + auto cost = field->get_cell(coord::tile_delta((i - 1) / resolution, (j - 1) / resolution)).cost; surround.push_back(cost); } if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile((i - 1) / resolution, j / resolution)).cost; + auto cost = field->get_cell(coord::tile_delta((i - 1) / resolution, j / resolution)).cost; surround.push_back(cost); } if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile(i / resolution, j / resolution)).cost; + auto cost = field->get_cell(coord::tile_delta(i / resolution, j / resolution)).cost; surround.push_back(cost); } if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile(i / resolution, (j - 1) / resolution)).cost; + auto cost = field->get_cell(coord::tile_delta(i / resolution, (j - 1) / resolution)).cost; surround.push_back(cost); } // use the cost of the most expensive surrounding tile @@ -738,19 +739,19 @@ renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::sha // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile((i - 1) / resolution, (j - 1) / resolution)); + auto cost = field->get_cell(coord::tile_delta((i - 1) / resolution, (j - 1) / resolution)); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile((i - 1) / resolution, j / resolution)); + auto cost = field->get_cell(coord::tile_delta((i - 1) / resolution, j / resolution)); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile(i / resolution, j / resolution)); + auto cost = field->get_cell(coord::tile_delta(i / resolution, j / resolution)); surround.push_back(cost); } if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile(i / resolution, (j - 1) / resolution)); + auto cost = field->get_cell(coord::tile_delta(i / resolution, (j - 1) / resolution)); surround.push_back(cost); } // use the cost of the most expensive surrounding tile diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 21aaa2c9d8..907caeb3d5 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -449,7 +449,7 @@ void RenderManager1::create_impassible_tiles(const std::shared_ptr & auto cost_field = sector->get_cost_field(); for (size_t y = 0; y < sector_size; y++) { for (size_t x = 0; x < sector_size; x++) { - auto cost = cost_field->get_cost(coord::tile(x, y)); + auto cost = cost_field->get_cost(coord::tile_delta(x, y)); if (cost == COST_IMPASSABLE) { std::array tile_data{ -1.0f + x * tile_offset_width + sector_size * sector_x * tile_offset_width, diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 7198ae5fbe..56fccc155c 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -30,11 +30,11 @@ size_t FlowField::get_size() const { return this->size; } -flow_t FlowField::get_cell(const coord::tile &pos) const { +flow_t FlowField::get_cell(const coord::tile_delta &pos) const { return this->cells.at(pos.ne + pos.se * this->size); } -flow_dir_t FlowField::get_dir(const coord::tile &pos) const { +flow_dir_t FlowField::get_dir(const coord::tile_delta &pos) const { return static_cast(this->get_cell(pos) & FLOW_DIR_MASK); } diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index 1c1adcfe33..6ae3882c5d 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -13,7 +13,7 @@ namespace openage { namespace coord { -struct tile; +struct tile_delta; } // namespace coord namespace path { @@ -46,20 +46,20 @@ class FlowField { /** * Get the flow field value at a specified position. * - * @param pos Coordinates of the cell. + * @param pos Coordinates of the cell (relative to field origin). * * @return Flowfield value at the specified position. */ - flow_t get_cell(const coord::tile &pos) const; + flow_t get_cell(const coord::tile_delta &pos) const; /** * Get the flow field direction at a specified position. * - * @param pos Coordinates of the cell. + * @param pos Coordinates of the cell (relative to field origin). * * @return Flowfield direction at the specified position. */ - flow_dir_t get_dir(const coord::tile &pos) const; + flow_dir_t get_dir(const coord::tile_delta &pos) const; /** * Build the flow field. diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index ff10e0e507..2a7fe7df7d 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -25,7 +25,7 @@ size_t IntegrationField::get_size() const { return this->size; } -const integrated_t &IntegrationField::get_cell(const coord::tile &pos) const { +const integrated_t &IntegrationField::get_cell(const coord::tile_delta &pos) const { return this->cells.at(pos.ne + pos.se * this->size); } @@ -71,7 +71,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptr IntegrationField::integrate_los(const std::shared_ptr &cost_field, - const coord::tile &target) { + const coord::tile_delta &target) { ENSURE(cost_field->get_size() == this->get_size(), "cost field size " << cost_field->get_size() << "x" << cost_field->get_size() @@ -135,7 +135,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrget_los_corners(cost_field, target, coord::tile(x, y)); + auto corners = this->get_los_corners(cost_field, target, coord::tile_delta(x, y)); for (auto &corner : corners) { // draw a line from the corner to the edge of the field // to get the cells blocked by the corner @@ -186,7 +186,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptr &cost_field, - const coord::tile &target) { + const coord::tile_delta &target) { ENSURE(cost_field->get_size() == this->get_size(), "cost field size " << cost_field->get_size() << "x" << cost_field->get_size() @@ -326,8 +326,8 @@ void IntegrationField::update_neighbor(size_t idx, } std::vector> IntegrationField::get_los_corners(const std::shared_ptr &cost_field, - const coord::tile &target, - const coord::tile &blocker) { + const coord::tile_delta &target, + const coord::tile_delta &blocker) { std::vector> corners; // Get the cost of the blocking cell's neighbors @@ -345,16 +345,16 @@ std::vector> IntegrationField::get_los_corners(const std::sh // Get neighbor costs (if they exist) if (blocker.se > 0) { - top_cost = cost_field->get_cost(coord::tile{blocker.ne, blocker.se - 1}); + top_cost = cost_field->get_cost(coord::tile_delta{blocker.ne, blocker.se - 1}); } if (blocker.ne > 0) { - left_cost = cost_field->get_cost(coord::tile{blocker.ne - 1, blocker.se}); + left_cost = cost_field->get_cost(coord::tile_delta{blocker.ne - 1, blocker.se}); } if (static_cast(blocker.se) < this->size - 1) { - bottom_cost = cost_field->get_cost(coord::tile{blocker.ne, blocker.se + 1}); + bottom_cost = cost_field->get_cost(coord::tile_delta{blocker.ne, blocker.se + 1}); } if (static_cast(blocker.ne) < this->size - 1) { - right_cost = cost_field->get_cost(coord::tile{blocker.ne + 1, blocker.se}); + right_cost = cost_field->get_cost(coord::tile_delta{blocker.ne + 1, blocker.se}); } // Check which corners are blocking LOS @@ -460,7 +460,7 @@ std::vector> IntegrationField::get_los_corners(const std::sh return corners; } -std::vector IntegrationField::bresenhams_line(const coord::tile &target, +std::vector IntegrationField::bresenhams_line(const coord::tile_delta &target, int corner_x, int corner_y) { std::vector cells; diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index c7c2bf5283..fc5f087404 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -13,7 +13,7 @@ namespace openage { namespace coord { -struct tile; +struct tile_delta; } // namespace coord namespace path { @@ -42,10 +42,10 @@ class IntegrationField { /** * Get the integration value at a specified position. * - * @param pos Coordinates of the cell. + * @param pos Coordinates of the cell (relative to field origin). * @return Integration value at the specified position. */ - const integrated_t &get_cell(const coord::tile &pos) const; + const integrated_t &get_cell(const coord::tile_delta &pos) const; /** * Get the integration value at a specified position. @@ -62,12 +62,12 @@ class IntegrationField { * can be used as a starting point for the cost integration. * * @param cost_field Cost field to integrate. - * @param target Coordinates of the target cell. + * @param target Coordinates of the target cell (relative to field origin). * * @return Cells flagged as "wavefront blocked". */ std::vector integrate_los(const std::shared_ptr &cost_field, - const coord::tile &target); + const coord::tile_delta &target); /** * Calculate the line-of-sight integration flags starting from a portal to another @@ -93,7 +93,7 @@ class IntegrationField { * @param target Coordinates of the target cell. */ void integrate_cost(const std::shared_ptr &cost_field, - const coord::tile &target); + const coord::tile_delta &target); /** * Calculate the cost integration field starting from a portal to another @@ -150,14 +150,14 @@ class IntegrationField { * Get the LOS corners around a cell. * * @param cost_field Cost field to integrate. - * @param target Cell coordinates of the target. - * @param blocker Cell coordinates of the cell blocking LOS. + * @param target Cell coordinates of the target (relative to field origin). + * @param blocker Cell coordinates of the cell blocking LOS (relative to field origin). * * @return Field coordinates of the LOS corners. */ std::vector> get_los_corners(const std::shared_ptr &cost_field, - const coord::tile &target, - const coord::tile &blocker); + const coord::tile_delta &target, + const coord::tile_delta &blocker); /** * Get the cells in a bresenham's line between the corner cell and the field edge. @@ -167,13 +167,13 @@ class IntegrationField { * the cells between two arbitrary points. We do this because the intersection * point with the field edge is unknown. * - * @param target Cell coordinates of the target. + * @param target Cell coordinates of the target (relative to field origin). * @param corner_x X field coordinate edge of the LOS corner. * @param corner_y Y field coordinate edge of the LOS corner. * * @return Cell indices of the LOS line. */ - std::vector bresenhams_line(const coord::tile &target, + std::vector bresenhams_line(const coord::tile_delta &target, int corner_x, int corner_y); diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 28f8a829ca..4ffdedff15 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -10,7 +10,7 @@ namespace openage::path { std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field, - const coord::tile &target) { + const coord::tile_delta &target) { auto integration_field = std::make_shared(cost_field->get_size()); auto wavefront_blocked = integration_field->integrate_los(cost_field, target); @@ -46,7 +46,7 @@ std::shared_ptr Integrator::build(const std::shared_ptr &cost_field, - const coord::tile &target) { + const coord::tile_delta &target) { auto integration_field = this->integrate(cost_field, target); auto flow_field = this->build(integration_field); diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index 027f627274..ef9c2780cd 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -10,7 +10,7 @@ namespace openage { namespace coord { -struct tile; +struct tile_delta; } // namespace coord namespace path { @@ -36,7 +36,7 @@ class Integrator { * @return Integration field. */ std::shared_ptr integrate(const std::shared_ptr &cost_field, - const coord::tile &target); + const coord::tile_delta &target); /** * Integrate the cost field from a portal. @@ -86,7 +86,7 @@ class Integrator { * @return Flow field. */ build_return_t build(const std::shared_ptr &cost_field, - const coord::tile &target); + const coord::tile_delta &target); /** * Build the integration field and flow field from a portal. diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 48eeb7d83e..300bc7d92d 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -2,6 +2,7 @@ #include "pathfinder.h" +#include "coord/chunk.h" #include "coord/phys.h" #include "pathfinding/flow_field.h" #include "pathfinding/grid.h" @@ -33,14 +34,14 @@ const Path Pathfinder::get_path(const PathRequest &request) { // Integrate the target field coord::tile_t target_x = request.target.ne % sector_size; coord::tile_t target_y = request.target.se % sector_size; - auto target = coord::tile{target_x, target_y}; + auto target = coord::tile_delta{target_x, target_y}; auto target_integration_field = this->integrator->integrate(target_sector->get_cost_field(), target); if (target_sector == start_sector) { auto start_x = request.start.ne % sector_size; auto start_y = request.start.se % sector_size; - if (target_integration_field->get_cell(coord::tile{start_x, start_y}).cost != INTEGRATED_COST_UNREACHABLE) { + if (target_integration_field->get_cell(coord::tile_delta{start_x, start_y}).cost != INTEGRATED_COST_UNREACHABLE) { // Exit early if the start and target are in the same sector // and are reachable from within the same sector auto flow_field = this->integrator->build(target_integration_field); @@ -71,7 +72,7 @@ const Path Pathfinder::get_path(const PathRequest &request) { // Check which portals are reachable from the start field coord::tile_t start_x = request.start.ne % sector_size; coord::tile_t start_y = request.start.se % sector_size; - auto start = coord::tile{start_x, start_y}; + auto start = coord::tile_delta{start_x, start_y}; auto start_integration_field = this->integrator->integrate(start_sector->get_cost_field(), start); std::unordered_set start_portal_ids; @@ -184,9 +185,9 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req auto portal_node = std::make_shared(portal, start_sector->get_id(), nullptr); - auto sector_pos = grid->get_sector(portal->get_exit_sector(start_sector->get_id()))->get_position(); + auto sector_pos = grid->get_sector(portal->get_exit_sector(start_sector->get_id()))->get_position().to_tile(sector_size); auto portal_pos = portal->get_exit_center(start_sector->get_id()); - auto portal_abs_pos = portal_pos + coord::tile_delta(sector_pos.ne * sector_size, sector_pos.se * sector_size); + auto portal_abs_pos = sector_pos + portal_pos; auto heuristic_cost = Pathfinder::heuristic_cost(portal_abs_pos, request.target); portal_node->current_cost = 0; @@ -233,7 +234,7 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req continue; } - // Get distance cost from current node to exit + // Get distance cost (from current node to exit node) auto distance_cost = Pathfinder::distance_cost( current_node->portal->get_exit_center(current_node->entry_sector), exit->portal->get_entry_center(exit->entry_sector)); @@ -243,9 +244,12 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req if (not_visited or tentative_cost < exit->current_cost) { if (not_visited) { - // calculate the heuristic cost + // Get heuristic cost (from exit node to target cell) + auto exit_sector = grid->get_sector(exit->portal->get_exit_sector(exit->entry_sector)); + auto exit_sector_pos = exit_sector->get_position().to_tile(sector_size); + auto exit_portal_pos = exit->portal->get_exit_center(exit->entry_sector); exit->heuristic_cost = Pathfinder::heuristic_cost( - exit->portal->get_exit_center(exit->entry_sector), + exit_sector_pos + exit_portal_pos, request.target); } @@ -291,7 +295,7 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_dir(coord::tile{current_x, current_y}); + flow_dir_t current_direction = flow_fields.at(0).second->get_dir(coord::tile_delta{current_x, current_y}); for (size_t i = 0; i < flow_fields.size(); ++i) { auto sector = grid->get_sector(flow_fields.at(i).first); auto flow_field = flow_fields.at(i).second; @@ -301,7 +305,7 @@ const std::vector Pathfinder::get_waypoints(const std::vector(sector_size) and current_x >= 0 and current_y >= 0) { - auto cell = flow_field->get_cell(coord::tile{current_x, current_y}); + auto cell = flow_field->get_cell(coord::tile_delta{current_x, current_y}); if (cell & FLOW_LOS_MASK) { // check if we reached an LOS cell auto sector_pos = sector->get_position(); @@ -313,7 +317,7 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_dir(coord::tile(current_x, current_y)); + auto cell_direction = flow_field->get_dir(coord::tile_delta(current_x, current_y)); if (cell_direction != current_direction) { // add the current cell as a waypoint auto sector_pos = sector->get_position(); @@ -412,8 +416,8 @@ int Pathfinder::heuristic_cost(const coord::tile &portal_pos, return delta.length(); } -int Pathfinder::distance_cost(const coord::tile &portal1_pos, - const coord::tile &portal2_pos) { +int Pathfinder::distance_cost(const coord::tile_delta &portal1_pos, + const coord::tile_delta &portal2_pos) { auto delta = portal2_pos.to_phys2() - portal1_pos.to_phys2(); return delta.length(); diff --git a/libopenage/pathfinding/pathfinder.h b/libopenage/pathfinding/pathfinder.h index 706e222318..1809b7524d 100644 --- a/libopenage/pathfinding/pathfinder.h +++ b/libopenage/pathfinding/pathfinder.h @@ -92,8 +92,9 @@ class Pathfinder { /** * Calculate the heuristic cost between a portal and a target cell. * - * @param portal_pos Position of the portal. This should be the center of the portal exit. - * @param target_pos Position of the target cell. + * @param portal_pos Position of the portal (absolute on the grid). + * This should be the center of the portal exit. + * @param target_pos Position of the target cell (absolute on the grid). * * @return Heuristic cost between the cells. */ @@ -102,12 +103,12 @@ class Pathfinder { /** * Calculate the distance cost between two portals. * - * @param portal1_pos Center of the first portal. - * @param portal2_pos Center of the second portal. + * @param portal1_pos Center of the first portal (relative to sector origin). + * @param portal2_pos Center of the second portal (relative to sector origin). * * @return Distance cost between the portal centers. */ - static int distance_cost(const coord::tile &portal1_pos, const coord::tile &portal2_pos); + static int distance_cost(const coord::tile_delta &portal1_pos, const coord::tile_delta &portal2_pos); /** * Grids managed by this pathfinder. diff --git a/libopenage/pathfinding/portal.cpp b/libopenage/pathfinding/portal.cpp index da3581047f..9b8f64cd51 100644 --- a/libopenage/pathfinding/portal.cpp +++ b/libopenage/pathfinding/portal.cpp @@ -11,8 +11,8 @@ Portal::Portal(portal_id_t id, sector_id_t sector0, sector_id_t sector1, PortalDirection direction, - const coord::tile &cell_start, - const coord::tile &cell_end) : + const coord::tile_delta &cell_start, + const coord::tile_delta &cell_end) : id{id}, sector0{sector0}, sector1{sector1}, @@ -65,7 +65,7 @@ sector_id_t Portal::get_exit_sector(sector_id_t entry_sector) const { return this->sector0; } -const coord::tile Portal::get_entry_start(sector_id_t entry_sector) const { +const coord::tile_delta Portal::get_entry_start(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -75,7 +75,7 @@ const coord::tile Portal::get_entry_start(sector_id_t entry_sector) const { return this->get_sector1_start(); } -const coord::tile Portal::get_entry_center(sector_id_t entry_sector) const { +const coord::tile_delta Portal::get_entry_center(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -89,7 +89,7 @@ const coord::tile Portal::get_entry_center(sector_id_t entry_sector) const { return {(start.ne + end.ne) / 2, (start.se + end.se) / 2}; } -const coord::tile Portal::get_entry_end(sector_id_t entry_sector) const { +const coord::tile_delta Portal::get_entry_end(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -99,7 +99,7 @@ const coord::tile Portal::get_entry_end(sector_id_t entry_sector) const { return this->get_sector1_end(); } -const coord::tile Portal::get_exit_start(sector_id_t entry_sector) const { +const coord::tile_delta Portal::get_exit_start(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -109,7 +109,7 @@ const coord::tile Portal::get_exit_start(sector_id_t entry_sector) const { return this->get_sector0_start(); } -const coord::tile Portal::get_exit_center(sector_id_t entry_sector) const { +const coord::tile_delta Portal::get_exit_center(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -123,7 +123,7 @@ const coord::tile Portal::get_exit_center(sector_id_t entry_sector) const { return {(start.ne + end.ne) / 2, (start.se + end.se) / 2}; } -const coord::tile Portal::get_exit_end(sector_id_t entry_sector) const { +const coord::tile_delta Portal::get_exit_end(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -137,22 +137,22 @@ PortalDirection Portal::get_direction() const { return this->direction; } -const coord::tile &Portal::get_sector0_start() const { +const coord::tile_delta &Portal::get_sector0_start() const { return this->cell_start; } -const coord::tile &Portal::get_sector0_end() const { +const coord::tile_delta &Portal::get_sector0_end() const { return this->cell_end; } -const coord::tile Portal::get_sector1_start() const { +const coord::tile_delta Portal::get_sector1_start() const { if (this->direction == PortalDirection::NORTH_SOUTH) { return {this->cell_start.ne, 0}; } return {0, this->cell_start.se}; } -const coord::tile Portal::get_sector1_end() const { +const coord::tile_delta Portal::get_sector1_end() const { if (this->direction == PortalDirection::NORTH_SOUTH) { return {this->cell_end.ne, 0}; } diff --git a/libopenage/pathfinding/portal.h b/libopenage/pathfinding/portal.h index 2487e449c7..6593eea852 100644 --- a/libopenage/pathfinding/portal.h +++ b/libopenage/pathfinding/portal.h @@ -49,15 +49,15 @@ class Portal { * @param sector1 Second sector connected by the portal. * Must be south or west on the grid in relation to sector 0. * @param direction Direction of the portal from sector 0 to sector 1. - * @param cell_start Start cell coordinate in sector 0. - * @param cell_end End cell coordinate in sector 0. + * @param cell_start Start cell coordinate in sector 0 (relative to sector origin). + * @param cell_end End cell coordinate in sector 0 (relative to sector origin). */ Portal(portal_id_t id, sector_id_t sector0, sector_id_t sector1, PortalDirection direction, - const coord::tile &cell_start, - const coord::tile &cell_end); + const coord::tile_delta &cell_start, + const coord::tile_delta &cell_end); ~Portal() = default; @@ -110,54 +110,54 @@ class Portal { * * @param entry_sector Sector from which the portal is entered. * - * @return Cell coordinates of the start of the portal in the entry sector. + * @return Cell coordinates of the start of the portal in the entry sector (relative to sector origin). */ - const coord::tile get_entry_start(sector_id_t entry_sector) const; + const coord::tile_delta get_entry_start(sector_id_t entry_sector) const; /** * Get the cell coordinates of the center of the portal in the entry sector. * * @param entry_sector Sector from which the portal is entered. * - * @return Cell coordinates of the center of the portal in the entry sector. + * @return Cell coordinates of the center of the portal in the entry sector (relative to sector origin). */ - const coord::tile get_entry_center(sector_id_t entry_sector) const; + const coord::tile_delta get_entry_center(sector_id_t entry_sector) const; /** * Get the cell coordinates of the start of the portal in the entry sector. * * @param entry_sector Sector from which the portal is entered. * - * @return Cell coordinates of the start of the portal in the entry sector. + * @return Cell coordinates of the start of the portal in the entry sector (relative to sector origin). */ - const coord::tile get_entry_end(sector_id_t entry_sector) const; + const coord::tile_delta get_entry_end(sector_id_t entry_sector) const; /** * Get the cell coordinates of the start of the portal in the exit sector. * * @param entry_sector Sector from which the portal is entered. * - * @return Cell coordinates of the start of the portal in the exit sector. + * @return Cell coordinates of the start of the portal in the exit sector (relative to sector origin). */ - const coord::tile get_exit_start(sector_id_t entry_sector) const; + const coord::tile_delta get_exit_start(sector_id_t entry_sector) const; /** * Get the cell coordinates of the center of the portal in the exit sector. * * @param entry_sector Sector from which the portal is entered. * - * @return Cell coordinates of the center of the portal in the exit sector. + * @return Cell coordinates of the center of the portal in the exit sector (relative to sector origin). */ - const coord::tile get_exit_center(sector_id_t entry_sector) const; + const coord::tile_delta get_exit_center(sector_id_t entry_sector) const; /** * Get the cell coordinates of the end of the portal in the exit sector. * * @param entry_sector Sector from which the portal is entered. * - * @return Cell coordinates of the end of the portal in the exit sector. + * @return Cell coordinates of the end of the portal in the exit sector (relative to sector origin). */ - const coord::tile get_exit_end(sector_id_t entry_sector) const; + const coord::tile_delta get_exit_end(sector_id_t entry_sector) const; /** * Get the direction of the portal from sector 0 to sector 1. @@ -170,30 +170,30 @@ class Portal { /** * Get the start cell coordinates of the portal. * - * @return Start cell coordinates of the portal. + * @return Start cell coordinates of the portal (relative to sector origin). */ - const coord::tile &get_sector0_start() const; + const coord::tile_delta &get_sector0_start() const; /** * Get the end cell coordinates of the portal. * - * @return End cell coordinates of the portal. + * @return End cell coordinates of the portal (relative to sector origin). */ - const coord::tile &get_sector0_end() const; + const coord::tile_delta &get_sector0_end() const; /** * Get the start cell coordinates of the portal. * - * @return Start cell coordinates of the portal. + * @return Start cell coordinates of the portal (relative to sector origin). */ - const coord::tile get_sector1_start() const; + const coord::tile_delta get_sector1_start() const; /** * Get the end cell coordinates of the portal. * - * @return End cell coordinates of the portal. + * @return End cell coordinates of the portal (relative to sector origin). */ - const coord::tile get_sector1_end() const; + const coord::tile_delta get_sector1_end() const; /** * ID of the portal. @@ -230,13 +230,17 @@ class Portal { PortalDirection direction; /** - * Start cell coordinate. + * Start cell coordinate in sector 0 (relative to sector origin). + * + * Coordinates for sector 1 are calculated on-the-fly using the direction. */ - coord::tile cell_start; + coord::tile_delta cell_start; /** - * End cell coordinate. + * End cell coordinate in sector 0 (relative to sector origin). + * + * Coordinates for sector 1 are calculated on-the-fly using the direction. */ - coord::tile cell_end; + coord::tile_delta cell_end; }; } // namespace openage::path diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index cf6ffaa755..f50dc209bf 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -61,17 +61,17 @@ std::vector> Sector::find_portals(const std::shared_ptr< size_t end = 0; bool passable_edge = false; for (size_t i = 0; i < this->cost_field->get_size(); ++i) { - auto coord_this = coord::tile{0, 0}; - auto coord_other = coord::tile{0, 0}; + auto coord_this = coord::tile_delta{0, 0}; + auto coord_other = coord::tile_delta{0, 0}; if (direction == PortalDirection::NORTH_SOUTH) { // right edge; top to bottom - coord_this = coord::tile(i, this->cost_field->get_size() - 1); - coord_other = coord::tile(i, 0); + coord_this = coord::tile_delta(i, this->cost_field->get_size() - 1); + coord_other = coord::tile_delta(i, 0); } else if (direction == PortalDirection::EAST_WEST) { // bottom edge; east to west - coord_this = coord::tile(this->cost_field->get_size() - 1, i); - coord_other = coord::tile(0, i); + coord_this = coord::tile_delta(this->cost_field->get_size() - 1, i); + coord_other = coord::tile_delta(0, i); } if (this->cost_field->get_cost(coord_this) != COST_IMPASSABLE @@ -94,17 +94,17 @@ std::vector> Sector::find_portals(const std::shared_ptr< if (passable_edge) { // create a new portal - auto coord_start = coord::tile{0, 0}; - auto coord_end = coord::tile{0, 0}; + auto coord_start = coord::tile_delta{0, 0}; + auto coord_end = coord::tile_delta{0, 0}; if (direction == PortalDirection::NORTH_SOUTH) { // right edge; top to bottom - coord_start = coord::tile(start, this->cost_field->get_size() - 1); - coord_end = coord::tile(end, this->cost_field->get_size() - 1); + coord_start = coord::tile_delta(start, this->cost_field->get_size() - 1); + coord_end = coord::tile_delta(end, this->cost_field->get_size() - 1); } else if (direction == PortalDirection::EAST_WEST) { // bottom edge; east to west - coord_start = coord::tile(this->cost_field->get_size() - 1, start); - coord_end = coord::tile(this->cost_field->get_size() - 1, end); + coord_start = coord::tile_delta(this->cost_field->get_size() - 1, start); + coord_end = coord::tile_delta(this->cost_field->get_size() - 1, end); } result.push_back( diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h index f39f1904a9..a4ba94fa52 100644 --- a/libopenage/pathfinding/sector.h +++ b/libopenage/pathfinding/sector.h @@ -57,7 +57,7 @@ class Sector { /** * Get the position of this sector in the grid. * - * @return Position of the sector. + * @return Position of the sector (absolute on the grid). */ const coord::chunk &get_position() const; @@ -110,7 +110,7 @@ class Sector { sector_id_t id; /** - * Position of the sector in the grid. + * Position of the sector (absolute on the grid). */ coord::chunk position; diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index 2c9775ca36..12c96cc30e 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -28,7 +28,7 @@ void flow_field() { // Test the different field types { auto integration_field = std::make_shared(3); - integration_field->integrate_cost(cost_field, coord::tile{2, 2}); + integration_field->integrate_cost(cost_field, coord::tile_delta{2, 2}); auto int_cells = integration_field->get_cells(); // The integration field should look like: @@ -85,7 +85,7 @@ void flow_field() { auto integrator = std::make_shared(); // Build the flow field - auto flow_field = integrator->build(cost_field, coord::tile{2, 2}).second; + auto flow_field = integrator->build(cost_field, coord::tile_delta{2, 2}).second; auto ff_cells = flow_field->get_cells(); // The flow field for targeting (2, 2) hould look like this: From 258c5922d135ae30ab07cbef5b58ba3b1068f7d5 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 21 May 2024 23:49:49 +0200 Subject: [PATCH 512/771] path: Use x,y cell position instead of coord types for most operations. --- libopenage/pathfinding/cost_field.cpp | 8 +++++ libopenage/pathfinding/cost_field.h | 18 ++++++++++ libopenage/pathfinding/demo/demo_0.cpp | 16 ++++----- libopenage/pathfinding/demo/demo_1.cpp | 2 +- libopenage/pathfinding/flow_field.cpp | 16 +++++++++ libopenage/pathfinding/flow_field.h | 38 ++++++++++++++++++++ libopenage/pathfinding/integration_field.cpp | 12 ++++--- libopenage/pathfinding/integration_field.h | 9 +++++ libopenage/pathfinding/pathfinder.cpp | 6 ++-- 9 files changed, 109 insertions(+), 16 deletions(-) diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index e85fecc3ed..3299084548 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -25,6 +25,10 @@ cost_t CostField::get_cost(const coord::tile_delta &pos) const { return this->cells.at(pos.ne + pos.se * this->size); } +cost_t CostField::get_cost(size_t x, size_t y) const { + return this->cells.at(x + y * this->size); +} + cost_t CostField::get_cost(size_t idx) const { return this->cells.at(idx); } @@ -33,6 +37,10 @@ void CostField::set_cost(const coord::tile_delta &pos, cost_t cost) { this->cells[pos.ne + pos.se * this->size] = cost; } +void CostField::set_cost(size_t x, size_t y, cost_t cost) { + this->cells[x + y * this->size] = cost; +} + void CostField::set_cost(size_t idx, cost_t cost) { this->cells[idx] = cost; } diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index 8b805c524a..a1fc87b046 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -42,6 +42,15 @@ class CostField { */ cost_t get_cost(const coord::tile_delta &pos) const; + /** + * Get the cost at a specified position. + * + * @param x X-coordinate of the cell. + * @param y Y-coordinate of the cell. + * @return Cost at the specified position. + */ + cost_t get_cost(size_t x, size_t y) const; + /** * Get the cost at a specified position. * @@ -58,6 +67,15 @@ class CostField { */ void set_cost(const coord::tile_delta &pos, cost_t cost); + /** + * Set the cost at a specified position. + * + * @param x X-coordinate of the cell. + * @param y Y-coordinate of the cell. + * @param cost Cost to set. + */ + void set_cost(size_t x, size_t y, cost_t cost); + /** * Set the cost at a specified position. * diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index d90d5ef39d..8e844d942d 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -567,19 +567,19 @@ renderer::resources::MeshData RenderManager0::get_cost_field_mesh(const std::sha // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cost(coord::tile_delta((i - 1) / resolution, (j - 1) / resolution)); + auto cost = field->get_cost((i - 1) / resolution, (j - 1) / resolution); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cost(coord::tile_delta((i - 1) / resolution, j / resolution)); + auto cost = field->get_cost((i - 1) / resolution, j / resolution); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cost(coord::tile_delta(i / resolution, j / resolution)); + auto cost = field->get_cost(i / resolution, j / resolution); surround.push_back(cost); } if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cost(coord::tile_delta(i / resolution, (j - 1) / resolution)); + auto cost = field->get_cost(i / resolution, (j - 1) / resolution); surround.push_back(cost); } // use the cost of the most expensive surrounding tile @@ -653,19 +653,19 @@ renderer::resources::MeshData RenderManager0::get_integration_field_mesh(const s // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile_delta((i - 1) / resolution, (j - 1) / resolution)).cost; + auto cost = field->get_cell((i - 1) / resolution, (j - 1) / resolution).cost; surround.push_back(cost); } if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile_delta((i - 1) / resolution, j / resolution)).cost; + auto cost = field->get_cell((i - 1) / resolution, j / resolution).cost; surround.push_back(cost); } if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile_delta(i / resolution, j / resolution)).cost; + auto cost = field->get_cell(i / resolution, j / resolution).cost; surround.push_back(cost); } if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile_delta(i / resolution, (j - 1) / resolution)).cost; + auto cost = field->get_cell(i / resolution, (j - 1) / resolution).cost; surround.push_back(cost); } // use the cost of the most expensive surrounding tile diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 907caeb3d5..d7daaf4675 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -449,7 +449,7 @@ void RenderManager1::create_impassible_tiles(const std::shared_ptr & auto cost_field = sector->get_cost_field(); for (size_t y = 0; y < sector_size; y++) { for (size_t x = 0; x < sector_size; x++) { - auto cost = cost_field->get_cost(coord::tile_delta(x, y)); + auto cost = cost_field->get_cost(x, y); if (cost == COST_IMPASSABLE) { std::array tile_data{ -1.0f + x * tile_offset_width + sector_size * sector_x * tile_offset_width, diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 56fccc155c..bb6c2d6b6e 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -34,10 +34,26 @@ flow_t FlowField::get_cell(const coord::tile_delta &pos) const { return this->cells.at(pos.ne + pos.se * this->size); } +flow_t FlowField::get_cell(size_t x, size_t y) const { + return this->cells.at(x + y * this->size); +} + +flow_t FlowField::get_cell(size_t idx) const { + return this->cells.at(idx); +} + flow_dir_t FlowField::get_dir(const coord::tile_delta &pos) const { return static_cast(this->get_cell(pos) & FLOW_DIR_MASK); } +flow_dir_t FlowField::get_dir(size_t x, size_t y) const { + return static_cast(this->get_cell(x, y) & FLOW_DIR_MASK); +} + +flow_dir_t FlowField::get_dir(size_t idx) const { + return static_cast(this->get_cell(idx) & FLOW_DIR_MASK); +} + void FlowField::build(const std::shared_ptr &integration_field, const std::unordered_set &target_cells) { ENSURE(integration_field->get_size() == this->get_size(), diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index 6ae3882c5d..4a055d3cb1 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -52,6 +52,25 @@ class FlowField { */ flow_t get_cell(const coord::tile_delta &pos) const; + /** + * Get the flow field value at a specified position. + * + * @param x X-coordinate of the cell. + * @param y Y-coordinate of the cell. + * + * @return Flowfield value at the specified position. + */ + flow_t get_cell(size_t x, size_t y) const; + + /** + * Get the flow field direction at a specified position. + * + * @param idx Index of the cell. + * + * @return Flowfield value at the specified position. + */ + flow_t get_cell(size_t idx) const; + /** * Get the flow field direction at a specified position. * @@ -61,6 +80,25 @@ class FlowField { */ flow_dir_t get_dir(const coord::tile_delta &pos) const; + /** + * Get the flow field direction at a specified position. + * + * @param x X-coordinate of the cell. + * @param y Y-coordinate of the cell. + * + * @return Flowfield direction at the specified position. + */ + flow_dir_t get_dir(size_t x, size_t y) const; + + /** + * Get the flow field direction at a specified position. + * + * @param idx Index of the cell. + * + * @return Flowfield direction at the specified position. + */ + flow_dir_t get_dir(size_t idx) const; + /** * Build the flow field. * diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 2a7fe7df7d..0219dd17f3 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -29,6 +29,10 @@ const integrated_t &IntegrationField::get_cell(const coord::tile_delta &pos) con return this->cells.at(pos.ne + pos.se * this->size); } +const integrated_t &IntegrationField::get_cell(size_t x, size_t y) const { + return this->cells.at(x + y * this->size); +} + const integrated_t &IntegrationField::get_cell(size_t idx) const { return this->cells.at(idx); } @@ -345,16 +349,16 @@ std::vector> IntegrationField::get_los_corners(const std::sh // Get neighbor costs (if they exist) if (blocker.se > 0) { - top_cost = cost_field->get_cost(coord::tile_delta{blocker.ne, blocker.se - 1}); + top_cost = cost_field->get_cost(blocker.ne, blocker.se - 1); } if (blocker.ne > 0) { - left_cost = cost_field->get_cost(coord::tile_delta{blocker.ne - 1, blocker.se}); + left_cost = cost_field->get_cost(blocker.ne - 1, blocker.se); } if (static_cast(blocker.se) < this->size - 1) { - bottom_cost = cost_field->get_cost(coord::tile_delta{blocker.ne, blocker.se + 1}); + bottom_cost = cost_field->get_cost(blocker.ne, blocker.se + 1); } if (static_cast(blocker.ne) < this->size - 1) { - right_cost = cost_field->get_cost(coord::tile_delta{blocker.ne + 1, blocker.se}); + right_cost = cost_field->get_cost(blocker.ne + 1, blocker.se); } // Check which corners are blocking LOS diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index fc5f087404..40e6780c8f 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -47,6 +47,15 @@ class IntegrationField { */ const integrated_t &get_cell(const coord::tile_delta &pos) const; + /** + * Get the integration value at a specified position. + * + * @param x X-coordinate of the cell. + * @param y Y-coordinate of the cell. + * @return Integration value at the specified position. + */ + const integrated_t &get_cell(size_t x, size_t y) const; + /** * Get the integration value at a specified position. * diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 300bc7d92d..22662c9e9d 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -41,7 +41,7 @@ const Path Pathfinder::get_path(const PathRequest &request) { auto start_x = request.start.ne % sector_size; auto start_y = request.start.se % sector_size; - if (target_integration_field->get_cell(coord::tile_delta{start_x, start_y}).cost != INTEGRATED_COST_UNREACHABLE) { + if (target_integration_field->get_cell(start_x, start_y).cost != INTEGRATED_COST_UNREACHABLE) { // Exit early if the start and target are in the same sector // and are reachable from within the same sector auto flow_field = this->integrator->build(target_integration_field); @@ -295,7 +295,7 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_dir(coord::tile_delta{current_x, current_y}); + flow_dir_t current_direction = flow_fields.at(0).second->get_dir(current_x, current_y); for (size_t i = 0; i < flow_fields.size(); ++i) { auto sector = grid->get_sector(flow_fields.at(i).first); auto flow_field = flow_fields.at(i).second; @@ -305,7 +305,7 @@ const std::vector Pathfinder::get_waypoints(const std::vector(sector_size) and current_x >= 0 and current_y >= 0) { - auto cell = flow_field->get_cell(coord::tile_delta{current_x, current_y}); + auto cell = flow_field->get_cell(current_x, current_y); if (cell & FLOW_LOS_MASK) { // check if we reached an LOS cell auto sector_pos = sector->get_position(); From 66709b4f913735e09a4ac3c4049967d2770bdef7 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 21 May 2024 23:55:18 +0200 Subject: [PATCH 513/771] path: Fix crash in demo 1 when path is not found. --- libopenage/pathfinding/demo/demo_1.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index d7daaf4675..801eea045d 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -135,8 +135,10 @@ void path_demo_1(const util::Path &path) { log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps"); - // Create renderables for the waypoints of the path - render_manager->create_waypoint_tiles(path_result); + if (path_result.status == PathResult::FOUND) { + // Create renderables for the waypoints of the path + render_manager->create_waypoint_tiles(path_result); + } } else if (ev.button() == Qt::LeftButton) { // Set start cell start = coord::tile{grid_x, grid_y}; @@ -153,8 +155,10 @@ void path_demo_1(const util::Path &path) { log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps"); - // Create renderables for the waypoints of the path - render_manager->create_waypoint_tiles(path_result); + if (path_result.status == PathResult::FOUND) { + // Create renderables for the waypoints of the path + render_manager->create_waypoint_tiles(path_result); + } } } }); From d26915041abd837200b7acfe32046a2408dddb99 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 22 May 2024 01:08:23 +0200 Subject: [PATCH 514/771] path: Flag in integration field. Speeds up flow field creation by ~30%. --- libopenage/pathfinding/definitions.h | 7 +++++++ libopenage/pathfinding/flow_field.cpp | 14 +++----------- libopenage/pathfinding/flow_field.h | 5 +---- libopenage/pathfinding/integration_field.cpp | 2 ++ 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h index d61c5fe43c..4e184406cd 100644 --- a/libopenage/pathfinding/definitions.h +++ b/libopenage/pathfinding/definitions.h @@ -52,6 +52,11 @@ constexpr integrated_flags_t INTEGRATE_LOS_MASK = 0x01; */ constexpr integrated_flags_t INTEGRATE_WAVEFRONT_BLOCKED_MASK = 0x02; +/** + * Target flag in an integrated_flags_t value. + */ +constexpr integrated_flags_t INTEGRATE_TARGET_MASK = 0x40; + /** * Initial value for a cell in the integration grid. */ @@ -80,6 +85,8 @@ constexpr flow_t FLOW_LOS_MASK = 0x20; /** * Wavefront blocked flag in a flow_t value. + * + * TODO: This flag is only used in demo 0 and can be removed. */ constexpr flow_t FLOW_WAVEFRONT_BLOCKED_MASK = 0x40; diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index bb6c2d6b6e..c005e6cf05 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -54,8 +54,7 @@ flow_dir_t FlowField::get_dir(size_t idx) const { return static_cast(this->get_cell(idx) & FLOW_DIR_MASK); } -void FlowField::build(const std::shared_ptr &integration_field, - const std::unordered_set &target_cells) { +void FlowField::build(const std::shared_ptr &integration_field) { ENSURE(integration_field->get_size() == this->get_size(), "integration field size " << integration_field->get_size() << "x" << integration_field->get_size() @@ -69,7 +68,7 @@ void FlowField::build(const std::shared_ptr &integration_field for (size_t x = 0; x < this->size; ++x) { size_t idx = y * this->size + x; - if (target_cells.contains(idx)) { + if (integrate_cells[idx].flags & INTEGRATE_TARGET_MASK) { // Ignore target cells continue; } @@ -204,9 +203,6 @@ void FlowField::build(const std::shared_ptr &integration_field // TODO: Compare integration values from other side of portal // auto &integrate_cells = integration_field->get_cells(); - // cells that are part of the portal - std::unordered_set portal_cells; - // set the direction for the flow field cells that are part of the portal if (direction == PortalDirection::NORTH_SOUTH) { bool other_is_north = entry_start.se > exit_start.se; @@ -216,7 +212,6 @@ void FlowField::build(const std::shared_ptr &integration_field auto idx = y * this->size + x; flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::NORTH); - portal_cells.insert(idx); } } else { @@ -225,7 +220,6 @@ void FlowField::build(const std::shared_ptr &integration_field auto idx = y * this->size + x; flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::SOUTH); - portal_cells.insert(idx); } } } @@ -237,7 +231,6 @@ void FlowField::build(const std::shared_ptr &integration_field auto idx = y * this->size + x; flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::EAST); - portal_cells.insert(idx); } } else { @@ -246,7 +239,6 @@ void FlowField::build(const std::shared_ptr &integration_field auto idx = y * this->size + x; flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::WEST); - portal_cells.insert(idx); } } } @@ -254,7 +246,7 @@ void FlowField::build(const std::shared_ptr &integration_field throw Error(ERR << "Invalid portal direction: " << static_cast(direction)); } - this->build(integration_field, portal_cells); + this->build(integration_field); } const std::vector &FlowField::get_cells() const { diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index 4a055d3cb1..51a7d9a90d 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -103,11 +103,8 @@ class FlowField { * Build the flow field. * * @param integration_field Integration field. - * @param target_cells Target cells of the flow field. These cells are ignored - * when building the field. */ - void build(const std::shared_ptr &integration_field, - const std::unordered_set &target_cells = {}); + void build(const std::shared_ptr &integration_field); /** * Build the flow field for a portal. diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 0219dd17f3..1d95c9225a 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -202,6 +202,7 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie // Move outwards from the target cell, updating the integration field this->cells[target_idx].cost = INTEGRATED_COST_START; + this->cells[target_idx].flags |= INTEGRATE_TARGET_MASK; this->integrate_cost(cost_field, {target_idx}); } @@ -225,6 +226,7 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie // Set the cost of all target cells to the start value this->cells[target_idx].cost = INTEGRATED_COST_START; + this->cells[target_idx].flags |= INTEGRATE_TARGET_MASK; start_cells.push_back(target_idx); // TODO: Transfer flags and cost from the other integration field From 60cf82a0f454233c2ace88ecb4acf8bde202d22b Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 22 May 2024 01:10:42 +0200 Subject: [PATCH 515/771] path: Fix time unit log (microseconds instead of picoseconds). --- libopenage/pathfinding/demo/demo_1.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 801eea045d..3d00d31b0c 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -133,7 +133,7 @@ void path_demo_1(const util::Path &path) { path_result = pathfinder->get_path(new_path_request); timer.stop(); - log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps"); + log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " us"); if (path_result.status == PathResult::FOUND) { // Create renderables for the waypoints of the path @@ -153,7 +153,7 @@ void path_demo_1(const util::Path &path) { path_result = pathfinder->get_path(new_path_request); timer.stop(); - log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps"); + log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " us"); if (path_result.status == PathResult::FOUND) { // Create renderables for the waypoints of the path From 6c7a471ba71e532073325df81ff3d0716e8a201b Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 25 May 2024 06:30:02 +0200 Subject: [PATCH 516/771] coord: Make tile_per_chunk type-compatible with tile_t. --- libopenage/coord/chunk.cpp | 4 ++-- libopenage/coord/chunk.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libopenage/coord/chunk.cpp b/libopenage/coord/chunk.cpp index 19cb606295..62ab65be32 100644 --- a/libopenage/coord/chunk.cpp +++ b/libopenage/coord/chunk.cpp @@ -8,11 +8,11 @@ namespace openage { namespace coord { -tile_delta chunk_delta::to_tile(size_t tiles_per_chunk) const { +tile_delta chunk_delta::to_tile(tile_t tiles_per_chunk) const { return tile_delta{this->ne * tiles_per_chunk, this->se * tiles_per_chunk}; } -tile chunk::to_tile(size_t tiles_per_chunk) const { +tile chunk::to_tile(tile_t tiles_per_chunk) const { return tile{this->ne * tiles_per_chunk, this->se * tiles_per_chunk}; } diff --git a/libopenage/coord/chunk.h b/libopenage/coord/chunk.h index 0b5a1d50c0..158888c9a1 100644 --- a/libopenage/coord/chunk.h +++ b/libopenage/coord/chunk.h @@ -13,13 +13,13 @@ namespace coord { struct chunk_delta : CoordNeSeRelative { using CoordNeSeRelative::CoordNeSeRelative; - tile_delta to_tile(size_t tiles_per_chunk) const; + tile_delta to_tile(tile_t tiles_per_chunk) const; }; struct chunk : CoordNeSeAbsolute { using CoordNeSeAbsolute::CoordNeSeAbsolute; - tile to_tile(size_t tiles_per_chunk) const; + tile to_tile(tile_t tiles_per_chunk) const; }; struct chunk3_delta : CoordNeSeUpRelative { From 308c913aaae67301b65e013d11e37b03664efc25 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 25 May 2024 12:26:58 +0200 Subject: [PATCH 517/771] gamestate: Fix setting recent angle position. --- libopenage/gamestate/component/internal/position.cpp | 4 ++-- libopenage/gamestate/system/move.cpp | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/libopenage/gamestate/component/internal/position.cpp b/libopenage/gamestate/component/internal/position.cpp index 61611382cf..7c4b7bbb83 100644 --- a/libopenage/gamestate/component/internal/position.cpp +++ b/libopenage/gamestate/component/internal/position.cpp @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the openage authors. See copying.md for legal info. +// Copyright 2021-2024 the openage authors. See copying.md for legal info. #include "position.h" @@ -60,7 +60,7 @@ const curve::Segmented &Position::get_angles() const { void Position::set_angle(const time::time_t &time, const coord::phys_angle_t &angle) { auto old_angle = this->angle.get(time); - this->angle.set_insert_jump(time, old_angle, angle); + this->angle.set_last_jump(time, old_angle, angle); } } // namespace openage::gamestate::component diff --git a/libopenage/gamestate/system/move.cpp b/libopenage/gamestate/system/move.cpp index 158beea6ac..0031a2624d 100644 --- a/libopenage/gamestate/system/move.cpp +++ b/libopenage/gamestate/system/move.cpp @@ -127,7 +127,6 @@ const time::time_t Move::move_default(const std::shared_ptrset_position(start_time, current_pos); - auto prev_angle = current_angle; for (size_t i = 1; i < waypoints.size(); ++i) { auto prev_waypoint = waypoints[i - 1]; auto cur_waypoint = waypoints[i]; @@ -153,6 +152,9 @@ const time::time_t Move::move_default(const std::shared_ptrget(); total_time += turn_time; pos_component->set_position(start_time + total_time, prev_waypoint); + + // update current angle for next waypoint + current_angle = path_angle; } pos_component->set_angle(start_time + total_time, path_angle); From e7f7ed98be5473d7a3695ea4b132dcb923bbd137 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 25 May 2024 12:52:48 +0200 Subject: [PATCH 518/771] coord: Reorder tiles and fix missing conversions. --- libopenage/coord/tile.cpp | 47 +++++++++++++++++++++++---------------- libopenage/coord/tile.h | 9 +++----- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/libopenage/coord/tile.cpp b/libopenage/coord/tile.cpp index e20868ff98..b1eb9593a4 100644 --- a/libopenage/coord/tile.cpp +++ b/libopenage/coord/tile.cpp @@ -8,55 +8,64 @@ namespace openage::coord { +phys2_delta tile_delta::to_phys2() const { + return phys2_delta{phys2::elem_t::from_int(this->ne), + phys2::elem_t::from_int(this->se)}; +} -tile3 tile::to_tile3(tile_t up) const { - return tile3(this->ne, this->se, up); +phys3_delta tile_delta::to_phys3(tile_t up) const { + return phys3_delta{phys3::elem_t::from_int(this->ne), + phys3::elem_t::from_int(this->se), + phys3::elem_t::from_int(up)}; } +tile3 tile::to_tile3(tile_t up) const { + return tile3{this->ne, this->se, up}; +} phys2 tile::to_phys2() const { - return phys2{phys3::elem_t::from_int(this->ne), phys3::elem_t::from_int(this->se)}; + return phys2{phys3::elem_t::from_int(this->ne), + phys3::elem_t::from_int(this->se)}; } - phys3 tile::to_phys3(tile_t up) const { return this->to_tile3(up).to_phys3(); } - chunk tile::to_chunk() const { return chunk{ static_cast(util::div(this->ne, tiles_per_chunk)), static_cast(util::div(this->se, tiles_per_chunk))}; } - tile_delta tile::get_pos_on_chunk() const { return tile_delta{ util::mod(this->ne, tiles_per_chunk), util::mod(this->se, tiles_per_chunk)}; } - -phys2 tile3::to_phys2() const { - return this->to_tile().to_phys2(); +tile_delta tile3_delta::to_tile() const { + return tile_delta{this->ne, this->se}; } - -phys3 tile3::to_phys3() const { - return phys3{ - phys3::elem_t::from_int(this->ne), - phys3::elem_t::from_int(this->se), - phys3::elem_t::from_int(this->up)}; +phys3_delta tile3_delta::to_phys3() const { + return phys3_delta{phys3::elem_t::from_int(this->ne), + phys3::elem_t::from_int(this->se), + phys3::elem_t::from_int(up)}; } +tile tile3::to_tile() const { + return tile{this->ne, this->se}; +} -phys2_delta tile_delta::to_phys2() const { - return phys2_delta(this->ne, this->se); +phys2 tile3::to_phys2() const { + return this->to_tile().to_phys2(); } -phys3_delta tile_delta::to_phys3(tile_t up) const { - return phys3_delta(this->ne, this->se, up); +phys3 tile3::to_phys3() const { + return phys3{phys3::elem_t::from_int(this->ne), + phys3::elem_t::from_int(this->se), + phys3::elem_t::from_int(this->up)}; } } // namespace openage::coord diff --git a/libopenage/coord/tile.h b/libopenage/coord/tile.h index e8e61a09c2..1fff2efa21 100644 --- a/libopenage/coord/tile.h +++ b/libopenage/coord/tile.h @@ -20,6 +20,7 @@ namespace coord { struct tile_delta : CoordNeSeRelative { using CoordNeSeRelative::CoordNeSeRelative; + // coordinate conversions phys2_delta to_phys2() const; phys3_delta to_phys3(tile_t up = 0) const; }; @@ -45,9 +46,7 @@ struct tile3_delta : CoordNeSeUpRelative { // coordinate conversions // simply discards the UP component of the coordinate delta. - constexpr tile_delta to_tile() const { - return tile_delta{this->ne, this->se}; - } + tile_delta to_tile() const; phys3_delta to_phys3() const; }; @@ -56,9 +55,7 @@ struct tile3 : CoordNeSeUpAbsolute { // coordinate conversions // simply discards the UP component of the coordinate. - constexpr tile to_tile() const { - return tile{this->ne, this->se}; - } + tile to_tile() const; phys2 to_phys2() const; phys3 to_phys3() const; }; From 58281773e4913792b75c19ec6f553193ef416e4d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 25 May 2024 12:58:39 +0200 Subject: [PATCH 519/771] gamestate: Use tile center for waypoints in path. --- libopenage/coord/tile.cpp | 24 +++++++++++++++++++++++- libopenage/coord/tile.h | 7 +++++++ libopenage/gamestate/system/move.cpp | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/libopenage/coord/tile.cpp b/libopenage/coord/tile.cpp index b1eb9593a4..e447819799 100644 --- a/libopenage/coord/tile.cpp +++ b/libopenage/coord/tile.cpp @@ -1,4 +1,4 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. +// Copyright 2016-2024 the openage authors. See copying.md for legal info. #include "tile.h" @@ -32,6 +32,17 @@ phys3 tile::to_phys3(tile_t up) const { return this->to_tile3(up).to_phys3(); } +phys2 tile::to_phys2_center() const { + return phys2{phys3::elem_t::from_int(this->ne) + 0.5, + phys3::elem_t::from_int(this->se) + 0.5}; +} + +phys3 tile::to_phys3_center(tile_t up) const { + return phys3{phys3::elem_t::from_int(this->ne) + 0.5, + phys3::elem_t::from_int(this->se) + 0.5, + phys3::elem_t::from_int(up)}; +} + chunk tile::to_chunk() const { return chunk{ static_cast(util::div(this->ne, tiles_per_chunk)), @@ -68,4 +79,15 @@ phys3 tile3::to_phys3() const { phys3::elem_t::from_int(this->up)}; } +phys2 tile3::to_phys2_center() const { + return phys2{phys3::elem_t::from_int(this->ne) + 0.5, + phys3::elem_t::from_int(this->se) + 0.5}; +} + +phys3 tile3::to_phys3_center() const { + return phys3{phys3::elem_t::from_int(this->ne) + 0.5, + phys3::elem_t::from_int(this->se) + 0.5, + phys3::elem_t::from_int(this->up)}; +} + } // namespace openage::coord diff --git a/libopenage/coord/tile.h b/libopenage/coord/tile.h index 1fff2efa21..ec685b8002 100644 --- a/libopenage/coord/tile.h +++ b/libopenage/coord/tile.h @@ -35,8 +35,12 @@ struct tile : CoordNeSeAbsolute { * elevation. */ tile3 to_tile3(tile_t up = 0) const; + phys2 to_phys2() const; phys3 to_phys3(tile_t up = 0) const; + phys2 to_phys2_center() const; + phys3 to_phys3_center(tile_t up = 0) const; + chunk to_chunk() const; tile_delta get_pos_on_chunk() const; }; @@ -56,8 +60,11 @@ struct tile3 : CoordNeSeUpAbsolute { // coordinate conversions // simply discards the UP component of the coordinate. tile to_tile() const; + phys2 to_phys2() const; phys3 to_phys3() const; + phys2 to_phys2_center() const; + phys3 to_phys3_center() const; }; diff --git a/libopenage/gamestate/system/move.cpp b/libopenage/gamestate/system/move.cpp index 0031a2624d..3da45ceab3 100644 --- a/libopenage/gamestate/system/move.cpp +++ b/libopenage/gamestate/system/move.cpp @@ -64,7 +64,7 @@ std::vector find_path(const std::shared_ptr &pat // Pathfinder waypoints contain start and end tile; we can ignore them for (size_t i = 1; i < tile_path.waypoints.size() - 1; ++i) { auto tile = tile_path.waypoints.at(i); - path.push_back(tile.to_phys3()); + path.push_back(tile.to_phys3_center()); } // End position is last waypoint From d5d437239c38dd2d653b93011ad9529be15ccb66 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 25 May 2024 22:14:14 +0200 Subject: [PATCH 520/771] path: Reset with std::fill. --- libopenage/pathfinding/flow_field.cpp | 13 ++++++++++--- libopenage/pathfinding/flow_field.h | 11 +++++++++++ libopenage/pathfinding/integration_field.cpp | 14 +++++++++++--- libopenage/pathfinding/integration_field.h | 11 +++++++++++ 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index c005e6cf05..3b52aeedcf 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -254,11 +254,18 @@ const std::vector &FlowField::get_cells() const { } void FlowField::reset() { - for (auto &cell : this->cells) { - cell = FLOW_INIT; - } + std::fill(this->cells.begin(), this->cells.end(), FLOW_INIT); log::log(DBG << "Flow field has been reset"); } +void FlowField::reset_dynamic_flags() { + flow_t mask = 0xFF & ~(FLOW_LOS_MASK | FLOW_WAVEFRONT_BLOCKED_MASK); + for (flow_t &cell : this->cells) { + cell = cell & mask; + } + + log::log(DBG << "Flow field dynamic flags have been reset"); +} + } // namespace openage::path diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index 51a7d9a90d..6d9aeaeb63 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -131,6 +131,17 @@ class FlowField { */ void reset(); + /** + * Reset all flags that are dependent on the path target location. These + * flags should be removed when the field is cached and reused for + * other targets. + * + * Relevant flags are: + * - FLOW_LOS_MASK + * - FLOW_WAVEFRONT_BLOCKED_MASK + */ + void reset_dynamic_flags(); + private: /** * Side length of the field. diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 1d95c9225a..0271f32f37 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -299,12 +299,20 @@ const std::vector &IntegrationField::get_cells() const { } void IntegrationField::reset() { - for (auto &cell : this->cells) { - cell = INTEGRATE_INIT; - } + std::fill(this->cells.begin(), this->cells.end(), INTEGRATE_INIT); + log::log(DBG << "Integration field has been reset"); } +void IntegrationField::reset_dynamic_flags() { + integrated_flags_t mask = 0xFF & ~(INTEGRATE_LOS_MASK | INTEGRATE_WAVEFRONT_BLOCKED_MASK); + for (integrated_t &cell : this->cells) { + cell.flags = cell.flags & mask; + } + + log::log(DBG << "Integration field dynamic flags have been reset"); +} + void IntegrationField::update_neighbor(size_t idx, cost_t cell_cost, integrated_cost_t integrated_cost, diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 40e6780c8f..7cce6d2529 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -137,6 +137,17 @@ class IntegrationField { */ void reset(); + /** + * Reset all flags that are dependent on the path target location. These + * flags should be removed when the field is cached and reused for + * other targets. + * + * Relevant flags are: + * - INTEGRATE_LOS_MASK + * - INTEGRATE_WAVEFRONT_BLOCKED_MASK + */ + void reset_dynamic_flags(); + private: /** * Update a neigbor cell during the cost integration process. From d51ebba060888034bedb4f3d158d1ddb4cd4452d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 25 May 2024 23:46:56 +0200 Subject: [PATCH 521/771] patg: Cache fields during pathing. --- libopenage/pathfinding/integrator.cpp | 26 ++++++++++++++++++++++++++ libopenage/pathfinding/integrator.h | 25 ++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 4ffdedff15..0802f00ee5 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -5,6 +5,7 @@ #include "pathfinding/cost_field.h" #include "pathfinding/flow_field.h" #include "pathfinding/integration_field.h" +#include "pathfinding/portal.h" namespace openage::path { @@ -22,6 +23,12 @@ std::shared_ptr Integrator::integrate(const std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field, sector_id_t other_sector_id, const std::shared_ptr &portal) { + auto cache_key = std::make_pair(portal->get_id(), other_sector_id); + auto cached = this->field_cache.find(cache_key); + if (cached != this->field_cache.end()) { + return cached->second.first; + } + auto integration_field = std::make_shared(cost_field->get_size()); integration_field->integrate_cost(cost_field, other_sector_id, portal); @@ -39,6 +46,12 @@ std::shared_ptr Integrator::build(const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal) { + auto cache_key = std::make_pair(portal->get_id(), other_sector_id); + auto cached = this->field_cache.find(cache_key); + if (cached != this->field_cache.end()) { + return cached->second.second; + } + auto flow_field = std::make_shared(integration_field->get_size()); flow_field->build(integration_field, other, other_sector_id, portal); @@ -57,9 +70,22 @@ Integrator::build_return_t Integrator::build(const std::shared_ptr &c const std::shared_ptr &other_integration_field, sector_id_t other_sector_id, const std::shared_ptr &portal) { + auto cache_key = std::make_pair(portal->get_id(), other_sector_id); + auto cached = this->field_cache.find(cache_key); + if (cached != this->field_cache.end()) { + return cached->second; + } + auto integration_field = this->integrate(cost_field, other_sector_id, portal); auto flow_field = this->build(integration_field, other_integration_field, other_sector_id, portal); + // Copy the fields to the cache. + std::shared_ptr cached_integration_field = std::make_shared(*integration_field); + cached_integration_field->reset_dynamic_flags(); + std::shared_ptr cached_flow_field = std::make_shared(*flow_field); + cached_flow_field->reset_dynamic_flags(); + this->field_cache[cache_key] = std::make_pair(cached_integration_field, cached_flow_field); + return std::make_pair(integration_field, flow_field); } diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index ef9c2780cd..660f8cbb7c 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -4,8 +4,10 @@ #include #include +#include #include "pathfinding/types.h" +#include "util/hash.h" namespace openage { @@ -102,7 +104,28 @@ class Integrator { const std::shared_ptr &portal); private: - // TODO: Cache created flow fields. + /** + * Hash function for the field cache. + */ + struct pair_hash { + template + std::size_t operator()(const std::pair &pair) const { + return util::hash_combine(std::hash{}(pair.first), std::hash{}(pair.second)); + } + }; + + /** + * Cache for already computed fields. + * + * Key is the portal ID and the sector ID from which the field was entered. Fields that are cached are + * cleared of dynamic flags, i.e. wavefront or LOS flags. These have to be recalculated + * when the field is reused. + */ + std::unordered_map, + std::pair, + std::shared_ptr>, + pair_hash> + field_cache; }; } // namespace path From e849754d33d6be80ba8f2cc8104f77f71320c81a Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 May 2024 00:31:52 +0200 Subject: [PATCH 522/771] path: Clear LOS next wave list before next loop iteration. --- libopenage/pathfinding/integration_field.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 0271f32f37..ce98114de2 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -183,6 +183,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptr Date: Sun, 26 May 2024 00:56:33 +0200 Subject: [PATCH 523/771] path: Only add cells with blocked flag to wavefront list. --- libopenage/pathfinding/integration_field.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index ce98114de2..1b42de0ef2 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -136,6 +136,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[idx].cost = cost - 1 + cost_field->get_cost(idx); + // TODO: this->cells[idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; } // check each neighbor for a corner @@ -154,8 +155,9 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[blocked_idx].flags &= ~INTEGRATE_LOS_MASK; + + wavefront_blocked.push_back(blocked_idx); } - wavefront_blocked.insert(wavefront_blocked.end(), blocked_cells.begin(), blocked_cells.end()); } continue; } From 805148cc76df3f5927c2bc1f7befd0d16bbbe192 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 May 2024 02:37:40 +0200 Subject: [PATCH 524/771] path: Add flag for found LOS cells instead of lookup set. --- libopenage/pathfinding/definitions.h | 7 ++++++- libopenage/pathfinding/integration_field.cpp | 12 ++++-------- libopenage/pathfinding/types.h | 2 ++ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h index 4e184406cd..a54dff576a 100644 --- a/libopenage/pathfinding/definitions.h +++ b/libopenage/pathfinding/definitions.h @@ -55,7 +55,12 @@ constexpr integrated_flags_t INTEGRATE_WAVEFRONT_BLOCKED_MASK = 0x02; /** * Target flag in an integrated_flags_t value. */ -constexpr integrated_flags_t INTEGRATE_TARGET_MASK = 0x40; +constexpr integrated_flags_t INTEGRATE_TARGET_MASK = 0x04; + +/** + * Found flag in an integrated_flags_t value. + */ +constexpr integrated_flags_t INTEGRATE_FOUND_MASK = 0x80; /** * Initial value for a cell in the integration grid. diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 1b42de0ef2..3916c40120 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -88,10 +88,6 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrsize; - // Lookup table for cells that have been found - std::unordered_set found; - found.reserve(this->size * this->size); - // Cells that still have to be visited by the current wave std::deque current_wave; @@ -108,19 +104,19 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[idx].flags & INTEGRATE_FOUND_MASK) { + // Skip cells that are already in the line of sight continue; } else if (this->cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { // Stop at cells that are blocked by a LOS corner this->cells[idx].cost = cost - 1 + cost_field->get_cost(idx); - found.insert(idx); + this->cells[idx].flags |= INTEGRATE_FOUND_MASK; continue; } // Add the current cell to the found cells - found.insert(idx); + this->cells[idx].flags |= INTEGRATE_FOUND_MASK; // Get the x and y coordinates of the current cell auto x = idx % this->size; diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index b90e932323..bf5a7de473 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -51,6 +51,8 @@ struct integrated_t { /** * Flags. * + * Bit 0: Found flag. + * Bit 5: Target flag. * Bit 6: Wave front blocked flag. * Bit 7: Line of sight flag. */ From b48c68a23b01d82527dfdd6e7b099c722bd2be1b Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 26 May 2024 03:25:05 +0200 Subject: [PATCH 525/771] path: Remove lookup set from cost integration pass. --- libopenage/pathfinding/integration_field.cpp | 20 ++++---------------- libopenage/pathfinding/integration_field.h | 4 +--- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 3916c40120..38639b89fe 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -238,10 +238,6 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, std::vector &&start_cells) { - // Lookup table for cells that are in the open list - std::unordered_set in_list; - in_list.reserve(this->size * this->size); - // Cells that still have to be visited // they may be visited multiple times std::deque open_list; @@ -281,15 +277,11 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie this->update_neighbor(neighbor_idx, cost_field->get_cost(neighbor_idx), integrated_current, - open_list, - in_list); + open_list); } // Clear the neighbors vector neighbors.clear(); - - // Remove the current cell from the open list lookup table - in_list.erase(idx); } } @@ -315,8 +307,7 @@ void IntegrationField::reset_dynamic_flags() { void IntegrationField::update_neighbor(size_t idx, cost_t cell_cost, integrated_cost_t integrated_cost, - std::deque &open_list, - std::unordered_set &in_list) { + std::deque &open_list) { ENSURE(cell_cost > COST_INIT, "cost field cell value must be non-zero"); // Check if the cell is impassable @@ -326,15 +317,12 @@ void IntegrationField::update_neighbor(size_t idx, } auto cost = integrated_cost + cell_cost; - if (cost < this->cells.at(idx).cost) { + if (cost < this->cells[idx].cost) { // If the new integration value is smaller than the current one, // update the cell and add it to the open list this->cells[idx].cost = cost; - if (not in_list.contains(idx)) { - in_list.insert(idx); - open_list.push_back(idx); - } + open_list.push_back(idx); } } diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 7cce6d2529..ad0b879b83 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -156,15 +156,13 @@ class IntegrationField { * @param cell_cost Cost of the neighbor cell from the cost field. * @param integrated_cost Current integrated cost of the updating cell in the integration field. * @param open_list List of cells to be updated. - * @param in_list Set of cells that have been updated. * * @return New integration value of the cell. */ void update_neighbor(size_t idx, cost_t cell_cost, integrated_cost_t integrated_cost, - std::deque &open_list, - std::unordered_set &in_list); + std::deque &open_list); /** * Get the LOS corners around a cell. From 352996fe3fdc30cb0dc035dd8bddb03b10a74470 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 30 May 2024 14:02:45 +0200 Subject: [PATCH 526/771] path: Check if target cell is out of bounds for grid. --- libopenage/pathfinding/grid.h | 8 ++++---- libopenage/pathfinding/pathfinder.cpp | 13 +++++++++++++ libopenage/pathfinding/types.h | 2 ++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/libopenage/pathfinding/grid.h b/libopenage/pathfinding/grid.h index 83b4c2b721..8f34a7643c 100644 --- a/libopenage/pathfinding/grid.h +++ b/libopenage/pathfinding/grid.h @@ -49,16 +49,16 @@ class Grid { grid_id_t get_id() const; /** - * Get the size of the grid. + * Get the size of the grid (in number of sectors). * - * @return Size of the grid (width x height). + * @return Size of the grid (in number of sectors) (width x height). */ const util::Vector2s &get_size() const; /** - * Get the side length of the sectors on the grid. + * Get the side length of the sectors on the grid (in number of cells). * - * @return Sector side length. + * @return Sector side length (in number of cells). */ size_t get_sector_size() const; diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 22662c9e9d..9da9b4a1c9 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -23,6 +23,19 @@ const Path Pathfinder::get_path(const PathRequest &request) { auto grid = this->grids.at(request.grid_id); auto sector_size = grid->get_sector_size(); + // Check if the target is within the grid + auto grid_size = grid->get_size(); + auto grid_width = grid_size[0] * sector_size; + auto grid_height = grid_size[1] * sector_size; + if (request.target.ne < 0 + or request.target.se < 0 + or request.target.ne >= grid_width + or request.target.se >= grid_height) { + log::log(INFO << "Path not found (start = " << request.start << "; target = " << request.target << ")"); + log::log(DBG << "Target is out of bounds."); + return Path{request.grid_id, PathResult::OUT_OF_BOUNDS, {}}; + } + auto start_sector_x = request.start.ne / sector_size; auto start_sector_y = request.start.se / sector_size; auto start_sector = grid->get_sector(start_sector_x, start_sector_y); diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index bf5a7de473..3285d9d170 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -16,6 +16,8 @@ enum class PathResult { FOUND, /// Path was not found. NOT_FOUND, + /// Target is not on grid. + OUT_OF_BOUNDS, }; /** From 906e47a7bb64f63ab8d42c11017a23d8a6769a74 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 31 May 2024 13:51:04 +0200 Subject: [PATCH 527/771] path: Set PATHABLE and LOS flags for target cells in flow field. --- libopenage/pathfinding/flow_field.cpp | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 3b52aeedcf..2641b343fb 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -68,11 +68,6 @@ void FlowField::build(const std::shared_ptr &integration_field for (size_t x = 0; x < this->size; ++x) { size_t idx = y * this->size + x; - if (integrate_cells[idx].flags & INTEGRATE_TARGET_MASK) { - // Ignore target cells - continue; - } - if (integrate_cells[idx].cost == INTEGRATED_COST_UNREACHABLE) { // Cell cannot be used as path continue; @@ -80,24 +75,32 @@ void FlowField::build(const std::shared_ptr &integration_field if (integrate_cells[idx].flags & INTEGRATE_LOS_MASK) { // Cell is in line of sight - this->cells[idx] = this->cells[idx] | FLOW_LOS_MASK; + flow_cells[idx] |= FLOW_LOS_MASK; // we can skip calculating the flow direction as we can // move straight to the target from this cell - this->cells[idx] = this->cells[idx] | FLOW_PATHABLE_MASK; + flow_cells[idx] |= FLOW_PATHABLE_MASK; continue; } if (integrate_cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { // Cell is blocked by a line-of-sight corner - this->cells[idx] = this->cells[idx] | FLOW_WAVEFRONT_BLOCKED_MASK; + flow_cells[idx] |= FLOW_WAVEFRONT_BLOCKED_MASK; + } + + if (integrate_cells[idx].flags & INTEGRATE_TARGET_MASK) { + // target cells are pathable + flow_cells[idx] |= FLOW_PATHABLE_MASK; + + // they also have a preset flow direction so we can skip here + continue; } // Store which of the non-diagonal directions are unreachable. std::bitset<4> directions_unreachable; // Find the neighbor with the smallest cost. - flow_dir_t direction = static_cast(this->cells[idx] & FLOW_DIR_MASK); + flow_dir_t direction = static_cast(flow_cells[idx] & FLOW_DIR_MASK); auto smallest_cost = INTEGRATED_COST_UNREACHABLE; if (y > 0) { auto cost = integrate_cells[idx - this->size].cost; @@ -174,10 +177,10 @@ void FlowField::build(const std::shared_ptr &integration_field } // Set the flow field cell to pathable. - flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; + flow_cells[idx] |= FLOW_PATHABLE_MASK; // Set the flow field cell to the direction of the smallest cost. - flow_cells[idx] = flow_cells[idx] | static_cast(direction); + flow_cells[idx] |= static_cast(direction); } } } From 4029276f196bbd49297817cf3fc9529b229229d2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 31 May 2024 14:23:45 +0200 Subject: [PATCH 528/771] path: Smooth LOS integration for target cells with cost > MIN_COST. --- libopenage/pathfinding/integration_field.cpp | 42 ++++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 38639b89fe..6b88380929 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -97,10 +97,44 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrget_cost(target_idx) > COST_MIN) { + // Do a preliminary LOS integration wave for targets that have cost > min cost + // This avoids the bresenham's line algorithm calculations + // (which wouldn't return accurate results for blocker == target) + // and makes sure that sorrounding cells that are min cost are considered + // in line-of-sight. + + this->cells[target_idx].cost = cost; + this->cells[target_idx].flags |= INTEGRATE_FOUND_MASK; + // ASDF: Fix display in demo 0 + // this->cells[target_idx].flags |= INTEGRATE_LOS_MASK; + + // Add neighbors to current wave + if (target.se > 0) { + current_wave.push_back(target_idx - this->size); + } + if (target.ne > 0) { + current_wave.push_back(target_idx - 1); + } + if (target.se < static_cast(this->size - 1)) { + current_wave.push_back(target_idx + this->size); + } + if (target.ne < static_cast(this->size - 1)) { + current_wave.push_back(target_idx + 1); + } + + // Increment wave cost as we technically handled the first wave in this block + cost += 1; + } + else { + // Move outwards from the target cell, updating the integration field + current_wave.push_back(target_idx); + } + do { - while (!current_wave.empty()) { + while (not current_wave.empty()) { + // inner loop: handle a wave + auto idx = current_wave.front(); current_wave.pop_front(); @@ -183,7 +217,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptr Date: Sat, 1 Jun 2024 04:09:56 +0200 Subject: [PATCH 529/771] path: Start LOS pass from configurable start cells. --- libopenage/pathfinding/integration_field.cpp | 201 ++++++++++++++----- libopenage/pathfinding/integration_field.h | 25 ++- 2 files changed, 174 insertions(+), 52 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 6b88380929..ab1dbb3c0b 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -37,45 +37,178 @@ const integrated_t &IntegrationField::get_cell(size_t idx) const { return this->cells.at(idx); } +std::vector IntegrationField::integrate_los(const std::shared_ptr &cost_field, + const coord::tile_delta &target) { + ENSURE(cost_field->get_size() == this->get_size(), + "cost field size " + << cost_field->get_size() << "x" << cost_field->get_size() + << " does not match integration field size " + << this->get_size() << "x" << this->get_size()); + + ENSURE(target.ne >= 0 + and target.se >= 0 + and target.ne < static_cast(this->size) + and target.se < static_cast(this->size), + "target cell (" << target.ne << ", " << target.se << ") " + << "is out of bounds for integration field of size " + << this->size << "x" << this->size); + + std::vector start_cells; + integrated_cost_t start_cost = INTEGRATED_COST_START; + + // Target cell index + auto target_idx = target.ne + target.se * this->size; + + this->cells[target_idx].cost = start_cost; + this->cells[target_idx].flags |= INTEGRATE_TARGET_MASK; + + if (cost_field->get_cost(target_idx) > COST_MIN) { + // Do a preliminary LOS integration wave for targets that have cost > min cost + // This avoids the bresenham's line algorithm calculations + // (which wouldn't return accurate results for blocker == target) + // and makes sure that sorrounding cells that are min cost are considered + // in line-of-sight. + + this->cells[target_idx].flags |= INTEGRATE_FOUND_MASK; + // ASDF: Fix display in demo 0 + // this->cells[target_idx].flags |= INTEGRATE_LOS_MASK; + + // Add neighbors to current wave + if (target.se > 0) { + start_cells.push_back(target_idx - this->size); + } + if (target.ne > 0) { + start_cells.push_back(target_idx - 1); + } + if (target.se < static_cast(this->size - 1)) { + start_cells.push_back(target_idx + this->size); + } + if (target.ne < static_cast(this->size - 1)) { + start_cells.push_back(target_idx + 1); + } + + // Increment wave cost as we technically handled the first wave in this block + start_cost += 1; + } + else { + // Move outwards from the target cell, updating the integration field + start_cells.push_back(target_idx); + } + + return this->integrate_los(cost_field, target, start_cost, std::move(start_cells)); +} + std::vector IntegrationField::integrate_los(const std::shared_ptr &cost_field, sector_id_t other_sector_id, - const std::shared_ptr &portal) { + const std::shared_ptr &portal, + const coord::tile_delta &target) { ENSURE(cost_field->get_size() == this->get_size(), "cost field size " << cost_field->get_size() << "x" << cost_field->get_size() << " does not match integration field size " << this->get_size() << "x" << this->get_size()); - // Integrate the cost of the cells on the exit side (this) of the portal + std::vector wavefront_blocked_portal; + std::vector start_cells; + auto exit_start = portal->get_exit_start(other_sector_id); auto exit_end = portal->get_exit_end(other_sector_id); auto entry_start = portal->get_entry_start(other_sector_id); - auto entry_end = portal->get_entry_end(other_sector_id); auto x_diff = exit_start.ne - entry_start.ne; auto y_diff = exit_start.se - entry_start.se; + // transfer masks for flags from the other side of the portal + // only LOS and wavefront blocked flags are relevant + integrated_flags_t transfer_mask = INTEGRATE_LOS_MASK | INTEGRATE_WAVEFRONT_BLOCKED_MASK; + + // every portal cell is a target cell for (auto y = exit_start.se; y <= exit_end.se; ++y) { for (auto x = exit_start.ne; x <= exit_end.ne; ++x) { - // every portal cell is a target cell + // cell index on the exit side of the portal auto target_idx = x + y * this->size; - auto source_idx = x - x_diff + (y - y_diff) * this->size; + // cell index on the entry side of the portal + auto entry_idx = x - x_diff + (y - y_diff) * this->size; // Set the cost of all target cells to the start value this->cells[target_idx].cost = INTEGRATED_COST_START; - this->cells[target_idx].flags = this->cells[source_idx].flags; + this->cells[target_idx].flags = this->cells[entry_idx].flags & transfer_mask; + + this->cells[target_idx].flags |= INTEGRATE_TARGET_MASK; + + if (not(this->cells[target_idx].flags & transfer_mask)) { + // If neither LOS nor wavefront blocked flags are set for the portal entry, + // the portal exit cell doesn't affect the LOS and we can skip further checks + continue; + } + + // Get the cost of the current cell + auto cell_cost = cost_field->get_cost(target_idx); + if (cell_cost > COST_MIN or this->cells[target_idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { + // cell blocks line of sight + + // set the blocked flag for the cell if it wasn't set already + this->cells[target_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + wavefront_blocked_portal.push_back(target_idx); + + // set the found flag for the cell, so that the start costs + // are not changed in the main LOS integration + this->cells[target_idx].flags |= INTEGRATE_FOUND_MASK; + + // check each neighbor for a corner + auto corners = this->get_los_corners(cost_field, target, coord::tile_delta(x, y)); + for (auto &corner : corners) { + // draw a line from the corner to the edge of the field + // to get the cells blocked by the corner + auto blocked_cells = this->bresenhams_line(target, corner.first, corner.second); + for (auto &blocked_idx : blocked_cells) { + if (cost_field->get_cost(blocked_idx) > COST_MIN) { + // stop if blocked_idx is not min cost + // because this idx may create a new corner + break; + } + // set the blocked flag for the cell + this->cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + + // clear los flag if it was set + this->cells[blocked_idx].flags &= ~INTEGRATE_LOS_MASK; + + wavefront_blocked_portal.push_back(blocked_idx); + } + } + continue; + } + + // the cell has the LOS flag and is added to the start cells start_cells.push_back(target_idx); } } - // TODO: Call main LOS integration function + if (start_cells.empty()) { + // Main LOS integration will not enter its loop + // so we can take a shortcut and just return the + // wavefront blocked cells we already found + return wavefront_blocked_portal; + } + + // Call main LOS integration function + auto wavefront_blocked_main = this->integrate_los(cost_field, + target, + INTEGRATED_COST_START, + std::move(start_cells)); + wavefront_blocked_main.insert(wavefront_blocked_main.end(), + wavefront_blocked_portal.begin(), + wavefront_blocked_portal.end()); + return wavefront_blocked_main; } std::vector IntegrationField::integrate_los(const std::shared_ptr &cost_field, - const coord::tile_delta &target) { + const coord::tile_delta &target, + integrated_cost_t start_cost, + std::vector &&start_wave) { ENSURE(cost_field->get_size() == this->get_size(), "cost field size " << cost_field->get_size() << "x" << cost_field->get_size() @@ -85,9 +218,6 @@ std::vector IntegrationField::integrate_los(const std::shared_ptr wavefront_blocked; - // Target cell index - auto target_idx = target.ne + target.se * this->size; - // Cells that still have to be visited by the current wave std::deque current_wave; @@ -95,42 +225,10 @@ std::vector IntegrationField::integrate_los(const std::shared_ptr next_wave; // Cost of the current wave - integrated_cost_t cost = INTEGRATED_COST_START; - - if (cost_field->get_cost(target_idx) > COST_MIN) { - // Do a preliminary LOS integration wave for targets that have cost > min cost - // This avoids the bresenham's line algorithm calculations - // (which wouldn't return accurate results for blocker == target) - // and makes sure that sorrounding cells that are min cost are considered - // in line-of-sight. - - this->cells[target_idx].cost = cost; - this->cells[target_idx].flags |= INTEGRATE_FOUND_MASK; - // ASDF: Fix display in demo 0 - // this->cells[target_idx].flags |= INTEGRATE_LOS_MASK; - - // Add neighbors to current wave - if (target.se > 0) { - current_wave.push_back(target_idx - this->size); - } - if (target.ne > 0) { - current_wave.push_back(target_idx - 1); - } - if (target.se < static_cast(this->size - 1)) { - current_wave.push_back(target_idx + this->size); - } - if (target.ne < static_cast(this->size - 1)) { - current_wave.push_back(target_idx + 1); - } - - // Increment wave cost as we technically handled the first wave in this block - cost += 1; - } - else { - // Move outwards from the target cell, updating the integration field - current_wave.push_back(target_idx); - } + integrated_cost_t wave_cost = start_cost; + // Add the start wave to the current wave + current_wave.insert(current_wave.end(), start_wave.begin(), start_wave.end()); do { while (not current_wave.empty()) { // inner loop: handle a wave @@ -144,7 +242,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { // Stop at cells that are blocked by a LOS corner - this->cells[idx].cost = cost - 1 + cost_field->get_cost(idx); + this->cells[idx].cost = wave_cost - 1 + cost_field->get_cost(idx); this->cells[idx].flags |= INTEGRATE_FOUND_MASK; continue; } @@ -165,8 +263,9 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[idx].cost = cost - 1 + cost_field->get_cost(idx); - // TODO: this->cells[idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + this->cells[idx].cost = wave_cost - 1 + cost_field->get_cost(idx); + // ASDF: Fix display in demo 0 + // this->cells[idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; } // check each neighbor for a corner @@ -194,7 +293,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[idx].cost = cost; + this->cells[idx].cost = wave_cost; this->cells[idx].flags |= INTEGRATE_LOS_MASK; // Search the neighbors of the current cell @@ -213,7 +312,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptr integrate_los(const std::shared_ptr &cost_field, sector_id_t other_sector_id, - const std::shared_ptr &portal); + const std::shared_ptr &portal, + const coord::tile_delta &target); + + /** + * Calculate the line-of-sight integration flags for a target cell. + * + * Returns a list of cells that are flagged as "wavefront blocked". These cells + * can be used as a starting point for the cost integration. + * + * @param cost_field Cost field to integrate. + * @param target Coordinates of the target cell (relative to field origin). + * @param start_cost Integration cost for the start wave. + * @param start_wave Cells used for the first LOS integration wave. The wavefront + * expands outwards from these cells. + * + * @return Cells flagged as "wavefront blocked". + */ + std::vector integrate_los(const std::shared_ptr &cost_field, + const coord::tile_delta &target, + integrated_cost_t start_cost, + std::vector &&start_wave); /** * Calculate the cost integration field starting from a target cell. From 52a6e862f2e8ad053ee4e1c2b27e0c3cb1025a3c Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 1 Jun 2024 04:47:21 +0200 Subject: [PATCH 530/771] path: Make LOS pass optional. --- libopenage/pathfinding/integrator.cpp | 39 ++++++++++++++++++++++----- libopenage/pathfinding/integrator.h | 29 +++++++++++++++----- libopenage/pathfinding/pathfinder.cpp | 5 +++- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 0802f00ee5..ac551957ce 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -11,26 +11,47 @@ namespace openage::path { std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field, - const coord::tile_delta &target) { + const coord::tile_delta &target, + bool with_los) { auto integration_field = std::make_shared(cost_field->get_size()); - auto wavefront_blocked = integration_field->integrate_los(cost_field, target); - integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); + if (with_los) { + auto wavefront_blocked = integration_field->integrate_los(cost_field, target); + integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); + } + else { + integration_field->integrate_cost(cost_field, target); + } return integration_field; } std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field, sector_id_t other_sector_id, - const std::shared_ptr &portal) { + const std::shared_ptr &portal, + const coord::tile_delta &target, + bool with_los) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); auto cached = this->field_cache.find(cache_key); if (cached != this->field_cache.end()) { + // TODO: LOS pass? + return cached->second.first; } auto integration_field = std::make_shared(cost_field->get_size()); - integration_field->integrate_cost(cost_field, other_sector_id, portal); + + std::vector wavefront_blocked; + if (with_los) { + wavefront_blocked = integration_field->integrate_los(cost_field, other_sector_id, portal, target); + } + + if (wavefront_blocked.empty()) { + integration_field->integrate_cost(cost_field, other_sector_id, portal); + } + else { + integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); + } return integration_field; } @@ -69,14 +90,18 @@ Integrator::build_return_t Integrator::build(const std::shared_ptr &c Integrator::build_return_t Integrator::build(const std::shared_ptr &cost_field, const std::shared_ptr &other_integration_field, sector_id_t other_sector_id, - const std::shared_ptr &portal) { + const std::shared_ptr &portal, + const coord::tile_delta &target, + bool with_los) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); auto cached = this->field_cache.find(cache_key); if (cached != this->field_cache.end()) { + // TODO: LOS pass? + return cached->second; } - auto integration_field = this->integrate(cost_field, other_sector_id, portal); + auto integration_field = this->integrate(cost_field, other_sector_id, portal, target, with_los); auto flow_field = this->build(integration_field, other_integration_field, other_sector_id, portal); // Copy the fields to the cache. diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index 660f8cbb7c..995c451aba 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -32,26 +32,38 @@ class Integrator { /** * Integrate the cost field for a target. * + * This should be used for the field containing the target cell. + * The target coordinates must lie within the boundaries of the cost field. + * * @param cost_field Cost field. * @param target Coordinates of the target cell. + * @param with_los true if an LOS pass should be performed, else false. * * @return Integration field. */ std::shared_ptr integrate(const std::shared_ptr &cost_field, - const coord::tile_delta &target); + const coord::tile_delta &target, + bool with_los = true); /** * Integrate the cost field from a portal. * + * This should be used for the fields on the portal path that are not the target field. + * The target coordinates must be relative to the origin of the sector the cost field belongs to. + * * @param cost_field Cost field. * @param other_sector_id Sector ID of the other side of the portal. * @param portal Portal. + * @param target Coordinates of the target cell, relative to the integration field origin. + * @param with_los true if an LOS pass should be performed, else false. * * @return Integration field. */ std::shared_ptr integrate(const std::shared_ptr &cost_field, sector_id_t other_sector_id, - const std::shared_ptr &portal); + const std::shared_ptr &portal, + const coord::tile_delta &target, + bool with_los = true); /** * Build the flow field from an integration field. @@ -85,7 +97,7 @@ class Integrator { * @param cost_field Cost field. * @param target Coordinates of the target cell. * - * @return Flow field. + * @return Integration field and flow field. */ build_return_t build(const std::shared_ptr &cost_field, const coord::tile_delta &target); @@ -97,11 +109,17 @@ class Integrator { * @param other_integration_field Integration field of the other side of the portal. * @param other_sector_id Sector ID of the other side of the portal. * @param portal Portal. + * @param target Coordinates of the target cell, relative to the integration field origin. + * @param with_los true if an LOS pass should be performed, else false. + * + * @return Integration field and flow field. */ build_return_t build(const std::shared_ptr &cost_field, const std::shared_ptr &other_integration_field, sector_id_t other_sector_id, - const std::shared_ptr &portal); + const std::shared_ptr &portal, + const coord::tile_delta &target, + bool with_los = false); private: /** @@ -122,8 +140,7 @@ class Integrator { * when the field is reused. */ std::unordered_map, - std::pair, - std::shared_ptr>, + build_return_t, pair_hash> field_cache; }; diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 9da9b4a1c9..e35d7719ae 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -129,10 +129,13 @@ const Path Pathfinder::get_path(const PathRequest &request) { auto next_sector_id = portal->get_exit_sector(prev_sector_id); auto next_sector = grid->get_sector(next_sector_id); + coord::tile_delta target_delta = request.target - next_sector->get_position().to_tile(sector_size); + sector_fields = this->integrator->build(next_sector->get_cost_field(), prev_integration_field, prev_sector_id, - portal); + portal, + target_delta); flow_fields.push_back(std::make_pair(next_sector_id, sector_fields.second)); prev_integration_field = sector_fields.first; From 7d8025c6e18f0ae080a68a5e3577cf5297f20d9c Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 1 Jun 2024 04:53:51 +0200 Subject: [PATCH 531/771] path: Simplify calculations for start/target coordinate deltas. --- libopenage/pathfinding/pathfinder.cpp | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index e35d7719ae..75b2591436 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -45,16 +45,14 @@ const Path Pathfinder::get_path(const PathRequest &request) { auto target_sector = grid->get_sector(target_sector_x, target_sector_y); // Integrate the target field - coord::tile_t target_x = request.target.ne % sector_size; - coord::tile_t target_y = request.target.se % sector_size; - auto target = coord::tile_delta{target_x, target_y}; - auto target_integration_field = this->integrator->integrate(target_sector->get_cost_field(), target); + coord::tile_delta target_delta = request.target - target_sector->get_position().to_tile(sector_size); + auto target_integration_field = this->integrator->integrate(target_sector->get_cost_field(), + target_delta); if (target_sector == start_sector) { - auto start_x = request.start.ne % sector_size; - auto start_y = request.start.se % sector_size; + auto start = request.start - start_sector->get_position().to_tile(sector_size); - if (target_integration_field->get_cell(start_x, start_y).cost != INTEGRATED_COST_UNREACHABLE) { + if (target_integration_field->get_cell(start.ne, start.se).cost != INTEGRATED_COST_UNREACHABLE) { // Exit early if the start and target are in the same sector // and are reachable from within the same sector auto flow_field = this->integrator->build(target_integration_field); @@ -83,10 +81,9 @@ const Path Pathfinder::get_path(const PathRequest &request) { } // Check which portals are reachable from the start field - coord::tile_t start_x = request.start.ne % sector_size; - coord::tile_t start_y = request.start.se % sector_size; - auto start = coord::tile_delta{start_x, start_y}; - auto start_integration_field = this->integrator->integrate(start_sector->get_cost_field(), start); + coord::tile_delta start = request.start - start_sector->get_position().to_tile(sector_size); + auto start_integration_field = this->integrator->integrate(start_sector->get_cost_field(), + start); std::unordered_set start_portal_ids; for (auto &portal : start_sector->get_portals()) { @@ -129,7 +126,7 @@ const Path Pathfinder::get_path(const PathRequest &request) { auto next_sector_id = portal->get_exit_sector(prev_sector_id); auto next_sector = grid->get_sector(next_sector_id); - coord::tile_delta target_delta = request.target - next_sector->get_position().to_tile(sector_size); + target_delta = request.target - next_sector->get_position().to_tile(sector_size); sector_fields = this->integrator->build(next_sector->get_cost_field(), prev_integration_field, From 36eafc475005a9c9b55f9e327e4844b871cca7fd Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 1 Jun 2024 04:54:37 +0200 Subject: [PATCH 532/771] path: Skip LOS pass when checking start sector for exit portals. --- libopenage/pathfinding/pathfinder.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 75b2591436..e3a1bd72ec 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -83,7 +83,8 @@ const Path Pathfinder::get_path(const PathRequest &request) { // Check which portals are reachable from the start field coord::tile_delta start = request.start - start_sector->get_position().to_tile(sector_size); auto start_integration_field = this->integrator->integrate(start_sector->get_cost_field(), - start); + start, + false); std::unordered_set start_portal_ids; for (auto &portal : start_sector->get_portals()) { From d328738d6e6fda47c8f104fabfd0d00a15fc8d77 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 1 Jun 2024 05:47:54 +0200 Subject: [PATCH 533/771] path: Exit waypoinnt search if any LOS cell is found. --- libopenage/pathfinding/pathfinder.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index e3a1bd72ec..d2fef7b2b9 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -307,6 +307,8 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_dir(current_x, current_y); @@ -323,20 +325,21 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_position(); - auto cell_pos = coord::tile(sector_pos.ne * sector_size, - sector_pos.se * sector_size) + auto cell_pos = sector_pos.to_tile(sector_size) + coord::tile_delta(current_x, current_y); waypoints.push_back(cell_pos); + reached_target = true; break; } + // ASDF: break if target cell is reached + // check if we need to change direction auto cell_direction = flow_field->get_dir(coord::tile_delta(current_x, current_y)); if (cell_direction != current_direction) { // add the current cell as a waypoint auto sector_pos = sector->get_position(); - auto cell_pos = coord::tile(sector_pos.ne * sector_size, - sector_pos.se * sector_size) + auto cell_pos = sector_pos.to_tile(sector_size) + coord::tile_delta(current_x, current_y); waypoints.push_back(cell_pos); current_direction = cell_direction; @@ -377,6 +380,10 @@ const std::vector Pathfinder::get_waypoints(const std::vector Pathfinder::get_waypoints(const std::vectorget_sector(flow_fields.back().first)->get_position(); - auto target_pos = coord::tile(sector_pos.ne * sector_size, sector_pos.se * sector_size) + auto target_pos = sector_pos.to_tile(sector_size) + coord::tile_delta{target_x, target_y}; waypoints.push_back(target_pos); From 0da61fa2d66fb05a1b365d482938bf6497d2164f Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 1 Jun 2024 06:20:00 +0200 Subject: [PATCH 534/771] path: Calculate flow field directions for LOS cells to allow caching of field. --- libopenage/pathfinding/flow_field.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 2641b343fb..6c51306d4f 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -76,11 +76,6 @@ void FlowField::build(const std::shared_ptr &integration_field if (integrate_cells[idx].flags & INTEGRATE_LOS_MASK) { // Cell is in line of sight flow_cells[idx] |= FLOW_LOS_MASK; - - // we can skip calculating the flow direction as we can - // move straight to the target from this cell - flow_cells[idx] |= FLOW_PATHABLE_MASK; - continue; } if (integrate_cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { From d651de443a163f18972c54babb636ffd5b7b81e6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 1 Jun 2024 06:29:25 +0200 Subject: [PATCH 535/771] path: Transfer flags for LOS/wavefront blocked from an integration field. --- libopenage/pathfinding/flow_field.cpp | 19 +++++++++++++++++++ libopenage/pathfinding/flow_field.h | 19 +++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 6c51306d4f..3b2dd3e1da 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -266,4 +266,23 @@ void FlowField::reset_dynamic_flags() { log::log(DBG << "Flow field dynamic flags have been reset"); } +void FlowField::transfer_dynamic_flags(const std::shared_ptr &integration_field) { + auto &integrate_cells = integration_field->get_cells(); + auto &flow_cells = this->cells; + + for (size_t idx = 0; idx < integrate_cells.size(); ++idx) { + if (integrate_cells[idx].flags & INTEGRATE_LOS_MASK) { + // Cell is in line of sight + flow_cells[idx] |= FLOW_LOS_MASK; + } + + if (integrate_cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { + // Cell is blocked by a line-of-sight corner + flow_cells[idx] |= FLOW_WAVEFRONT_BLOCKED_MASK; + } + } + + log::log(DBG << "Flow field dynamic flags have been transferred"); +} + } // namespace openage::path diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index 6d9aeaeb63..5dc91fed96 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -132,8 +132,9 @@ class FlowField { void reset(); /** - * Reset all flags that are dependent on the path target location. These - * flags should be removed when the field is cached and reused for + * Reset all flags that are dependent on the path target location. + * + * These flags should be removed when the field is cached and reused for * other targets. * * Relevant flags are: @@ -142,6 +143,20 @@ class FlowField { */ void reset_dynamic_flags(); + /** + * Transfer dynamic flags from an integration field. + * + * These flags should be transferred when the field is copied from cache. + * Flow field directions are not altered. + * + * Relevant flags are: + * - FLOW_LOS_MASK + * - FLOW_WAVEFRONT_BLOCKED_MASK + * + * @param integration_field Integration field. + */ + void transfer_dynamic_flags(const std::shared_ptr &integration_field); + private: /** * Side length of the field. From da34efce76cce05c8c5cdba8acf8050181a50bbe Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 1 Jun 2024 06:43:25 +0200 Subject: [PATCH 536/771] path: Apply LOS flags when getting fields from cache. --- libopenage/pathfinding/integration_field.cpp | 7 +- libopenage/pathfinding/integration_field.h | 3 + libopenage/pathfinding/integrator.cpp | 70 ++++++++++++++++---- libopenage/pathfinding/integrator.h | 38 ++++++----- libopenage/pathfinding/pathfinder.cpp | 12 ++-- libopenage/pathfinding/tests.cpp | 2 +- 6 files changed, 92 insertions(+), 40 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index ab1dbb3c0b..fb62372d67 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -99,6 +99,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptr IntegrationField::integrate_los(const std::shared_ptr &cost_field, + const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal, const coord::tile_delta &target) { @@ -119,6 +120,8 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrget_cells(); + // transfer masks for flags from the other side of the portal // only LOS and wavefront blocked flags are relevant integrated_flags_t transfer_mask = INTEGRATE_LOS_MASK | INTEGRATE_WAVEFRONT_BLOCKED_MASK; @@ -134,7 +137,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[target_idx].cost = INTEGRATED_COST_START; - this->cells[target_idx].flags = this->cells[entry_idx].flags & transfer_mask; + this->cells[target_idx].flags = other_cells[entry_idx].flags & transfer_mask; this->cells[target_idx].flags |= INTEGRATE_TARGET_MASK; @@ -429,7 +432,7 @@ void IntegrationField::reset() { } void IntegrationField::reset_dynamic_flags() { - integrated_flags_t mask = 0xFF & ~(INTEGRATE_LOS_MASK | INTEGRATE_WAVEFRONT_BLOCKED_MASK); + integrated_flags_t mask = 0xFF & ~(INTEGRATE_LOS_MASK | INTEGRATE_WAVEFRONT_BLOCKED_MASK | INTEGRATE_FOUND_MASK); for (integrated_t &cell : this->cells) { cell.flags = cell.flags & mask; } diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index fa9e910613..a8564eccd8 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -88,6 +88,7 @@ class IntegrationField { * can be used as a starting point for the cost integration. * * @param cost_field Cost field to integrate. + * @param other Integration field of the other sector. * @param other_sector_id Sector ID of the other integration field. * @param portal Portal connecting the two fields. * @param target Coordinates of the target cell (relative to field origin). @@ -95,6 +96,7 @@ class IntegrationField { * @return Cells flagged as "wavefront blocked". */ std::vector integrate_los(const std::shared_ptr &cost_field, + const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal, const coord::tile_delta &target); @@ -168,6 +170,7 @@ class IntegrationField { * Relevant flags are: * - INTEGRATE_LOS_MASK * - INTEGRATE_WAVEFRONT_BLOCKED_MASK + * - INTEGRATE_FOUND_MASK */ void reset_dynamic_flags(); diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index ac551957ce..990dab37dd 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -27,6 +27,7 @@ std::shared_ptr Integrator::integrate(const std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field, + const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal, const coord::tile_delta &target, @@ -34,22 +35,36 @@ std::shared_ptr Integrator::integrate(const std::shared_ptrget_id(), other_sector_id); auto cached = this->field_cache.find(cache_key); if (cached != this->field_cache.end()) { - // TODO: LOS pass? + if (with_los) { + // Make a copy of the cached field to avoid modifying the cached field + auto integration_field = std::make_shared(*cached->second.first); + // Only integrate LOS; leave the rest of the field as is + integration_field->integrate_los(cost_field, other, other_sector_id, portal, target); + + return integration_field; + } return cached->second.first; } + // Create a new integration field auto integration_field = std::make_shared(cost_field->get_size()); + // LOS pass std::vector wavefront_blocked; if (with_los) { - wavefront_blocked = integration_field->integrate_los(cost_field, other_sector_id, portal, target); + wavefront_blocked = integration_field->integrate_los(cost_field, other, other_sector_id, portal, target); } + // Cost integration if (wavefront_blocked.empty()) { + // No LOS pass or no blocked cells + // use the portal as the target integration_field->integrate_cost(cost_field, other_sector_id, portal); } else { + // LOS pass was performed and some cells were blocked + // use the blocked cells as the start wave integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); } @@ -66,10 +81,21 @@ std::shared_ptr Integrator::build(const std::shared_ptr Integrator::build(const std::shared_ptr &integration_field, const std::shared_ptr &other, sector_id_t other_sector_id, - const std::shared_ptr &portal) { + const std::shared_ptr &portal, + bool with_los) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); auto cached = this->field_cache.find(cache_key); if (cached != this->field_cache.end()) { + if (with_los) { + // Make a copy of the cached flow field + auto flow_field = std::make_shared(*cached->second.second); + + // Transfer the LOS flags to the flow field + flow_field->transfer_dynamic_flags(integration_field); + + return flow_field; + } + return cached->second.second; } @@ -79,36 +105,52 @@ std::shared_ptr Integrator::build(const std::shared_ptr &cost_field, - const coord::tile_delta &target) { +Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_field, + const coord::tile_delta &target) { auto integration_field = this->integrate(cost_field, target); auto flow_field = this->build(integration_field); return std::make_pair(integration_field, flow_field); } -Integrator::build_return_t Integrator::build(const std::shared_ptr &cost_field, - const std::shared_ptr &other_integration_field, - sector_id_t other_sector_id, - const std::shared_ptr &portal, - const coord::tile_delta &target, - bool with_los) { +Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal, + const coord::tile_delta &target, + bool with_los) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); auto cached = this->field_cache.find(cache_key); if (cached != this->field_cache.end()) { - // TODO: LOS pass? + if (with_los) { + // Make a copy of the cached integration field + auto integration_field = std::make_shared(*cached->second.first); + + // Only integrate LOS; leave the rest of the field as is + integration_field->integrate_los(cost_field, other, other_sector_id, portal, target); + + // Make a copy of the cached flow field + auto flow_field = std::make_shared(*cached->second.second); + + // Transfer the LOS flags to the flow field + flow_field->transfer_dynamic_flags(integration_field); + + return std::make_pair(integration_field, flow_field); + } return cached->second; } - auto integration_field = this->integrate(cost_field, other_sector_id, portal, target, with_los); - auto flow_field = this->build(integration_field, other_integration_field, other_sector_id, portal); + auto integration_field = this->integrate(cost_field, other, other_sector_id, portal, target, with_los); + auto flow_field = this->build(integration_field, other, other_sector_id, portal); // Copy the fields to the cache. std::shared_ptr cached_integration_field = std::make_shared(*integration_field); cached_integration_field->reset_dynamic_flags(); + std::shared_ptr cached_flow_field = std::make_shared(*flow_field); cached_flow_field->reset_dynamic_flags(); + this->field_cache[cache_key] = std::make_pair(cached_integration_field, cached_flow_field); return std::make_pair(integration_field, flow_field); diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index 995c451aba..c27d3c1eb1 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -37,7 +37,7 @@ class Integrator { * * @param cost_field Cost field. * @param target Coordinates of the target cell. - * @param with_los true if an LOS pass should be performed, else false. + * @param with_los If true an LOS pass is performed before cost integration. * * @return Integration field. */ @@ -52,14 +52,16 @@ class Integrator { * The target coordinates must be relative to the origin of the sector the cost field belongs to. * * @param cost_field Cost field. + * @param other Integration field of the other side of the portal. * @param other_sector_id Sector ID of the other side of the portal. * @param portal Portal. * @param target Coordinates of the target cell, relative to the integration field origin. - * @param with_los true if an LOS pass should be performed, else false. + * @param with_los If true an LOS pass is performed before cost integration. * * @return Integration field. */ std::shared_ptr integrate(const std::shared_ptr &cost_field, + const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal, const coord::tile_delta &target, @@ -81,45 +83,47 @@ class Integrator { * @param other Integration field of the other side of the portal. * @param other_sector_id Sector ID of the other side of the portal. * @param portal Portal. + * @param with_los If true LOS flags are calculated if the flow field is in cache. * * @return Flow field. */ std::shared_ptr build(const std::shared_ptr &integration_field, const std::shared_ptr &other, sector_id_t other_sector_id, - const std::shared_ptr &portal); + const std::shared_ptr &portal, + bool with_los = true); - using build_return_t = std::pair, std::shared_ptr>; + using get_return_t = std::pair, std::shared_ptr>; /** - * Build the integration field and flow field for a target. + * Get the integration field and flow field for a target. * * @param cost_field Cost field. * @param target Coordinates of the target cell. * * @return Integration field and flow field. */ - build_return_t build(const std::shared_ptr &cost_field, - const coord::tile_delta &target); + get_return_t get(const std::shared_ptr &cost_field, + const coord::tile_delta &target); /** - * Build the integration field and flow field from a portal. + * Get the integration field and flow field from a portal. * * @param cost_field Cost field. - * @param other_integration_field Integration field of the other side of the portal. + * @param other Integration field of the other side of the portal. * @param other_sector_id Sector ID of the other side of the portal. * @param portal Portal. * @param target Coordinates of the target cell, relative to the integration field origin. - * @param with_los true if an LOS pass should be performed, else false. + * @param with_los If true an LOS pass is performed before cost integration. * * @return Integration field and flow field. */ - build_return_t build(const std::shared_ptr &cost_field, - const std::shared_ptr &other_integration_field, - sector_id_t other_sector_id, - const std::shared_ptr &portal, - const coord::tile_delta &target, - bool with_los = false); + get_return_t get(const std::shared_ptr &cost_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal, + const coord::tile_delta &target, + bool with_los = true); private: /** @@ -140,7 +144,7 @@ class Integrator { * when the field is reused. */ std::unordered_map, - build_return_t, + get_return_t, pair_hash> field_cache; }; diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index d2fef7b2b9..92304c5ffe 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -116,7 +116,7 @@ const Path Pathfinder::get_path(const PathRequest &request) { auto prev_flow_field = this->integrator->build(prev_integration_field); auto prev_sector_id = target_sector->get_id(); - Integrator::build_return_t sector_fields{prev_integration_field, prev_flow_field}; + Integrator::get_return_t sector_fields{prev_integration_field, prev_flow_field}; std::vector>> flow_fields; flow_fields.reserve(portal_path.size() + 1); @@ -129,11 +129,11 @@ const Path Pathfinder::get_path(const PathRequest &request) { target_delta = request.target - next_sector->get_position().to_tile(sector_size); - sector_fields = this->integrator->build(next_sector->get_cost_field(), - prev_integration_field, - prev_sector_id, - portal, - target_delta); + sector_fields = this->integrator->get(next_sector->get_cost_field(), + prev_integration_field, + prev_sector_id, + portal, + target_delta); flow_fields.push_back(std::make_pair(next_sector_id, sector_fields.second)); prev_integration_field = sector_fields.first; diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index 12c96cc30e..2d54dd728d 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -85,7 +85,7 @@ void flow_field() { auto integrator = std::make_shared(); // Build the flow field - auto flow_field = integrator->build(cost_field, coord::tile_delta{2, 2}).second; + auto flow_field = integrator->get(cost_field, coord::tile_delta{2, 2}).second; auto ff_cells = flow_field->get_cells(); // The flow field for targeting (2, 2) hould look like this: From e67deef83f9020eb3ed6290fffc6a64691cfd412 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 1 Jun 2024 06:52:31 +0200 Subject: [PATCH 537/771] path: Limit sectors where LOS is transferred between portals. This avoid edge cases where LOS flags would be set when LOS is actually blocked by an obstacle, e.g. a cell in a sector that is not visited. --- libopenage/pathfinding/pathfinder.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 92304c5ffe..5d6a84c5a9 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -122,22 +122,27 @@ const Path Pathfinder::get_path(const PathRequest &request) { flow_fields.reserve(portal_path.size() + 1); flow_fields.push_back(std::make_pair(target_sector->get_id(), sector_fields.second)); + int los_depth = 1; + for (auto &portal : portal_path) { auto prev_sector = grid->get_sector(prev_sector_id); auto next_sector_id = portal->get_exit_sector(prev_sector_id); auto next_sector = grid->get_sector(next_sector_id); target_delta = request.target - next_sector->get_position().to_tile(sector_size); + bool with_los = los_depth > 0; sector_fields = this->integrator->get(next_sector->get_cost_field(), prev_integration_field, prev_sector_id, portal, - target_delta); + target_delta, + with_los); flow_fields.push_back(std::make_pair(next_sector_id, sector_fields.second)); prev_integration_field = sector_fields.first; prev_sector_id = next_sector_id; + los_depth -= 1; } // reverse the flow fields so they are ordered from start to target From 0eba33c874dbf4129987271adfa1ebd413ff92c6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 1 Jun 2024 06:59:55 +0200 Subject: [PATCH 538/771] path: Fix time unit in demo 1. --- libopenage/pathfinding/demo/demo_1.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 3d00d31b0c..f9d7cbc92f 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -96,7 +96,7 @@ void path_demo_1(const util::Path &path) { Path path_result = pathfinder->get_path(path_request); timer.stop(); - log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps"); + log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " us"); // Create a renderer to display the grid and path auto qtapp = std::make_shared(); From 7a48ef0474cbeace1b363376166d5ba053b4e34f Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 00:05:29 +0200 Subject: [PATCH 539/771] path: Align bit positions for shared flags of flow/integration cell values. --- etc/gdb_pretty/printers.py | 6 ++++-- libopenage/pathfinding/definitions.h | 8 ++++---- libopenage/pathfinding/types.h | 14 ++++++++++---- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index 1a7803619f..de3b3cc1ce 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -333,8 +333,10 @@ def children(self): # Integrated flags INTEGRATED_FLAGS: dict = { - 0x01: 'LOS', - 0x02: 'WAVEFRONT_BLOCKED', + 0x01: 'TARGET', + 0x02: 'FOUND', + 0x20: 'LOS', + 0x40: 'WAVEFRONT_BLOCKED', } diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h index a54dff576a..7391d9523e 100644 --- a/libopenage/pathfinding/definitions.h +++ b/libopenage/pathfinding/definitions.h @@ -45,22 +45,22 @@ constexpr integrated_cost_t INTEGRATED_COST_UNREACHABLE = std::numeric_limits Date: Sun, 2 Jun 2024 00:09:47 +0200 Subject: [PATCH 540/771] path: Use integration flags in flow field shader. --- .../pathfinding/demo_0_flow_field.frag.glsl | 19 ++++-- .../pathfinding/demo_0_flow_field.vert.glsl | 9 ++- libopenage/pathfinding/demo/demo_0.cpp | 63 ++++++++++++------- libopenage/pathfinding/demo/demo_0.h | 12 ++-- libopenage/pathfinding/pathfinder.cpp | 1 + 5 files changed, 70 insertions(+), 34 deletions(-) diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl index b5ff4042a3..8dd4a2dd85 100644 --- a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl +++ b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl @@ -1,24 +1,33 @@ #version 330 -in float v_cost; +/// Flow field value +in float flow_val; + +/// Integration field flags +in float int_val; out vec4 outcol; +int WAVEFRONT_BLOCKED = 0x40; +int LINE_OF_SIGHT = 0x20; +int PATHABLE = 0x10; + void main() { - int cost = int(v_cost); - if (bool(cost & 0x40)) { + int flow_flags = int(flow_val) & 0xF0; + int int_flags = int(int_val); + if (bool(int_flags & WAVEFRONT_BLOCKED)) { // wavefront blocked outcol = vec4(0.9, 0.9, 0.9, 1.0); return; } - if (bool(cost & 0x20)) { + if (bool(flow_flags & LINE_OF_SIGHT)) { // line of sight outcol = vec4(1.0, 1.0, 1.0, 1.0); return; } - if (bool(cost & 0x10)) { + if (bool(flow_flags & PATHABLE)) { // pathable outcol = vec4(0.7, 0.7, 0.7, 1.0); return; diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl index 2c2190e57c..03900a8daf 100644 --- a/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl +++ b/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl @@ -1,15 +1,18 @@ #version 330 layout(location=0) in vec3 position; -layout(location=1) in float cost; +layout(location=1) in float flow_cell; +layout(location=2) in float int_cell; uniform mat4 model; uniform mat4 view; uniform mat4 proj; -out float v_cost; +out float flow_val; +out float int_val; void main() { gl_Position = proj * view * model * vec4(position, 1.0); - v_cost = cost; + flow_val = flow_cell; + int_val = int_cell; } diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 8e844d942d..c063445192 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -111,7 +111,7 @@ void path_demo_0(const util::Path &path) { render_manager->show_integration_field(integration_field); break; case RenderManager0::field_t::FLOW: - render_manager->show_flow_field(flow_field); + render_manager->show_flow_field(flow_field, integration_field); break; } @@ -137,7 +137,7 @@ void path_demo_0(const util::Path &path) { log::log(INFO << "Showing integration field"); } else if (ev.key() == Qt::Key_F3) { // Show flow field - render_manager->show_flow_field(flow_field); + render_manager->show_flow_field(flow_field, integration_field); current_field = RenderManager0::field_t::FLOW; log::log(INFO << "Showing flow field"); } @@ -241,7 +241,8 @@ void RenderManager0::show_integration_field(const std::shared_ptrfield_pass->set_renderables({renderable}); } -void RenderManager0::show_flow_field(const std::shared_ptr &field) { +void RenderManager0::show_flow_field(const std::shared_ptr &flow_field, + const std::shared_ptr &int_field) { Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); auto unifs = this->flow_shader->new_uniform_input( "model", @@ -250,7 +251,7 @@ void RenderManager0::show_flow_field(const std::shared_ptr &fie this->camera->get_view_matrix(), "proj", this->camera->get_projection_matrix()); - auto mesh = get_flow_field_mesh(field, 4); + auto mesh = get_flow_field_mesh(flow_field, int_field, 4); auto geometry = this->renderer->add_mesh_geometry(mesh); renderer::Renderable renderable{ unifs, @@ -720,13 +721,14 @@ renderer::resources::MeshData RenderManager0::get_integration_field_mesh(const s return {std::move(vert_data), std::move(idx_data), info}; } -renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::shared_ptr &field, +renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::shared_ptr &flow_field, + const std::shared_ptr &int_field, size_t resolution) { // increase by 1 in every dimension because to get the vertex length // of each dimension util::Vector2s size{ - field->get_size() * resolution + 1, - field->get_size() * resolution + 1, + flow_field->get_size() * resolution + 1, + flow_field->get_size() * resolution + 1, }; auto vert_distance = 1.0f / resolution; @@ -737,25 +739,39 @@ renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::sha for (int i = 0; i < static_cast(size[0]); ++i) { for (int j = 0; j < static_cast(size[1]); ++j) { // for each vertex, compare the surrounding tiles - std::vector surround{}; + std::vector ff_surround{}; + std::vector int_surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile_delta((i - 1) / resolution, (j - 1) / resolution)); - surround.push_back(cost); + auto ff_cost = flow_field->get_cell((i - 1) / resolution, (j - 1) / resolution); + ff_surround.push_back(ff_cost); + + auto int_flags = int_field->get_cell((i - 1) / resolution, (j - 1) / resolution).flags; + int_surround.push_back(int_flags); } - if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile_delta((i - 1) / resolution, j / resolution)); - surround.push_back(cost); + if (j < static_cast(flow_field->get_size()) and i - 1 >= 0) { + auto ff_cost = flow_field->get_cell((i - 1) / resolution, j / resolution); + ff_surround.push_back(ff_cost); + + auto int_flags = int_field->get_cell((i - 1) / resolution, j / resolution).flags; + int_surround.push_back(int_flags); } - if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile_delta(i / resolution, j / resolution)); - surround.push_back(cost); + if (j < static_cast(flow_field->get_size()) and i < static_cast(flow_field->get_size())) { + auto ff_cost = flow_field->get_cell(i / resolution, j / resolution); + ff_surround.push_back(ff_cost); + + auto int_flags = int_field->get_cell(i / resolution, j / resolution).flags; + int_surround.push_back(int_flags); } - if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile_delta(i / resolution, (j - 1) / resolution)); - surround.push_back(cost); + if (j - 1 >= 0 and i < static_cast(flow_field->get_size())) { + auto ff_cost = flow_field->get_cell(i / resolution, (j - 1) / resolution); + ff_surround.push_back(ff_cost); + + auto int_flags = int_field->get_cell(i / resolution, (j - 1) / resolution).flags; + int_surround.push_back(int_flags); } // use the cost of the most expensive surrounding tile - auto max_cost = *std::max_element(surround.begin(), surround.end()); + auto ff_max_cost = *std::max_element(ff_surround.begin(), ff_surround.end()); + auto int_max_flags = *std::max_element(int_surround.begin(), int_surround.end()); coord::scene3 v{ static_cast(i * vert_distance), @@ -766,7 +782,8 @@ renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::sha verts.push_back(world_v[0]); verts.push_back(world_v[1]); verts.push_back(world_v[2]); - verts.push_back(max_cost); + verts.push_back(ff_max_cost); + verts.push_back(int_max_flags); } } @@ -790,7 +807,9 @@ renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::sha } renderer::resources::VertexInputInfo info{ - {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32}, + {renderer::resources::vertex_input_t::V3F32, + renderer::resources::vertex_input_t::F32, + renderer::resources::vertex_input_t::F32}, renderer::resources::vertex_layout_t::AOS, renderer::resources::vertex_primitive_t::TRIANGLES, renderer::resources::index_t::U16}; diff --git a/libopenage/pathfinding/demo/demo_0.h b/libopenage/pathfinding/demo/demo_0.h index 7a07ebfd7e..adf5651971 100644 --- a/libopenage/pathfinding/demo/demo_0.h +++ b/libopenage/pathfinding/demo/demo_0.h @@ -92,9 +92,11 @@ class RenderManager0 { /** * Draw a flow field to the screen. * - * @param field Flow field. + * @param flow_field Flow field. + * @param int_field Integration field. */ - void show_flow_field(const std::shared_ptr &field); + void show_flow_field(const std::shared_ptr &flow_field, + const std::shared_ptr &int_field); /** * Draw the steering vectors of a flow field to the screen. @@ -160,9 +162,11 @@ class RenderManager0 { /** * Create a mesh for the flow field. * - * @param field Flow field to visualize. + * @param flow_field Flow field to visualize. + * @param int_field Integration field. */ - static renderer::resources::MeshData get_flow_field_mesh(const std::shared_ptr &field, + static renderer::resources::MeshData get_flow_field_mesh(const std::shared_ptr &flow_field, + const std::shared_ptr &int_field, size_t resolution = 2); /** diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 5d6a84c5a9..559a761639 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -338,6 +338,7 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_dir(coord::tile_delta(current_x, current_y)); From 4f843d88bca4a3d20117767566e381d033993685 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 01:57:48 +0200 Subject: [PATCH 541/771] path: Remove FLOW_WAVEFRONT_BLOCKED flag. --- .../pathfinding/demo_0_flow_field.frag.glsl | 4 ++-- etc/gdb_pretty/printers.py | 4 ++-- libopenage/pathfinding/definitions.h | 17 +++++----------- libopenage/pathfinding/demo/demo_0.cpp | 20 ++++++++++++------- libopenage/pathfinding/flow_field.cpp | 12 +---------- libopenage/pathfinding/integration_field.cpp | 6 ++---- libopenage/pathfinding/tests.cpp | 4 ++-- libopenage/pathfinding/types.h | 6 +++--- 8 files changed, 30 insertions(+), 43 deletions(-) diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl index 8dd4a2dd85..b003945619 100644 --- a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl +++ b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl @@ -8,7 +8,7 @@ in float int_val; out vec4 outcol; -int WAVEFRONT_BLOCKED = 0x40; +int WAVEFRONT_BLOCKED = 0x04; int LINE_OF_SIGHT = 0x20; int PATHABLE = 0x10; @@ -21,7 +21,7 @@ void main() { return; } - if (bool(flow_flags & LINE_OF_SIGHT)) { + if (bool(int_flags & LINE_OF_SIGHT)) { // line of sight outcol = vec4(1.0, 1.0, 1.0, 1.0); return; diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index de3b3cc1ce..fdab2f12bc 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -291,7 +291,7 @@ class PathFlowTypePrinter: FLOW_FLAGS: dict = { 0x10: 'PATHABLE', 0x20: 'LOS', - 0x40: 'WAVEFRONT_BLOCKED', + 0x40: 'UNUSED', 0x80: 'UNUSED', } @@ -335,8 +335,8 @@ def children(self): INTEGRATED_FLAGS: dict = { 0x01: 'TARGET', 0x02: 'FOUND', + 0x04: 'WAVEFRONT_BLOCKED', 0x20: 'LOS', - 0x40: 'WAVEFRONT_BLOCKED', } diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h index 7391d9523e..0dc9594935 100644 --- a/libopenage/pathfinding/definitions.h +++ b/libopenage/pathfinding/definitions.h @@ -47,11 +47,6 @@ constexpr integrated_cost_t INTEGRATED_COST_UNREACHABLE = std::numeric_limits verts{}; auto vert_count = size[0] * size[1]; - verts.reserve(vert_count * 4); + verts.reserve(vert_count * 5); for (int i = 0; i < static_cast(size[0]); ++i) { for (int j = 0; j < static_cast(size[1]); ++j) { // for each vertex, compare the surrounding tiles - std::vector ff_surround{}; - std::vector int_surround{}; + std::vector ff_surround{}; + std::vector int_surround{}; if (j - 1 >= 0 and i - 1 >= 0) { auto ff_cost = flow_field->get_cell((i - 1) / resolution, (j - 1) / resolution); ff_surround.push_back(ff_cost); @@ -769,9 +769,15 @@ renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::sha auto int_flags = int_field->get_cell(i / resolution, (j - 1) / resolution).flags; int_surround.push_back(int_flags); } - // use the cost of the most expensive surrounding tile - auto ff_max_cost = *std::max_element(ff_surround.begin(), ff_surround.end()); - auto int_max_flags = *std::max_element(int_surround.begin(), int_surround.end()); + // combine the flags of the sorrounding tiles + auto ff_max_flags = 0; + for (auto &val : ff_surround) { + ff_max_flags |= val & 0xF0; + } + auto int_max_flags = 0; + for (auto &val : int_surround) { + int_max_flags |= val; + } coord::scene3 v{ static_cast(i * vert_distance), @@ -782,7 +788,7 @@ renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::sha verts.push_back(world_v[0]); verts.push_back(world_v[1]); verts.push_back(world_v[2]); - verts.push_back(ff_max_cost); + verts.push_back(ff_max_flags); verts.push_back(int_max_flags); } } diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 3b2dd3e1da..04649bcbe1 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -78,11 +78,6 @@ void FlowField::build(const std::shared_ptr &integration_field flow_cells[idx] |= FLOW_LOS_MASK; } - if (integrate_cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { - // Cell is blocked by a line-of-sight corner - flow_cells[idx] |= FLOW_WAVEFRONT_BLOCKED_MASK; - } - if (integrate_cells[idx].flags & INTEGRATE_TARGET_MASK) { // target cells are pathable flow_cells[idx] |= FLOW_PATHABLE_MASK; @@ -258,7 +253,7 @@ void FlowField::reset() { } void FlowField::reset_dynamic_flags() { - flow_t mask = 0xFF & ~(FLOW_LOS_MASK | FLOW_WAVEFRONT_BLOCKED_MASK); + flow_t mask = 0xFF & ~(FLOW_LOS_MASK); for (flow_t &cell : this->cells) { cell = cell & mask; } @@ -275,11 +270,6 @@ void FlowField::transfer_dynamic_flags(const std::shared_ptr & // Cell is in line of sight flow_cells[idx] |= FLOW_LOS_MASK; } - - if (integrate_cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { - // Cell is blocked by a line-of-sight corner - flow_cells[idx] |= FLOW_WAVEFRONT_BLOCKED_MASK; - } } log::log(DBG << "Flow field dynamic flags have been transferred"); diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index fb62372d67..346b8a1a4e 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -70,8 +70,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[target_idx].flags |= INTEGRATE_FOUND_MASK; - // ASDF: Fix display in demo 0 - // this->cells[target_idx].flags |= INTEGRATE_LOS_MASK; + this->cells[target_idx].flags |= INTEGRATE_LOS_MASK; // Add neighbors to current wave if (target.se > 0) { @@ -267,8 +266,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[idx].cost = wave_cost - 1 + cost_field->get_cost(idx); - // ASDF: Fix display in demo 0 - // this->cells[idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + this->cells[idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; } // check each neighbor for a corner diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index 2d54dd728d..c68ef007f3 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -94,9 +94,9 @@ void flow_field() { // | E | E | N | auto ff_expected = std::vector{ FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), - FLOW_WAVEFRONT_BLOCKED_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), FLOW_LOS_MASK | FLOW_PATHABLE_MASK, - FLOW_WAVEFRONT_BLOCKED_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), 0, FLOW_LOS_MASK | FLOW_PATHABLE_MASK, FLOW_LOS_MASK | FLOW_PATHABLE_MASK, diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index 9a5c3806d6..629f201035 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -55,12 +55,12 @@ struct integrated_t { * * Bit 0-3: Shared flags with the flow field. * - 0: Unused. - * - 1: Wave front blocked flag. + * - 1: Unused. * - 2: Line of sight flag. * - 3: Unused. * Bit 4-7: Integration field specific flags. * - 4: Unused. - * - 5: Unused. + * - 5: Wave front blocked flag. * - 6: LOS found flag. * - 7: Target flag. */ @@ -87,7 +87,7 @@ enum class flow_dir_t : uint8_t { * Flow field cell value. * * Bit 0: Unused. - * Bit 1: Wave front blocked flag. + * Bit 1: Unused. * Bit 2: Line of sight flag. * Bit 3: Pathable flag. * Bits 4-7: flow direction. From 7b7117906eb2f5de99c1c51069097885b666c0cf Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 02:22:23 +0200 Subject: [PATCH 542/771] path: Add FLOW_TARGET flag. --- etc/gdb_pretty/printers.py | 4 ++-- libopenage/pathfinding/definitions.h | 12 +++++++++++- libopenage/pathfinding/flow_field.cpp | 8 +++----- libopenage/pathfinding/types.h | 6 +++--- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index fdab2f12bc..fffce7e63d 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -291,7 +291,7 @@ class PathFlowTypePrinter: FLOW_FLAGS: dict = { 0x10: 'PATHABLE', 0x20: 'LOS', - 0x40: 'UNUSED', + 0x40: 'TARGET', 0x80: 'UNUSED', } @@ -333,10 +333,10 @@ def children(self): # Integrated flags INTEGRATED_FLAGS: dict = { - 0x01: 'TARGET', 0x02: 'FOUND', 0x04: 'WAVEFRONT_BLOCKED', 0x20: 'LOS', + 0x40: 'TARGET', } diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h index 0dc9594935..0fc8268291 100644 --- a/libopenage/pathfinding/definitions.h +++ b/libopenage/pathfinding/definitions.h @@ -50,7 +50,7 @@ constexpr integrated_flags_t INTEGRATE_LOS_MASK = 0x20; /** * Target flag in an integrated_flags_t value. */ -constexpr integrated_flags_t INTEGRATE_TARGET_MASK = 0x01; +constexpr integrated_flags_t INTEGRATE_TARGET_MASK = 0x40; /** * Found flag in an integrated_flags_t value. @@ -78,6 +78,11 @@ constexpr flow_t FLOW_INIT = 0; */ constexpr flow_t FLOW_DIR_MASK = 0x0F; +/** + * Mask for the flow flag bits in a flow_t value. + */ +constexpr flow_t FLOW_FLAGS_MASK = 0xF0; + /** * Pathable flag in a flow_t value. */ @@ -88,4 +93,9 @@ constexpr flow_t FLOW_PATHABLE_MASK = 0x10; */ constexpr flow_t FLOW_LOS_MASK = 0x20; +/** + * Target flag in a flow_t value. + */ +constexpr flow_t FLOW_TARGET_MASK = 0x40; + } // namespace openage::path diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 04649bcbe1..26b7592da8 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -73,12 +73,10 @@ void FlowField::build(const std::shared_ptr &integration_field continue; } - if (integrate_cells[idx].flags & INTEGRATE_LOS_MASK) { - // Cell is in line of sight - flow_cells[idx] |= FLOW_LOS_MASK; - } + flow_t transfer_flags = integrate_cells[idx].flags & FLOW_FLAGS_MASK; + flow_cells[idx] |= transfer_flags; - if (integrate_cells[idx].flags & INTEGRATE_TARGET_MASK) { + if (flow_cells[idx] & FLOW_TARGET_MASK) { // target cells are pathable flow_cells[idx] |= FLOW_PATHABLE_MASK; diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index 629f201035..1b48a49654 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -55,14 +55,14 @@ struct integrated_t { * * Bit 0-3: Shared flags with the flow field. * - 0: Unused. - * - 1: Unused. + * - 1: Target flag. * - 2: Line of sight flag. * - 3: Unused. * Bit 4-7: Integration field specific flags. * - 4: Unused. * - 5: Wave front blocked flag. * - 6: LOS found flag. - * - 7: Target flag. + * - 7: Unused. */ integrated_flags_t flags; }; @@ -87,7 +87,7 @@ enum class flow_dir_t : uint8_t { * Flow field cell value. * * Bit 0: Unused. - * Bit 1: Unused. + * Bit 1: Target flag. * Bit 2: Line of sight flag. * Bit 3: Pathable flag. * Bits 4-7: flow direction. From 82ce34ee0ba7ce76726290b47515e0747741e244 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 02:31:22 +0200 Subject: [PATCH 543/771] path: Check ifr target cell has been reached using flow target flag. --- libopenage/pathfinding/pathfinder.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 559a761639..8b4eb45f39 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -312,7 +312,7 @@ const std::vector Pathfinder::get_waypoints(const std::vector Pathfinder::get_waypoints(const std::vectorget_sector(flow_fields.at(i).first); auto flow_field = flow_fields.at(i).second; + auto cell = flow_field->get_cell(current_x, current_y); // navigate the flow field vectors until we reach its edge (or the target) - while (current_x < static_cast(sector_size) - and current_y < static_cast(sector_size) - and current_x >= 0 - and current_y >= 0) { - auto cell = flow_field->get_cell(current_x, current_y); + while (not(cell & FLOW_TARGET_MASK)) { if (cell & FLOW_LOS_MASK) { // check if we reached an LOS cell auto sector_pos = sector->get_position(); auto cell_pos = sector_pos.to_tile(sector_size) + coord::tile_delta(current_x, current_y); waypoints.push_back(cell_pos); - reached_target = true; + los_reached = true; break; } - // ASDF: break if target cell is reached - // idea: target flag for flow field cells - // check if we need to change direction auto cell_direction = flow_field->get_dir(coord::tile_delta(current_x, current_y)); if (cell_direction != current_direction) { @@ -384,9 +378,14 @@ const std::vector Pathfinder::get_waypoints(const std::vector(current_direction)}; } + + // get the next cell + cell = flow_field->get_cell(current_x, current_y); } - if (reached_target) { + if (los_reached or i == flow_fields.size() - 1) { + // exit the loop if we found an LOS cell or reached + // the target cell in the last flow field break; } From c1d5637ddb4e04b89601c05d03ba3da3395809de Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 02:40:08 +0200 Subject: [PATCH 544/771] etc: Add unused placeholders for integration flags. --- etc/gdb_pretty/printers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index fffce7e63d..28bfb5ca22 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -333,10 +333,14 @@ def children(self): # Integrated flags INTEGRATED_FLAGS: dict = { + 0x01: 'UNUSED', 0x02: 'FOUND', 0x04: 'WAVEFRONT_BLOCKED', + 0x08: 'UNUSED', + 0x10: 'UNUSED', 0x20: 'LOS', 0x40: 'TARGET', + 0x80: 'UNUSED', } From 9206916dfc6ac4b23b8833948c7ad11b63182baf Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 03:46:22 +0200 Subject: [PATCH 545/771] path: Optimize performance-critical paths in the code. --- libopenage/pathfinding/integration_field.cpp | 26 ++++++++++++-------- libopenage/pathfinding/pathfinder.cpp | 22 ++++++----------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 346b8a1a4e..4b71f3c586 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -119,6 +119,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrget_costs(); auto &other_cells = other->get_cells(); // transfer masks for flags from the other side of the portal @@ -147,7 +148,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrget_cost(target_idx); + auto cell_cost = cost_cells[target_idx]; if (cell_cost > COST_MIN or this->cells[target_idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { // cell blocks line of sight @@ -166,8 +167,8 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrbresenhams_line(target, corner.first, corner.second); - for (auto &blocked_idx : blocked_cells) { - if (cost_field->get_cost(blocked_idx) > COST_MIN) { + for (auto blocked_idx : blocked_cells) { + if (cost_cells[blocked_idx] > COST_MIN) { // stop if blocked_idx is not min cost // because this idx may create a new corner break; @@ -229,6 +230,9 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrget_costs(); + // Add the start wave to the current wave current_wave.insert(current_wave.end(), start_wave.begin(), start_wave.end()); do { @@ -244,7 +248,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { // Stop at cells that are blocked by a LOS corner - this->cells[idx].cost = wave_cost - 1 + cost_field->get_cost(idx); + this->cells[idx].cost = wave_cost - 1 + cost_cells[idx]; this->cells[idx].flags |= INTEGRATE_FOUND_MASK; continue; } @@ -257,7 +261,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrsize; // Get the cost of the current cell - auto cell_cost = cost_field->get_cost(idx); + auto cell_cost = cost_cells[idx]; if (cell_cost > COST_MIN) { // cell blocks line of sight @@ -265,7 +269,7 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[idx].cost = wave_cost - 1 + cost_field->get_cost(idx); + this->cells[idx].cost = wave_cost - 1 + cell_cost; this->cells[idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; } @@ -275,8 +279,8 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrbresenhams_line(target, corner.first, corner.second); - for (auto &blocked_idx : blocked_cells) { - if (cost_field->get_cost(blocked_idx) > COST_MIN) { + for (auto blocked_idx : blocked_cells) { + if (cost_cells[blocked_idx] > COST_MIN) { // stop if blocked_idx is impassable break; } @@ -380,6 +384,8 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie std::vector neighbors; neighbors.reserve(4); + auto &cost_cells = cost_field->get_costs(); + // Move outwards from the wavefront, updating the integration field open_list.insert(open_list.end(), start_cells.begin(), start_cells.end()); while (!open_list.empty()) { @@ -407,9 +413,9 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie } // Update the integration field of the neighboring cells - for (auto &neighbor_idx : neighbors) { + for (auto neighbor_idx : neighbors) { this->update_neighbor(neighbor_idx, - cost_field->get_cost(neighbor_idx), + cost_cells[neighbor_idx], integrated_current, open_list); } diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 8b4eb45f39..ae8a911716 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -309,8 +309,6 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_sector_size(); coord::tile_t start_x = request.start.ne % sector_size; coord::tile_t start_y = request.start.se % sector_size; - coord::tile_t target_x = request.target.ne % sector_size; - coord::tile_t target_y = request.target.se % sector_size; bool los_reached = false; @@ -318,29 +316,26 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_dir(current_x, current_y); for (size_t i = 0; i < flow_fields.size(); ++i) { - auto sector = grid->get_sector(flow_fields.at(i).first); - auto flow_field = flow_fields.at(i).second; + auto sector = grid->get_sector(flow_fields[i].first); + auto sector_pos = sector->get_position().to_tile(sector_size); + auto flow_field = flow_fields[i].second; auto cell = flow_field->get_cell(current_x, current_y); // navigate the flow field vectors until we reach its edge (or the target) while (not(cell & FLOW_TARGET_MASK)) { if (cell & FLOW_LOS_MASK) { // check if we reached an LOS cell - auto sector_pos = sector->get_position(); - auto cell_pos = sector_pos.to_tile(sector_size) - + coord::tile_delta(current_x, current_y); + auto cell_pos = sector_pos + coord::tile_delta(current_x, current_y); waypoints.push_back(cell_pos); los_reached = true; break; } // check if we need to change direction - auto cell_direction = flow_field->get_dir(coord::tile_delta(current_x, current_y)); + auto cell_direction = flow_field->get_dir(current_x, current_y); if (cell_direction != current_direction) { // add the current cell as a waypoint - auto sector_pos = sector->get_position(); - auto cell_pos = sector_pos.to_tile(sector_size) - + coord::tile_delta(current_x, current_y); + auto cell_pos = sector_pos + coord::tile_delta(current_x, current_y); waypoints.push_back(cell_pos); current_direction = cell_direction; } @@ -425,10 +420,7 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_sector(flow_fields.back().first)->get_position(); - auto target_pos = sector_pos.to_tile(sector_size) - + coord::tile_delta{target_x, target_y}; - waypoints.push_back(target_pos); + waypoints.push_back(request.target); return waypoints; } From 2ac96380b7d4e727553143b029358e71b627c46b Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 03:50:49 +0200 Subject: [PATCH 546/771] path: Remopve outdated TODOs. --- libopenage/pathfinding/demo/demo_1.cpp | 1 - libopenage/pathfinding/pathfinder.cpp | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index f9d7cbc92f..cd67e7b79f 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -167,7 +167,6 @@ void path_demo_1(const util::Path &path) { render_manager->create_waypoint_tiles(path_result); // Run the renderer pss to draw the grid and path into a window - // TODO: Make this a while (not window.should_close()) loop render_manager->run(); } diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index ae8a911716..bc1621f217 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -191,8 +191,7 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req // list of known portals and corresponding node. nodemap_t visited_portals; - // Cost to travel from one portal to another - // TODO: Determine this cost for each portal + // TODO: Compute cost to travel from one portal to another when creating portals // const int distance_cost = 1; // create start nodes From a7415abfa81efed004998422967fbec9c6bfe5e1 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 04:02:53 +0200 Subject: [PATCH 547/771] path: Fix existing unit tests. --- libopenage/pathfinding/tests.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index c68ef007f3..5f92496ff8 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -29,7 +29,7 @@ void flow_field() { { auto integration_field = std::make_shared(3); integration_field->integrate_cost(cost_field, coord::tile_delta{2, 2}); - auto int_cells = integration_field->get_cells(); + auto &int_cells = integration_field->get_cells(); // The integration field should look like: // | 4 | 3 | 2 | @@ -60,7 +60,7 @@ void flow_field() { FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH), FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), - FLOW_PATHABLE_MASK | static_cast(flow_dir_t::NORTH), + FLOW_TARGET_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::NORTH), }; // Compare the integration field cells with the expected values @@ -86,7 +86,7 @@ void flow_field() { // Build the flow field auto flow_field = integrator->get(cost_field, coord::tile_delta{2, 2}).second; - auto ff_cells = flow_field->get_cells(); + auto &ff_cells = flow_field->get_cells(); // The flow field for targeting (2, 2) hould look like this: // | E | SE | S | @@ -95,13 +95,13 @@ void flow_field() { auto ff_expected = std::vector{ FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), - FLOW_LOS_MASK | FLOW_PATHABLE_MASK, + FLOW_LOS_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH), FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), 0, - FLOW_LOS_MASK | FLOW_PATHABLE_MASK, - FLOW_LOS_MASK | FLOW_PATHABLE_MASK, - FLOW_LOS_MASK | FLOW_PATHABLE_MASK, - FLOW_LOS_MASK | FLOW_PATHABLE_MASK, + FLOW_LOS_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH), + FLOW_LOS_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_LOS_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_LOS_MASK | FLOW_PATHABLE_MASK | FLOW_TARGET_MASK, }; // Compare the flow field cells with the expected values From 2abbb3db7565e64a1ec0509ad002e451fc9d5a30 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 04:27:26 +0200 Subject: [PATCH 548/771] path: More helpful log messages. --- libopenage/pathfinding/integrator.cpp | 31 +++++++++++++++++++++++++++ libopenage/pathfinding/pathfinder.cpp | 14 ++++++------ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 990dab37dd..265c5285cc 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -2,6 +2,8 @@ #include "integrator.h" +#include "log/log.h" + #include "pathfinding/cost_field.h" #include "pathfinding/flow_field.h" #include "pathfinding/integration_field.h" @@ -15,11 +17,14 @@ std::shared_ptr Integrator::integrate(const std::shared_ptr(cost_field->get_size()); + log::log(DBG << "Integrating cost field for target coord " << target); if (with_los) { + log::log(SPAM << "Performing LOS pass"); auto wavefront_blocked = integration_field->integrate_los(cost_field, target); integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); } else { + log::log(SPAM << "Skipping LOS pass"); integration_field->integrate_cost(cost_field, target); } @@ -35,7 +40,11 @@ std::shared_ptr Integrator::integrate(const std::shared_ptrget_id(), other_sector_id); auto cached = this->field_cache.find(cache_key); if (cached != this->field_cache.end()) { + log::log(DBG << "Using cached integration field for portal " << portal->get_id() + << " from sector " << other_sector_id); if (with_los) { + log::log(SPAM << "Performing LOS pass on cached field"); + // Make a copy of the cached field to avoid modifying the cached field auto integration_field = std::make_shared(*cached->second.first); @@ -47,12 +56,16 @@ std::shared_ptr Integrator::integrate(const std::shared_ptrsecond.first; } + log::log(DBG << "Integrating cost field for portal " << portal->get_id() + << " from sector " << other_sector_id); + // Create a new integration field auto integration_field = std::make_shared(cost_field->get_size()); // LOS pass std::vector wavefront_blocked; if (with_los) { + log::log(SPAM << "Performing LOS pass"); wavefront_blocked = integration_field->integrate_los(cost_field, other, other_sector_id, portal, target); } @@ -73,6 +86,8 @@ std::shared_ptr Integrator::integrate(const std::shared_ptr Integrator::build(const std::shared_ptr &integration_field) { auto flow_field = std::make_shared(integration_field->get_size()); + + log::log(DBG << "Building flow field from integration field"); flow_field->build(integration_field); return flow_field; @@ -86,7 +101,11 @@ std::shared_ptr Integrator::build(const std::shared_ptrget_id(), other_sector_id); auto cached = this->field_cache.find(cache_key); if (cached != this->field_cache.end()) { + log::log(DBG << "Using cached flow field for portal " << portal->get_id() + << " from sector " << other_sector_id); if (with_los) { + log::log(SPAM << "Transferring LOS flags to cached flow field"); + // Make a copy of the cached flow field auto flow_field = std::make_shared(*cached->second.second); @@ -99,6 +118,9 @@ std::shared_ptr Integrator::build(const std::shared_ptrsecond.second; } + log::log(DBG << "Building flow field for portal " << portal->get_id() + << " from sector " << other_sector_id); + auto flow_field = std::make_shared(integration_field->get_size()); flow_field->build(integration_field, other, other_sector_id, portal); @@ -122,13 +144,19 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ auto cache_key = std::make_pair(portal->get_id(), other_sector_id); auto cached = this->field_cache.find(cache_key); if (cached != this->field_cache.end()) { + log::log(DBG << "Using cached integration and flow fields for portal " << portal->get_id() + << " from sector " << other_sector_id); if (with_los) { + log::log(SPAM << "Performing LOS pass on cached field"); + // Make a copy of the cached integration field auto integration_field = std::make_shared(*cached->second.first); // Only integrate LOS; leave the rest of the field as is integration_field->integrate_los(cost_field, other, other_sector_id, portal, target); + log::log(SPAM << "Transferring LOS flags to cached flow field"); + // Make a copy of the cached flow field auto flow_field = std::make_shared(*cached->second.second); @@ -144,6 +172,9 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ auto integration_field = this->integrate(cost_field, other, other_sector_id, portal, target, with_los); auto flow_field = this->build(integration_field, other, other_sector_id, portal); + log::log(DBG << "Caching integration and flow fields for portal ID: " << portal->get_id() + << ", sector ID: " << other_sector_id); + // Copy the fields to the cache. std::shared_ptr cached_integration_field = std::make_shared(*integration_field); cached_integration_field->reset_dynamic_flags(); diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index bc1621f217..7c063821cb 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -31,7 +31,7 @@ const Path Pathfinder::get_path(const PathRequest &request) { or request.target.se < 0 or request.target.ne >= grid_width or request.target.se >= grid_height) { - log::log(INFO << "Path not found (start = " << request.start << "; target = " << request.target << ")"); + log::log(DBG << "Path not found (start = " << request.start << "; target = " << request.target << ")"); log::log(DBG << "Target is out of bounds."); return Path{request.grid_id, PathResult::OUT_OF_BOUNDS, {}}; } @@ -64,7 +64,7 @@ const Path Pathfinder::get_path(const PathRequest &request) { } waypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end()); - log::log(INFO << "Path found (start = " << request.start << "; target = " << request.target << ")"); + log::log(DBG << "Path found (start = " << request.start << "; target = " << request.target << ")"); log::log(DBG << "Path is within the same sector."); return Path{request.grid_id, PathResult::FOUND, waypoints}; } @@ -97,7 +97,7 @@ const Path Pathfinder::get_path(const PathRequest &request) { if (target_portal_ids.empty() or start_portal_ids.empty()) { // Exit early if no portals are reachable from the start or target - log::log(INFO << "Path not found (start = " << request.start << "; target = " << request.target << ")"); + log::log(DBG << "Path not found (start = " << request.start << "; target = " << request.target << ")"); log::log(DBG << "No portals are reachable from the start or target."); return Path{request.grid_id, PathResult::NOT_FOUND, {}}; } @@ -157,10 +157,10 @@ const Path Pathfinder::get_path(const PathRequest &request) { waypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end()); if (portal_status == PathResult::NOT_FOUND) { - log::log(INFO << "Path not found (start = " << request.start << "; target = " << request.target << ")"); + log::log(DBG << "Path not found (start = " << request.start << "; target = " << request.target << ")"); } else { - log::log(INFO << "Path found (start = " << request.start << "; target = " << request.target << ")"); + log::log(DBG << "Path found (start = " << request.start << "; target = " << request.target << ")"); } return Path{request.grid_id, portal_status, waypoints}; } @@ -234,7 +234,7 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req for (auto &node : backtrace) { result.push_back(node->portal); } - log::log(INFO << "Portal path found with " << result.size() << " portal traversals."); + log::log(DBG << "Portal path found with " << result.size() << " portal traversals."); return std::make_pair(PathResult::FOUND, result); } @@ -293,7 +293,7 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req result.push_back(node->portal); } - log::log(INFO << "Portal path not found."); + log::log(DBG << "Portal path not found."); log::log(DBG << "Closest portal: " << closest_node->portal->get_id()); return std::make_pair(PathResult::NOT_FOUND, result); } From 09e22df6ebe27c21288588fa3f2e853a8f04c150 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 04:49:10 +0200 Subject: [PATCH 549/771] path: Fix waypoint finder exiting one cell too early. --- libopenage/pathfinding/pathfinder.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 7c063821cb..37e049c3e9 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -319,9 +319,11 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_position().to_tile(sector_size); auto flow_field = flow_fields[i].second; - auto cell = flow_field->get_cell(current_x, current_y); // navigate the flow field vectors until we reach its edge (or the target) - while (not(cell & FLOW_TARGET_MASK)) { + flow_t cell; + do { + cell = flow_field->get_cell(current_x, current_y); + if (cell & FLOW_LOS_MASK) { // check if we reached an LOS cell auto cell_pos = sector_pos + coord::tile_delta(current_x, current_y); @@ -372,10 +374,8 @@ const std::vector Pathfinder::get_waypoints(const std::vector(current_direction)}; } - - // get the next cell - cell = flow_field->get_cell(current_x, current_y); } + while (not(cell & FLOW_TARGET_MASK)); if (los_reached or i == flow_fields.size() - 1) { // exit the loop if we found an LOS cell or reached From b651e3697f6bc5bff1f646570508463954c1c45b Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 05:10:49 +0200 Subject: [PATCH 550/771] Fix compiler warnings. --- libopenage/gamestate/terrain.cpp | 6 +++--- libopenage/pathfinding/pathfinder.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libopenage/gamestate/terrain.cpp b/libopenage/gamestate/terrain.cpp index 8625c0b727..e7d078f2c5 100644 --- a/libopenage/gamestate/terrain.cpp +++ b/libopenage/gamestate/terrain.cpp @@ -45,7 +45,7 @@ Terrain::Terrain(const util::Vector2s &size, current_offset.ne += chunk_size[0]; // Wrap around to the next row - if (current_offset.ne == size[0]) { + if (current_offset.ne == static_cast(size[0])) { current_offset.ne = 0; current_offset.se += chunk_size[1]; } @@ -61,12 +61,12 @@ Terrain::Terrain(const util::Vector2s &size, } // Check terrain boundaries - if (current_offset.ne > size[0]) { + if (current_offset.ne > static_cast(size[0])) { throw error::Error{ERR << "Width of chunk " << chunk->get_offset() << " exceeds terrain width: " << chunk_size[0] << " (max width: " << size[0] << ")"}; } - else if (current_offset.se > size[1]) { + else if (current_offset.se > static_cast(size[1])) { throw error::Error{ERR << "Height of chunk " << chunk->get_offset() << " exceeds terrain height: " << chunk_size[1] << " (max height: " << size[1] << ")"}; diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 37e049c3e9..b734f4a304 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -29,8 +29,8 @@ const Path Pathfinder::get_path(const PathRequest &request) { auto grid_height = grid_size[1] * sector_size; if (request.target.ne < 0 or request.target.se < 0 - or request.target.ne >= grid_width - or request.target.se >= grid_height) { + or request.target.ne >= static_cast(grid_width) + or request.target.se >= static_cast(grid_height)) { log::log(DBG << "Path not found (start = " << request.start << "; target = " << request.target << ")"); log::log(DBG << "Target is out of bounds."); return Path{request.grid_id, PathResult::OUT_OF_BOUNDS, {}}; From 3790ffb5f289d5fc9b7df90a8a11a477b38d66b2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 15:55:46 +0200 Subject: [PATCH 551/771] path: Vectorize and unroll loops to maximize performance. --- libopenage/pathfinding/integration_field.cpp | 107 +++++++++++-------- libopenage/pathfinding/integration_field.h | 4 +- 2 files changed, 63 insertions(+), 48 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 4b71f3c586..6e76542cde 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -222,10 +222,16 @@ std::vector IntegrationField::integrate_los(const std::shared_ptr wavefront_blocked; // Cells that still have to be visited by the current wave - std::deque current_wave; + std::vector current_wave = std::move(start_wave); // Cells that have to be visited in the next wave - std::deque next_wave; + std::vector next_wave; + + // Preallocate ~30% of the field size for the wavefront + // This reduces the number of reallocations on push_back operations + // TODO: Find "optimal" value for reserve + current_wave.reserve(this->size * 3); + next_wave.reserve(this->size * 3); // Cost of the current wave integrated_cost_t wave_cost = start_cost; @@ -233,14 +239,10 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrget_costs(); - // Add the start wave to the current wave - current_wave.insert(current_wave.end(), start_wave.begin(), start_wave.end()); do { - while (not current_wave.empty()) { + for (size_t i = 0; i < current_wave.size(); ++i) { // inner loop: handle a wave - - auto idx = current_wave.front(); - current_wave.pop_front(); + auto idx = current_wave[i]; if (this->cells[idx].flags & INTEGRATE_FOUND_MASK) { // Skip cells that are already in the line of sight @@ -376,52 +378,65 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, std::vector &&start_cells) { - // Cells that still have to be visited - // they may be visited multiple times - std::deque open_list; + // Cells that still have to be visited by the current wave + std::vector current_wave = std::move(start_cells); - // Stores neighbors of the current cell - std::vector neighbors; - neighbors.reserve(4); + // Cells that have to be visited in the next wave + std::vector next_wave; + + // Preallocate ~30% of the field size for the wavefront + // This reduces the number of reallocations on push_back operations + // TODO: Find "optimal" value for reserve + current_wave.reserve(this->size * 3); + next_wave.reserve(this->size * 3); + // Get the cost field values auto &cost_cells = cost_field->get_costs(); // Move outwards from the wavefront, updating the integration field - open_list.insert(open_list.end(), start_cells.begin(), start_cells.end()); - while (!open_list.empty()) { - auto idx = open_list.front(); - open_list.pop_front(); + while (not current_wave.empty()) { + for (size_t i = 0; i < current_wave.size(); ++i) { + auto idx = current_wave[i]; - // Get the x and y coordinates of the current cell - auto x = idx % this->size; - auto y = idx / this->size; - - auto integrated_current = this->cells.at(idx).cost; + // Get the x and y coordinates of the current cell + auto x = idx % this->size; + auto y = idx / this->size; - // Get the neighbors of the current cell - if (y > 0) { - neighbors.push_back(idx - this->size); - } - if (x > 0) { - neighbors.push_back(idx - 1); - } - if (y < this->size - 1) { - neighbors.push_back(idx + this->size); - } - if (x < this->size - 1) { - neighbors.push_back(idx + 1); - } + auto integrated_current = this->cells[idx].cost; - // Update the integration field of the neighboring cells - for (auto neighbor_idx : neighbors) { - this->update_neighbor(neighbor_idx, - cost_cells[neighbor_idx], - integrated_current, - open_list); + // Get the neighbors of the current cell + if (y > 0) { + auto neighbor_idx = idx - this->size; + this->update_neighbor(neighbor_idx, + cost_cells[neighbor_idx], + integrated_current, + next_wave); + } + if (x > 0) { + auto neighbor_idx = idx - 1; + this->update_neighbor(neighbor_idx, + cost_cells[neighbor_idx], + integrated_current, + next_wave); + } + if (y < this->size - 1) { + auto neighbor_idx = idx + this->size; + this->update_neighbor(neighbor_idx, + cost_cells[neighbor_idx], + integrated_current, + next_wave); + } + if (x < this->size - 1) { + auto neighbor_idx = idx + 1; + this->update_neighbor(neighbor_idx, + cost_cells[neighbor_idx], + integrated_current, + next_wave); + } } - // Clear the neighbors vector - neighbors.clear(); + current_wave.swap(next_wave); + next_wave.clear(); } } @@ -447,7 +462,7 @@ void IntegrationField::reset_dynamic_flags() { void IntegrationField::update_neighbor(size_t idx, cost_t cell_cost, integrated_cost_t integrated_cost, - std::deque &open_list) { + std::vector &wave) { ENSURE(cell_cost > COST_INIT, "cost field cell value must be non-zero"); // Check if the cell is impassable @@ -462,7 +477,7 @@ void IntegrationField::update_neighbor(size_t idx, // update the cell and add it to the open list this->cells[idx].cost = cost; - open_list.push_back(idx); + wave.push_back(idx); } } diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index a8564eccd8..c9149cc82f 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -181,14 +181,14 @@ class IntegrationField { * @param idx Index of the neighbor cell that is updated. * @param cell_cost Cost of the neighbor cell from the cost field. * @param integrated_cost Current integrated cost of the updating cell in the integration field. - * @param open_list List of cells to be updated. + * @param wave List of cells that are part of the next wavefront. * * @return New integration value of the cell. */ void update_neighbor(size_t idx, cost_t cell_cost, integrated_cost_t integrated_cost, - std::deque &open_list); + std::vector &wave); /** * Get the LOS corners around a cell. From 91e84a9da004da51f2864bdd4e7b4b56167c2e82 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 17:40:18 +0200 Subject: [PATCH 552/771] path: Speed up flow field access. --- libopenage/pathfinding/flow_field.cpp | 38 +++++++++++++++------------ libopenage/pathfinding/pathfinder.cpp | 6 ++--- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 26b7592da8..a3ce56f331 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -68,32 +68,36 @@ void FlowField::build(const std::shared_ptr &integration_field for (size_t x = 0; x < this->size; ++x) { size_t idx = y * this->size + x; - if (integrate_cells[idx].cost == INTEGRATED_COST_UNREACHABLE) { + const auto &integrate_cell = integrate_cells[idx]; + auto &flow_cell = flow_cells[idx]; + + if (integrate_cell.cost == INTEGRATED_COST_UNREACHABLE) { // Cell cannot be used as path continue; } - flow_t transfer_flags = integrate_cells[idx].flags & FLOW_FLAGS_MASK; - flow_cells[idx] |= transfer_flags; + flow_t transfer_flags = integrate_cell.flags & FLOW_FLAGS_MASK; + flow_cell |= transfer_flags; - if (flow_cells[idx] & FLOW_TARGET_MASK) { + if (flow_cell & FLOW_TARGET_MASK) { // target cells are pathable - flow_cells[idx] |= FLOW_PATHABLE_MASK; + flow_cell |= FLOW_PATHABLE_MASK; // they also have a preset flow direction so we can skip here continue; } // Store which of the non-diagonal directions are unreachable. - std::bitset<4> directions_unreachable; + // north == 0x01, east == 0x02, south == 0x04, west == 0x08 + uint8_t directions_unreachable = 0x00; // Find the neighbor with the smallest cost. - flow_dir_t direction = static_cast(flow_cells[idx] & FLOW_DIR_MASK); + flow_dir_t direction = static_cast(flow_cell & FLOW_DIR_MASK); auto smallest_cost = INTEGRATED_COST_UNREACHABLE; if (y > 0) { auto cost = integrate_cells[idx - this->size].cost; if (cost == INTEGRATED_COST_UNREACHABLE) { - directions_unreachable[0] = true; + directions_unreachable |= 0x01; } else if (cost < smallest_cost) { smallest_cost = cost; @@ -103,7 +107,7 @@ void FlowField::build(const std::shared_ptr &integration_field if (x < this->size - 1) { auto cost = integrate_cells[idx + 1].cost; if (cost == INTEGRATED_COST_UNREACHABLE) { - directions_unreachable[1] = true; + directions_unreachable |= 0x02; } else if (cost < smallest_cost) { smallest_cost = cost; @@ -113,7 +117,7 @@ void FlowField::build(const std::shared_ptr &integration_field if (y < this->size - 1) { auto cost = integrate_cells[idx + this->size].cost; if (cost == INTEGRATED_COST_UNREACHABLE) { - directions_unreachable[2] = true; + directions_unreachable |= 0x04; } else if (cost < smallest_cost) { smallest_cost = cost; @@ -123,7 +127,7 @@ void FlowField::build(const std::shared_ptr &integration_field if (x > 0) { auto cost = integrate_cells[idx - 1].cost; if (cost == INTEGRATED_COST_UNREACHABLE) { - directions_unreachable[3] = true; + directions_unreachable |= 0x08; } else if (cost < smallest_cost) { smallest_cost = cost; @@ -132,7 +136,7 @@ void FlowField::build(const std::shared_ptr &integration_field } if (x < this->size - 1 and y > 0 - and not(directions_unreachable[0] and directions_unreachable[1])) { + and not(directions_unreachable & 0x01 and directions_unreachable & 0x02)) { auto cost = integrate_cells[idx - this->size + 1].cost; if (cost < smallest_cost) { smallest_cost = cost; @@ -140,7 +144,7 @@ void FlowField::build(const std::shared_ptr &integration_field } } if (x < this->size - 1 and y < this->size - 1 - and not(directions_unreachable[1] and directions_unreachable[2])) { + and not(directions_unreachable & 0x02 and directions_unreachable & 0x04)) { auto cost = integrate_cells[idx + this->size + 1].cost; if (cost < smallest_cost) { smallest_cost = cost; @@ -148,7 +152,7 @@ void FlowField::build(const std::shared_ptr &integration_field } } if (x > 0 and y < this->size - 1 - and not(directions_unreachable[2] and directions_unreachable[3])) { + and not(directions_unreachable & 0x04 and directions_unreachable & 0x08)) { auto cost = integrate_cells[idx + this->size - 1].cost; if (cost < smallest_cost) { smallest_cost = cost; @@ -156,7 +160,7 @@ void FlowField::build(const std::shared_ptr &integration_field } } if (x > 0 and y > 0 - and not(directions_unreachable[3] and directions_unreachable[0])) { + and not(directions_unreachable & 0x01 and directions_unreachable & 0x08)) { auto cost = integrate_cells[idx - this->size - 1].cost; if (cost < smallest_cost) { smallest_cost = cost; @@ -165,10 +169,10 @@ void FlowField::build(const std::shared_ptr &integration_field } // Set the flow field cell to pathable. - flow_cells[idx] |= FLOW_PATHABLE_MASK; + flow_cell |= FLOW_PATHABLE_MASK; // Set the flow field cell to the direction of the smallest cost. - flow_cells[idx] |= static_cast(direction); + flow_cell |= static_cast(direction); } } } diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index b734f4a304..6f67d254be 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -315,9 +315,9 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_dir(current_x, current_y); for (size_t i = 0; i < flow_fields.size(); ++i) { - auto sector = grid->get_sector(flow_fields[i].first); + auto §or = grid->get_sector(flow_fields[i].first); auto sector_pos = sector->get_position().to_tile(sector_size); - auto flow_field = flow_fields[i].second; + auto &flow_field = flow_fields[i].second; // navigate the flow field vectors until we reach its edge (or the target) flow_t cell; @@ -333,7 +333,7 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_dir(current_x, current_y); + auto cell_direction = static_cast(cell & FLOW_DIR_MASK); if (cell_direction != current_direction) { // add the current cell as a waypoint auto cell_pos = sector_pos + coord::tile_delta(current_x, current_y); From 156165a4f1f368ce6321423fb54a9893490453c0 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 18:13:36 +0200 Subject: [PATCH 553/771] path: Optimize portal exit node search for speed. --- libopenage/pathfinding/pathfinder.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 6f67d254be..a316a06acd 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -491,22 +491,25 @@ std::vector PortalNode::generate_backtrace() { std::vector PortalNode::get_exits(const nodemap_t &nodes, sector_id_t entry_sector) { - std::vector exits; + auto &exits = this->portal->get_exits(entry_sector); + std::vector exit_nodes; + exit_nodes.reserve(exits.size()); auto exit_sector = this->portal->get_exit_sector(entry_sector); - for (auto &exit : this->portal->get_exits(entry_sector)) { + for (auto &exit : exits) { auto exit_id = exit->get_id(); - if (nodes.contains(exit_id)) { - exits.push_back(nodes.at(exit_id)); + auto exit_node = nodes.find(exit_id); + if (exit_node != nodes.end()) { + exit_nodes.push_back(exit_node->second); } else { - exits.push_back(std::make_shared(exit, - exit_sector, - this->shared_from_this())); + exit_nodes.push_back(std::make_shared(exit, + exit_sector, + this->shared_from_this())); } } - return exits; + return exit_nodes; } From 7eaebae6109ce0d01d71384839427900c676f4d5 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Jun 2024 18:43:23 +0200 Subject: [PATCH 554/771] path: Optimize access to integration cell in LOS pass. --- libopenage/pathfinding/integration_field.cpp | 36 ++++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index 6e76542cde..26134506b8 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -238,25 +238,27 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrget_costs(); + auto &integrate_cells = this->cells; do { for (size_t i = 0; i < current_wave.size(); ++i) { // inner loop: handle a wave auto idx = current_wave[i]; + auto &cell = integrate_cells[idx]; - if (this->cells[idx].flags & INTEGRATE_FOUND_MASK) { + if (cell.flags & INTEGRATE_FOUND_MASK) { // Skip cells that are already in the line of sight continue; } - else if (this->cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { + else if (cell.flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { // Stop at cells that are blocked by a LOS corner - this->cells[idx].cost = wave_cost - 1 + cost_cells[idx]; - this->cells[idx].flags |= INTEGRATE_FOUND_MASK; + cell.cost = wave_cost - 1 + cost_cells[idx]; + cell.flags |= INTEGRATE_FOUND_MASK; continue; } // Add the current cell to the found cells - this->cells[idx].flags |= INTEGRATE_FOUND_MASK; + cell.flags |= INTEGRATE_FOUND_MASK; // Get the x and y coordinates of the current cell auto x = idx % this->size; @@ -271,8 +273,8 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[idx].cost = wave_cost - 1 + cell_cost; - this->cells[idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + cell.cost = wave_cost - 1 + cell_cost; + cell.flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; } // check each neighbor for a corner @@ -287,10 +289,10 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + integrate_cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; // clear los flag if it was set - this->cells[blocked_idx].flags &= ~INTEGRATE_LOS_MASK; + integrate_cells[blocked_idx].flags &= ~INTEGRATE_LOS_MASK; wavefront_blocked.push_back(blocked_idx); } @@ -300,21 +302,25 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[idx].cost = wave_cost; - this->cells[idx].flags |= INTEGRATE_LOS_MASK; + cell.cost = wave_cost; + cell.flags |= INTEGRATE_LOS_MASK; // Search the neighbors of the current cell if (y > 0) { - next_wave.push_back(idx - this->size); + auto neighbor_idx = idx - this->size; + next_wave.push_back(neighbor_idx); } if (x > 0) { - next_wave.push_back(idx - 1); + auto neighbor_idx = idx - 1; + next_wave.push_back(neighbor_idx); } if (y < this->size - 1) { - next_wave.push_back(idx + this->size); + auto neighbor_idx = idx + this->size; + next_wave.push_back(neighbor_idx); } if (x < this->size - 1) { - next_wave.push_back(idx + 1); + auto neighbor_idx = idx + 1; + next_wave.push_back(neighbor_idx); } } From 3a63dc69463d4321966da8f46d2bbf80edd4a50d Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 11 Jul 2024 02:03:48 +0200 Subject: [PATCH 555/771] path: Make distinction between cardinal/diagonal checks more clear. --- libopenage/pathfinding/flow_field.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index a3ce56f331..e582345b48 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -94,6 +94,8 @@ void FlowField::build(const std::shared_ptr &integration_field // Find the neighbor with the smallest cost. flow_dir_t direction = static_cast(flow_cell & FLOW_DIR_MASK); auto smallest_cost = INTEGRATED_COST_UNREACHABLE; + + // Cardinal directions if (y > 0) { auto cost = integrate_cells[idx - this->size].cost; if (cost == INTEGRATED_COST_UNREACHABLE) { @@ -135,6 +137,7 @@ void FlowField::build(const std::shared_ptr &integration_field } } + // Diagonal directions if (x < this->size - 1 and y > 0 and not(directions_unreachable & 0x01 and directions_unreachable & 0x02)) { auto cost = integrate_cells[idx - this->size + 1].cost; From 498beb32cabaed2e459ffcd868f36b2322529ea2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 13 Jul 2024 11:57:13 +0200 Subject: [PATCH 556/771] etc: Remove pathfinding TODO from pretty printer. --- etc/gdb_pretty/printers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index 28bfb5ca22..fe6d43e397 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -416,6 +416,5 @@ def children(self): # TODO: curve types -# TODO: pathfinding types # TODO: input event codes # TODO: eigen types https://github.com/dmillard/eigengdb From 966218697f089ba5f5c91b2d8a62248ea492b915 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 27 Jul 2024 22:28:16 +0200 Subject: [PATCH 557/771] path: Check if target cell is impassable. --- libopenage/pathfinding/pathfinder.cpp | 38 +++++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index a316a06acd..288f7875df 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -4,6 +4,7 @@ #include "coord/chunk.h" #include "coord/phys.h" +#include "pathfinding/cost_field.h" #include "pathfinding/flow_field.h" #include "pathfinding/grid.h" #include "pathfinding/integration_field.h" @@ -31,8 +32,10 @@ const Path Pathfinder::get_path(const PathRequest &request) { or request.target.se < 0 or request.target.ne >= static_cast(grid_width) or request.target.se >= static_cast(grid_height)) { - log::log(DBG << "Path not found (start = " << request.start << "; target = " << request.target << ")"); - log::log(DBG << "Target is out of bounds."); + log::log(DBG << "Path not found (start = " + << request.start << "; target = " + << request.target << "): " + << "Target is out of bounds."); return Path{request.grid_id, PathResult::OUT_OF_BOUNDS, {}}; } @@ -44,6 +47,16 @@ const Path Pathfinder::get_path(const PathRequest &request) { auto target_sector_y = request.target.se / sector_size; auto target_sector = grid->get_sector(target_sector_x, target_sector_y); + auto target = request.target - target_sector->get_position().to_tile(sector_size); + if (target_sector->get_cost_field()->get_cost(target) == COST_IMPASSABLE) { + // TODO: This may be okay if the target is a building or unit + log::log(DBG << "Path not found (start = " + << request.start << "; target = " + << request.target << "): " + << "Target is impassable."); + return Path{request.grid_id, PathResult::NOT_FOUND, {}}; + } + // Integrate the target field coord::tile_delta target_delta = request.target - target_sector->get_position().to_tile(sector_size); auto target_integration_field = this->integrator->integrate(target_sector->get_cost_field(), @@ -64,8 +77,10 @@ const Path Pathfinder::get_path(const PathRequest &request) { } waypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end()); - log::log(DBG << "Path found (start = " << request.start << "; target = " << request.target << ")"); - log::log(DBG << "Path is within the same sector."); + log::log(DBG << "Path found (start = " + << request.start << "; target = " + << request.target << "): " + << "Path is within the same sector."); return Path{request.grid_id, PathResult::FOUND, waypoints}; } } @@ -97,8 +112,10 @@ const Path Pathfinder::get_path(const PathRequest &request) { if (target_portal_ids.empty() or start_portal_ids.empty()) { // Exit early if no portals are reachable from the start or target - log::log(DBG << "Path not found (start = " << request.start << "; target = " << request.target << ")"); - log::log(DBG << "No portals are reachable from the start or target."); + log::log(DBG << "Path not found (start = " + << request.start << "; target = " + << request.target << "): " + << "No portals are reachable from the start or target."); return Path{request.grid_id, PathResult::NOT_FOUND, {}}; } @@ -157,11 +174,16 @@ const Path Pathfinder::get_path(const PathRequest &request) { waypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end()); if (portal_status == PathResult::NOT_FOUND) { - log::log(DBG << "Path not found (start = " << request.start << "; target = " << request.target << ")"); + log::log(DBG << "Path not found (start = " + << request.start << "; target = " + << request.target << ")"); } else { - log::log(DBG << "Path found (start = " << request.start << "; target = " << request.target << ")"); + log::log(DBG << "Path found (start = " + << request.start << "; target = " + << request.target << ")"); } + return Path{request.grid_id, portal_status, waypoints}; } From 38d05e6d3ef9b7f283c068c5d3db50e570618327 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 27 Jul 2024 23:00:41 +0200 Subject: [PATCH 558/771] renderer: Update animation frame based on keyframe insertion time. --- libopenage/renderer/stages/world/object.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index d4629f866b..0a707807ca 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -98,7 +98,7 @@ void WorldObject::update_uniforms(const time::time_t &time) { auto angle_degrees = this->angle.get(time).to_float(); // Animation information - auto animation_info = this->animation_info.get(time); + auto [last_update, animation_info] = this->animation_info.frame(time); for (size_t layer_idx = 0; layer_idx < this->layer_uniforms.size(); ++layer_idx) { auto &layer_unifs = this->layer_uniforms.at(layer_idx); @@ -123,7 +123,7 @@ void WorldObject::update_uniforms(const time::time_t &time) { case renderer::resources::display_mode::LOOP: { // ONCE and LOOP are animated based on time auto &timing = layer.get_frame_timing(); - frame_idx = timing->get_frame(time, this->render_entity->get_update_time()); + frame_idx = timing->get_frame(time, last_update); } break; case renderer::resources::display_mode::OFF: default: From 703250569b3d644ad8370b018e18e00afaa2a703 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 27 Jul 2024 23:11:45 +0200 Subject: [PATCH 559/771] gamestate: Do not spawn entities outside of map area. --- libopenage/gamestate/event/spawn_entity.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/libopenage/gamestate/event/spawn_entity.cpp b/libopenage/gamestate/event/spawn_entity.cpp index 9a728a71ef..66f6df9be7 100644 --- a/libopenage/gamestate/event/spawn_entity.cpp +++ b/libopenage/gamestate/event/spawn_entity.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 "spawn_entity.h" @@ -19,6 +19,7 @@ #include "gamestate/game_entity.h" #include "gamestate/game_state.h" #include "gamestate/manager.h" +#include "gamestate/map.h" #include "gamestate/types.h" // TODO: Testing @@ -155,6 +156,21 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, const param_map ¶ms) { auto gstate = std::dynamic_pointer_cast(state); + // Check if spawn position is on the map + auto pos = params.get("position", gamestate::WORLD_ORIGIN); + auto map_size = gstate->get_map()->get_size(); + if (not(pos.ne >= 0 + and pos.ne < map_size[0] + and pos.se >= 0 + and pos.se < map_size[1])) { + // Do nothing if the spawn position is not on the map + log::log(DBG << "Entity spawn failed: " + << "Spawn position " << pos + << " is not inside the map area " + << "(map size: " << map_size << ")"); + return; + } + auto nyan_db = gstate->get_db_view(); auto game_entities = nyan_db->get_obj_children_all("engine.util.game_entity.GameEntity"); @@ -183,7 +199,6 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, auto entity_pos = std::dynamic_pointer_cast( entity->get_component(component::component_t::POSITION)); - auto pos = params.get("position", gamestate::WORLD_ORIGIN); entity_pos->set_position(time, pos); entity_pos->set_angle(time, coord::phys_angle_t::from_int(315)); From e999fe4ae8c43695a6376f31b4df63f487be6e26 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 28 Jul 2024 02:17:11 +0200 Subject: [PATCH 560/771] doc: Document field types in flow field pathfinder. --- doc/code/pathfinding/README.md | 5 +- doc/code/pathfinding/field_types.md | 78 ++++++++++++++++++ doc/code/pathfinding/images/cost_field.png | Bin 0 -> 118593 bytes doc/code/pathfinding/images/flow_field.png | Bin 0 -> 173423 bytes .../pathfinding/images/integration_field.png | Bin 0 -> 219179 bytes libopenage/pathfinding/definitions.h | 2 +- 6 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 doc/code/pathfinding/field_types.md create mode 100644 doc/code/pathfinding/images/cost_field.png create mode 100644 doc/code/pathfinding/images/flow_field.png create mode 100644 doc/code/pathfinding/images/integration_field.png diff --git a/doc/code/pathfinding/README.md b/doc/code/pathfinding/README.md index a7ec70d1bd..19d1dc8d19 100644 --- a/doc/code/pathfinding/README.md +++ b/doc/code/pathfinding/README.md @@ -109,13 +109,12 @@ a path request is made, the main influence on performance is the A\* algorithm. a limited number of portals, the A\* search should overall be very cheap. The resulting list of sectors and portals is subsequently used in the low-level flow -field calculations. As a first step, the pathfinder uses its integrator to generate +field calculations. More details can be found in the [field types](field_types.md) document. +As a first step, the pathfinder uses its integrator to generate a flow field for each identified sector. Generation starts with the target sector and ends with the start sector. Flow field results are passed through at the cells of the identified portals to make the flow between sectors seamless. - - In a second step, the pathfinder follows the movement vectors in the flow fields from the start cell to the target cell. Waypoints are created for every direction change, so that game entities can travel in straight lines between them. The list of waypoints diff --git a/doc/code/pathfinding/field_types.md b/doc/code/pathfinding/field_types.md new file mode 100644 index 0000000000..d7251da686 --- /dev/null +++ b/doc/code/pathfinding/field_types.md @@ -0,0 +1,78 @@ +# Field Types + +This document describes the field types used in the flow field pathfinding system. + +Most of the descriptions are based on the [*Crowd Pathfinding and Steering Using Flow Field Tiles*](http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf) article by Elijah Emerson. + +## Cost Field + +A cost field is a square grid of cells that record the cost of movement on the location +of each cell. Higher cost values indicate that it is less desirable to move through that cell. +The field is usually initialized at the start of the game and persists for the lifetime of +the entire pathfinding grid. During gameplay, individual cell costs may be altered to reflect +changes in the environment. + +Cost values are represented as `uint8_t` (0-255) values. The range of usable cost values +is `1` to `254`. `255` is a special value that represents an impassable cell. `0` is reserved +for initialization and should not be used for flow field calculations. + +![Cost Field](images/cost_field.png) + +- **green**: minimum cost +- **red**: maximum cost +- **black**: impassable cell + +## Integration Field + +The integration field is created from a cost field when a path is requested. For a specific +target cell, the integration field stores the accumulated cost of reaching that cell from +every other cell in the field. + +Integration values are calculated using a wavefront algorithm. The algorithm starts at the +target cell(s) and propagates outward, updating the integration value of each cell it visits. +The integration value is calculated by adding the cost value of the current cell to the lowest +integration value of the 4 cardinal neighbors. The integration value of the target cell(s) is `0`. + +Integration values are represented as `uint16_t` (0-65535) values. The range of usable integration +values is `1` to `65534`. `65535` is a special value that represents an unreachable cell. During +initialization, all cells are set to `65535`. + +An additional refinement step in the form of line-of-sight testing may be performed before the +integration values are calculated. This step flags every cell that is in line of sight of the +target cell. This allows for smoother pathing, as game entities can move in a straight line to +the target cell. The algorithm for this step is described in more detail in section 23.6.2 +of the [*Crowd Pathfinding and Steering Using Flow Field Tiles*](http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf) article. + +In addition to the integration values, the integration field also stores flags for each cell: + +- `FOUND`: cell has been visited +- `TARGET`: cell is a target cell +- `LOS`: cell is in line of sight of target cell +- `WAVEFRONT_BLOCKED`: cell is blocking line of sight to target cell + +![Integration Field](images/integration_field.png) + +- **green**: lower integration values +- **purple**: higher integration values +- **black**: unreachable cell + +## Flow Field + +Creating the flow field is the final step in the flow field calculation. The field +is created from the integration field. Cells in the flow field store the direction to +the neighbor cell with the lowest *integrated* cost. Thus, directions create a "flow" +towards the target cell. Following the directions from anywhere on the field will lead +to the shortest path to the target cell. + +Flow field values are represented as `uint8_t` values. The 4 least significant bits are used +to store the direction to the neighbor cell with the lowest integrated cost. Therefore, 8 +directions can be represented. The 4 most significant bits are used for flags: +- `PATHABLE`: cell is passable +- `LOS`: cell is in line of sight of target cell +- `TARGET`: cell is a target cell + +![Flow Field](images/flow_field.png) + +- **white**: line of sight +- **bright/dark grey**: passable cells (not in line of sight) +- **black**: impassable cell diff --git a/doc/code/pathfinding/images/cost_field.png b/doc/code/pathfinding/images/cost_field.png new file mode 100644 index 0000000000000000000000000000000000000000..8d820b93faa52071ed4e6b1a71b3bd8d553dae87 GIT binary patch literal 118593 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 %ki6DfoBZi5QYrob*b}o1Rs1u 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�K<#c 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

%Rdmx&F65d5>q8)F0A2L3xyPn19y}E& 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

U9Kc)Cs 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

KY;#PN__?XyW!nbDj|LO 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�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

}P`erBRgQzL=l`z#HLkDcyLB2To%ook!@U2vO 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

m-I6^2ZWf$Rv{Rb#in}DPEfLN@Ii{7Z% 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

X*^D=dU{~ZmAb2tx_lpWjXJ_FUF!09 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⩝!ud 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⊤gK_U?*6ZL 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 z�Vqc6CHBPN>HDTj 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%0A*zSgw_~n8?>BBJS&UJw^uQW3 zU0Pb-pKyCSIE*B^y48>SG?pS17XKbY8OqdeivWVIlp21RvnYv3_6Mo)mne~*;$bE0 z021r~i9~sup)55dTqSazTb~WBvaNoKmRzuXINo&0)0I=y7ymO`Q0uYu)0XLSVChu) zD*o0*@jNhys-g2gxMZB51WTDs4##6pN7}&H;0$=bD?EcU0Q;tJdeS^HrqS`{#t9iO3 zZwO2U+T-g-*@Y|m(`mWE+gZ@_A*7fXeibLQ%a7msBr#Kl?%AS}=85qusbt9JZSA zR#J-8=1WO~#UE87U)DY?XP;PXBzOl~som9ge6MQ(YFk?GFHdy3bMxumOTFgdv zx|I;HOHNzKDUM9%lO|@0#XxoOHnQ4nQ>50?RAK48LvV@!PHsr5S?bEa2V#*DjM1~9 z1Q6UkNBhrMZUWY{KYss-Pph=rDurj$^($i~ilMT}*epomV@L__v;(DyQeYP(i34O^ znSj8g_ozrQ7*KN1IFx`JX7%p@v%p#zxv_I4WU!9={wGU20!&To+wzlQGx~BJL_q*j z3HrC)IWhC&u67!^S3UL#LYKceBheP@!yz8F5W(MG^n|>!XG+|wqD$qk)QioTbk9ji zelH$WukS;hG!OSEwvldvJp@anNOjG)V+Til6e^X){aGp~$VZOFro{)! z5K)^KvSQXo7JMEVb2Od~?!2IcRyeCxDRc7Kkb>7=uzIn-`!WeMO)y46v&7H zg#0^Fx&Hg$k>O42LQQ8dwQ3j?R8wz9^lWpDh`N};6|bz zXOd5wXYcG=aBmNG5N`_uBMSFhZTiHUS0r3RdCAbDSfqzl`3m6 zcWZIps!Y?Hn(b?-1RdZ9p5>H#RyV$nY+H!e`ZhK6x`81f9+mfQGfbYUYV@o&`pF?HLCQ7- zkWXdW&qY3bua`3>!VLKLoXRDiBP0aL7IB{X#ND}7X#EB`bG2DuZdkVr#+iCUE{%4LF*pX(*`zQ4T@s`=u!nnR`32X02}=oE(>+Wu*U47B2)dBeudkYC{|M(8 zuTHsPe*N<%(w;JJAyiHR{aY1{0#hQc~u47swMZraApWJD0aGMr${Lt{ zg#9-q(wmrZCf}&<>`q6DsUnhCZaez-AF`^z91LKX1QK^;8N1YE-P+?-HMX1c4P$Oi z3P^6c`r_~6_gld-2n=38JxRH~n<$lZ;X(JY!QOtOQ&}8ILLT1z5%D))bgzlv`(e(@ z@&E{oAC3s~WV$YtPKzggy-Uy-i?ia|>o$!+uGu#7Hx*R0c<$@mgVirHgvaatE%>=G z^E$)0X+CfYj~~UQo_U@>q1T{7!0)Vmz1BJ(NbH*%tYH0iHO}s3I0R0GiJfB_DK#z4 zO6w_*vPBptv=zn#_NJNdkFr;AowMA@$XA1)oSDeum=_hAO|TFF3pB7M!|s~K>A|N{ zR1&dUbl6?J56h8%m4D|1davlo2gaZHbdxLnast!P&|GGIX2bm7lf)DijX<~Ic(p^t z20)tZMVR0G014Y^pNI8qM$802MD)%BA|r6h(%{L71;a54EPLe^HHq!h0jT8oND@bF z%O5woMdR0*9C!qbKf?o0ou6tfrQ{$l|NBfgc)UELQV~o#&w>P(BK6*}Z(L5U=msvB z)c=RG_l~DJe*gau4jIQL$H=jFaqPWk_6m_>kCIU;BuB?_ti!Rl%*ZH7gA$IJ6)7Z& zgAkR2)WPYT@2lRQ_h-PJv+fj~=*K=Ig^>{q)a}GWbn#25HMk=vxT%P9~28Q8u zPyIU3krf_qp6HqB8j^5wLAoVUM3K5YY`q=Pui|K!mgvZ)jT(SqY6 zj9E;2PmldlNYBA4gO==?YQhCg+)!o2Poy9KCW#=q7ZCC8gOd= z&y}AG=QW3)5Q8-xvc2xXfV`T<_pGAC2G>RqNI6I{ri_KXAAfGxL%UtJ>1E{Iir9R{ zLuku36(8*HT5FY>&bNboD7qp_M|0HRJ|&{=39W#o6Z7a^%A+lap9BNIY*eARuIIH2 z9HgwqvTc&&hIXeBs5M@?4M#r$WDo$_UK-r5pqSUx!v!)2(R1RnQ{;pY5K~k(@onTi zld2GH(QlXO)ysM%E{*5{WL~EVT zy=?+4)g%6Pah_KrI^wX^=b2WOBAYYLH;6;?dY7Ceb{Yb%>$a`+%d8tfZ1B0@uo$uu zuZBN2b>!8YNuRJ{c>9A^1VGG~_(5g=X}Ul1rh2klGw%&hN=W(A3UEmF((oo$tTpb& z?D>hNy-FUv+}a~9%p~Pk z0o=bI2^F#I5Z(IYferLb*$#2=;riJJNh)0Hu)V01p24>fdB}`C!%tO)32CpYMs?fc zGOd99-H!>1X~`1-pIzu9(Q;^hCYI8}d& z@PFhS$NjK$9Xm9E-YNvC9MsQ#i~6lfB?`G$?(%o{$%uW~HFjf@VPv=r;OVfSnJ>kg zdPlZD=st~K-=Kb(4qZix>9tQqBwh$R+@MnD`feN@8Km%lcaDi*TVjm%K74AQ?Du;m_nbitpXb4g;amu%TQu{JNXs*SP|-9@hw6&5=b(2HvRm;*1sY>a#=VPv1%!c{jX`I-r{1y9teL2 zM@-@vJqwoZB%17TB;XY;rpqrJRp;O;MCxXARx{5D6ztePm3W!^O9$lvHAxA`h3TJ) z&;wDag_o9FBRmA@dwemmxQG3d{?SUnOLuBcjx^(f|o5!nhD}+6za~-IN zx&idNfxkV7)woc?o+y8oqkyXGYGTsTk5vs_*>_hLIy#eR+;zFWy52uSaxO9SyCxJE z#}s}_wlfrzu2Qq_FJdQ6tpYqNJGaBXs?tH+WrcF8VQ-A-Ik;XdP#uOV}z>Lyus z2-|=I;T?)A#5mf_*Z_65aO(1U$qVx7!5Qc@xQ6=n)YK$H<23E6)ZRcllR^-K%AHGT)2#8z(W0 zPyKjI8gPnZlaM|%O&{=y*zJmuxsd8FcF|2jnI+qFY(1A;f>^!PbWRz>udVvX*f+P& z9QoE_lZjGtF1!fU5>q)0(V2)tV7J`XMIJBkC@&9lNE&v@u91R312P{?A4@zv=_JES zVNr#*$S^WNGQK$t$pCeYP5qUCkAkP~{`Ruv%s-=*rHJydEx{~i2#d4}#mw{L^R6hJ zF_*TogASZOWXyZm3>ewY<@B<-G3WG9zoaw>O zRb&uS9(O1#E*#?D6ePF9w39rk%(1E}@L-$;<{P6h75v*-`WH$1YgXd+*R;?CjmGZ! zkk6w(LhSr>)eQO_z4efSGm&%dD{QJLSUY#mTdn@Vs>FESs8P3)Z*=^0FHciW5^Npc zm7cKQO-dLT=ie>S33DvSjpeVOb#EXc>(1I+V)MQkj0c27ql?4kW&HJ7$RW#Gk~T+2 zAH_d$7%(4LV~#wF_h1r{l|#fkiJd}$dhQKEj3=HHs#x8UvE4@+HZzzBb1pmQwkTc0 z3-%(!R-eTGPM7{e)rk2Qu!b~1=UUPI%fE9>xP?%p_GN5I9&o+020*j_NOTAO@|k}_ zDMTT`8BdW1hnM<0?5I7=4GLM5$v41m7N3A1EdJTM1j-*Zj!~&yWqn_Jh7{XalKF_q zom9dM)6y@3%jW{G9_o=|DDlOA5v}l-Z_)X8s+3@My;bA}?S5q?QVNQnWFtRzAnkJ;j)edKrA3-1MI zm&i<|FR2Gh=!G=5)$*cHH@?@Ml@-~uUq{s6iUsK0=36yGko8wcAsB^5ialY(v~h0l zA#p45XZbUVGMsg(Rrb+UFmxKBc~jv#6eAjL>-phqk42@; zL(DV2uW?_>ND`~&s`qq02|g)2d&@6cnmtXU#X3 zAJ1CvdM)7v`?+tTJPqo1BkWDa}FbS;xo`B+65hh;}J<$VTed=Cv|keyx=4Q+1X$+0ankRGbTEMkP7} zO^Un+hHbPxo%uEUmCP4r9oG7^?rQ{o-xQ~k5bsXOh(NbJzzj+= z`CAMrZ(Et&*Zqu8mR!YCAoc;vnnltWv=_u#TvRBfbEpWsc_}~qqK!c~Z-6;`rwD)A z2V`qulxNNc@$MnZ1{oPubDdx75VjVvK&CytKYxp8tr2R;FU9K0h9Fq;@Fz2+YxcfM z`y`$gD@_KZ?h^UMR$;o5CFy$E!(H;ym&ettuFqP&Mn2-_XHz51i`M3~<#nQhnc7;v z%srWe{X&%#D|iix96@+n#$+Pcfq9-J%0Hz_c~mTL!@`^K;>j}`!{dWlf~o#Zg3oaO z&0!a!#~ zZsb0=bX0(&=NORdfDiPpp603Hm4YqTDAU>bR*W+ovzeVAN+F5&y_K020isCx&`iU2 zfG$Q&9DB8UC@u4;{`r#Rl;y0qHWQZJTm*Mft(QIY3E`SU?tb5GOl~B}QJBRuAz#$S zgFv8FB_D-&9{0Y>yjs{y4*x`uQapm5msdrS*L31%Et$uh!<8`=H1VhB(MNev_v1C9 zVfx~G6T8DpsC4LXHlh7du-`fTGpSNO0WzOPn~-=RUtbIVl~wasIRn)>Vp2m2F@CDI z(|Da-g^d_oVZ#i+QepHQpD)VIfL)~yfbv4Lgm(6BJsjn$Z&wg=Wmv#%-~m7RrMozX0mI3 zx|{A@uMQL1S#Iq!2Y+>AWQ&gs0NQF@A33|Y>ga($IxIsqf02xf7myCDAW~p~H#1S@ za&xw0)zpVhe4AJwBu_aCiDm)X-*2shZ?UcflmO2qlzuoo55_n>JuSc~2nhH3boYSB z*dQqcTJxOaDi)B8blHd#iUs&sS>Fe-WevQ;Ie!~jKx7ap}zJe!nLN#@n9tDmg0FSr4%Rp`Dnj*;aB`Q#N3 zMV7M+LDly)D6siTtbq8t%u1^MTf>BxT*3~Fw~j7_=G}_qy`Tc4>-jT9#FZK(2Rvu- z=ln9)s&71@Z};*SlsdwO zCg;W=&><(!@3w^b{~@zlHDbJ9Pf`h3eky4uUcbNxSP5iEz38npMet*~Kv|=qRJ~cPj9c?_9029-+P~~2m6O0l z`Y$xe;xA0(i0@dN%r!WY(J0bP10I=6_BW*EL`gJyd9D_xF9!)suu3YTOo z{`pDWqIQBMG0m$u%ML>u1m&P#gmWS#@v)Q1%9>)FZ?$|xn7H~KrmGCQt$%FKP5dnCr|hO$*8(=Z9NX4R&1?{3paTXAdIR%<~B(3EuVS~xr}ojjY;$nom~o*)qP0I zNylWpusgBpRyxyI)f=SGLk1D}3tIfbU=xFfa7+)C)6BFi%(+}6Q7kX?@hNu**QgeS zP9ewkc!xR%-GP1o2c0f{8{kR(6Poj%3$)*cfRg6$@1PWxW1sRo5Ac9noMEX;7rC;8 zEFKSEN_kN%d17YSK?Ml2@IYpp)VsOia{PqcaMG;*bp*BzeLY&h33! zOAhcynHQ_U4*>U+hzKL#jMf*xuXx87wjwncG*kUI~5SxL{! z7nUy%gnxV|{1Gvv)b=|$j2B+NzBtS`_-Y(GJ4{3u7Dgz9u&S4EoCO*H|1y#PyW0N= z>-;Y&NH0Vt2t12v+>I~Ije-%&BSEm;E5WF=;!Y#czK0J17+2B-rA5#NW#5EYE)k|p zKQRe!Q34Jl<+>sK1%s7CCoGu0oy;3nZ?n5*9^t%1|5zEJd%Q$1geaw%7BYeHm0<4} zGQf%e*M9yEoJ_g#?|^ap|B=S|kM+6H;B9HJyQjGJqupfBu^|baOXa-EoPDB3G4X7+ z;o}6RkkFBf_Z~j-n2k3V;8%p&08F-8=ex+%SACA>a0A|q^!Y>l%J;vV5z~CE&N($Q z5~&D&a+1^0j0v#Q*Ed*ri+hOP$npUwQ2*US|4$Op(VrROiN7;q7%(Hw+`D-#Zx}2H z4QikM&66F`mm8N%#IrK%tG{{FC}2RbRrW(0@b87I%JCZcnmCjk)LBlLAQ)rRlI)>= z6_o^gjXy`t#FxvIB7z+U!NVd;ISUd6uQ8KJW)#CvnIp$pSumIa(C1f=w=ig%iI*o@ zDdP1u!-59%-i)87lf~m@xnDRY9M0*o~DRIHC>4Yl6#CKRdbQ(*kU0Nqo#6vEL^QMS@(fCQ@R#sI$dxs zmDnK74LRXhCos)6c!G$R8$m2$%MRd5hRwWR&IY7C?Uq#!)aedTd?>?|rtjdMMHg-x^<#z3x=UE14K)Mmhpe0SV1>CwZFxu9R^a91L6@RrqU=xORuxC$R%; z1E`6n6v9_~Z%cYJYqQ9YyfGL&P~mr}BXv*@<<8~hs;qAbcIuyj;d*S!RV0Lrh>q1c zY3?gbafFdh?1P>T;&W-fKMxLpjW<7mS#wo}_~$uEA|uWwURm=xt?b1U)z9Lr-3yXT z+u8f^K2Ll(PZq!UnZm3aBXWn+0^;ZgUlG6h!gf>&-n5{ zDX?2n%tQB%ZZT#NB(8Sr0sxd-e`WglV9ZXh;oqGGK^4{!^+$8SGc=n1aD{swrb{eqo3=;w{cwI29Tz zQ^7sr5?!~Je{2u9475|pT*Fs0G6M}S0A-yJe-Z6)x)m*Af4NA$DPR)tSJr2XMaK-6 zRCY66y4Oj&cCVO&-l&GglnQ=^bdoT;CU}}6epjGg7*#?YaK<@92DoQL05!5Pi=IVB zi-x{7+&GlaFFh$mPsJ%{-r>csf*f9mkOM34UIkN#YZ#~0!uoI+r3;Z?mYV+ zSoU!Sz+IyPs7`=K?H@JDpBMfkv9a<`q4KAS`4>Da(A@y&bpL@%cb=7i7cUnd8sc2K zH48cTP082u3UL<}j8wGI&KXz+Up9&P$tkvzTa3JM@JVIgw&T$j_JLC9Rg+lTEyUl>%b%-@6mRT!JDNlI+O5UtQ|C9q^x6v&B%JZf?KcTb(el5Mu^CYx6qs}rtq-w7PB%`q_rIH1 zoOnO(|A}YaNh>4lTL9MlW5rUu`tqr>>SZWB>9?Zn+z*%|cF;Scr-$4WACNb9=8H0~ z^@r2Ia+Ox62~7(9{f%P9`L2^?&hMEJsP9-L>qkj1@eYv`c56-Am^xppQ8axl#XpQ5(?rGLSxd!xcED|mX`u50)0*7ZB1&)vxgj<|6a>-|m$_Gc(xny`P|x;+Ed1Llo~;6bn9+a55*C00>9ANuJNa5nPeCVCz=(ju zUQh99s%zeho~qnlQsjTs$DZ*m3v$|`qw@1^EsBs8NJk$&WPS&W6uB*6W&D4}KK~14 zR*wmp3pZ6tF&GhtI37P9-J}lKC92dDQ!AI#@3MZG>uPGKJYF0oc%bQ9mZ&Q&Emc+C z{3XVvRPBQ=r$uiY<}CfYt^a?zu)SI0u=}$DNH~s8pyqo0{{ydi^?u|KG=0r*c7;jB ze{U)uh<6^~vc?KgSwEU9c9+M$eV}o%J~J})W>5;K;xkL@FhknK18zJPU4ae0O9fzs@NSchE6S8%a=2ar5TqjpQb%oSlN|1h@bY3gv+Aut3@lh-H z-RG`0@QuTJ7jeRbm`{4HVU779mg4(SugwF^4M9s^O5w8!Jga!xSTtA%Y|_pxJugoV zdn7z^rvLD3FU{S zdqKAn({RKFU}XOhQw^W-_kz!e48Pc^=wUX0ea(n*ADh>Sk>w1X-a)Z_cv|YzK8-N7 z%(AdghPRhiq2ohq*45`LDLfN65FJ7=&HeV0b*={Ky+xYv!kEkoyXB^5D$06OAlPBm zTLTAX8hUN9I5K~E3( zE1i@uLnmly-@jTpK?>Jpw7SYwN&0{!qOwQ=eI#Rc#1uzJrRTV^FXcRbi-& zTFwoTsD;Mu1b)`i0^qpu|Ezxg`8)r&$KiQqprw-KpvzwEEJ9zM*A$qG6sfLdiV-AEzNFc)+ivRWjChV zb{YEydcy}(XJV4SX2e@;-3N6p3})YSusR+AK8)DTnb!t|M5XxWowF>4uXsMVCy zb5vA@+2bmw_tSI=Fph#4|9dvC;kI(*t3uX)XGs6cX19%!g+i+fj7}k0o%T-&jk+xt z>_15%x(2`V4d|FYAp=At#B3Z=PR**BS)Q`-Lz`}KUpbtO$LaKd)vx*Vm72wq+yus2 z^Pu$vX?@m!4nCqul91t+3eXQ*4R6i4Ua3t4bd6PAw>(ID4c;*0Q*5)9%SFYpj2A99 zh3t_e+vg0Mph~KyvF&#we+KbC-_ll7_%$Z6{m#ed3+V={Ax&m)q9SB5uv@qJ%gsS* z0vy-Z9&;1Bbwa<^s8M&j5^_55w1J?mOTd=qpaZ_{b&5$jf##B9qt#rtPma`NUw7>p z|A&$cuWw&DdV?Xw{vY=K-;vg3kf8?Chd?JqO-)LwyK+fzWE@|tusC=T%%*6r38H`7 z>Td*d!G1dR)J=VeEzM_$TSbSGY4kCHCohwcTW7BR(+A06165rrdmxB5MI29iM!8ry zkOfiqW@q4os4aG=)TNdp{OmHM3S?`Qn+ST{of>)RG!W?Zv}s~UCAyhPoOwE0|Liy9 z=!hg&nxgSkmqSu6I_>$jm8YMbG#UBGIvOvw!$Rf1--0yTcQYSMUl?h{NrlcO$IO;I zYb{98yVh5zz90;5Rb^#-I$yc@-#_;Mx)6?*o0|oOls{4dBD5w4bJ!{aKQ;bza;_!y zfS8whY08s2w6An3J6wHYJu!P?`h9#@YJ2r^QXbAL@6DS|j9^{2T?Kzn=6!SPL{Kb? zA>$3dtgLo(9rDWfdu{URncInP+Lb-IyUUHCu#`A|--aX=)(%v@JzzfACM=HI4691{ z@7#+MJ+C%2I}aU6!?@lmoWERQcJp`f`)@{#jO_hgQ$h{m?orrcCEuP+lQ+DN4M~g3 zw{P?}J3lGuF_8=qqrL&_c13LTtk+Eu8>HAlpgs2D&~&cx5_PQEuc1A^nkEOIV?gZj zRdgybOT+;fC|@-xWk99AkqL36AJ-vs2l-0FRbcwuND`;o)~8R@r_}$gi9)6%boe`g zz4jzt=o_$|Hs?zwt)Hc#@#k+}C53;{HlXLaCT-80xSI$c5gKa;4*uGMY3x6GTSB7t=HQ7MNOZs6*xVIr+;t|G1w|8%AYWoHk} zE3!o(<8m)%pBMCOv~D<1yzPca&&8A?^3=S%MkIXvxq|n;NE3@#`{?$sO&E<2&tz;( zqB28X*Oi=j8Tm|2yXiC+^@BaloPr75B{A2BlS!;SGZ+Exc^TxY}Dz+ydsJ%P$WRdfdyuso2hDhaBGeBHD zM`b;IdMPEeSlXIs=D{Nx{SmR8^-&D$-68h&vH@BAz9ZMm%OqRTv)KDq0v$5ziTL&wwddkwo;dtZ>8ycYd@~ADW*e<5L2+fJym)B= z%_!GN#9Qgp)@$LP*ZP%UO`SxO6Fj|Jg!QJew`rPoHiwOioMzn>7azD?kx&s>8@`u8 zv_?&+6(>2&A(O9}Ke6lX9o{9|rmHXP!*|_rST(|y;(*12NHI=NnWQ~8t09`X z6q$p9ljI(->X+uu+fwB2*FV|;*bSfKi|3y{oF{la^XS^Pd48C!G(3LKn<=g&NojbA zaKCfPV9Q_1o1Of@RQ#y(U?(~Bl7xnVY|NXyosv{epcJHe{df4RnBxuIGs=7lrkUFv z%S56N4P3}?RWx%%w+16U*MEKkgzcd-+EKR-i}x)A4OV{KZ#;*XYyN{w9l@=15K)KK z7ArqL15s6|bQZ($8q3o{$ik2i_Vu-Id8mxIx6QcIs`oB6cQZ*l*_bS?SB(!7!Y{@( z|K`m;ruJX6x^#4u(CCJ;T=7e3CL^5F6gBqe_TR;C<0U=EWi=(5+nq?)u#I)8E>c$udl;&sE74+Yxcic<%K9V#_907WAovb;iB_+ASJ;YB9`d`G7Kw^8jnmT&4lB4P zpukVhyPw$;8NUBcsiY!|mF|R#%k-P9z5FGSJM^mrH^NtilzL=YULv2@Oz!&S#qr*s z6Ut7#lF$M0B$IyWf6IN$KJpAmoV`?=3o|Kqy}l|;(I0q)2Zg9acNGEZiLF2ee^X6H z1EBAs*fXbCXhUJT41^L%9L96Z0L0HcSCXA7M>6QPR%U%p?e3{3jXVwp;O7l9UdQ(n z7@JDvyX8GPQ#N-0WL>KlAejzKB#9=#raJd`E~c1cUued4B7tlD3sgeNs4BtW@B6jh z=`IbZC*Aola+V#>C*mWu0~j7f?mP}X6N(_;aPi;)0D|-tmIa>fy@utG?5*`t4yYfq z;4&s8taWeUE?D=a8$O~`t$}aiz8l{K*61bHsLcwbbN`qNo$WK=HAS&h>CaW`zDa-F zQBm-A1vhThY~A{x%>=FqRdyh$t88t+fN=yVy^68f&doNua|xiPXiih4fyU%=N|z4< zoAkRc(wy?z47#UR@ifO<(b-1>XOIRJ%NXEc75h8&Q2Z0ghRp_SXI4j@03+;yo&8?W z4+Eyzd)IQUi=^kIRprx13)|s`SK&V1OsDI!e)S2BHA0)E8I;+9uu_mhN@G8%r24M( zHd>os#_SZ7_?}v5*Mp8LP?kBmyS+r;eKr{Sc(OHqMqGfPk1<#9Uq_-MSeKJwXFHaG zEnU~*AJgjpJipR!&22?}3J>pQ+|(-e87&wap&95~U-v3K>{v&X0a%K|jwy_i@7)-g zFOT^EzS=Mz<`Ut>btRR{krE-y2cR)kInuL(uP)=zhwnn&GFDNv_c#T%WX1-MiY!A+ zW8<(rd}UL!_b#;{G%&AdSSI@BJwI=$HkYAe&Lc8b=foyxLpFg=De7-S+QXrJ~$}?pGH_E(jM)q7($PeY?6i7KZ4J z_ae=3Qu0YOU$v{W3V#XrqJ;_z*^tW4=X$6Vky0`G^3836a57>D_WEIfV1JqrVFxR2 zcih6ief(P{u4C$<&PrE)-r7!4z{)|*2#`&NYJcWC;?u~0etf6zR6$?3+TPCQ+?uE^ z149L7kvqbbeh2}1Fy_U#Xh3XBXjw?jJg7hTE=f%RK7oynXrMA4h;&B(%Y zjtPt%@5;zLo(pS3@~OLx8V;}YE)driTeg1?*l!|HXQHZ?je90m=~(c|*Ed7!dz5@^ zLSx?inmoV#!vHTFkiL#sFS*5z7pnY3l9=(#Sc!$U$P$r!LiOwF-?}eOZKG^P`E2xG z?$43>{D~i*Kc?EHMxWikbUD(Z7zEBgg01|l_q~WsXg!dNfBML8dx`H=-(}|qOjn!% zTb`H3g6nU%`mK03)^BnVWR+V*jJ%Tw{Z+&6=bc_0Bb8N28N2nK?Q1wN{lVC(P1oW) z(YlNV`T}qR!Iu0szI5e%;f5E70Cc>#_eyeuUC_u%_}u4t>G>1d3|< zXAvCgOkI#Go;OR+cBCWovD#Dn!yQuFPM&Nb02VASI>75cZugf0$Ry`OLJy)`z(Hyy ze0{F>ANO1@UiZhJcJv$_eR;*vQ(oU44;+19Ek~Q-c*!h|s9b{)ISFyNKKG{inrR`B z(Rbf1Co{hP;@k6`K=`EA#m3fA{@eGIIvRW>AuS&FLnA--zY1$5Wq$jRS#m}5Gn0V2@R<~i z-Fk1X^$~qK2gJzdYHcizD(= z;dsx*?2Fl#$Uz$@EglIoK^R{cnhW|oDPyV|+IJE(Uu!$duR0O#CyB_YCds@a;6EDs zIT{1zF6oCp#qd_qUHx0xhUWO*wu=5wFr39FjVk;Cb?+X4cb&I}K`h`(Lo#(=@6s7s z5J%-^%qmgZru~iYfrffTb6E8VHU6zhe+OOA`H0D0#+?9H>IUT0JD-=eh`5DYWDN?c zqPwDp9%&ivV52d~Euk5eFNZ@-3Tm00e}(6i(}Wkn`Ih>m^+^vfc6wJ!2_4gng##U^ z;mxERHW}3znfkLBDz;5-wwmT!a4_OLbprA{mQ zG&bwqWvbTmTaZPLxuhBPo|)fqU^jxL&ZmOk@<+!dN}!(m!rfDcMT-aIbx0Qv)uLxh zV^qjtK(I>xZY>Xz{j3L#>u-bKV$U#|eWjIvdTQfcNqnhZ{LkphvlX%MWI_mFS~&fu zT;V?CF~lLJgn6mdz6whe{wHsMnaQnRhGXnze&7l#CF^y0L5K=GMCX4wevceu;G~uD;oDif9)}E9%@soyn&RA|3xN-_pp7*vMS|r z6u=;mS!oRPz<@37;f^Z%L#p7xCYY?nQtM5);4?L{cNy8j|EY)g?VPLQY0+mJ2$|GU zHo?9q<+^mGtWCD#wr}YhOPgTQ@k*v;>p=qHGGyJuGIN{IGI@f~#iXcS(~INp@I2Mi z!vFEa>z>USQDT$+Tgl?q#asGy^#KiE)gF#~ro@Z5fy)FJ*taL%^QN-i3BcA=n~!QU zT`Yae%^`ncxwU1)F=>_I3FMJdEu)+g`VGP8i@%!Ipu)YP?~eiAY!T zpus&!O$a);lv(FiM{@4)$rKHC;vU8OSebH0tBpvLKKU4Oz7|gPOP_z|8z^09l*eQ+ z_}G58Yf;yJw-^;NLjrtIBPoxEArBoLQdg*N4W%a7!Pk`@&Mbj<>nxelRN&B`RKW?_ z_&(Y?(s0|xP;}8y%!>t}flPzVXS9mM>qD7VdNN;ce)bgO!xkxxpm`nF-&ZSpGU^@J zEn&BBi;nxPlaEYNp)~?^!aOSq3~Qj=6 zNt{;z|LVG+C=R)M8S6@NE^9!)4@C9L9f~V1`cD$;C$LRMYYs!MA{j&3;vRW~XCMZv zY449aApLXqddH|pDg|UJcvZ?9DPuf0_TGSKTOW}}`c8^dOAjy7kaKmv+!)=XB|i)fQn$nKCV1zD#Cr4LR26a@Q&o>9?~-*pK3a)K z+K-M7J@;*#B-Z)SyvWJSzAB`1T;eCr5hc0Ae4T;bB2Q4nMV>?D5j@BttA{MveOCGkJ8B?xxONM7=5tuLlo&UF35k+P?nB8tSai;Dow?JFElt7#~K&Ud)d+e;;%o)!80n0lB# z51}(6Qk*3)kD%kj2$O57twZ~U{^Ge$O6p%*-##}>g+I1G8Oom0;AF-_FLrift0IPw zzy0|`9XFfeH|q9JVwEHEH1hWSqfAimI^+I*GVBVJd+o zQt$S)9rHxoZeBmzvPAeqV-E>EOD;$do}M|vX!hd$Y~2bHRJGksDy<7y@lkYR&d&R% z$)3Mi>xr1rcRbH=%`k%~I8B{kyx!g~H-Ti*G|#Wm>BoQUSe`^IQRjw|Z<95vi_S_j z8ZVB_BPkohsu}(1Ao$IVp`W|U%GtNRMh5H8kvOG{%a_cXG3WO5RbA2cB+*UffCkT8 zyJ6C~;oKYo4K(DYtjY3@LF6~A)cDAne^y%DA-aFt;vLs6&gc9U$D^0_`|InDz#Ze8 z;u8IIW34r0yZ^LT{6)qfU&q9^+|wV4)&y*ufloLPYj6lpUDDXenqZUFPnn?bK0rrq zTK-g#Qk?AwK}uElt~l4F`AiaK%b!UHQjd=BE9X-9>dz`B2}>iV4GuV^%~hA4<=){f zunK(Dx4lYtcDMF-t7+sC=E$0~;p@#i@vZUI4Sn9lBnoo?JS@cU!tfoeBfKg#x6dDl zx_bRly`eX5S9Z;)h54A-4r$uQD?j9j~)ZB%%u88aT%X8xR` zM#axIx&jQ+AoHFB)3vvqViC+7&Hbg%+;MnkUdy9+_(dhXHX8DctJ*yT&;|X%f|&p@ z|2am;hz<%g=X^GH$fLoVD>rSJLr|dsZ&Mz3@AbWTsR}>e5ph~3D)H{a0g!VKL!G}P z#@m6sfmcuwbrzkFbh%)@fwzgutz%Z5g`9ED6Idcx6;GhG&Rn{!go7T+)&nX_4gVpN zLK!DVh^dHaUb{*Q>AJ6206s614{{HDLMl9gZHLVBA@O2GvU*kThJXN{#MRZX9M}x2OVNiIa}${_`n(HSOPK&cb__Su0jwZ6$apwuYZXA$v~^i$QpkFub4Ls>0ZSn6@2X8 zoxm7SkFkr(=81s%DCuwi#NaQQAjl!WE+`F$kqcLIcFE&&AKrB$N!&Xa4^0`I5faK_ z{V7_((VIv`>yAt1%8uhT$lg96$k02iOzI^d`Nmw&f`+CN&@mWhTSet2nq($TVVm?7 z^Dw7Pv;eX|%#AwpDuGiJMy&9Ilyx=7mtjMtzhFP@~Yxx8xM-~SH2|z+2;*hUj>#h*o zh;E}V$3qq%mabAxG;Mn`subwcdVc{%2B%*s7;q!`NRum`$JK*DCI=&GB!06 zwqXg(I_3m%T2wUy3uLol8{}_gjkG8T}?^IxS z3U*0Kks>F2F|u3BC=0Cxa5Ocwr23&8K!8Q93R7MBm56SptG0CSl#j{c4s%~-^xTH_ zocnnr{8jlOiHV|%j!0?yK3&bX_b#nbY)Q@mwTsi_kdr#azxo`<^Gs5HOA>tvkM4vC z1gw(W6em_$2;?b}Ief9vLf*6A7}Q@MfM=A=rxK zJS2UmDuKX}#j}a3hu|iCAn|odk&3qv`mP?z9(wsbNhFIAF_NwW<96Egb3HBsT4t{? z#^JzSe@LHW#6HZFdi3fgpWNYjRCmg38}=BDTlu}4^c?Kr)7rSiJ_xFfw= zF!tl&HgYbP%qs)K!eqp+h@O%D3&BL|;mk_it1YQ3aBQ?A(<4mqa^Y8(MBmnWpzk32 zLQ5cl3Yl8+6V932`!Y|K9%0SyS6opM%nHMdj#I=1s)uQ4YJC;)rzMAMa&g+D0USMj zQMViY*iRLXz3<}L0&~@l1gDb1FnScy7Icrz=i`JHdw-fNZiKyW?jn-ul57eVQi(d> z#J6=DXQ?#e48#_Ni@dn^b2(8QB?>FT+u{?4u(=gz;UKMeu9?U_E408nu$Y8U*aluL zue_-Lmb5c-1Tg69ibP}LI-wN(C<{YnM5OxSXv_d^HQA~`huwCxExfc@Dq zT&HG(9-Ab#%KY%HiJg~5@FG#?R~aCtzNa!3gvuET5#UF+OfCeW^sg-!zIbV#roYf% z&NE};GH%ey=kj_K>i3kY4QTL16b6kmeVG^n3ffYv(D78Z0}tR%Z&g+)^wOGi-3S#? z1}LKqOJ}UOO`_vRM$&bQAILT-2 z-82Y|y^a7V7s}T^+c46q`n()B=SNo3yZv^xL$So<&zfps0uU4?nYF$kIKiet$VG+Y zyoigts=GzilSnnnHNyG4wbewB@*o6qqML1p!n}s@jY&OHx1JjsmUB!|I@Is9MW3`b z;xp#Ex~d_Hq<{F8Ltu`P)yc%Ji|18M)Y`z1=6d@0BI*6w^V!>~ZRvyaiVh#i$gBNe zCEyC9^S9|~kYk^$`F!8Jr_}d8(+lF9m($#y3YZ^VBL)5n@86A=EExbH2l7nTkS>mBAlJ;N*B?^j zz{uYEHV;X!r$EQhULs28HLEmsyc(-TClvdL!LCgSm;KpMmZkekMMOV2TS(c@0(vj) zo7|-@ght`a^RE9vF8bdJyVd{}WBqv*!(onI;usk&ub|$Z4Q#dA6ia>zlhRjs78P)g z4LSu$_)Etx0s6Vz)VLI7m3Asg2IrM5Gn}pRz;KeQl_@5|$bi%p*7|t;;kNIfEMLPonWl zIy1-`To7{;7d)jB9wgo;j>J~owI&>4l35wXmHX!y*@s_*#Wxe#u4&!(?34^gYDU2^i*E1x1>2X+4EMlYaHWP3arl6@q&Jc6>)TXR zw>7C3|A7A;awVo8L3qKJ>^nMHS{w)4@Yc`nvEMT}m_*r$!!z?_W<7FZ6@DP;pEC?{ zx%Zz#_%0WADt%weub6R80|dza44P?GWA4k7{b@&NqF?s8KuBN@0B(Ax<_lZKv^B~u zfmLFz0LB9@Lc7PO^fruVCHRT(@O4LyBp|6Dk6KQYsL#$wt*&*L>(^6LseK(EbE)#Z z?-;!XWnBUPY#`QdM4-2iB;r*u^c+niRps%m<4-;>I30z_Jxxblkl!fbqtR_+Ft zstGjk^_91jNMU?HEOE}U&1BvKDb8fqu)0|g=nI9VD+UBsdmu250Dvei2ktX$nvAT1 zOt*;jLm|riG$50X8f$EY;Oj>@GEXSX3HI1CXH_VqNdfw-bv0Th$F=r>cy%BT^u!J- z=0?FpbSI#aGaobrRa`8TgQarD=9iQT>630WIs0nYartx0{mD%{i`|ZqB8A6?`)PFp z;lhT)!eO_Y2Kw|(3QX(H9M@k15Ww-(I#>w(;zx|GGmx@J3tEGKM^&%+m%R5xW^VZl z$zpHJ2t3OAS|ed240wK02zEfhj(zHQBgS@C>GfDLHM2#rx_ z2=f&;QP%__WB+92t>^*9a+KLuItk$Ml)l>+L zO|4q3t!B+mRYZtgqgowQf@+IaP!$?-U;W=Np6AW;`8@9lBG+%6=l490>FHFlb`~9x z7YX;u^Vc>ih8AQ;7yE&e01ek*d2s>|WBYifXGOfU43K+V;Sv<=GHc$CCe(P0VueC% z*1Z6L_ml~1LA(gH5tb#&zd??jx-v3WFuS4!sA7E+Hij`)!1KNhOQtPLoi_)9x6Jx_ zG^P|Tmli-R%cA=@dp3ZjZuRKjC!n#AEJvJjSi^!)6@`XAtsD3xnpPO0c1_}N(< zDE@!!Rh}3rC?~X&R&l5iYccU;!?&p^3@C`%{MQh1Sa-i0J=sg|X{EjSxP!tm%DY^$ z-ytmk@D5ddm?Qk0Sp=uBtS$fp3^3Q)dBsOJ>YlDQMExyfovGWj9xz{(#EXB&OO;}z zfxd15RFjF`>x?dvYuqY8Z2fnB3^{#0yB70mfSg_S-j2exUR03lXMFGBDNAeTukH_Q zPo-ZC$-D$m#9>kXIv9j9WW_ubtK^2YODOEtok4pLhuFo%*Hfb+=z;bhWm#5Z27P%7 zqe8=d^AO8}rU&;NpZ46wR&SGD&J)Fjg%%MaT zU@E_=(MK5)x^VkcB1pe-V5oD0_VLIowFHWe)^fCf0iTMP2CE2jgujP%DmQY*%_o*9 zH;7iNp1c6LHV&AyTpcN1xiMF&I3+9ta)Nff&K=ExB`^z|e1q zRB3f^*?8okleQX&ni zQ)mAZ&>Z5>vC3{rCWu47<|+MyJ9#3!wZD>`n>BO)vL1hBORK&78{AOgtbS8q{9_Q} zv0_mjW(o9cbgAdXEPndcA5`l3VXjI)I-wB#_;4&V_YiXH{ZWXJ%b4>GpLmVjT%%Ch z?jO=4YZiioCUsgHFY3Gze>#J&wiu=l)JeQJdJ6%ZrInD~A~pf1-MlP(L|P#~$yyzZ z)m=+=S>5nsj=45<4-kdcC6ndGQ_iSloEqT@ID%>$;ALWdFK@Lp19blO3jt-BOSWr- zT7m8R0tuN)nsTbt-J2?r0L-5kw+uvWUZk03g3oz;d?mU>9T^pz+YFmt5ONEOmutsi zz)qZZgXCJ2@9NaxuJ7khl`zX#nhQ{g*$NO1Tgfpyd@zdN7HeKKT!%FkT3L<1W~0OJ zVhUgbIsNY2`_WqC2S`p%n<^>YEFV=`$uC0dBeOSL%K(5M@$_=Tw~93V>uUTX3tmya z=WV8#cO&E?Oq0GRDwk#SGDLQu(=XAnw5Xu3W~NKrt;vpt(Nkc~H_s)Rf;5?m=5BX9MSmJrx8uhgTsK;*7Rx>=)h0NTCgO)m3@@BJlmIHLKUk zS?wXM!0eI5@*UL9z~HKx4YdLOB%Bur&&U1O>2p|jCg#RrFfz4mO|8F&7CSIHD$%f} zbhNzu3Eq5we=UW(uz|F~VCQ6f$JCgg^zr(`XyhzZKKgj$RB+qWW}tMetcn4tXD504 zhrHL4g7HMW-1<7gX~d=vt~XuYQIhj0%3;w6tN5V{wkV&Prwfb~w)N`9XUznoz= ztM%459Tqu-I52i+EWg*d2JiAb8MO&sg_@jf|C(Mn7}9KxK41Ig%K1~(%qQP(GMz1* z4D5S$g<>Q%e=F?y{mI)WT|La^#0c%V$-vil1a1AA_~PZpvlC3lSU6)T4(wM%jA`WQ zPWiq;%4u7(J$k44M_83$Dp>!ghCZj3k87t1Y&5-D8CJoC9FX?+V9U#i}H zoFXCnHi7F%v0xf{{nnu+&7ChaOgYYsNC%{u3M{{ljM8fR(tQUR z0#U^>URS=WRtt(^c=?XM=CQ8L1N~sAM0}!2^AB!KWYtYAL^1M-`&;2auS*AaFQ!+d@1BbApmXKZoT z)A+nD^j|j3Xdr(YZHgF_A+mA`;g^sXdJOHT)1D0JD}svxw~}evpcCus$H(+gsP2sb z0E>)>^ggpZX})aMDCakh zH2eX7!RmTkU(XvM*Mk6&__yfa5S}eW|B+U79zRjE6aSKkq zl6JMQi}8XVy9L9j{E0#pX*ijA)j?hx-E{_~T4;oWlp)f^x1YxJ3qF6yldEaSOpdNj zR!HWoB>OirO1o6Zt+JlxWLiO5W%PA4==5nWm=YL`nG1hi&MVJ@d{lyT@;r9XbnX!NnVdyW zqOuOLSkk~$2eTpUvZ$>1Tf7hGfihFTLW_7xGNg&YzEXskos_Jn^E%O}?cv24_Kyy2 zOEB2|bHTZW{qjbF5DqC$E^^qT+&B8APo*H|+-KT&5=9qo*Isk{%sTTUhdRpIh(}%* z`;f>?nTQ|%s77`Pz5G{gmq@$Hdc%QaBF+PiSafUYItgN6^kc_SbU;Sz%{rIM@+8ns zC@-gkP%8SAtv? zbWW;>Zjqte1#6i0=x=55jKie};xFDY0J;^~_69o-yAb;)(JR|+f z@8j=9rl;0UUWIA7_xbO&OFdc4dDU*R2)6#)gZahXb;oKh*>y4JQD^?xjHvEu2uqtf zR;Eo#mR*3cSjJ6J*pR=qX3efLYaqvb{>2wzjDo;l1OAY!1;b#Y$HF*@k(-4S*=ES> zOf58^AKNg4?->qUi^^kaST3eOg3NEdGFrUC!2AqgN)St~9C&x`za|xB>m||)!P&h= z(ZNZ`&nI>MaX)kKYdBMaHgxSM(!VaPY+BiuOF*znCUKVA-&v~IZPLjdp3|V5*qJb`UZNqUxOlY#t;E^mavl-j` zF55&AWZFb!cosK)S3ueRz;(;~8@d9b6`;-Tl%n`i7kNJT;t>5&m(K1brG8q9o|}Ud zE5RVPHdLBD^}_yHDrXO$ZuLd7^wcm${+@r`3)5esH-}#O+v``hWIFHyRcMryo}<|t z`dbAq>Mo8fl`Q?uu$d-ijN>4UNF)HqG}}XTEw%QekcOjGBwMJ-cy{Xr=+*8k zJyAolC(pzZT7|XexY*6?taZI-(ATE$FuBQnO1SmxS`Mf7_=6+0nLksHn-aCGt)=n9 z)B88`6hRJN`bKay`isRKrlk4>HDDIKfb&=X!(Ask4&l;3U=WSbr=aU@8dDgnL51 zN1le$Vm;RS(&7S++;oxTDFWiWbpy}itYJTaU_h1-LtignnZJow`h5ygExWjERRc}3 z%Dw(-UjW*`e^=kvIM$2TznVr+zpCybhAJpH<7MInVtYD-m!1_8ang2rob~P4uU;XL zL#6LLVAG=EE$|y4n+yk%zm((gb>_~#Q9_I>%+ZcHQ_ev~Q#PsI6pnx&!1Q7H<$BY| z18-(&=m2_|*JBGjKnc?W%X~dX&=3@zFdpmTzSt^i$<#TbeP)OU4fn;1Dl}|GfnmD3 zqfFXWM6370mdNjq94(^qA1Hvn5B;z@4*ny@=?hBQ`iD&41z?aQL`^`Z1xc8cTFg%&G8uc_PEQ9K&Fct%ypa z1ZhL%-xGyni#)zp`29 zC++yHXu@{;^y~t4B3L_8T*m_kY4}X34h9tNTTT$;z-xgnpqO1c>LG z=Ew&<(0?*RTd+7|bw`&sn2<7lA)QaN*mDb^&^aI&>He6|^B{fCKT>euJe+vC{ffN& zsYQ9u0C{c=gSjIBx$Xtw2Mdh7a&!Q8o0{3#xCe&ZDx&9K0fjDx;2KPx10IknU$=1citYP3-OW`wB(#k+0OY8jY6Q1s{2RhD7>Lt5lH&Rc0UT*CU zKWta13Jh2cKSjbv=~wss_OC7o`0xY0EZV?-!4aM#9w|NQ-{Vs!1%PPzwFaQ~z&p0t zn4f^2NcR?u zPN)=+>URp((G5B|Ee{T&CCl!rvfh=}145Pl%tdiS+|Ezy3aQLN6D_CAt<8|?5WN%l z*Md~XA|ud`4m-?e&GnHXp{Ngu0tOOAFGc@}4A6-&KMD!~xLw8fMZX<6C9_Hvxv3!? zH!7*hT1~|Tj;?e{MbkxKUvjGo?d(l|fEP|L@?UEj1&*Y9!CCFk=GzmbZjvHg~{G_=&nI> zzFDRmpvKHrg*Gl9&IUB5DZ<)95kFd-nU)w9D(I)b(Wcbig{NCusVo0=k*}b71$=DN zdmk1lGpraNjf$kgpQE*bH`?uA!yWiv#-&9OexUXNspo{z4Cp59Po2mU$fm_`%C~dw z#24+dQ@GNI+oF`T)7wNiZV^HF)pnh_t~Jy<+yLOau}VqcSJC z^cSLs9hgCtvmZ$fL#z7^$la7XR>SG6O?*Sys;7pdEsL6o)^xO{aP*I4+ExK(u*NSW zzgrN9?$gcz3cdNM;OOo8V~?{dx<7$Neh^@qmA0ze4;WZDhzrK@O4r4%J~inol%RYV zb~xBU3uSUfkP|khh7n(!BwQ9pXI!t(h=NDkb$*9A4pe?d%Es~*YX3{K(=q(ljdjTP z35B*M`jodfz~7yAAz_WqE{ppwTFG*)!a3J$45HxE;4hcl_f0775}8w@xLaa2#w9>- z3{RHZ4i#b3L80~33d!yK8@W$3PSS0t1}I)xB-ahIZKPJ2gaJy84*yJmfO>jqsbA-& zhvXfWZy!CiD^QgKxo1Y!u`m2#Zfw18Rnd)WrW|7siQZy`);u;hYim@gX>oFr6KN*g z{ZRj@*SuT{S+eP!Et4xbqBzHfc*L(p;an?G(aIpo;o#jJGbQ9_f7b_qSDn`yIqHv@ zCd*{OWqm7;BvVHzI>1E7?PLFyLYdICuDlz9jw=1>*~+UDj7Chn-~r9hV?;UmmBt?l zl(O8A3GD$YYaz3KW3T`0El@+DB2rHTipo}XoOCVOhr&uJd>2pE15Nf{r&s>E|8i>^ z5-lzCps2)qja4X;nY^(+fDONT;eO|r(N3k}{CiV3`j$PdA zYbTi#H}T4a`1;*AVW{C{$~!<>bH23L@qL?#g8>SE`(4(?Glb0k*HMf17tQB3YJnWc z6Ora`ZnEKdB`snu<3bz`73lK_aAAiugQr-tl4QXK2QStRSUIHsE7Usxh5D?iDXe*f zGCJo{nD=Qja4lKB!R5V6dAz=d`Z_B9r0?+P1K%h<>m#smkxbuY0O70Y{rizCvc4I( zzI&S%@js#wKPD#0?U4~VNR72(<@8j#3KV}`J{q;_6}|fb0v7Yd4FG@RC#yc6khx12 z*wO4zj?C8~fP~;tML4+%+Jfr0BU@H8m%7>X!g*(LDsT9X&c-_o$*?)2(1w7PPhlgq zTj7}5?NmWscYc3y+KPBzU>$ET!pKkN6n;BHMJGn!FM1tNuBa)rin_N8uG<7joSx4I zL|(((rm~j|mB+y=8$Z)-m`{&0EHcDBOUk$Pf{R?()6rOHmNh`}ExwcYfB5a=ea||5hxLCEnJ5TfQ_o1J7RCDHP5)Jr+;~ zVh9CGe_IZhnE^1be(K__QW+<*Wc~dj{o?S%gde=h%{*4hJ<*z&{?Tmj_nbi; zuj|*Ve4gE1PF)K(m!1$5^_Gx)rwX&AJx`g^`rZpCaJ_cF^Y=1ZDO6~zWCi-FU0Lh7 zR>S)G!L;5yrMKnG>dy`t6~)&cw;plus0JOiU|WD;v3`K3;abKM^AKQH1aPhK38HQK zxPpU%^pRZFS%)L$Gv;2*1cYf7e`=@*But0ub2q88+Ta!SixwS=}wG9*_Ndt>o)CHBpOy2KCT2K837+HyzyQx?AzP5$JG) zM1a}*L7Pujh?Nno-+JK;3G=9|vXsQgqpjb#oR7z+FZHJY97x2M4D|tWSzsBG-@g1xt&*`h3S9-!RMUoscb8|_|L z-lmom0?W+yv&2$zyJ!;Fn_CwJB*=FkrPdH~4R^oqkStZO&oBpAp~-Gl3S3$C=+QeM z<@~vewBiy+iP#~eOSKD-z9IdPGn6f<(jDf5TA_{8;6Me1(oAEji(q z;E6xlhqqB9&2bI3K}E0*rBOKcZ$!8=T<&=t4H@#yHU&U-YRB1hW#=Wx=t+5H)Z1@&njN$P||-U#YwUZziKpHc-oE>|1g?F zGZEx$@%U7zJqM}!T6(biNhC$*>di;xP`Eb|X`vZ2?t}|2M5^IBQ#43x3esbzJ$A1Jlpcm?+-Y$`cYf*BkPj&viL0_A|d2L|Y$l zK=fchDqaNIH#Jt*&%J+JXi7=2rqz064Kct4fx`mo8o=0`U#+Afm=W-=HH+T_?0!q7k;S-4 zl_P6A%aJ3ojHk}(#()H_Bf5T=Ge>9#zDB6X3vSoc=3wR7A~cSJuU}hRWIn@0w{w^L z`*mro20SQaCJ}OP^fE^NQ^Vr!!o&wS*g7@824eZp+PBYBV)E?qy_9B#vr%ceh~T%F zR(S(rJI|w=NDcq@pUGmx4-cweSPc`SM~UuhDVCuanf(MXM$+^Qs#-4m`&k@!bRHPI ztx>3`LD{gZCzXiUg=9V_ROQvBn98W>^XE-zRNzz;hPE<4A!Nb-E~p0_3j5d z$7QnnVV_FoB9ToKbFeIL@q`EOuS>cgin~9^VB{xAa5*?q|G5JWhtf)W0VvupA&o zr4B6aRuWgJ9gBa?Ff4+nMfz`WUN(6S&Mj`x7Y$1Nd%_ty%E_W@jxKe)%x_0G2yvy1U@QeZDFj)A|MY)yLWl-cDvEa~Xbs!!@h zLY%<~mQ}Uv!%~rEb?+e8M+N(K{l1z0hw(eGe9)bCk$;K)EIy)BkD<|(pU05V-~P#G z5N5?Y)xdM0B6XHzHw)IM?)K?CIMAJMBA%ROFjaL1RK083Y@tM>J1G+=0VcM;w zt{@iA@B5txX4+zTB$J^}ffsZ*MN+PZO;WBX198_i8^P3b8uPIrJAPL&%1qwgJ||wi zOzoUQ+EJ-ElQ-^I2(NYEZC<> z?uo|U#kZajPR&n~Or6kPGQwv!%X9LCsCoA^tgM-udx{maUW6dk@QoUO_RbAN4M0`u zCYT(+fhFJ9#tzvJ*8Uav9rpYn@D=SQe1zLCj&%+x|6Dd(7nPdFiCQ_VXL``e;SqkB z0#Sdy$E~bcD==l5*$FfgPwRuq&fUMXbMQvg_Lt~1=-s4|Y{wlnsQFoZEFCvTbf#jy zn|^Abo0B4>BKv#z+A3ePj84VnvEfKUqjApe#=}6}b!!wFw8#3T4nay4_V+(4&Qr4{ zmP9%=v`~D;qLI}%b(cPi+ChbX9!=xl9iSeNFD#KZ*SDWifrq&7gF)ncqoVde9d+^h zE}(b(cbow(|B2=|PwxxktT(9(2f*zfD^k)1l)U6ZsXiH6r`$I^8kk;0n`TH*r<$vb zvfkp8YgC7zD;pb;ROzp5#79@C{*hH3#J*$61sBs6XyCw~@Lw6IA*z`Ew04}S-Sg8= ziWBEbe$Mv@q{WP7I{kkD2r4RY-?@-nc!so66T7zI5jG^Jo6`zmXd-S%GYn3LTW?Z4 z)h=iOb+z2wgw~WvOYi>bP2hFUs5JyntRKtY$ioqtaxxKm-=t@-B+u3 z`*IV&rYT52h&0s?Ppj!;Fg*NrN)veXx9Rh=>a)k8u_GOwpOP3XHwQYXB#zVk6EnuP z9x)CS`nX7wKznBK&I&cDP{-m73P5Le80@zT!cQ5(>V@H3GdMYMP;k8sPQ+O*o?u2Q}qhk3kiTQgZby*W0kp?@Lm&7zA0RywjjUfZw-5%eA zQJ8#oth9asmC|Nkg8Q%;d`3Q^nu8P5qCqy&MN4#G6=!909xgvzWwa0_G-?R{VD<8# zVU$m%(@HzUe;lpiKEGdFbIvc@<>*c}mGn!TKqC;HDR)`J#4km9e(7kPdN;^_ zLz;odY6uC%H7;!@vc#yPg9<8|ve|Fg)*q7PE9l*dw>S*8IBe3#UIC~~Jr9^vlrdqq zo}E;0kTf8dS24YNMtbPG>TGWuvFs)EZlfU#U}HpWOXu`dwGc|yzjC-b8Z?m-;7#&r zdNLq(S386i=S*RT4_WceQ6pZ!`p&7yc7S8ReV95Z3zbSyIWZVud@(4dP;Y(JbT|gm z#+odjeH*84EC-;bHYaA*n)Xf27IqNRTPH2Q;&XZC*y1h6hQDC!9)Cf4`FBXl6{e_G zu^)BYJ7mAeYvHpw_5N9f!w5BuJ5df5oQqj(`adpU0ht;B6`yh@hL6V#9LhmkDh zRSQbdRCY26`wD;0G&ZPw!D!!jmgK%>VejY6=zM8PU6}Bf(1W`po@K!1ARi=3Og|7{ z$}lz6nGltyz<80$3_>4VY9iFbYJ0a zW+Pn`} zJ?f@%VQQGIzIdQe=#t9CeJRyb(Sli{!c9A`*G9X@WyN?70a9@RuSbpgJ`h7Nzw3*VcbopD$T--5Ppn z{_}JWN{Ft04Ek z4TmOkQVPft&*e@?9i|0Qp&V+4am4E(|8naFf;XAyfe{te*q@mL*qg!Q^5bvV(?|V7 zE$d3%>_yQJ!u0Ula3Q3d7s-or_m>vF8t8!{tl1850qZ|@xejmTm3MKcj9s882-&F( z*Gf-a*Sk%J-~DPdaVt1%i>M)j^Mo)&-=^_$vgx$2Q@kpjTQPT|Z}$zmvbFMP^; zjpG`$6TMBHxf`@4p$}k#7}Cy{M1vhXDV!FvB9oF|Nw)c!tl1V507DB=^w%WI>X=;e z93cN%42g65&f#M0)^)JwLmew<=ogB||HOSHY?tIG=~{(lr2=Yzk)5gMUM131Mn!F`*sL z1?ysL>!`{(^o5jnBFXT-i78jAMF90^iyXfz&qR5r&;=Sx^1RE(iOgbLlJ!O3ehJ_ z!NGJ3IB$621blsA)#&R56b_=PPK}V_R<8l!X6v-hYK^iI(!GiGj9HS3`8F}RF!)N;q9B$(fta%J3J!96h*^N zcIia>CTaHyypHfswNY*BYbE=rd+(XcY~7fEQm%{h(!HSM^QUmW0ASz-&vlhV+cpm9(E zYq7@%AUHmf3la^oi!yfZ09&&^n0fR$2h!A0R4^oMxn0Fai9=ak?oX9}A^hGG+I>1~9VWmZ@h(OAq%{&y$@qh3Cu8 z9cVo;5h-wdSrGm*UBu>rGHr!VVPer|W=J0IdGUMwEv*`=ft>*bTRZ@bKoI2FLG zcO8(2Pz(>a){h=Mvb<2(!RsI+kvmLf?3z3)HA6<7ug86m9^+txWJLv$cd>*@;FJCBzoIw%ANGY6Jf&pfshZ1r_mDh0 z`)yUuMd;SrFog18X{^s+K}YPm@VRTn7H2ybSAzn$0GC>u*T?QMPj1)obf6S?`tX;& zzH&{W@br4oR}D=nKKj2cAcxHw+h#mPiEv`NWFP9h_zbb=&iCBfml|ap@Lk}n14>1 zEGDR<)Q?*=(2aaPC`6*4AO_#uw+i~*?`a9RoBsRkMrUrUxyqF89mWJ_Z939C4WQRL zleLYLeh1*;<3Ii^WU11UksvvJQb{5{4wPubn!kM6+Z_bM7)HoI&eF#FN5(Q>2K}!3 zq1G-TNTZ=v*qUJWp%1R7_6EVYJ5eA-$6}NKpyISx-~GD$)d8c!QLN-tQ$q`X%Yv=h zMX5^z$AepaD>$eCYz5Nd{>SvmE)rqRB+U^Rs1E~91*IE&Ap==g+gWraU}JktWXUy5 z1f=111)BssR)ocXqyLe_=;SENJ7ZdXt~jaNs~aDs-95hJ8}(s1PCzb1z;c?;DBC#? zD=J|MVs-}bWxi1dq{z!6O;)y9m;lL`HZ;T_x|ER=S9Z-2eTXzWTLJ z+K4m9NeS!^oH|BDKq4=sr|L$Tlyzs)x>Fwbd98`;{6T0Lr4I#vqB1#1BNd!1Vchvn zt@+v(+2$(F}9Vt0qT}#$v#hcf`RN zF!Y5F*bmErdiiX`@>P6=Od98wG2qXo2qAw_$W zYG)SJgc*(4D~4fBA7#BC$_QRZFWlx!`2PH7ul8x_#b4I`jNZQ-zX)+FPr8?j99poG zavvsy;Xj^X$Go~my*Azs4XJq!;xBHEyj5CuaKg}tmMvMa{#?+vglZiVbW{^jBw5}E zeH7vTS*7`rWc*iD3bA(5fUjtM%i!gne>b6wBfgj5x5CbW*fWXRsH0}31!{F-nbovTji7hu2pxI+ zB347!vd_`c)MjR7y92=Xp(Hwi3f=mCL(zQ|<0Lhy;c2D|vd1?}B_^>#(LEWez}(3L z*jI#CR0fb~SG?j{{4^;U&S%KAJ^D)iEO7liLFs?_KX~?fvyNjoGWB6CP~8Q6|F6yE zzV7^@KZvPj{|;Z9)h-IE9x5U05?wD^(p=pR`B?~SF=>F8j$Kwq@;S3wli?t{pXC{Z+fwFTj(os*Fv2+jO?FE7g51O zHx%_n;b5i0gOR!RR`P{9*UjDzB4k}ntz~oIers|tM*Bx)$N_oYYI|!sq27>#_*tIr z;+a8;v@e&}+$kUFo0EKn?vbSL=(vx_dxx&#Ga_(1z5sNnNT?NT|gIpY)!ay0d{vyv$Gagvl4E*Fmym_z>0e zgGQZIVs@;NUFou_anZ+%?~ZRIgh2&{h22^ru)ISh8jp-htF;Dsm|K#ne?_|B5yN z<6(+I>y#cMBW1l}^Y>b{Hd=QF#B1|+Kw}V=@a&>67pD~6`{zc>$+y>!?3#N)+8`Wb zI{)^$Bbe6)lt`u=r6#Yhj4z{nGOc&$kT75`%X-d%kI8{Hy%kEG;WOqTfQUQnL zx%Kkp5N&Y^K;YKCkmrCCaLqTPZD{0#f8`Qd0k-EMHhT?gkJ23Ane35_84?l_-M~N9 zmvOaBC8OT4<-zJj9J2CvsKAIkmy~fm1ANS&JM17IHq z2Y~3$l?ixgj~-QvJUtD;d!rblat7933-R^ytZkqw>@y~qIDEfaSih&T-twRMW)*TP zNLM%JGlb;6p!YJDOUI)<*i;at>b`cXd1QB6`Tp}e*|#?ybsn-fJ~S|u6D@^FWWt&+ zmjQ}F0Ypnckfz~TDY20})%BQqca;b#XhGn@_qlQ4*#xA7{_I#0nAw6eBRdmMly1MT zu^^p-lK$_o1z;Yhu6iQ){KqLACG3@flt2W`FHHIC(-O+F4Bt6YqwDgYpp2qui6!X3 zB=iCmqXRMZow#!s>h^P%<9moGx5Xr$RakTeK6yz4v%+}qWSsKM{*9>?OdcsIO%>+l zPN)-20gaX+lapGgf1~DQ=+?7^sjc5Sa;U^#XG6PAK%q+H*yiZ7**9|nVkC5)aZr#KsnN_%^%oSp78S(xBJ4OOp(nJiff7a?uA-m>gc{D2)SUyLk2 zUK)Fr#^j29IsK>+u#Nlm26szx4!%{Q-ZB3mL{C;4Jy#|R`cN86yu$Ndb^W`%Q1LHh zN?;=~Oi-8Y?%Cb#w=l-i|JLdJEn_C*0~7M;jc!|ng(o*azan4 z3Jh#yT}ZZ?!|$J}VX=ihUgoicObs24S!R;$0#_eh(~^8UQqNRKDnY%hW_Q+9`GNHn zgb@~8nJJ;Zh5Hie{LsX4-#^&Wr*>0)<7NxV10 zsihCsz+`4TCg)|4TXW~Q6d&w%4)3xpP4L~ogPYD|2lOc0_r2THUPQlQasp2|h6EYJ zPLxa%S`5D4BV8#Fv`BljDv1`PkY%eigAlATYOOvR z+ptR>DJeQ<)@U*D!ya>$}kgd5T_8e&>9RoS%IaF#lu%T8 zCntBi65j4R!Q_P{*6jPV2)nG9$HRQ!<|d|Lb*-uHnH=7bXWx=d!gn37Y5|!H1@3Y` zJ30FYvf+!2kVISF1y>I6#6ElGH=n0R*MgY8pTr$*AVxm0K8=*zaTs2RkLgx};|s1T zF~9r+8|d0i)L7zl2Y8|`nofh_f(?OX3qbEFS$n5ai>frR@~{2>J1UWLEl{6f=hVAn z@5cBdgO~pXm30g&*VVCRX@lut_ZKOG&4$anj*8lu_=h!m1tm!E;@^7|llQDcPVVJw3 zq0CyX^gE3bRjx3%Q;(DuE{QpFC6xR{u!JkdY>0kdoGacNiDt}SvyO6q;2$`|G74w@W5L1s&5X(tjaS07mZh@3sRh3ULb|!yV&`JA6RcJMd0?)gnt)d_Mw&NJ zVm|GzezxoE=N}%MRCP4tQ?guhzel$XRbZhCa)vg5`NTq;$`aEpw7OpUYXsbv?Yxr$ z+TcT95x7}Fp}Tb<}jLO_crHZ)C}hR3G%vN^|emy0K@T8 z{wS&@ejYYVI4M1u@DVD@?HB3oPfFyf=9%1*qI}T~;Q3!R+b(g05=lKlfv!{vdp*1l z#6%7bE6zMV?p&qLQMZ1_lYu9w=f5s;zj@pdeVHlz__y@{siJp8?kpS&2b2AkuaT!w z$i9yUQ7-^ZI@~s9caJLTu3M6%+H%NETq0++&J29@Bvs*OUsV5J?a;gNO@#G>wCe-e ztq9=3Hdc|)S?cP%n>Tphj#S+3GyvHL8L}W4{gc5vY?nr*k4{xBTw*fvGZrwH}B*1$f3V*S+&+_ zvTR^~ZJYdAUDe)>%Bu#gzW`5b3n~5Z8OHQEVBp)tZWa~mAj#|c!3nGmaMp6YIuuk* z;Q<=4WEoQIExEK4FSV}XHV*euPNNs05%PTgogqL`+htW+flT)xAxYSDaSpL^xDtoLgww;pnc>CWzrvFxi` zs~gT~av`RJNj35&Uz-#SPz*~8cHisd9~?#v>8Z z;wFmL1(kG?obEeg*1x$a@5c_O={r;e_}?Y=eKDb3B*mGo?!(mRrvjHZ08W{8S85Ts zPO4Ru|HFHj-JgC5-NPZ(;C!>#b7OBA44V@L*R8CT*NkUdpoXCTq3XS(*?#}{Z(HnL zvG-QQrfP3$&$h%KrM4DLr9xwiSyWM@YL%jcR%wjHENv;RwvvWWI$~95n%wWt_xHQc zeeS=UlRt8tB-i`8uIKCdd^{7jexVPpS#z?YfW#S0o><M-~NILgjbH_ z5$&Xz=-%9IB&$+Kp88lusfS(=w=j^xrSSc+q-k;`-K#)L832y2qvVZ^=JTgFm%pVe ze>rMz06hg4uj8uQv9GivQcj!OhIWbHFOi&zJ!gfKdK_x*K=sNr#xAD>L!bfjX_?X* z+Jmyw-?d0KWeQ?IldpicFA|>+zbC95I*{aE0U}3ENwN=q5^OU^JO zdRG8j2-WA+5=?j1%x0?z{VArmH*z=(n_>H?p5G}Eg3i|fQ}tb%36kc;+hh+QTuqz%Yeo(M8Gkk%QS2LxJh=Zn%(mEkoe8MU%$J-UkS-s=Q5Id+uPDUn2 zs=+F6poJC|o_iu4Om8(6Rq2hP&-!iebq| za{^zDK9)A+-3*U#53&cWAr1b|6~ZbgY9RpHVY=!sQ-&|zRJ>X?E`QS+q`8+I@1rBq z2s)`0+kIU7OcSW3*ZKBKE+{MBCzD-?UR4xp;qdX8tX#~%SL&V@K@k5nDM51g=Q$bv zLS>WplszV^Z5(yiw14-#8xKVk(1U}jBhY2Hvw1s!aN0~HNS)Lc!P3E>J7R>IRjnQ7 zPfliGh#Pc?g3YlS1+uHPC?>g6rM~kY$m{yN(dJe=4PAj5VT`%p8rU$BtHS-^FJh)w zzLOgjHV`MfWlHECUSU;uG< zK%5Y1|2re zvaGjeDozb0jBtWr0?sD5E+t{_yxMEHgkfU8S|PnFOF+~03E$rJf0g6VJ4JrI^39m@ z=^CvOtfSbS&6ujb@aJam#dLI zaZ|CFYhMv^`;1{Mq{PjDe5sO{+t=vv!bjhN?l7Ea!v(MZ@ZJM>QHnDUBBxS zcWBJ@10e4A?s-UdMN@X|7v*d;vTybP7opa?`n}fJ7fnzqr$5_1Ux$iT?)d7VqvT5T z+@#*2brzF9ouh=C!07MS#&7&;hzP)7l+7KYy7l!s*#nJVAsym>SW@n+T?=ZM`sCV0-N@htt7m44I+8GZ51C6hS zP&M0h6INTc+NAL9S!tou_rAa*&rl$jWQyEh!9^Z$EYS0XQWYa7-+Veq=7sS)KlBCg zKwXyt<;oume;^f#Or@^l}j7GUH_)$!{(DN>{4tyo!uu& z5G1F1SCe~mU8k1L2h$5^?99+#nM^-@hIQr;*q`j!?Os8f-jdO3)IM*IP zW1XNIAI>Y~oYU6Q(=pN^D-=bafPs@37^Nffa8H1Rk=yB!rlM9dko(uO~p= z>wXes@!}!s%vQ5EIa7~{G zE2obQ$zp=w5v(&8Vl7G)cbN2hK>ETb$a%G?fSSQ;2kf?X+`I`Jf7ofv;ct^T`?cA7Cw@KK`1V0k!2}U9Eji+w3 zCNl8p)Lvm?qiUAr-mlUZP(s^2_|hL1@*8%!pd_ZpB!h&VX9&UXM8kEttH&!hbhDu| zQ&$B6XpYuP86Tbd?|L6s|FfKvTgJnT%oGC3*Jc-?~rKK6@ z;o3P#HsxA_LVUh4mk?b=xf=PS;tFS&bl(B5Gnku(iD^nxJcu@FehCyuHD_vcEdf1! z%FAxD40LxJ%*Mo%c>kel<%=>o(r}9y0ka?fUo^!&A9~oeM>}T&>_aRj{}b&mZFqJ7 zjJL$hGZSf3vW?ZbVs|GrH5G&>Nia13_7!%$&uZbq;6A{@SNp!gETKEK^J;F*vkN*v zY^7@fb2N?eZ|b^qA%5R2*Nlk|XMS-s`cd2Szu~+pHU#xxQMLH8l}6x|Q2{4D%_kq2 z8$>sbFdDv8v5st3sK`$u_2N^fb>*y>G57n{aX$zg_lE_xtPzznGw3-JxtYX{N@qx9 z+7-=-y(XqaETn@mko{13Hxp!Xdw;0E!5%_FL2Qf0`3Vg^vl9vR2=@04&XMy)w%`Kd z2uIpRXa}?X=dXhB68t|iY?<6c5=!#tE2R`wsD?=?nyUX)+3E?vSgW~S!oaL6k$Na% zNNPrqM)eqYz!}Kjk79ZE-($3<&A zXRXl^2QN`UKO$OxnCK90$3ktyz*k3wvAh6}DotUtT3qcq8CsL>g=YvAMtV^1WzrDb zx>@c5Y?mInNo&kMJvmp|w}3EGv67P@J+DvB>u&yGvQ%6lg(!}0>RiXQI5#+BA4m!J zO|@uSs_an=^Ro%Wn_=&iDFSJ$%QKE0R%51dK5iS|Uk>!yA@atky4jZ~{0Z+kdtc&d z5f6+H8Nv4y;+~&aGOFZPlI1?R6E%4fy<(n2?;<-##7bwKPyZ(7=rb_lhvuGmYXLk$ z^n)P${z2p<8I60MOLBPP-0~Z7F3c?sQG=b_SJpz?_8AaK`S~bjQN3pNDc$MH1UeSY z!r7Sn_H!YVnH*Xe=0D=QZ)~`VY)DU$wPqE%Yy~{u?Mdl?WtE<$Z!=Jdo{Jyy{`pMRqfGe?JvwB?j zz4g)FOAaTW&t@D17oOaRd?=QlH5DctE>73G5KlU6NQV5$lKWB}wMOGQvS41>y6-?8 z9ZyM8?k(zXzG@~Mk9K_g{5^P2; z5Dy`09e!L56#XeNYcfh-91>4;{%eCPu;eBNFE4%_%pdOj&KZ{fnodfDV8w?~HAg>M zB7`V*m+Ge}Jk}Mr3AZskcfYXYv7B49)+S3^ESHh*9w7fcW~&6aJN?%Tm!f2ZhKlCv z7hZ^6CGRvnHwKKh&K(dbe9ZkG5UX{HD0wLcOqG0mRj~z!Xy>RDFVW84-KyQ5KbMHB zYeUu$lz0zWVb+^|##ea|?7%A92x(`Dmql?o#>FS^AWbitn{g~H`GjD5`#G)3BdYqy zS1^dW`vTqwK4Wdzgh1Z5lbUZmJ2};cwh2+%*E~whr)n0nJT%LF5JR^*i1? zn9mEVK*aG9%=1?7Ppj}b3UX<3C2MK3YyRQ0MH-OR3AeI*JtA3Fa>1aBx9k<~Xq3{fJ*ieD~Jc|YiC z*{c!-ZtaG%jZ(Clzf<@~G;sdM73Hk8!k>!QUk|HbLoTIFP9m6=wXT%vHiJ&u|UEg3dVK!pKd}QiYEi^TNa@ltrlVI$U2DwwhQbv434V_OI zo}QI$jcmIYubIhm>A;qSQ}8LC#uOMh*}d4Kn_x>c+iSg$IIzF^6qvi^CUD+y?R@wI zIC6<7hw`hN0fL!NfB8#9S&Ad2fRUZpQfDUF z2_>Ht($^>)+VhNZIiYbsJQG9&F}$Q0s8>6hA)hC^ojwTk!?#?wB7Hd@OnHLztf34p zlFwoX&k{p*pWSq9WESAWGO*75VdeM6&xNi?|K83neI?9w@`8FrzXv=n7=Q6XOCq5~ ziX|6Up`b?Cq4|4{i(+PIB(#Zw~yS*QhLHsca|4ZYtDY#MeNk_sF9D!}sW`cZ%dp%Hh%$zr+qX=5$Z%Xllcj?wGe*PaODkXkFhzXxV!6kn%( z&Z$**OEfoM&w6#0A-pk}%H;y3xeT+JU9Ncs@Myc!Fm9oa=+72fj?)q=3T~>b;{YdYMc@L$8Q8|*T^0Bg9@Cv zG#p^(m(PTNmZTfYKOTsnT77XPU9^KMHAD);E=jdJBsxNS?c3dCD@S&g)mCK?}xTY+VmfB?iJl%C%ncy z$=fIX`jGK}e7&wm&mlncMC38=jO*47qf8}{N|p~`79W5GaBH5J}R>le^_SwZ+Wri{~$C_+?oV9 zZI^U=2){i2F$PH4K-2qnlURih;iptdQ(#s7pNI`mXmP?=;I=}&lHxGB_xAoW^VS1s z$y9qvHh0`yGDjWCZ^Zy*El`cp-1%KsqA@hMbnjdvUuya>rOT9!#>a62E9Do1(&)P@ zG6602z=@r-tWd>3FbE!pF5F)CeeP zxpIhP6Z|NN+?C`LSfI5+R+*AM`%*Ay_Fm;WuIdWc!VOXlXT%;-v+ zk(|6e)0S#X0F;%qV%kU5QM8a-awb^;877~RY%oZ&%GD4yg1Pi-M`!%9@-jo5b)GPr z9v^_rg#{(-AyK_Ui$Y$k-*zlbYIlS_rVt^$H`^H#O8ACj7NBy^mCVTBYx>6YqZ8cG z^yL2_8;th<(=g%xX_#LjL9sD^aVS)tXlmc(fTxnYXFRk9%`_l0T_uv>`5)}a2+0Su zyDaB$&687O&Vs7|2wF^>jy{^bg=0=EC|eli>SvA@ycw z89jMZ$175{qkVCI_Ufo?iiq~TG0nox#WD;%eY*v&d2_m6=jH~n2ZfgGhX~Kw$huYo zxDn?oTYDt;6!*^dsTUUwPOb*9`XgrRLieRMovmr2W-*A== zI-jZvJ9E;0K5_1XnACZ=9CKQU`0N?IGs0XKXQ23~;*)31vwt)RgQ&@{hrmFY-n4c&yA#HJeJc#7oJpMS4fZjciuV z?-@>eZNKGb`*-46njx$@>|SKGa_3>Av2Gnd41ceHhv%}HFlN5*iX$+(=Ve;o_yOnK z{g7h{-oajV13ZOH5n+Tl{{E zm1Thn3PZI@B~+X@RcrhrO{U!(-l9xrzJopOr~_O;xkos#-PZ?gUjac6F_Z;Fcd4F4*n_ z&RMMHBOERCj62q4n170Y$e~hoU~^o9tONoXA)d!u<|@ zcU$V_$uVXRegF0Ay$O8}CQBm~-GIYYtEV3ZZ`9S8$0#<<3s5@?$8K5;39geKuzJtA zrL!YS%R|qAL$w-lbwgR0ne*|aV-N6;B58H5xtq-fG&C!4aJAsJzOG{zY&OTs1$qh1 zO@|EYaq)M=!<%ZR<_jpicGV5NNF_TH{c-KowQD)RTK?xI{yWOOl4NX*L_ z)nMgx?aQ&?Vdg6oqoXroVj5%QqG}-9(^W1cZ^4t^mOLYDYC2MsBTO8S@d=$iIL}ou zO~aL^Hsu|YWl;`)d^N2U)8flnc1_y|P`u%|kL$BmBdMH-az-KuC7rzi9lwawefmd8 z`#qMxl`&6krK20NL&~9Cw1N6)Hb#2*bJc}!rw^Zu#E!Fs6(XB7WJg9a>Y*DsML-}j zp~uA7)N#gbh4^E%t9rtITx2O96#afr!pD^&JpV^9bV~PCE{5e9jjuct;%o*4u!c0h zN<2^tC;NnOzVjxgzweDTeoSNkY~d#@FW>Z_@$EyXUl-aMIK}{-1%PY`2V~0v%wtUR z>(v#SK-0#ljWLwTza8SGy)LxTHVr$wIQ>MIPkHpLCNBG`8r>a;E&j2b%u33K_tMdH&SBFl6wQsPRllt##6!%3@x9MMcK3qz zv~wvktEU^(m zjUv*~^#R-lo4TSet9101@VXkr1pu`M6&K~4Yb27ePUE~^Q9nCZj`=e1D8;$47*!|+ zSuj(r@u`VCVmNJoZmMZJiaB8&Ovly>5XuhtjH}R!dVCRzz#bA|q=}aD)Bevy^sc>3o$C&uz@aP45P)#Qko-SM6T0DxP+4a^B3XrA^;0@g zDHH!@jYFEhj_Z>c1%jV*ck4F!#pU5NIDK_2+6bR`F4&UlU z6^l^$Kb%uH{EP>9ND`-eEfhXa9U(X}7lsQHR zcJ3CF+~@a~TyaUcNsJs{X-{cuoR1=N=9Z`zqSH=lzREjt<<&C;u8YP|#oCWo2+uE3 ztQ(3#96}9jXGu14e5G@vU$K&+Fun_fOB6kMV~3TYrA$z=vcJo%tudL3r~+_~}XFyaujryvEib*9TzfU*z#E7LX6 z@@CF%@=7)jk>^Ca!Z?{Zis0ti`4W36RykCJlc3>9AB^9d&!BK~-wqv#;+p2xcDjF2 zL~NaB@1~MTIaV`f|1HZ>CcDlJ5s*wrijlLaraT>eabXi4H;}TzbVuQWVWF#VsF=?F z>fQ?uv~|9*mGwl1gKzH1t6+9buP&meX|90pc;L<8)A8&kB4&<;C`7V>6*eY}M7_w$ zwdSwUg)J9tToDD|o5g2YuuA}uU-HIw;2(+@QwXN)r}kvXT*e-dk8z z4l#E&zm>m-=E9X=J{@NlLr4zdQ4L3v^QRJdN$18SqZ|kkA~JMz5ZA&|I!5HJtu;+% z-XF|-do%9s{JQabsX^uTFiF-zyru+`wn$AYkWgU$KSj6D4bP$7lS0;>iuU;Fna1xq zAGb#OE})fUbD}+P6&>Y5rh&RO=<{gxxnvbEbKsWZOXRQMCe|Lu-fwbV4Oz$y?D723 z&p41j*y#+37a936w;=*%uo zgWr9!!aaQEX=Iow{9|t&*ZX)PuYp`~`Q}W#R!`VBVf2efO5N?VwfVbca{Uh9^(LM%Jyh-i0tRj|39M7N25xt;SH= zJ0t$j;8mqSc4eZCtsd@nmii)ggk7i7Iw)BmLfh@lYZo!Kv?vBJ65_gca~36_E_ok! zijeb7lb6Cl*dZn>KA6#Bn7i=XJFOlCK0H9f>UK6QXG0d8(_7~ZmKv{r3cX?{0Q>yq z7KLxeRg7Z9#|w_IkHN&j5snp}E%yZMdQnt*07z{}!NLaxTcbMj-A*o_yDO=F?w{U` z_xiMk5#)FjWDs@^$VP?=CQG6nX2kwrq#6>Vuxfbffk@xe33ATxy}s?Jri+WU$kybuPLEA$`b7Hs=Q1sK$Y0qQ`Gok4fhwU|G z!b4p%WYem#>9q05B6iwTL#k?aW8qs@?Tu(EhxP*-TW6=(m0>=Tf1F+s=JkMuPs-U> z)wHoA4+uDGa`I}w1zlT-Nx zoXLbYv5Ec%t5m)BCv6*87tzB+Qn05Q!IR4-$Z%a0L zGj-COFSwu^)`e!ht6I<+n%(#ubBDc?VT(Hfv{l_(0`=?)QWX0{J zbj7H}Z{)9f_JO+E7OgFoO!o*ZV-M$@zXb}MDB4EBrRL4_YqYe%4lnf3Jfa-jG~qHU zXLoO4h(5o7Hn1J&zI8`B7_X6h)_|vpqmk)r1ZWbxAzKWTz3`IZb}%T{g3mK5yQ*RS zHTxtBJtF4?ci5S|x2%J747%CH1$sOrE6xj2pG!819Y{a~hi*9-$6^ZTj>rQQ6c;l^ zqG1+o9WtS;y5^$#ZN~~3YEUl4?fk$i3O8Ddh2Jg9Z=wqAKi5+`Y4ADk$Wu6QCCMu6 z)y9aX|4K}T2fxLq$332K?EbV+^L%%7_786uSUV;OvNvw3a zGFR_A6jz|NT>W>+1qD6p4Cfbj`>QVUO|I}ydc{rnKVy>YTFO5!iXL#GD*aCz1fK}9 zWNKbgkfu|yI{Mu(QhU4JF>-d};q8}}hf#Xa(7uKS7gy4RYyzIvscZD@L#P2tYj@F1bN5LdR}-oC+%l-9S)lRIYmbwbjAKdbboTWcUp98Cdgzh$2b##Bqn-+zY#yAUlLNS#Jh?Dr{oel^8<2dHB-bY zvH~%9c!s4Ej{k*Wo5OZ#KDRWZiM_Ejq+`ghpr`ZVZOb%vJ$x723F8a9c>~WQYMAEq z^@1NQH|=_PVOha<-?<2F-IzV&uo?M?+rp@wJBC{?A8$NFhF@j#eJ^S=1RdcSz|~p# z;Y+w_O#anZi*(vmem*G6QGC}*Q__(NB=^LXbVv{DZ8Vc{^J3PV!ie*%60*bh>iJ@D z&EA;z${b0=zZ8M!>DAcqv+FmwJU<&fdV1iN5*WyP2oy{KvnlPGal0?pd)xEXjza%G z&tfL;a}(pCb7$c++U{?Co=Z9ZiQKyXc@=@58{k#^PmX)VA5Te%(lgiK-r9GeQAb4Y zDD5Ywp@Yo~DxjL+HeRk?8sg*mQ!40F>g}}R@W4tyylCOe51TF!=Vi8a4t@=u|Epx` zp#5~>lee6Tv02HcR|G4oE!}QQ$|#3d*8csndDD?J&TI>+6Mte+nnH1CrS1m4@C;PS zqW|vvMza}OC^-~pxADkOsJz-RNV(bneYolI)Hox(0gdmzb#Ry?+n^%grDomW0O#se zoQL%qXaC-*Pjj+;5>bs0pbH<|s5#&P8@xRldvd4MJEZU*@BAAV0pfn-HlIguv*h%` z?LW+-d}o}X#aub}KvGIL#7Cb>fnmR0DU;y==lyLM zRcEoT${1aMCb5Ku3W64a6Mp;ugf>)Jpcs}nYDIj=i6Vn7B1h$6o~_J>qQn)Frt@e_ zRKL>;&iyYmAx^mn$NuCYxc&24SH?Ap}})OTfHTqz!;?dPT5;gQL19;l!>4t+tfIV zOTWFqWNHDqPWl4?8Jasuhe#nf-<=GX%5*9kZn?pCXiQ6=s*gfLcWFpU&}DY2(fSam zOlfB!*TjW`BKGoYzzTDPL+=Kf4rvmN0{^3DAyiQD`u-lm-6K)>2`(8 zj-lVx??;+Id1bYu-WnAJ9e;%C*$<2p?aH8>t6EW%EL-?yxHe93F>y_eCoP#R)+IWW zL37V+^}N~-kC>gV0Du-;PY)j0X4|Hmj-oa2^sNyPzjQeUrC@nsWXn9q-BSy47v&7m z)26Q^c}1bJ`Z%{EM}a;~0p$TZI#FW(koL!k3+7$Y2G85x)e+1Db(-JO2O;QXfHxl3 z5y?~YS{Qq_jd@{iF2S`>dOyj|EN`jk!VB`htB<^bTc{kZ56kn0yI!efau}`{YVq?s z)jTIhe3CyP^wx5$9UD;DuB(*jJCyRwe$&nXe4i1=X&z)6nsZK|Ik&g`b+#UR8|-q? zy*EXbloT{#qC4-+712yk5U|Ew{wHLi%3P7e(Ec%J>L}@RWy|p!xUIG>3?j#|EUHI- z=kiJk9qB{-+i!2NPB>9n7GQL)Twy-LyJ)UhiD}##GqDxKW`*Yax+y}xrL0+pa0q6` z#pkTZCrng)zHwu`e^-D5Q_OWoX%NmU9<>W zGNvr6&46DIfB3n=Ov%7@#%5lGjBt!xu}})^8KB7MnzkA}8vz_t;P6+`mA%xsGXC^O z-kLy2%l-FJ$e?1yIrc}#Xx-`~udZ2wAhU^Hm>y`B`tf*A$RRi5JeMpt{Xdscgy_4o zfH?Y{hl~5P-c+_^0F2B8?D&bZ6|nocuUHc6p1fsN3Y6G2Wdme(`j7vUhfsh#)G#Xg z;`JvQb8Ip|9U^V;l7blh$6vbtjgnRS2S1(V z%m+;{nExjEFRg3r8vI6TE0-4XCqz|2;%Q9cwk+Q}EBo_u)9yi_y3V{D5}6!8I7QmR zqZ;FO#^JzxHK27&hO@sE74xTrTQyi7?|nk^`ez&e*C~>CP-rbt=FYOT9i4 z$}Ox5(Ha)?-Q!8}+w$z1c#dQmBF=w6D`arwzb`2IL#^|@;juzulQ-<>{aMJ?Y!P`R zRPfK7bdP4gytZ||3E$dW$3ZHTS-@k60hg14WMB#<)?lGrM+O1fGyu+1u5Fq2uGc`y zd7-X?Z#<4p<8I}r#zr_!_(EL+oKD0Hj)rFp%O}T)Ht%mBk?q5u=OE-{K(iO3T&1>V zqz^~oPIf6tTh-I+XN`<`CdsSylHi8w>Zz8Mk-#X}(Lef<%YZcc-JDe8f zp-YwREU|-R-O8eZ+U~X>rdsJ56>ZI!orMmJ;AWPnn9(d;G^cL$p*ni`;Fx)B_-NaHgX zNVywOaE+b9KmA9%GdbyQqaL;bQa3k9 zm}rud1^4B}w&$i!^ektY9^!KQt0Bh|jt{;_b;<)B1USmSR|z&%ObHmbbHGf*2)Jca zy;u>%xusEk8tP*#g%4Y5$jhSY27{REcx3l?QXvnd5Kf}ckls=nu4J{%$}c8gXK0`k z$u?dQ2(sN8UPyQs@}U~xoK^69FN3bFtG8Ow9tgC~h%LJVQSe%M(`Z2fK# zM(}pD@I0f(N=|yq)L-$mt^cnaY{$AMXKdA3{{JFeXKu=lJ825;|xdMngb%kvUJk=tp zt}B)E(8kN7f3%`XlnmE@oIa|53ndiH; z2)`Ro5!|$ioOcp^u&&Wc%X`|Rmn(koce5Ek0-iyJ3dqk){&S>pT*DUULuy6fwHNwM zu6%?PmgsuKVoZCX%xNmT>G>)htJ#zNj+^tBipHyEwTcO4h z^p3H1!uWhnt5=KxZ(O=}PZ;H}nV|1ISkOsl9iJ4g`BgHI;r-xHE``X{Dx2ku!qAw$ z>YkDGTMm`tOjfS7)eQv#gf>pgScFM3Sc$AiFsD}*ED3o*Xrf9LGgK3P-!WjK-*}KV zzi{9$YKHJ5Ei(|UEze2mZWomYXXB&Las&}08zp5Zy8AlW#hV6JHx+N`>>X#D5fKMt zT$z#>=yIFK(Es*eMa^L%t1k%Z51TDae}~0p`;V=&t#ITXOy{0N>h$Kwcjm6N1ZzZ? zijjV7qyEMsfHOk;o&_sBKVm zc~$Lms`#d?hcK!bEadP=D$|E7xAUH2(68@UnFEWe)ubU@-i@M>Qk9YO{G39EBK8*p zj=nf9d~3r%Mbgr5*q89IEpyW@I@|a=ukMGN$PQ=%j#f`(RZmt%W=P@Mnk)8Nc;UL~R8V4{bo4RS zTYpD4bh}uJCm9mtmSnAiDK$G}<-8`$zV!7|guD0NVYS(b+&D{8!O3t+Gie=Ds@cWz zf$jc}7=-$qb2wG(r^xMnP87SRN*9{jSU1iK$Cv2S8lsgOBF zDB%d;?Um)orPN%m9YvpcctRd4J75p^!ZAupIG7dWk)jC0?-!bS&-tsx+u3Oo{<}K;rbup2#qLCMfhdD z^p_Mf%o&pzIVQvdqWy^I&h>n0Gm($nVg^NuoXj~hQ9Dg{;fS9_c@Hm~4K|=51p~|w zl4nTNglJaX(CRaWdg@YS1=4S)R-=4hW>`AfWIdV;fZ8dRrQ$E z+1gk97Cnr-y}EcEEncOj=rO^wkL`h@t?_q?x8$b4 zoKPhZTcU~3{^u(f3iJu>nWmh;2sU(9jmRXdZgQQhvCaEyu}=23_w{kAi;YHz0!Aq| zX(Y5JJ}pg!fU3oAk~2P>%^>&ann*?j;aSnF5bf2Y%L;MlZ+0idkS6;|t?Nj!EYDA# z>M22y-=AMqJ^WD^G&*c%vz{uOmH}+99)P@&`5)BUhLu-tMRXxo1#Et2b(|)q>Qgt& z?+EVe2NG=L5FPr)DuQG@igKltBIx%z!-bKzB)s&H*{<0eUr^E(aEkCS_H|I733ApN zpTD01Pvi8~Ponz)G@&uu>u23{`s}7x^CALo4CuoELSCKow7`{^baS;L2aO zV7}}kOsf5`ZksI@=i%qZqcs7*?(R~!=V+^ysj2wIVyGTU;TwL?j`OP<{oUB$o021P zoSNPpPvmqWm47d2prBIuH`9-#6Rmzcj?633{79mmrv4X5J?)^`^^QWN+Cu>2ht+@D ziU{Dlx^jc^Lul>O&`}cG;8&BR-^gyYTXcynw>|G5zlH}lBj5whPkS-Pye?pI;Krbp z?R7okN@o>4Hblhz z5FLGaKL_Iet7gjd(D>8kv=H#Mf6I*`@5SkJfu7}>2nla_#_$2!`ekFY;%*hzTf0h1 z{@RHxs#Kx{Y5wK*lklXeyu1RKrgbyh&JJNEW+!2>8L_>2#Z#O;jRPcKz z-a$2BTUAEuw=Y-K_a#uktfCM5QXL)tK?L5clRdkmyj{F2D`Shs97AwbH1bys6Iq@x z@>qmGO<=qE!DQrj)^oigs}KR{#cda3MWTO4bUKn)}^Fn~K&B1U+u_LJd$h;yitm58vD z*7p~2kX-s>Op^6n7$*}(9g@tuEmjG!%4Tnsl-L-161$QtgtATJag|IxhH!Q|jTn?z z$GX4QtBI8MOZ)f%yVBA631Btjvjn7xYcFnP8D5c33(xEFNFitLD8nbC6t$Ht!t}wV z@yvocnYY4%f}5paaV9kb2j+8%L)uwCrVxs4Np`35bDt>j63(4NFY6vT_uVU8@hK;7 z6v}9oU8acz^%ZN@6-j#`SXw^5;Jmn6-LI!UdYShvkE814;{;-uU?ZV*M!8a-E#o+W zCPzOa)0w>X=q3dB7Oe8bYEbrG1*_Mqv48y2Ng>{(&;E6m^Z|aq#ELSwRZq2wzrH!V zRPeJ^;(85yf;=7XC#$MJ)*$3dl$;j<-b`P=eH^^6V>@EP{k=EngE!Bsv5y1YIcndZ z9@oU!SLEIXcGy}^w#4np11?T|>cie2!Ahb(MMZnYla+S~eH<*P#Ty6^Au^y@+cF?6 z)McbLmj}UY=yLDExZAGo@;XsOWH1X5{QllQOlKlz_=*yQj<1X*Kiq$W{5heI*B|CF zXrxCeds7y~g2gVjz#skw?LAkiObyohMuSpE<0ZwR7#eK_u&Escu1hDv&%%#RO<;hm z!~Z`yEYk*T_<#$(`&N;LwaxZo!2N>D|EBHYJ3`<`H+GH_FTTTB9^NSucjJ!>O5M5C z7$8igThp;waF<94buCCee>^*W({8JGuR-ZNEM@IQnaoofDbC^ls%9E9Z39lp`0wTV zAFtKKNgKyNP`|C>GEgOYZ9A*JXjn9({wO$Xwozp0HjUlBG)3- zk{dm}3F?DX-MMfUpbpft+YXnuu(DJaP6 z#*dTxwA=DyXCkZIev0T%ffs{|sA4KKT$>FClRh(Z7E(2yj^Q_D7wf+~*kC6eJ0@?K2e4+N|DEwc3uyK!Z+A(YeG>ixs*)KK&G-G{%C!l`~_e=yR5hpR% ze@Q(iczshaQljlTR7MZ4yC7a(6?{9S+`(i*yMor?`uZ9k{D?i;?8k%PhC!DB2xH(9 z>_4%O>F9rAUt?JyyGcS4pTz{{(;Do!(2k+j9nLdrBEJdYj_K>2*&rQw_U280vd*yMMaD_2Xa;`NB<+O7foFaEDqs1ZKSqj^tfu@OV{bc++vD3!BIh5Km{XP z4y#S%oqWccY`moShop|@b&Hw2q%T6BgH$z|w13%}oJosYvU|`{;{3chE|@}oTy5|3`i*Pe130yF${_nbQaRo)FyumJ_vn#C-Uz2#a|gzo$K#8_ntkn9xJcQdLJx00D+I128HmbQ@ zq`jkk@djek=9B==(yJc9EM2^rnq?L>5LV?Icf$%?Rd=ecfHrFFubn$GqlvU(E2XcW zr%qfM`f1bN2pV{)@rT0iz@VjXlGGH9FF?s~pEY2hL#0=`f#js-j!Ev7pT}#8LNM@c znGG^2Z{@kzoE>{iNCsOSC6SfMM9Yt?Ub1^2-Di<4o)D}K*1UZDH}T;D9*hZessc*e z%c1PCW1bOk31QaxbzDo%$!g6TBi@B|vGO{;2XAIYvu<}YTiJfUImAHER!^hlyg+Jy z;cT4rr=J_ql8z0$aC5B;t8poGG;#%I73P9#g4VE zJ^IC(Fd1+h)m}u=mPOz^%efk!P(62XN2$jMCBW*Zju)R8>f zD!*uCE6!~>PrIr}lS@n>vAT~#7C$ytXxk2$D9Kdi>ATyC`0~SSbyL$DAo77iSd%mM zMhCA44J&#Abs>__1Sibr0PcA{$?)3Iv1j0E&GsVc8;xQj7puwFbU}iN-0TAoN=ENU zm3uQI#fI*U4a?ls%IQqm7AE^8T_anTS#)?nj?RH6(^=MdlG8N^;<8oV8EK39ZPyXD z*bNS2aTTpOmMye~7n24p6%=dEq z&L6KaFrC@G%>NdQNY=I+=)05Li+3<8@}Hg4vg(uG%+(*cVOqJIlc~RQyBazL|@!yU3b*5`1n_I^}x=m|TKMcfNU)y`e!syF0 zUZeDrF~WFRY5fh)c@G-%d47J|kg}bz+W}HMTc#j+caQ$5+kYmx!6&d5F-CA6$w`(N zT-0M|*C??FoE6<&MocNAo4GGcLDImfvgCitabuZlF#+V_kh{mu!ig_F>yEazG&%Ne z5TxA)Q%8$`5v2)Z40*Rb=i2jDgC>4;J_6zc-%GQd#=jH^Y!Ek&kV>_$bVRPcXph>R z*Byq7ha3H$4@z_FUX;3eDBCJW#EgRqLzT=K9tF2wOkabd$YbZ(i{gaRR5Dy1+!oZi zPK-GzTmNWBNtCzSHF;dc)xqjpOv)9pgMdbc@a4-X0(0R((uPo4@qRM+2jB2ejtjDL zjJ9R?IO^G3MnV+d{V7O^qO{5iONRrC$F1tmR${zHBcQZno1!$&ds;F-9KCo2__fNd z-{$5vi!U}3GEK-t|MnNeH~BfPpU2OBp@zET*E?ZOBgnPP&i*TPH=E`~kF>pcXoxo= z`!fUlJZihw&T4K_FDkY^L_#KtcD>2CK{;#T{Iu)$b(F{7FA;2$D{U;HAywtOl-RXy zEDB^pN7B&Nv(9*JP8j2I^&HdRE@N6!JTgPXNgsx%56yWfLngUQcuDTgGoX++ZWk&WI zq?SP~_B5nKJfa}&R-<$eU>t8E51!HTrU-f^-q{3o6i1}NnZ)%}v?g^doIg(%jF>px zxoLAdP?}ZvBfw_=XB1?~Rs63gRi|@^oZ)=P zRW@&%;`(Q;jH-Wi?;j7ocsB=TbnySs_11As#eLX6F<`)GkkOr@bazN74FW1X8flbp zFmi;zC~2e=2_;NW1jgtPkThY824$2Eo!fKtzMtRy{PW}W8vAl~*f@usv+ub+pX+_) zG(Rx{4{0oY8c-&NWmL2voy&dthWGrun(5mgwjxNJn8i}t38Gr0aj<<6JD{021^m`k z>%>T1im@b2I1(0x7PhQBY3@pT-zJeIkK@NPW^q3%$e_hS&7%j!H$#+i> z+j-?;<{TZl(AlD+6dg(_s!|V3*6?t);9=&dM}y|KZH)y5lbWuA?mf_F@p@4fEh&#L z=pYLM2FEkUtz2WtEA7KGssxD_)y6!5)YL^vCt_@(th!cv@1hgQXnFUD_d~lMDu1!j z-)mO=hBVp9@<6bKCykp|4bl8tur4L&GLT67w(kw<9r;9;Qq4(KChQ`!biM_#@0i6Zwj8C*y^nij%ZLikxJ% zgJ1LAkcr7@oDxl>@(rFomzlI~>EI&hPa2{+E>k-Pl7}L^k&zjpWk;uOV!KT>d!e;j zWe}V*kFO0DsULy5yIC7=4LVcN;fw_5HQwU#y}XJ22y4K;Gd(B2sl=5FG`e2Cd%<%R zZ#N7wL)Ot4A&{E=R9>kq4?U2hZ89&br*MW|_^qoR!z(8}oL1!;1}b9qK&}JFvp4*z z*eqKgPu6ORBJPs`opbK|kCc+@2SzN_Q85xsih|Hx@hGU(V-b16NR9ioyD$LLT>QQr zRBmT3EMt#FssDX@`N|S?t4Z>CCd%(7J-uS3_b6>=UvK;TIbI@ch03ShiNqU?EVa=QdJr9ff0FGdHo{ya zah%!cg<37Sx*ZQzk49~I4Emg0pOObhUXk?an@bx0zVApvU(^YgP^Fy`j1+tHh*rTu zBg}Z8)3dQ;t>VU?pHeV}kx;G_0S4TTUtX`Pi`XBFCverGJsgH+%>&Ie+3ilId1ids z2y9quCap&wEIxvKDA+JLQN!@Ls1Bm)81IIkY9Oa!m&-8WpW$H4sNvn@UwCx4JG){wu%Myy9(AELxWd!Z=zs zd$6#Wk@SF$gI=?_WynAG8;u1Fh>3>l7dUhcj! z&CBEr!v3_WThk#!7b7|6XNNljVeKf~WuRYG*fSW%)Ftkqc7;eaHAd%z-*%LS>Lb3#8i%@) zshinfb1-@-6t-hRl%G3NP|zp*E8qG51u=(OVE=SXH}$sWm^e4V#w^tzFCE)9|Dg*P zoGT#@$X!MTmsxA;obNVyK@jm#o~!_{%+Zh7GvCN%GgKSHp`)NTuetH3@134sq}S{k zOt(qVt8*E+x{0GNKRSE>m0h6Y6bPAsD#}{yMv(qAmBfD{{}NrwFYi3O7lqMZ>fOz) zS)zp=`M2zS%VGNVd^%lWi=Wfh&H3sPVe~G91;!u?K$}-9F9f}*y@t(|c#I4MHvh_Q z4YuocklE6x!hrrW5`A}>)r&9qgQDV~O(^!9l?y97;pmPb*Wgd%VZdP!I8x2Li$r!{ zpS{VSqH+N93A;V9ml3X9!NJPbV|~_D8FIX;3tT=4Zt4=YJ1#=q!Pi+?kei;+Q7W}_ zQj%sho2)8h#w4&#tg!ieh-*Lw>&hmGQo1-+gs8vj^}0b(b^wb9C-c?olfwGHOXSVCE|SRs0ZV?OS2S4wZS!;s_<*7AWLF`&yy_(|ba z>opFCUZMSc-hf)m)Gry$u1g+ywY+i_U_C~~nJyl;TfZN$s2GLc@z?9$`vXcBEelJ@ z^LnyPr)|G)}%NEs5B!^noxJL-Th;$F`L1| zXEmZoEuz1PgKKnf(8KZ3W4SLiuIe|go?shkMGL1>!i1a4Iuw%(0;V%kzUoNfKaZwb zdYxwpAHG=~e(?D`*(=BW*d-uJ8h_37vLD8(deTMBq=6`57^J7Sw#N9nC048~UF#-X zOi}_L3xD``9P1ly>chPCPh|!)04vh$AYs( zEezqdZd%2(pQaQ_T&3_Vf2_C%m77bu-8EbMCq*S1vO?w;WXYjfVHx2jB(f?t+~p|f zd1>0~SOr!oTw%rtJiG|w?NM(ITzX~jwLtsnX({IE=(w7>gxR<9%Y;3ZC|svh?z6{+ z)14;_XAqN2onma*3i%xsRR(|w|Jxu6RH9XZpVtQ zX|-?2nkpYTD+se-7UPxfUvnsPgLW${6#eXr!am=Rc0=24yffDPx7Q)7oG>L0nxcGJEmVN3#ra zy&0m^(Sf#Wt6Ifw*_gnuBlkUg+jJ!vsDnLkwHZHCJ5 zii?A5Gi}>EH~>YF1e9EVZ}ZCTgcy%UMlI zlwiML1-U~@4<8plI0>oF*QJ^8e3QfI!in(i3Rz1#H(_XHvp``>n-2g zNIo}up-n@R%$7scq<~8DVJ`s0Zc;SMLB=Rbf6$qf9N{b# zBhiObH0;C%)qp-P>y)|Xzj5-(&vs7vVkXs%ji<3iEe+#LT?9FHCntxx^~g@t7ux~U z#igsouZBmGK`!EIio=vtjbAOrAolNuSi=b3YnD7j8K9VN`Q6h?B#Xzqt~(75PY8iIb{ zAU8rB2M(@A#t+nncxa2@{r3XmLNb4(D z(oAAP+OI&WuA`X3Vs#if?+NhisQaZ{NSx!xX{441T>rCCwd&^huK@QqMnWm=V5s58 zbqvJucDM4xYM~c)TzCrcI|FUT&&$vEcAoc`sK1<>>Nt5_M$*>0!@$9Tdltqrghiok zq~+pbpoO6@k{%~+3<+It^b3@{3&y60gMu{lt~q`lLDP9+mRBcbs4Z=6v^azcERZYm zEF__^m{)>qOq!YZg8Vpa6wpVXhx<~#su;~l@s3rakdo$p^mGPa=-h}d9|O===>U{V z@IpHB?ih*Tyvzc5AGuTB`&FNXDPp&7;zxroUCR9%xTAFprr5B8d@74~z)kiPOBw`# z!>wZpH=1y3Ll>kU>IGd@AVX$+$i;}h!_WEaXYd}@4`s@K1hCr_6ck=S^}ZLV-e)L_ zw%|%-5GlI03#dxT&govh=PxGsxpnkQjIVg;B6!}k@hvLpo&S}U`)kOJFn^z3=LGI#2Lp9fxK{%^C5nyBL_kfO7c@CYKRoaZ2?5VS|huB1zq<3?N>bbe9E5x$|kq7s? zOdpxNZmh{+vC$>zvORU4Qq1h%en?61Z|NJ!u(8tDg_rgJbrx0waMN5Pz)EIQeZn6p zHa0>9BEBf%aJ5zy{jqeA2eHC`c^)0SNrMb&2J5$uVYqBiMp}gBs85fpG3-wPZlnm; z5zSdQJPP(dk2M=rwxi)*juM z=mpf^-Zjodjzt)TDX~jrI}^yzbK#{_yV+c*X;MmFt+<~PxrUx2{py>Z*kBiU>mfhz z)^TNNbv&CZv3aSr2J}o3)>&>nu_pWF#kes1y$eRKOY#eSVQ#6=+o&bncaHML*H6z> z)q>5sNUY1T%rB-9TMd8Nxs(_FRmj>4+=HRmQus@1^|2}S_ww8_uPuiU4tUz)&zh^8 z&X-DQct+iV0c9V)CpSFaGoMSFCbYvD0HyDBr?2fw}j+=Q587lwZA)tnbOj|||EY17pfBJ?6P zCWh$+2AK2|-T#zzz)8K;UaOd7=+3E~VhR(2n$_9m4z$zAU#_5GSNVQ{T_=w;k>G8P zT8slU347*8=rD_*LO|x@Q1m)xSClxGnEr@ge5rZ@QQX`@$C#H-B%C51_l7h}?T&9e z5r;}(^`S|^mBwKMdbehAbaG9b*6qi>0%7=}=qOh6+q@*j2l`B#NvoOn8n}3{q)-Gfivp+;pw0{m~_+nUlVJ`WaVKMN>AO8~}+W`@>Xav$@lN)4o zeFG)D1DE=!qBy;=IXbewpT!>TF-ZZ+zVOg-?!3a=@KV{ULTp-X#2Cl;794%AyP_E2 zaY)q7;|Te1^vPpv7@2{+@6EtFmy07>g)FY1xs^6k_3q4By(AC*P;P$N2P&IazXJN` zEL@bK#cEYm3~(j2gW%vb~!2dR^P=4XPUFl#BFoewTzt8aG>% zYl$zFz+;K7$nr$juj!lX8AHdzPlP={@fG|Ya@)BKgtz`i4bW|U?g(03ltLD@MdbLn zIRX@UYXzI@vR`!lYUvF!x8qxT{Fk0VE|QS7|5W92;$!&hmEC7JbJOFt=MlAR;-H_&zjnYpZ6z5;40 zjr7M`_!yt>8-cO3BSiu>E@{;ui2(z@eayX5Elv@we_SpyHbDog#?K=Vx^VRYvNfN# zUbmQWJCl8D=rlRWikum4>E0W;Xr{TuAJTU|mzhxxy%_lLT`@Kr)yB7Md z*I{FxQzqP7&G$3HmUI&}tIf0U7RGL*V+S~w%N8Tla}>kmm0lw)hSl>NToEWe+uI$H zWr3WU_9{Em;g_HfR8QtiqkG-1@?qV5CEabahkMgBO+Ob6B>Jfoxi0t^L4lV_r`>cU zw0Tl6@KU}KWpUPvKLcAr&tVA{j}=V{t{kWRcjiTG7S3oTvPEn?;3INcG1RJtPyp%a zKXNQmSMz|6m(|MWtv7@&Ru$mQrsX_zQOE+7=sX_Rs-s?rw#k+QV33GYKEMj{29uMXGGCt*TiA=NASp)@*)aD@ZpZdY4Va0=QfA2Y>B4ISK`2mk zfd3a#v-ADF8uQny4Z}@7WzN4Io;@|nJ#^O-NjXP^|7?5g5=K!3-#tt3aTfpMEMq~W ze^I-;ZlOdUg}0i!u~3(v@H*vG+-vJl!1Z*N3tx|VL6LPbM*=t+zjPD3>DYm63$hMC z-ddrkCJ(Rt_>Uix+f5%s-dRdM_vz8@qxBD}DbxdhZETr;XIgxJEt%@IB0E#QV=WZ9 zxc9AQ4G-t%)Mw~bg;Q~HD#D(e&vv9nwUD%eAJfVjz=l8cQZ;X9@)Bi6Sk21QI-;~t zrkffMS+HM)Mkm))VlpfekTA)Y(uZ%2Zn_Qn&Q1M@$4(ZsB{hQ zRl`wqL4xy@%`B^_7xN#&;bBZ4BN1FqsDGa_Hsd!&$>WpSZ3{<*>it??*Y##g2UTJ-x0#9!<4E5or@1^6S zY3V;YSfjMCV=>`3Rf4ibD;%IfKNM>N?AuBD*d2pdYJLIJH2s0+_~7AujUSgsLjh;5 zpuY#vT7`v*?Pb)|kGbSi-2~R#0t>e5Zg-j@W8Sw9r%xGtZR4wJ9PdS{5c0qWfVc7K zRoz6s>`%2XbXQ_uOC;BN3AGYmS0KEf)9_ht`cWkhkzbM9KaFH?ANG+?2}YkcnB_Ox zNquIhB;X(hqGW;DG3h_ETzU=QqG zTt@is(UJu62^~T7*x~oaCTz6`74_=)=y?3B>zBKG=q>yT8RHv)KcPg>KTF)J?9wij z4nYw43KRR185c{_V`E%C`oPA1@&dfz)IXj0;O7xZ8D+Hh_KJN*ksz}xfe4O1}0LoABQJ+|A zK;2c0P2QhgZK*4w{XC9V(acb2m>dm|sv&>oB>aeFJ}J)))R|P_MZm+Z6(s$L?tji| zAAE>(gJ+snBhXgM3bF3N!YYcVEA^+Vj|4Oq>s(4aN3OSn)XM>uoe46oWG=^RI$d(p z!X5d*%djI5q_=3LRyvJ;9AE%M`V_7g!zd{gKJ(0NMhxC)M_Wm%WE2=N$KwG#66YH*>o_bO zYO8b8CFzrP9hclD13;NSH?0YF3D^1sW4J^)jvCJFRMtXAGT;S#_(wgc|vAxOcE zR7y&242L0T(r#O9P6B<;%$UEsxLahkzTW`(dqh@LuULoOZkU1LP>AN-714W&448B_ zXT0;et=hRo*LxPoEa=sX_4e&dI784LA^0&-6WV$D$A&0^iBJgOE1RtMJttrC!00&( z=JvgND1gmr4^(liaMVS8VvM=2K$8QQ{Q<=8x_dU?H5h;EfSw<{u@U97YDg1D9t>@w z9DXI0GDE7-PMRRMTKz<+);yQXc$B}?m!YYXZUS#N^)2IteCMINf`ai8U8-BucGZ_S zBjV8vf5Z`az*?f|L+)wJ*a!#NzJ2!?KOA8^RO+~ZU@^*zbihDI-iR%|6PbG;FHghM zbCZ+v(CaRnA`t7>3ZwQsQ88~QqQ8fLSoUv>C~8v`4GWE2ye%(8SMgw&@BJaaW^I)PYIA7l8_1wAGY9OnvuCiAi}k_56Jq20_eq;27@&4k0QINbyjn;fOi z4p!;kBX(?Un$AL%ng~qUZls3ZZ`5!hzuC}lF=JDW5wu_$EQ2A9%z|Un&joQjVZpMj+V?SGDKTF z3&F6Y7HlOq{!r`NE5tVYM<{3Q+a<-gjKJSD3_txgr951G2&9ybh9|8iZZhG$y~`hg z^x)wUmJ{2+ib}m|cS zSRrv4ziw@V^kJAbaCOUx&P|hzpgTND^QYQ(HxJ(BIyXyzk+=r;HM-%?T~Qx1^P;3f zYbW4}kG>=FZ{I=yjKNu&>I0WyAIUHl{3GGy7=)aT3^k=IGc|w56~mHbL=9*`N}2Z% z{gd$3MolJDrf?F&&+UQX39XJVtV#C`RJt|{5lOnHz5MQM6oEOVmb7A~T#uU=vx;32 z&Cdgid#p322XTnMbWUm(d83$BM316S!7#jCiE(yFV)`iEBj&_yKocqkDds$RB z=dq#P1Ysqxp8H7vkdyumm*e(flh1c^3MY>zhPkNbyyV?A$l&6c#=z}=m7@Cx+9G)JXqGRy~h{hQ&i#GL6h=Y|Ritbjhmb!EyBvR-0 zNeYei3xYT%ywSNt4$kRlJ}3Qp$Rb5C<|o=D2V%$RM`+Yg<0C5moiB-IHWuesa;EK( zizSuYQ&=jSlmJedDJ}~`;x8JVLg-baSnxG&f&$SeSJL}i;GVj9vX(*wY5dBw=u+T{ zBgvEsbj@AlLk=qsZ1q{%Bvb??(tV|PWO7n%WJOSkC{nM0P_LX6R$DCs_-)ILQvt7I z?qX3AOpL{>zlCu32<{~bpFK2)L|?<2hDWalJo2huTkdbd(4gBM z_#*BIjd)vV%{RcZAdhxZlz*n%6=~iLcW*)3{Wxc;!+zwg`KS+5kuR(t@s5naFUoHf z*W|i7`n*Bu#jtF$q^@OJl16{!h|E~w6^U9m&OIt@c7S&DQx`Y~qRM>{xz$WESZ zc8QS0q#coc87W;__I%E|=O;k8>0UxOJVMI_i|OF>O?gg$lbp4#!PrAZq?wsJuLFQ5 z^{%GEL}z0zF7!(R6GUpCf3i~_AUkCcBWU(MD3+V{R#M-*=}Fk-&kYk-EVUEPFapMpP89AZRk7jqx21di=(Dw#A$|jgKRzXW$BCC z|BK&>Ap_^Vi;rIEN@o(hGqyrRrU!yPm1LDv0ba%>K;tMibo!hwJ$$AD_ zN}0ZXWHoQZA-WaAAHDJ3-(TwwQ7rf{sYh`Hl!jI2Y?LBtsqiF*sx4CExAKg*31;nP z$X|DJhBek-W+BjKN<}LBGbR?XDL>I>7Pp&y2)**nteb?8ek!r&tszom0%hrsiY@#D zwM*jiXG z6o*yB&Iw!tHy_8&7>c~2pj|wb`DR0=+MAlzPZE3nUTkmYa@UIgMa{46r~XS_#zqq z#9~$v8Z0hR#>iR<%=a+!TFwKBw& zka)q&5QV?uYPTN=%>sgiIiugNQZQaH>xHv#Pkj}~rVKGO;dl_~p<3@Il0JO)u9uWL zd;jSA%0aI1b$5ENJe#|@!k(V4C-kskcJrFo4wcWhUL54$*mNW}RK7sG8kz#gDQ=&h zwWBKlhw-}&h0~_UQbkUEka}!9ZS?o}KQ_JjnWn(w7D}QgzfHzS13mZe@`J3*O2^+g zlJt#X!(9PbAmxmo)n*mNigC|g1p4&8?1;M2NzZ-=n`b|)HHdtNPo-E@2h#6mG8OL= zF64Z;MsbMrI6yXxWtU7ec<2ZqQut#(=Nfs<$ehLsucG)3>q{}wx$h&mh}+YN^ujGE z{EF?{bC)N>74F4)FHC(+d3Mo@kyh*KupH2dVU82Er4lYYl9$mSF7N1mD1R@MF z!_6Vl1Ry#hjemfuf2;}`Z^TB~#@#B!w3#X)LDVTEFk)CYKDt#vUX8Btj34}A_0`g0 zJDXv)mq$uNV!N!3nN4N)$_B#?^@s7q1$=obNC)bYF3)mv_&l##t!|^9b{JZ`%xSc! zm#^?{;oUP&5_r=y&@2xEeY4sCEhxUZsTl4(j=nk<7{Z+>`OHMJh z0K3{mV=a_Hn9Z|1d^r9f)+j)_+uKBc_1SPTrJp=MThp;Pj>-NbFZCZPI#8?(mu&~&7S@!Z zp5b9{idQKTk&d5x$)#X#>l5TQC|obz005$sHu4uOgVrlU<- zEGhu<4|xrn#a5j^!ealS;k6l3QW(2$mwNBd6hLFHb@Uew)CvTMgb_5`LfEWK*{0&1 zT1+S9lYnxMid+)oZ*b5h`cj24IVBGP+3sIM$j{v*jmz7X7t#hU_c1+xH){*@#Z~-M z>ciIU0hXmOJBcrQ4JQelfyQmVwn6bjxjCP#d|S`JdE&eBjbB+~S2tXxbUae&p@YPay;-uIJ(qTj55C%Y};Un~2~dE^Z0=G_<3(wnE}N}1a~muE^QrMOxo*D8rNmId zAL{66%6?RKsg4ckVph2%^g~t&3x@X@xiB;y;A8iq-dwm+^VQ@9QV zLrNxx56+x32*1Aej<`owIkREHB(uNMq>U;pBK zrTQ^zumckZCT#f0T_Sgku`<^|24V4v>7!dIhJ)N-#`(tnqysCZOd#Vk7Mr$Y;dJh) ztR)Re;PIqFXRU#A@8n<_d2!NlOUJY)PUay|1pjs0Nzf|1%Cl&Uh32!w3@VqK1s(z$ zVu}s&!BPXb=?^m>S_vnS(P}F!W|OD66}-HZ(7WNVqM*m}1)IwCc;|8xG4Ex;*&1pv)%FZDS8_r0hK~rZ`Bjwy29g3I2M&7zSnn$GDn-zslRA!(oaOIx zZU%uJ+0Q5`00JYvF19HwC8q(R4>mDZ5h}UwAOwyyWRjTpSUVHm+ zkUC3%i^Qn7db#3Rt)isj&kxu)4{Y#5E>_BonGv*PD;}SP9*)CrTvWy>6rCbkdwDvV z8K>0ds;w%k39h+!voO+iO|f!#l!2StW8sMeJb4KdRi$j-Oie~AplG;KpX4~x8YGF( zO0h=tO?ZF+R*BAU9Zp~Rv8mDeYrnDCLDPAzpNC|LH^Z!ZZD@E>DeDc4Q>ZRMd{OB- z^IM-dgMqBf8pgP?e#2^GKF0yUaqQspJ{sc3P$vgZSW*@u-(Z$kke+n2a;ev4XUP!G z5wTm6CqX+FnT0?qNk&eAX}EHn=kf>l>AGZUCBIKTT(=E~LAxrJ5aFj!2H6pZxJVnx zDE52>ok?}>kD7&C$NJ!xf)0nM+3ORhD(0uF1FXR@%@A-sA-_I;LW9IKeYi!>}<-g#UD# z&En$EI(9ScPbn6>A=BRUF+7gZ!>&l2FAVKuy6cHPs!)iV&hRTk+A6rh8jvm}e#BW` zCB!|Zz;c`17gvgJdWwg%;$$vbU?NS5b?a&BC@QuTNf6DnJ>Mf~0r2HB%V$uUQi;QE zT*;{VE+w1B+=Y-znD@)v*xR*O#-eT%kRvJpk35?F*xygmrp=Ol+j#X3u8!W*KuBmL zBXx=SUVjqlfiOXQ;~t3{McL0o5RLQVUi@UGk3R`i{Fy>&0l^#%rqa~-DQN{+xHCLy zO{TadL`8Slhyt*g9QyNfH4@Ych}D$L$$)jmjOgm7dh^mxrDfq*iVl^+t$jBb=8A}E zc#=O_$cSkSR1fZQYV-1AHsN^uONu-uw<{yvb3S(x!a}Fal(zpqBKtSBz1|DL?I8g# zKG)2V2HMVjgvY>!)l;Ib6wHECjrI03-_g^P-wu6;P_bz1G+I+$i_D%2o>f`ug1=<8 z8PA^C;BUiy?h%4QlPj3S$^5ccX~PbWx&lO1lQPE}RqgI;h!D@0r=zK41FB`4I?1ag zmnPpZl%&t7C`#l`BOB~I#hvkwmNIPKU%EtO$BmWd^xv@`wVKXbS4x&epih3@p_8v&4?&;PPwmrd&V9MQhE1cZeAp=0)Y&S&kGO*X=vdG!gsNF_D3fIja@|C7~B~ZS%kK$T=MsO0GP!MJg6`Eo(S)(r{ zw{FA>w1ehYorHfxvBL0(^Z=0HgnA2zMlT?ycCKCSkSA@Q zlg_M;(u1GzXER)P>9EU|{rF;?dfE^}4KJ{I`|m&0Ba_kez?6XMO}+ujdzV^eoV*c5}5n&)Dx(lIcZ&aiV)FV(VPoX^R%%jv1wJK6vX zY7$~v40vo}v`f@jsM}F>6@hM~hLrwoe*-aGoUM{eOh$R`Ohc~|n+z|67S_0r$VPhY zYQu6gS%C?&A3;3#j6QZ+kd2jL5}oXwX=3|^C#YVP||Yn}4W3X|+{!clYQ0CZY_JflJWW*Q&0$W&jS`CxBIwif2ajiCw2QU49l1N<<#yt_8Syr_Un` z{q{|-YWI9PtV*VEQ13Ld_1RWqivd)bSvo9Zygc}FZdam=D_9DKy{Jf5W?$q(5WFtU z{rXRX9DnppzZ4D-%~#}}#!FMesBk6&QL%FQ`6N_sq~<(%fIoOankv~>m0j)5d zaYkFAUhkMZ4jzyb4{1JuxE!up7(43rTsUqsOyAq&cv5KmJv1ksC_lo)Kzrew?>PiG z@0>GcfEX-WX6PwQuU$qa@m=55=bK-TJ(de!9Z^}|xpl&}f9OT#+XE{unQK=+q4HMa z;vHmemAvO;B&#g*F$;wblYnvwOnP9MH3DoE1D;7Y7b|cQ{5xQi)jt~4`)HuEJv&~o zz(OMg-f$8yPfd-6gbQoDM{B14%5_a5at6P_Id*0MI*;SK8G~zwsp)R`tFspk>)QD` zcHVT7Sa`klaj=S`G+|8@eE=xA_s5^bC=X-`ltqkUKk4h-2^dE zMT;*L5JJrNc~{i$cn>;n@A0F!NkUd`^IofT zfCf}^_g!B|ZfS*u?OcLA@sxnou}0Kh3J0$;0veaegZA>dUQTXyS#oHQqE~tbS};&?1>HJQ@p7>c_+L8>wJOQ zvDuz|%w2hJ$}dC7sBYqL)PJLF$W}i^m%o_2B%ktbxM}a|%IeFPdy2VxEr?bkcLZlz zG5{&b^8npe_FvJ3CncF?Hd@&!uIWq$tpJmqGO>Q(0H`^vHdT0Q_fDhiB$2N&KkTY? z7}@#P)7L#0FxNh{1DP-3wf};#YQq1LonQW~Z~|Mc*X}hW9ZXi$5?>)~&jcm83QVt8 zlmEa~fIZnTXDESg2?lS6Q6{K@bg}!OIx#b z?{RtneH!_Vk?vAL;||-ssnNJTd2k0~?y0x6`Czoi)ZCRRH@Nyd41!3zDZsa}ZA?kU z(c-9W)}9+Pj5qdojA@G}eA@7w+QhaBYHK6NLE!Bd^nLZCk`3wfW2 z054AXX8RM#FA2x-tEEM!$}C4SKIWdA;Mg#g>q`Qizc(H{7FwurY@Mo7P<1^$d~m-Y z3*g=g8s{G1&6?0f9jz&Op_Z4{*6oPl(E#9<4Q1phHL(n?nV1h3ady&+e<5!}{80Aw z-l)XxbYWP78Me)CY1s6x(8S;kUumAy5@Fz-iA4YDx6^=rn|a)B zVCnCGP0asjdptm2JU?r>hghF7CF{Z`Y=>#da+!H%@bjB6rjO^CQKT_Q>^-`z_8i69 zZ4&(%i`q!uyS~9AUu_Av!~o>-)ycupp2eai`db!hmz9WO1;(5$Ow1UaTZ55_jj*Zf z_gGTM{0a|v4o>EbwDm%>;9>)p>@T_8hkb`AVhB67zH%Sjgi5QjMhRDtucxBGdhx`; z3vaI+Tu)J`CtWA6mfi*aSX1vlP_@YO#`AXz%y6mozurd0uHO$@Ufry(O?=UdR+dn*xE5on+-HdFKj^2l{X#{>=RHs*=wX#`kt4-US zjFw>W5CXxQkPqT_?=dFI@?$D@Jx0DdQopL z`)u#>W2NNg;bU%|LYP`5&D)*Z~vm z2Nbt(Qs<27br!UQi^b(fLo-3S&pTHsxQ0$L(jKs{Pjjm%awz!^xPF_nkXOyNG{)OK zf|eMjXn^_96fn?+31;Z!^ij3bmEEb7tWxoM_Y`x>>FOUdhI`XM{gtgtzmSwA~Xg1ENyW=*Og)g1xO7h;fJGFtEj|d{*vWd4;6; zpc>crj}$yLLMCc(Y5#?G{@*^t>3`}*)VggRn6NsNb}uNDHGsRQw6X(yv3X|+$@z0* z9VzN#3umknnNOj50-{YSQ}|JHwzsVj$U+nQDnktV>(_0Et@h&Q-G?0YErL(t_V#I5 z-s`hH9u!odpx$clSXOxdg8F!A2RR$0pYh0KOp?WcU)WQ*eJ%>&(hGi(YXpsr^fcfD zdrl=JqqHV@01WC_H3yO1G~YI^X#~xkLqX9u0V@acwM!{qZIKnadyZ+Gkgl#@Xw8N$5>m;B_Jf>aU znHodtuU7LMCRLWdupAo`rqD@C7wWQ97|}YJ_3NP{pS{|N@`CDsg@(Gln#0&a!6ti5 zZy^E0@rT%R`rG94qRtoVRf~na)`uI%VWoBDl!VI)3J7EbFQ6oiB|;P~eR+->2)q@w zyK(Cd5HbX>O`{_RnyvEm%aWR9{Juv=XubQbd#xy4xKvbF%EiaO7oD6zx0k$D zD~Hd$OWr4l?9MHztLU9qq#KC`mpM3DM$pG ze^_>)Z)#Yj?f;qRjt#%lM;&&Gjp>YY<1MbGFpnb{uj_dfkmbe?bRD2>L=b9sae?I?}TAL*x{1W>+T`{|9e|^qo=4 z)ce)b<1EN(RfFk@cu0u6U!--%vHigC0B`bS;TI;9{^iYvjF!%u_putpp_O#0?Nxp7 z|Do&6qoMrczhS$P!Pqjkv9DzrJITIe-$Tj1MzW-&VHk{U#-1fbQiQU$P{J6-o+YAE zW(+D?#+Fgj%yWIa@AI7BbDsPD-#N~?K6724_v`h3EjwMcEY}Vzqf@A|>&YXs_*jW3 zB%c>|=;7j3p*kB@>XrMOd=g9Euy&rPj|bysZ$&Od9-5leU6pWU;YiY(`Fn}8jH3Q5 z_PWYXQ$~Q;7L|C92-eqG^d4aR{WlXz9tpAW zwtIO?S7N`I_x_5~x(OD;_}+3Oyi@bNrNb%ZoYn z!HyH5>!0T}HJ2)`gOPDLInpgin#%OD!CO{@^S99Q@k<5ueD*Nyw>(@!2sz%Ms}do! zDMyf$=&ei^wx?Q=fuogvnf5%Zl%Ib4*A@-EtRl5O9xdpNCx0@?u868Hxt&aO%ICo> zHEQ|5aXhS+$7ZFnjgx!AgTS9oa-sKm#X2pkf*Q;0VUiS#`C!=_pt;zL)ZSfUnZ~u5 zPWkF3Vhgz_|A(9!C~acAYaZ&8nN!7}ZK|x||4wlPn-@Qdd*vm%@rrJ9$#@B~K+13e zogHfnhib|+jnY)`SId|r0$oYlKPTQ5sO)LNW%a~=qCj6K zOKYBXmXuXVJYWq^>fWA!nWg*ibjwn_-!lPO5vApgL4hK4eL@DmfyFr7YKFOahdn2M zrm^%ni5FTSV)f3I%qjrHXX%^4*YYOd8FA{t`pOR;d;(irWUq2P^UpS0_6M|Jw~M?y zx7)F&3?wm_{$uN|uQfC7%bmL7D93uFX$eP?Ol+u8lioMn18dt4k_(S@wL=BG2wg4a zpM-cd{to<2OT|e5ui4*!6L*@mV2$_PwQ^m?tvf)$g~3xESMJPSTG;@j9sMAWMq3$7 zmm5LDX39c;CnIU7Fw#ej)|M;hX?^-pX|V<`dP4B~wJ)FUH3umMDPJ>@-AZ;D`0|zF zLe1t1_z}xrb-F0&syLF)RwVr=8Oand6tmikk7f{8$_{8C|0k~!C<{FXF1dt2Q%xpq zIJv9qeZ6K7vz%lQ9VEkQmMva)M!(OY(fX}^9fRFKlvPl3=!~T~2z9etlQCm{UQ|M7 zyv%IAb71Py8`^8B<4$zp(Lu$Bp{{@jFCd6*>wG)D)>n(x7LpR;YEfi^k?5fhru zSDyo$WdOeOnG!3ETR8+;)S>4uYACO76UIj$Hp6YET65m9b^lG(-Utz1Mv@#SlriRZ z)>y|T*U4|jLijm;TUESP8N|z<8bf9#-({=JHyx=b8B)Xw0=nbMh0)CmJDo-hb`LJ| zFVXCr!FixkWGbQ9(zEJ-J3I*eT7YOV(4Lk2q>%Z~S?<=Gi>NL7jB{TD|JXiPR(ctr zGz8{s#~$X@5`PJ-^tD?K)D6NZoK>8cjq-IY(lBQn(7=}2@w`N zc7xIppWP3=5DOC>vnLUKfq~!X+y?fG5fV`ERK1xHa`N&bC;AVeQ;GJy^COc?p&7IyAo+{IqSs6UKUiEE%im}WFT^uPioa@O>uE{~(Mvu-VQ}MFW%H)qYUI#j zeS(us6U&M7{rTR6(xkR4reH74b(s;UL`mE%7D&AR%$EB3Ry(o5P7vd%g+*Vo2vVgK z>6K3u6hb6@e_f@)>j@8yjRr(^`%)2t)L1#SK|+rD&MV(NK+Rh^NBA-|91b*`@U{=DhfocjFhYygAU#^B=sz!aBu<~`< zUVI5Vm&IKtM11-UGF>hTsnXwoPIppIJgzSIb|#4Pq{R}!6b}O@hb~BVeD#R2%27L~ za*_3mAvQBZzRxs3%#K3WV`u$n1D&gg$ILn*J43q2h?EYz1sioFFrsLGadD`)ByZ#qp7bp%x#pbF&s6aArR#M%u<=|$&f-0@> zfoPu9p^op^5?1xAA^NW!0?ujezYD;fRJ`oeZ6B_=q>=p?Fw~xi&HD1O@=w;c2L4a5 zH9&ZB_1aB$;Pm1ia(3c5ydC{-p$?9u2~nu_ScPg?F^l$)GuOc`tH zA%Q2P3p|G?mI*cn8YI&Tb&BHTOKs_4{cX^qn%yXlHKzIcCu$=y5YDvN7J=^ z*;;xxM3(Zc#IWBA;x9a@@&fB+`FN(9`SWG`J1G#pM7&->5V|LTq3>bhU9 zv*GzmD-6KbON=Iefw&bCWaUa!%Qc9J@HjIQ(DDwNqO0@HFlZ{}d0!TmEyeI+g1Q0b z6%3=D*&nit*6xvbj8lst`W=!ubULUs5xVVNvP};6uPhv2-wmErj39@wuv~6Mrx|Lq zmS-7AJ3Um>{h&3qAflo*8VH7i(pRfVS&7L{R(E49BUs59t{{%@+&h>4Bf?_8DAA z?Q42Cw*OawxC$r`*GUu8ZZaVj*Fo>N?gbbYeogimTln+?9!FRXDRoC*ioZrUz&}gU z-}!oB!b@64%wf~_O4M*ozy5vQO2z5b`nPcJ-%-!bF$g>H6zmKBvBJ=MA+TU;N5|L8 zF6}PnK@aqKNeXD81)YuL@tZCI7hJq)ssEIRH)Q^^E4GWTr>ATF8u}92sJX~h=fEIa zPp0Ybk5^%M-mMJr2r0Oq%~T5r!9T!0Vqi;hKS6~H8nrFzS-q6Nc|r-fxzA}hZq;-2 zW4}pv#|<&bI*4)wDf0Cf%cH(!7J=gryp8x{_xN^ma$eTa^~d%k^SpgBd~G$yTo;Vw zPRd!UPUws%f>?I=a*}7IbxBimZ(ABnzu3anq-?ataIHGYCkt~g$(P8FPnU*e|Gnv= zY;v6H(A7JqG5YAeD8>67zhmT1`kD+h$&^e3;@Q95&EY&hAgL&h{R-CK&x9;H0uRxH ztGj0mZo(1}3M3t5_bx!Mhw1oW^dBQ8kYfBeV7-sKH<+%D0Hav)2EkE4oEVUiOnqDO2>?U5G+%woCqWLT+ zH-D1tEmA}t5!ArGMv?y2-kCha2M89&r`gKFQz`2!RhQdEmuP+~!@^-4TzQ#tF!jc) zIQ}Xv^i?<+5h%e|6B8kkGOD|`X_`nG4rbn|iWNp?@d=}xV>KaXOhxd2x=?0r+2K^x zA^`^2$pw>BNXFT!l6X~K2Ftpe;=I_)PIi3S`SQq8_B&mp1!SF4Y62@gY9!+Du0HCD zIjZbA?=MPZNf9Ung%1IeVtHv^?kXYQgE$^X4P-+@U_;FwQl@aB!`AuemgtK$-MuR)LwEA2BoW644eC9dGUn z_&NeT`LL=2Y9kD3vyYu@ahO z23M0lt+S%oADMsqQf*7!S7`M>=(EwPd7PWyI_=#&q^kb!o9;IBc)|r1BwB8~F6(sv z8UNm%%Be5|t0mh3DAeQxMnDf%9KE*wMQ2l{%p^Kw8fXzZmF^?0d)uKEODcGH`(km_ z#YWbrRSG}%@ja)g4~fMKIyQyXUuYt`Z7mkcrxCixttWNs@H7l`s@9$_4|xL=B_sHj z!Cq_NM`mBpda$4qV=QCllLsFbzi(-o$(d|no=L33iivV|^`5V~nf%(7iT`fL&ysW| z-R^HjGcrZkujtLUot+r%o)O!(Vdn6{o5{0to~2Z}6xmyLG=o`I1u$SSoU%;Hgrd5n z#7y+7>!)mHnINCFgYr2AK>H1Z+eP;zTw+FTGV1RRu*2+a(OmZ$W_DhIl?H|8hTquQ z)tSAyVu+TwAi>}XzxVz|FhP|HEwrDEx;W>yf zEl{`(dDJtQ%pp>+;U_bqA7f>oEZ^yaW*_X_NuAQK3M|zq*w8Y7ykavP@N6B~xA|x( zW*p%1vI@cpD~WUWgA-84Iwpe(w3@_H0CT}3P3skhbm5XQ3!+(I<*t_BUi@|$slY?C z*BU#~P?_^&0573`)^7H6JE8S2yyS50cMVxMO(W;eYn@!Ong?KM5Eshx0|#1YkRHrE zGN2}UY6X8S^kV3vZ@{1*T(kWhztzLkLKXg9sXd@V>O6RnwsAFP+v5KDRcf> zV+~_aG=0)z$))@73k%sj8T9^1*mkQFIDWLH)3?(GPLqr9-$tw1-T~Vcow+B2w;0kp(U+!o6%&djUwU`{bd9)O)>&HKkr zUIWUJYAbWg#_;~P0_4f|92TMMq#7Z3;T4)Z-W$meH8{z(W5nxj9gP&?{_A}6p4p+t zUzBLy55x(r@`-C->Rr?Knnnrv+}=Pjb?KGu@pDGRisIRmH>wiZUii{wf8uMPFLb#2 zDGonyn`iJMp+l3z%DKlSFIDb;u0O6R@N_T$mj)lIy{@q=Ge3GhNgN$NQwqI5plK#` z46h}4B*PcLtE9x8^ZOWCh-x!^JF7@H;wYeYg>_)jF(GCF3aG;Ko~oMB=PMRm&&IPURaYKpVe;08`U zitU#PXkFely+rBv+PW+HX6NYe4Jp(qsJrZZ8y@=B%Z+zhPkQAa;-%)#=kS7J&6nQKZWp1uD)Iy=RV6l^1)|#C;P4g8y{s9CLknM z;b7@^EA(98k}A-*0Y+gQkBA|!d&0R0{`JQ&0mq?C zVuSwuVjKKe+be)&BN6AT+#x(%^KsH#KoL|VkC?JbA~-o#kmX)G`|u^~*i!Z4W*TBIprYsH$|{y39c!`pUHnFK5m#hWypg4Q={s%S~``Na_tYoa~+0LPriv1DL8~;zT%!u9LT!#*f=d6Ya{FVgF(K8DUwSy zF3f&c10_tj+QLKy{Z)QL=R3~8hoZ5`0Uc-#E@(VE*Rb#(epCxbS-2FDs2Bqn2qS-I7(dg^&ZrL%s(GX*(B!mBe zGM&AUaHM-&(J=52i6L&-=D3nK^hkgxndyia7R5p#2sQLBVeyRU$G7lRV&3E3?Vby2 zVmr?yV&D7bRY@Pq63=a}YRi;>o>*t&QhX$h6{L6(#*<=llywt|6tC#xZ1l7bud*Lb zN-)8ApO}K}Chq5XH9mOrlbk0vWCJAW28LW~gz9W-4i0to&mB`bP3A+Za^0yj`gQMf z5OP`**R*FL$?U4f!t|KegzIQnur$alL9@WS0)DN+6ckU&e#KK1CR+ry$>M0@U?Fh5 z!5Y~93aU<`o}a?8vAf|N6mpQTK++6SQfwLI}EpPiN=>uDv zZa7=5t67ZO08U?5J;-tRB{?nfV(wNPKG9ai_JXYW3iP;go_nZ!ekE0Qm!Ofi`9Nru z^nyc&Nx_eaS*EBe2q4<=CR{xW`8KJz^d~)L{xH(Er~{cNE#`T|=Q9QwkCH>cV-8R! z87H}F`+%a?*CR;z@0RhuRphd5Ll}{*qr`iK%`ElIR4c>+H~H{y0Pn}` zx4B!{INGwi`k510Ddw#N!;tpLWV_9xZ={hb|BjBg z$z&A&@__V;{8ZC$(0I?=!kW{j@=8D)oqXV<+YMgo?!mlQD(eo4_}U2pLR_cf*Xn5= z;S$On1LC1=9?G2BP*5sq$uGiB_wo)fEks|eW7iG8e24$7oWxiggRR@_cbJ+xEZ-H4 zf8-cO@xqVE^eCddp-{_d{xs@2@mwuLt>X+rXK8@q4PH*lE0WiP>;L^-MsPU-LAO;c04orS~-b*hBMg%ho4@+yvv?JQ(sE?n)?}M8t-$60b z28!LYp~0ZQGwy$no6=3i%so-4%f^;_qbUJD;*UhPSJJr1&DB6vsj^Jt*EFH(%%ml3 z)k_FzHW$KqdG^;Q2O2-cTt63YaM95-(@yo=?;fkn6Bl40rCXzAkg}I`*YD4pKNk%! zCRizsC)Zl-qF9M76^K)>H%-E^vc~!j_I>edPjtFriGb6_aWKsR0%M5lm$Kfo4UAK` zJak{+(u9;ByIu-9ykFZr@^+?=qOscho{Lt%%kXu~xOs*_m3~*2*sO0?|2Izz|2q`o z2<0O-g%m?L#v#FClDUCF?mu%<8ggI|wJS*jvyh~vyGH&X1Cnk=XVqEFArCP!E`UCT zVk=msU54kAaUj>?3AeFutUG6(dkQPOvYi$?-5SH=Y85t^e&Q`(0G~hJ2h44s96-Q0 zl{*>Hl7kgYU=&D_e2T;UlLqQ6RmP+d_?~1Kr{mqu(JU$Kx6nI6c^hmT^{mD^rFq1b zx^L3d8^?-C*GNAg-54+I%3b%7yLS+To-|39>}Q(8`id+}-#qLPiL0dN|JvT#H|*Gq z};k_|Nf=~9S$bLKr z1~Ptj@Wb7PwJVrcA;2+@cbSFaX31xjzNp7^oP(vN)PW|qkh&YL?N>*llZEu)3zL#D zEm2R5nKbsmYB27MrAaY^xB$>Tn@+J<<3kSV;R=iwdx!r3AL8QKkV9|yEpEn6r8~#x zK2U9{R*Ls>4oI6 zv(d;oApwy@W}b{uZ|69@_Uiw(z-0FLZ-EK>Pt*oBeiz$HMzNHLcpn*Ru9?7g5i7$9 z?hTf`29LS^v_jZE;`kDC$#|QV!VK2tvRhC^PGCV13KKC z=_T?Le;XqHj^Z*kT;ASq&&A_Cl;-4EZnIu{?5KXxwQG`-@Htwc$XOQaZK1YOeoNBr z-Y6sIDA!4WUutuE7c6H!{(>gph?cCo%8-0*AvO@^TO6+aeCH!%)+dI?YcYbKXntA> zh1raf&{|2OUMs9Yl*>V)+dD#<*{+eFDZuX(51QY6KX`?Ed|@!1dJ7iaLW)@sGjCyc zR;3b`URfVp={o@I+)Mi2zXujc|0`b4gp4nsuKq*%2{|IWON@Yq(LvyFiT{><|2t;y zUyul|n&tm$@go0a{H9*?sT*DBix!egN-loSpz6#2MY4!I);RWcTegVL(LBnFcRqv( zW%m2GXdpfnp=)P9vs&lTK6Fd+d@E!+=Jj0x*&_syj(sZD8L!A7)^tqt;BQPF9%*r~ zbpCEk6B`v1TzV~aios@5jR{yUjN+!I-2ZwY&cowi+h_79>}ESFbnU579#U_sf#u znIX5qi~Co^tW`(@`T;g8?S4uMUudvdzwfB~$kT3)_1YKX;`MYHM^L%sU*}pfo zyrkIpzl>J@4u`+@A`}S3aQ-E5IqNQg6Sv6j_3cl!%4wfsd!_`A1_oIERu7BIc^6Cb zRo>3hO`GBQHCm8eSanthIddTtE5DMcOyl3T@rV2AkAYZPsfqq;u_@H3Tai+J``e^_ z>45G(Keg`Poc^TQ=b{L{qjp&20q!fc$XL}Vl-~8qI`iB+frZ(LY1Y3j^~?4{Im#an z-`$J3nSknQbl+W@kz43>T2FZhl9;_84o62>%$H~|NEgn_VCAw{W-nq+!3(XvAMY3I z6pnhi=FK4a=Bn*K_HSqkpb6lsqjrYQ`r%(^*$JUDlBWz-WGJQp7>r^760CQuji1PM~9sw3nQbZGa2bJb7 zpSbiz*5xPqi*-f6ue-zWD6B+1D=OTS^65n zf0nr9Zw@6w@j3&d6Vl#glKZ@I>F0Q80-m4B{_a7}QyfOcUWapXx#Yx^;*TYWCG>?( zG_|)iwbD%y|D(Dm=fpivUfl&G1BK0&P%ml2nc`gJAqN+DQume!br*C*%48j+vR>Q* zmse8T8bK`fW+k-aaX(~_pMUw9p>(VE1OderK@+^Dd3jueF18L4EdN;v8zt5hv5r1w z7SQB;D6`q68JRn9I&B10g;nS7Gs<1R@&UI@?NuqshF4#u$`M$=9S6I~a*=$mYPnMN zF73HquP-WSTdChYU|zCeEif(9JjU zcyB4_*kM&gZS4If)XMy9%3=}PsLciA-WYk*^HNeva;lh^NgOf?V3>uX=m* ztl%Qo$S78d1K!;O3QXWp)pbXPjs0hiB4>*0`h+V@`D^Z`vWtRDdoQsSeQv+UbKaSY z2t55y|7ho*C1y*QCJVnf8AzxHQ(l^L<@#M^;&2Wr_Okx=7wBg{5MM!INPZaPJr|Nx zryp>0`>4AmU^Hl~jKh2q^~GF7DOR*h`n8MSyhR4hR*C$kE`MFR7}foIlDr8upw2Cy z`An`btvrux3CJ?Mn;RoIc<$y^eGrcncF*EI&&!fpHX|1urFKI5JsLlGwvfS>0C7Fw zx)q|!FNw&_R}g0t!9|#q$g`q<{(J->-8fD0UUbcKOmiRLb5}*ZSf7%Ck0ZOH(HoSq_)-bHFuDwyL37ayJ^q~y^-|3_Jn(2?LY<(Au1jren zOrX_UX0vkoK7+v&;bGN!h`*Ma#ghx@>#0sRttoaeb(j-}iNE*&0kPjoO%Z!KeRp2P7N;nYrx_~IqjZ!C zl3o9<#2ZqShlNJ7lP(3UFwqsPO=&}zkW5V zKuaKJ5e9)1e)Nx4y|M0QsQ4YL!RGZkjGhut+0$kq4tC0KnXY52|He(h6|vw*H3RCh zbW1EUcU1IRaD5)_K);;<9t-}F@xvxMgYl1{#POV$00|l)Ae%GmE09rf=&-5bjsP-P zRPa%ELg+sDw!kd*Xg=kqr2aYeUig!idzlX=$Y-SZ4(P!obdbCr%sJmVO|`-gEUox96AbE5s~XwfD%v*A`Q+8KW?IWwalL$31= zaHj4FmJ@X%${FW41TTf=S4=ndrJIHlE2qauMK~On)l5cgWdS#Gb?82=orSNtUYb6XQ_yx4tVgP0G(m^oe(g*elBn!3Z4YeG6kYn-+NP4Nj>() zF}$39)nA0^(a^!=c}k9?;iAWpK=BeP|0-h2XO&o271#(b7v{*<$nLRU3_+bVpxobH z=Qcl;_<(9LDwZYQ8G9b;VA6e-5C0b&h~R%f6ays;w2xZ9VLp8m$EnHG^7*(+%aOZ; zR^|itqH?*@{QYiY7o05lk=PWv<-NgW`C|DsKpc z?S|B_B{QD|nG|bOT^Q~bV0|uzCUmjo{vQ#%aZJ<_ek_%vP+SV7Zp4!kkfX%L5@;rv zqCi~>Uxx{jw9B@!pM&an7Ot+}90|_>Tw4Km0sv(wT26_DsRXN>tV}rZTOuFm4KdM^ zSto3sp#BX&?^?LN--11*5|{mSkG2o;RL9OwQRU{G4k#Z(LsreI8l(D^$|DK3d3ovm zelcC7AIG$KB+K1ZUJ3;Oa}6u=^6!JVl|Ux_H89TY zvq7JgJEheZP5lH%#Z2#pSn|^a2{KY*5_PEaHri#?MH-b@unh!(z$j)rZ}H@&&fK|j9el}qWL-?N z?5?IRLLkoJ>}k&__piwX3qbTQ>4dEmQ#Dm~AtU{Xrn-oHP*)+VvP!&Yxteg?LsvZ{ zBJSix%^q(Zwh*J%IxD^N{$)dcH(>GEsR=0cHg7OF#Jx{z1JfEn)7Llud)zR1^-mGl z=!R59a7d@=jGNngMpnb1#Uq8MPP&E~c-^K`tTCLc;?90ru9YZn^k#cN`Q9q(Tx4XT ztphOUv@C53WVs|#&z@e;zh0OZ)tmEMOpv*~C`Yl`_CVyri76=e)FOHF0qRcQ>9>sW zdOm5?o~;&Dj#x?Hl>Vaj9mKineH6YYCEq7kCEMltzKS8{$f>hRN;9tV;Vr+et$|)r zOQ5I+5AFRSXRsVCP@UP3MUMx@h8hR>C`AS@jF{zh zV~hHS;>((CeMzFtW>;V5YeM8xm4;H!i0aQJ+fK78KULM14wc18v72bwf7})t3vHc# zAHY`55pNy!uwQK7@=g0u6|aWHYzAc}wC(rcwT06me#hhXB$MY@S+(vs;{XIlS1Np( zQ<5)*n$cNI5B<_)_=P+iPihtrJ3ZudJ=`@E6OczoP1i|0#p}D%f!(UMxVDESL_({8FT2;hb=Li{_gfnii8p~-+jN~6?T1p?M%6aP zF>=j2c@zalrMM*-m|%!^|{-k%=$4ImU06flu~raQ;k zIG!4wGi9S^O5EwzlVtI^Y9;?+-XXah=qdj-sga%17Sx5O{bqsS4=YTI-Uqvsjo0P2 za{N4*d#?z0h2Ur&Q9R5*&QP47UMpdDx6@;uynHz@)ZHI970C}#1x+N@b=O$7j#ru4 z6rXy`9yD%s5kn-XKNjrok3;E|u9COpr0ZpPln~|Q(#BuITpzCK9l&6KM0#VxOXuahm6zwD z8SLW6tO>e*1mmyVpLj|QF496fCjN-XFQUB@p&8zW@C5uilkG5BLw}T=tUP4HJrb9D zZ8A`ns$49UE^h=LOxFo>0csKCK)OHVDyJHT$Lp%4rx!;Kw0MPb(o=72t~cRMLy4Kx zb&E_s%HQ*sN$Zwh*5HFumr_1PHL2ac;U@=nIHW^+vD4Hv2kZ5b*E&x5D+x&iWP9zv z(D55`lZ%xold>4ZE1e~-5OBX#zfCxhEHupP&KPArlFb4BH7lZ&p|2?|2q!ieW30Ok z68a5+_4@SKD9$!S>tvoVJ*I)HwAaiw-neAzDbr0VR_L~gO~VpD--KwUq58e5G%>Qm zkAA}e+jZ>K&xku)6-C356R%iN=k#&lx?Ow=i^hjNT&=H*24kM@32*H-IuSah>5Xr_ z{(yD@JUmwA`XR*4({w#{+nurVG4$bX2=(%V)|^P4yD|D1tPd0S_S;Lj$>aI-`>!Vg zN@p6Zi4}&5^<`WJ!{ePR`qT)Yr5Dto%L;yx^fm2p^_9+t3^qgS4T?huq1LymrRdI$ z=NRM6+t!$2Z?7ieb1$uo=hIS&MD(rX{f4HY?lZV?c;;pO0G?@k*b?eiEF(}#T41ca_@Q+`S_aS)oAm~BV7Yx|R`b(V_81bv}k&Z!5=18jPlhjvpD zB7{vh9;-RBlvv$g<)^>&@+;bpEq8?(ClmWKpBN`oEES`YcFz}SkZHj(Y=LMljAfs4JAW%fh(YnIV5p?ec1& zXtGBU6Z6Tic}3T%V7Ke9uVU$3Dn$~y3}Sn$bEapf4Hg_!!8n~J7(^b^n7$>|kG`I) zu^z>0wV(xV@!HF-7}lZ*x+d{}gNs7jcSo7;j&`D8$F$pCa+sM3IYR`RhD>y%!^y%o zSRJM5%{fD`i;g(FnmcNIff|fO;}lgkg9@kYGsU4rmr=QY>J|uplr7?b0%SXQuo|Dk#4CdTZjtrEPT)@wg%>3jwdnX3E2YB(1-MVz! ztg4%nM}~W+Mr=Ko^@DFnkkcAXVca;l!U3m&Ru4&x)#an??>4`zZ~lZX><>aGDV=Q- zo3r4!U%B6Kf&54Ibdx1igq6hVMq+@jxR%I^@T9*rFfux_Y=+nsQC<~Vb61{$`;2_k zplmqai~CKei9v3(u#7d~P58C5`Wpq*AHM1d_S(TffqZgfUh{i2!$;_o!&3~BdWhUK znikYvNZkuBWfD$~m~P#Eu}Q>j9;yIi2vY=RDe{DlU@| zvf*ZCf^d#X_pYiPe2Znsb4FH*gJqSuX#RW?uV86d>2VQ7625 zcZ4)osshIAY6rlwJoI09H#NFbbCLsv*8D2ET&z+*AXt40(<}z476P54XIp z8%YJ5nC15wzo$sygwLXa)@yn+j~^3XlN&Eg-@F=IgyS_#Nwo-_y1muy2Oy>>WWL%F!v z8yPOG^?%UtC)4qhzV%5j84|sm8^9g4a5&@_W4D9nZi`I24U`jpR&K#=qYU<*&$pkf z!aER4Jrb(zRCf}-W_bz7deM&W!Gk>@*AN0W;6rp3mn!9>$CA3l3{?(mCdzp?9_C6} zndBAa7sy)fRlJJC!E}PeXHUni;L|umP@P6cn5L zREV|m6Qsjq);i9YRkHd~UsTzxcNUTU8XgPy4aj`AhQo(zX^O=&DF=zd5vy24LBI zG70!C)rHCW+r*mk4{oh>0laQl5T9E;Swe$4Ai>7x46SX)3!SX8aU=|0*v)%AMic1| z7d`4$w%6c}u*$rCp<8xoHXrS<6Tg|w5i2)cwDyui{MA8cP@fdB^!<6-H5eP;P2sLn zlh>sHmZeV8oiFsP$K4Z490K>AnTzJ?+@nSM3F!eDD>{jncvWXH_bMyG)vYN-x(0hz zyat&f7}Be`J3><~N_IY803Hw`1Qls-#7zt7SX*DL5>t zYI}1k&lnQ4wU{RocE2&H&@bTb4A;&;4hfOwatnF}&%c*)MH^2u^fhXKD|ShICk;M4 z%t?D%zjU%3TZ>dfdyPTGOx4Ypz;!rS7`HM@n_Lcm#F5NF$Y#q2%5c!jo%;e9X${ha zNxf-4wY!aMC5yBROwOuSPY@xmX!)!whQ9Ml@_+#|XMLvl+m`2NZOwt$shE^x8_h+I z^WVfutnV&9^fNRz#hth~c`!>R(EF`!cIfpvSzUhj`US@UO;-Y<(O%m**PPo2pB3*81r8q;4HSXAMe;p=3B2fw*=d{owSEDN49pJ*C3FfaFiZ>M`BIk7{rRR(%5WSa> zI>7D*e=DduL0MGl^AqLp!$}uWLwX8LDGhn(xqJnC#W z&%V(`j`}WlA52FtdvZKQq{8%>Ozx*ohQ*lf`5+#KyRqV$C9n z|HVH~SUO`Ox>)!qzejc*nYctpsbsiF1bw;LiWh((=U`TPmotO*! z9LIJ4lDx69Iznx#(eY_Kpl`iJi;Gx57m;FPXCw`m$d*$RNbqdtuCxp8RBu9K&cU$j z?D#%MXv6(iabDkf`o3|>+@2%b{XGm8ZSqU&c_XQn2bsUioV?p2Lp_TgGrHqW&yaa? ziNS9qvw2}y%+-2Y5h*OaOq7?+ghg}G_&x9OJf|LQrTarP-=pADr!+EY*8*=woVa9t zQ#OFQwfSxPmh`A8gH5ZBd_Rnf&eg8^cscJSO=uHj_q1ww7>2gFMs1C+XI|Q7h{9qv zI*Z?_KgHiOa7AR!(Yf}v#kNF(=V?w2kn2|_>Aty`jPx68${a00nfe_#DYoO=3P$j| zOj0xM9V;4?!y;)IlT3Y)bfDHbf##3afZ-^G|4tU2O3b2NL!XJ308jbVOA#ACdGq@E z%rYJ8JY~XQ`^a$}TTpRT?hh#~6JVIHo>xodjHx7Da-Y0o`y3vmF`*Zju3AR(n{rkt zVqvBEGtJ9CF%+V?gvbh2LDXi7FdzNk%8NiY>s4h-SJZ^Mewm1T$4Zq$eJNM;u7VD< zV0dSm61wky?D~*^*tE*+35A!$c6V8~?J8@Fif@8|cp}U1L({>31v98K*E(PRA!e3g z^X(SKt~H23oM;IQ>Wku?3??5S0qC3|xa}EDjyDejmPySKo{qPsWZ!t`IRRT`r1IR)~P%5h)irZhSSUH6;lfjA*w*QSJ0&{S z{5WQs#+bB{SjZ?IqgzqgWRiSsd>>RimW@rgBgYuJP)wf7v=7Pz3U5AO`7#Y=(+{1w zkG51l(Ef#lBnJ^e|7GOdA~=N5{P64t1a-stBW%VH(AD8o!DJ&7iY( za{Yg`3D%9It1W3_=}b&a9slt!(*J1_K!{mnEyGgw?E8jAy7T*^L>8vGHo=ZJes@@C zN~8&bZa^zd*YBeB&Jvc9ik5qqZ2PRG)l=0J^xbh(p?-!k>Lhy>Us=R5QOnqYT`&Ub z7%qrU+VFn{guFE!&7o&vu8ky}#RQME{o)Awi*tzy)|!;Sz4Q9Gn(CIfXCo_pS0n3% z=Ff0|I}Ut0sg;MNK}MUdYAo(8%{v>2WoZ77e4Y3I5f;D`gBm>GLH?f?|4aJ3O*0BJ z0h;y9%%b1=K55o#{tpQNcbv5-wn$FW?{v%5@Fi_qrp!*q-y@auW6>=sWZqhausNUY z-5vA*#a8@M#^DK+dNbpP3mv3lBP*n6d+tT5ZU2BI#)0DD=}rg+BZ6LjT|)VKRx8d+ z29VBmly-zTJf6Wy%+uxUZ@~=aj#`{(+UItUIC!4*X-BE0nI*b-_SnNKZO>%&-#&dV zo%qUF3N2^B%8}M)b`u!=8vo-OSoli(V=9lD&)LP_04KXWlsyCDlrsU5SxfT&2v6G> zoCQnMAk_EQymiK-DQ>>TwRh;^W94gYK}gWqayrd^z3DE$1kf#cMbOWrxz+oz1^#y--d!#^GkF4;#qupxho?B6X568 z;s{N9EG$MTek<@l2Wbb>YxH)%O1!_IHnR5>*E1s4^EZX&4Sw?&aV?+*z`}BtFR3o9*_6B5K) zlh0kHO9OZ~@kW8yFBaahc4EGBtV{2njVKbQzHl?dEcW*096woKcYCpfmVdPa2iuG{ z?OX=2ak;TZI*X@rrPvIZ6%qYhiNm!HCtextIy=r@Nc!1I6W+Ui*Q&bva0Xdegc0{j zi zaB|gwBl)|()6r!UBPYXXn;mHY2S-! z-S{?|lTW;7U`~(JKzK$Wt5E%q%w6Qu-39|3)P{Q2n5l0)_&f@d5D8pChy9>Nh*sg1 zq&3wB@C)6C%D~5l8T=p61s)hsC^(+>3W1C=t~HK$1d)u((Ug{JYROa8tY`ZyK(VdY zv=lfU_h};3VQ2I)E;w=ZGw3+~#UH(Yj|!dAK*?W}PqG)$FBlyekZoIQBA13}Q($a` zak)i6RZva|jBOE&CFYB`uUNMq^(pMWq;H z3%?pfgUOtyuJ^p>J?DDg>-^#3n*Th{cb?~S-=BLKt|qtZl4m2HUgw__4eD1@3RsXZ z162W1n+WT;Yn5?fqV{S5hCSdlwz(FMejC#8r`qhP_;4Oi%Hl8w!EK|1D4K}xjeY(@ zIlr(&ziysEh_7T44F0~^BBO_dgM;MMaTR=cUf2N6wKoD#>iG8dX-^&?OY&eGJ0MZu zdU*dsdC7pwW_up+v&SgzP$j+2@bGm2yn1&H5j<=_? z7AGyOFRn;>CnmQUu8Dk2{kp^ZGQ8P*HP6t$a7C(ht|kFib_(dL4e%0{fRilaRc;*H zt8#!|8&qiRURvZpjAfGNbyAyVJO!+Sg~GW%;_wv%k@~DPW*Z&vtCN+2uJ{wiO2u$8 zZ}3LFZB%s6z3?)imS$-Rh9)FizgY3A z$7}mROF-yN8n$ueyCw7dshtnkQtG;BjsOI~f)pJN+$Ft%1W1d`#;eUk``fM|X8_+1 z;#8>zi*-s5e-;H0;c7N{F&U|!vdDv-?HaMg^7k9Fm0jt9i%f_92eOh^9rQOkCfPap zv%;)}ZQ>zqKd;Tjze>8%=CTCG761*Mdb1basuam%<va}m4jJ}oi(V=J1`BMt#?+t*W|ZB%?YxhAB)yG3=4*a1CQ0ZE%wb9JriY9Il2XZ0 ze@*_Gj_r;(4%>I{>LQL>);=g_;`nY|xh`F2a2&jxJESl2J^XLLY!uH^pnXDi7JH%} z*SYdr>kr90cc6b#6Rn)~@mb1p`HPC;C;Ji5GG#O|uzC|3>p&N|w9h7!5PEB;UnLym zp)SEbEOsSJ=Fi=(k+x{@e9Q$FF6EvTwoJR;IL;kkv9>_Qmih>?7*VJ?g~;o3Rri?v=m#k7Vh-M)o^lftf0>eR;{ zlASFz=1*q1K*FhEWIG>O*4oWlP!L%)aYwjN;Src)MLS5IMi=6rqxe zH?bDda3jZ7Ad_!ut9trgg6A1|)9G7mmQXLpDZ&uW?&K%$6Q{J8sGuX$H1t-(q3kC} z%r=@>=h>|zhTY6Mh|$*8i*UF3Y#Vt6qmjVdDOZQHCT1Yiw2~5$Yeh=;Yf6AB>k?I# zsPNaP^6RN~ItJB>IsWnk5p5J#PNnVjIf$u1zP(#h(6Y_(%gYN z2>k7c-8keaZD>KKiAG zRr#NK9#2GZMcDiM{e{}HKv2zp)b0Hr;AP;B2>u&g z6aJ5*t9h2`Df`;`-hSA6_y4Q>`TBl0f@I4KJ5$6~a8fP#l~x(6m^gbWyOu>*Yw4$O zbyZW++-;5A1mVJZog+iK^e}c?w1r8_@*RV{U^o*_=kukkYG^N&&nO#W996~Zy)@Q#o(Wg+H=d4t1EgnUO{^C0`q%)Jgem&~E*!IeKjtuB%{Z=LJ5lv=Cs29_$s`Hj#L~6=8-7Iqy(@s= z!W<^^C|#QyXV-4v@@p)@u3-VSwV*ZWYvQO+TfylFi%_&=F0*G=Z$o=k-8#F;Yvls8 z!}n;}v_lkA;-{4?)D`Ik_}e#otG_02RVwUqrws_~MVBm#AdhctaXc|au<|AttZaF6 ziI9OZTBzo;hk}@ZclzSot5gLBbO`U08oNgzvu4xarv}w2M@_Yty-G(=*)7TR8k~6x zWOH2e`K=}&e1K2%_2zR_xr2lIA|Rs);WEROz0Pi?L0(+o1enw$Et>Mqx;{9wnZ zIg*Jhoj)(<_xlm1o$C1jQ>Mp_-(W=QKrK4<&cZ(T99IxuJY?uZLv2vRm7I z${e1v*)puokcKQ64>Q2B_=@gVb9lG=`EI8*Sf^L!w+~xoEa)kNx*98mqc$J3@4>P> z#ZW9_ODcVC%Am@A@(YNZe8_dyC87?)jicT2P*oaE8JS%VL+a|jT}Q83*zGNa_q6<12#($J>?(NAaTM2 zD0%uaiHIZEqD?jkTmu|dnt$`ZNWOhmnR!c}0eBw_o_$d9pWi1>M&Z06KNn8*Y{~D) zwB7kUZH_}h|y`q`lWM?Xug*yc1%=$>G9_?#CN?bIUe zD2#*e&Ct}SK7p3R=fv~R@Z50m;^VDT44Sn8rU$bBJ zH1`LzGlLih7*17KooYw@;^z`WEyEM#wsA!gfp@skcsbn9^1de!%^nM4$qa7NZ89$X0aa^$5NQfW&6)) z0rrm;d|qsR4Nb1oaBG<5i<`y6yXcuRU#e#=tG$%iT)7B&*`!?v@Osd3?CG7yY(Dh- zAfzvR_HPx8c;@^97jb3qgg8r{tMWSMK%cdDVncoN~v387=+ZcwZ7NxnCz@ z*Wr))PAuy43u4_?|4}`Rm;LPtY7$}aM8?;TZ~kW%DcH>aGbA``b^U6E%XU@2z8dd) ztWuIv^PM|O53gITj&Mp1-^`}#i)QC}AtvI~w!QRt&Wx`B^jgSFU-24wZdoP~I0Hap zE&ypn07!$HcG>T6Rxt^%qKb%U`hR8$5cX!!$d@1oeKu(+wccHBbu1(0rim@5dX|bp zlqXF1Xw<`oc4K}+*)KY0?Z$blE`M^|?RDuaqTzVV6#}h9LP4*EAC+Mq*bR_Ohy?~8 zDu1Fdf$%*vV(sBwfia_s&(Tk9NMYgZ29_7aCjKP#ULKIKC?N=|C0$xBG8+P`Pk&I0 z1$h}rN0?GG38QDl+Sx_OeY@rb?VaglH*AmQM;J3#zP2T3mcN@rF)Y)Br*;+>=aqafouKZV`%m1q?N_p6 z;Rt$ruo!C^_q8HGs3r!@JzhK^w+CWcA%CBEV>jTbEXd8Gz3!H5B={&C8srcFtKN*0$u88z`Kf&`V9#Kcz*a> zog1}}POVaX%KqlU(iQ*1glRx7EPI#2At*r{hGHvjyGpv)mCH%#KbPGU(DeyXvmDP~ zrNAz740>|jx;K7;aJuor@{;HE{&o0*sZ7re>EIRqXUfguWohuaCl}k?(=@THC=Yb%a~G zAukTeLpM%{pn-?{7%Ix$a;Z4s*+JJumBe#DY`^&%fZ%%}qf74_$6U8p(|B!b75l65 zKHtFg(8E{X{`^Lh-Tc|y`)XMIRZaWdBjl;qnGG?fB;mbdCZNf$3Bur2y%)7|fz=+6 zR40lZME7?{EvIDx_3iwW&cla>gu9@;uWg`@mBx1G_~*}^6-kx-o)K8i*r&|~RK6~! zt9U&j<%-yDX|=G2aYrB2{Gc&Ph_LZIVMa5Sk29Y}M>opU@_|~vug{8}3X?9a-gYcS z=wWV~&%fCs6EYFCMs4EJecyPzGg)2wy3g#UR$kl{EMPEAQYFiF`u|~ZEspNh`GA;F zezWEz8fbE^NS%od;yca*3E}5gQ26Xf#lN}eu0WP1oYgijFX$=}^6;3v#KpJzD304^ zPEP{*%hK(BOM;S*#KXHB$({sErgTz)W~US-@U8tsZW$F+{?4k8+ZMM_!jvn}d28RC z#|Jio6|7z&AXz?R)0vKZ#2Jc_%k}2cFkg5yjwLsH%~zvi-vr^V;UkEhe=8lHGDUi! zrshY}MkSAu%Da$&ta$by{LWm`BYxk}kv?uG7pR|;mj=TC)cQUw>S3@hojQ~Ewmu92KA(syqee7 zSK2T5{n07UaL-Ihc2k&lSkYRwb|L4yPoh4s=&b9aL_`@?h z&ASJM=z)LcYx)>R<(Qo}%CRO<^GRwt59%SY@MN`nlzdTNlw8sVKC*>n zral?$u5O?9WYW4^>+H;pw@v!oY6e_*N6H-KnRqppNmi7zCsV*sYz6mS{#!mXwOn8O zD4Fh6jjVmEC;IW!g2pPI4Xcq@3f)smkIAOXB{jtxDrfUD7gfC5L-dPm*Ne*HHsd;^ zS{cR{jZTO()3c|E2?Cw|7BGFC_ug#AN0dCx)OgTUIWE|y!*cTv;jiE8q?<;8F+!K8>MmlPm z?~IVrAI-=7Ro8_n^3u%T_nsW=GA88Ii%Yff(M=o{ zN*{H@1Pal-6+%Jo-Q)#qcvcQSHBUpg>|@5`_!QPRjXs{NwnWM4fd|HXd8ZbnP6JK7 zID>k^51vlwEa3=8=O;v-SWC`*3m+J9Pp^JlMEb^IkGD(L3wwp4^~nuUh1loNpn5*DxJ>Wl;v5d; z&gNEG|5tM(nr#fvz(n?c>6A>ny*bOmzHgtK%Rdt-!Y_6zG-7?~xx0U7W6KQZS=F^1 zD$h<``cc1*v_PsFDHpwQyrT-l8lz|`OVhb-xYlRY^kK!z55x|NxtS*Qv+md0T5Y{Qdf z61Z$&%n(Qh15Z0dz;m1GdYl>PwsDM4 z5H2{RPgIt2XGzv!1G(_O$L>2qK^;WW4{oehdcrkUX~-<4YfQf`oiD(!ge~sm&;b&&i$WYqmjKF< zk4XX*AVtZvdYxb|tm2_OiWhDY4dbts{)7PteP@f?cf@(aXc$h^rlNb# z6;`B`+!;W}xC!=X=|UwGBk#Rr@=RT~Nc=T$4A%OA1d}%#hgj^ky{M?r9*t+h4NX+g z;z-82!+k2-0gLhFDNy}jm#8@v;O-rTS(sgQ&77^xF$P*UsRUk)?|+bT7GeaYi#+he zSp_EYlEgk(au2@lpnG8!J13VrX$BDq72VL%sVo$e)qrEZRiG2!76Z@bPlJ1b3MK@kIEkQYy5`p`4vxT88lq zeumXz5;MQS&PO2FL9@VvRq8*DWLJD30ry@>$i<)Vbx(LX!gr(JL3ms~C0unvqGN)a z-WrHyT#B|INg1q|Uw$# zae1WSfVKscKS$U6YRmRf082>NR-(Q0r~Mvgq};Vr!vem+{B6B!fdhuTULNDpYrAht zU#d+yrKz85j1W~^@%W#j@yx%xmiQ4jt4`ma%M5}Xkuc$`-X(A>u#3z4Qd?SZ%0YBg z3JMuhpQEO|`cEOKk+TfbNRNCJqQbFw%iYMMbWW*GX7dDJXB($e@?twX+v*F9@?Pts z3L5md;Cd@NTk_nsqv>K>vclFawbpB%#AvM8uwYUWzqFJQ7ps4X1H88GS_j35>#zmY+!4U86R4GCU46Zl^2+HKzw&a3!c72|Q;7&6CSZXog>k)@n}y)BN@M)dq! zgDF`*-_tX4StDXlY5nU@I=0O9!CP1}E@s>eGT-h}?O-bDTd^CHzb@<~E6je#jA2nH zH~ge|iTT0xO8v21Y2;h;!h;#6`n!u9xbsoXd{QRPuJR~Lao0QY_c4QXFdd|l7fmVb za#D8IG`xVm;dR?F#9?$ioTKTuB(%t3f+kq7~PWxd8 z@hp(L`yQ`JdxA)-o|qJshU$Y4L571@B&jUqNK=swjSJeVHMs5{r_5TbkGCsu@0dwT zj(1OmQtO!`evP2T7tA>Q|BxRgyL%4`$j+IpE!RKSg$P}4ADI|I(N=>Boo@9jqZKiD z6m9}3L`Gf_uI$}KYJ7PU0aErPwEP&)4VCjr}!7$)y1Pe zy>yS)-8t=G@+~Wkvnjp~p}Bk)K9!I+1LU}F2(i$>ioa|>X9A$fzqH68bptJRkx*{!FBRafdm<^)n5IDyRfMu=I!r1+X9UY#h z*>30unp&p+BT-iPHLqAVSZKi>^hIRgtA%?Jm3+R2-*E`eB;P;5=4 zx5US8D!+v}vibk`Jb3_*P_AuVe_j5&l79D(l2XpMM{6^lDSNQu z1g$sUeLzTpGeKIzog3&AX2X?-u4xc2dl`UY`l$@S^~J!o@|2GWg0-~vrcWCV=}8ZZ zM9(9p8G&WMr=UV*3S0slR|HLm9yY7;$tEGGY~fL)w295j6H_^$m>m6iJkrqH5E z$Df$h_nJm!nAwZ8kI-^LJVlgS{3E3(x3SyF6s@CqDbZz&d%J=)Dsi zJ}Z(#NS9Q0%4N;mT(e_p@kWmPh9x21WTmp*RKyCVahQIoNnKgB@MjtdzQ8ACHl%+> z#&PKGbod?l5@TTy5*KwXp)9ZT!sgy&{TAJ1t4>t|@P ztAn~Q&;-}KJf!()Z%bJCY#4>pPOOqg8GW=dITAVlO*I0<91~62Y|q)fv{1@MXs=D} z<3ZRD6eAMUAZ&oi?hyB5FU$zyH!!4u?}p)@wi4G(9;{p!Tpb2Ip5mpZYS&bmriu=j z31kvam5oLq(I05-LGFR8ot-jA+gn=vBF(O@c3B7sq51D=A72z2rn(w-)MzFUJg#+l z-ShL>TRb9PJlZC_@EMqOjkE?awF)+5ZO{L>hBXE5YVALgNf{uSQuk#atQ`3dIhQ6K zZLepB&GvC#Ex-L5D5+O3C@Yh%Fnf8md}gb*cJ*S-zYV-fjL3? zN2o0Owo>~L1kn$bz_aJ;tvWxl{&6~CrB@01K1gq?L&`{~9&WyLPMojQR4w^!j6a0a zUmu9e8XIBGl;s8$g7L1%xWMg?ZZoh2an5Sw46Wo1v7A)W(M>#lgU=)Vs<4Q#SUPwwDnN)HKrm{VNrsT~346z{09ZgNe~E!*UbmR!5`T>l=`=5=^n ztH5rJ*WVj7l&M^Sj%JXR!9DmO10-#+R@O%MHyYnr=3`Y-ej;iph;dWafGIQZY(nNV zyRzmr3-zo9Dhl+fodLY1(KIjzR8^d8K|V@rl-S?M&ywl8@oygQH%(uuE{z7y>M|z$8RleC}^#ChfvM;wB5I(~gu)+o%GpiiXqz45m=; zgXsNL{P|z#@P*fp5yA_uk5wM4{^!@0cfWsOY~;Iedvx#;zvp7^o#UX}w|o3=8N~77 zvyo$hT{Ki<1%!o8tyD@=?EMq|Ju|6!wLB)X(Vt*rTf2?jOwl|2aM8 z*I{`}39_=x%Uw?O6aF*wM2j;pv*6&oi5ax)SxN=VOjnaBLWev>hvvYMqOpPoh{e$Rsi6}Nbmh>qr4d*(sR>bn5^b$1joy0AFiD{_{TY|$0yveKx6k*`F$s8 zaqSX97D!j+G6qclNlC}DWB%Mo`h!|%@NPt1Bg?U{4e9cFebCQh=KvMJM=gh)oqXpo z>tNmmsC5zO(P0~(&Wv^qOILZlhz}Xh4P5o11;RR!kL4)H=7cj!s3EWEfdO2xbKd+u zG#AWv-!7#LXfeyKeEYvkU}UiP$9-f0+=r)>F2{jGv|Tl64~`we&#IAmwI;Pm3l2ZjMo~Abg@K@h9lvQ8|gKYEM*sWXX`TPJ*(cR zjf(F}B;uC`TjNi8_u+EKALc2XE)bl^$k3uMaw2`)B9t8K{>sSn+#HUv0J7RAHO}T} zI4sgrrDgH;KMJ?q>m-=PrKGb06d57Q+1i)e81OAt1g<&SaZvwg(Q&ojntV$;cnN=| zxlam@!Wmzdj3%F|aq+ViscDMXVh|Tk1=csAgLwy8go(4zdNu;Oh%8%wP5T^B2uBI( zSlRo4`dIxYJYUncs=+1!tRrgnHu;8b+2x9B5tQ2sn1WJhy9i-a{$;25Xs*EZxTT+% zLoyrPPgJ`9y^1dXubG5C;ya}Qe9nBja#oILLl=Efs`xu%w}^Yck_jKTrBaV<@q^@W zOyOd*=ZIa%CF2`}oDq`kJR4JO?P#McSr)@!E?c&d|FiA3-b6ht&?1@~A^g()P>O;v zNR{fwTz^T5G0k>wy4mR27F|FX;MFep8ijlRfl?ZHhg$CAY+}er!URsNOJS1wZ;;y% z->})xG$47z_7B*LJ(rZDqkL4q3dUJ#k^9$1*`tHps=@9ioP9x8CissMu+di~c572i zMhbu=DkJR)$Qa7Zjp7?A( z;~Ap1_!dw_3_8`-sl|y2B_JUWPn`W)XMRIDMGlbZbsjVC;5lSb3|*Fqa0EQ|yt#!R zQU!l2>+Q!J*EmmjA&3@#9%Tn^yXikPvxl3s2STuIQ1qHL6f@&9EQvuI)6x1W z9D+N3rag5SS7c7cXtsNEg3lZ!)52~hPWJtd?c<1=Sgz_q;l>T99EKN7hu76j_WUyn z62Q0VbWW;4-39z$CJ>>|dHaL*?!KshniEUyMRfx|dArLV6mX+Ve-oQrcARO<4(MXB(n*S}%N zvFn0ieBJt%;yh>9$e*3{L!+Y0Vc0xX{fUIE4JVL;#&J1ziJ$~yWh%2$#vNc7dj#An zGc2rscB-DmWJ43&LnYHve~F~@nwVK=iE&&cLm7(oM`J|{DN;4E@oB%nUc&S%>pnP~ zV?0HV|KT27lmBYbb^|1hz`3@uBLPolvK~=whkeXHM(kVw|X!ZPI z14R;Ke;WAJqT0$ZwRHBiGVij&8feBq~9~`&*{o`G-)l@@7_WcJ6>bC#)5gAcO1mn&>-d=44TW==A+l zJG%lp7{GGgw2qf@2kWyFSI+jaXP=XAex{oG@FWq5?k)W;Rz$QEVg;(A>IV@M}pta0Hi(_tVlw_MWRJ`qbd22#47WH8WYogj}XCnzl%8XEFH zX#Rh;cK?-|7_KDp@xRdAf7Ws^N~O>~(0OwHMD*c=(Zk`^Y@r*FmTjU}_meK{|58%= z=(=7N*Za{rwd7&jbn$Kzm)d9R!<1R-w8*=FEvS`M!<1-uzzFkWcn>?3n=9 z!>xUwK>aTQkNSte4f3x;_hnV-ld<*lfHm#(Ds($5d-!0LtD~y_Ir~xl6f;BbZFD>T=<@gC_q}{)^1w!j%Kw?O#qij$o+en1dqLG^X1=&T()- z)!&7}fjcB+wMFuqtnD_?DRE-dsf;R!F!cMK)(^3c4{`1Yb7Xxsy>a&41W)PEP!W?@ z3TJq`D+?wr9_8CtMszIo0rD4!m_@%u_&e4?*enkrz}O9lvARXN^yxv&cP8G&qV0;* z##&&)CChG^7U!FKg5sVC$DY%N&YGZ$X6ZAwI@VZo3v>^rEq1HO@%v`#)SEzS$o9C zHU)l53;ZfMQex+UC2bKdUw$V)_6tcS^m48uM{Y2Vg-}N(9*$kEC1Y@TnCeZ=UoT5k z>xX|cttJEVp~Ze4RD+1!{mN7x^Bmj*&V>w)aRkjWtUr<{%V{whkv>{mZlOk%jz>+~ z7nG;iw%j{DL#Vc5g#_|Rp-urU>z#R$jg#y7wQC_G2-X*lWAZ6#Pk1- zXFpOGAx7Fvir%=pekdffyU(Co*YXp*63825(uizF&uP?nTp2*=yJ8x z?Fw#>zR?o<3j|XBVQ_k~3cI-99$I zOd{D%71zAW>_M15N|bzqV7N4T01(l&wM|;yKFoXJwG3Fm74RlkP&2p?TJjqd(Je0x zVwlHD`8p(ZH}~3nHW9_T{xu-t;s3q*sZw7;rmXv(DA03F>-!Hj8vw8w{W6D*DUItp0E`v$ zk_X;|;HxkV!q*<9^^n)XlCv+*IVErJz0LW34!3#h8jA`iMH%1gt`S@)QQ<8^jlmXY5mysnR`{5JQ{zKWr&vwO`zB64zc9+9`^Mhqt>U3%Fsx|S4 z9GINCq1^~sjc2>1cQK}91o5%&g}egFoNdwWH5=@O>K|vd2C?b-)p}??TU)c=WY2Pt zg*Imm&-sn&H@uUuuS!D+7dNh#a%qL?ieA8i-#Qi}Xfke4-HxLDziqAWBCofHa&&g5 zGZFe>u&6)q7sudZ-*VkGFPES>|Tw z{hGpZ-{Coj@ZhZP7pEo4e|E{o5zy4^EGVor9|Jk_S3><9w7W z(!$<B7aq~U+qE(OTslSR!`5JqNsK&+6ukU6=?)+)D8&^ZWbSfvM42e zsesr^9D;wTPR^~oN>zdItjM2-s=>P`$s9?w1f`gdX^Yor@p}2mqAv)U=fQOX;5(ik zs~k_*e)PwA%t|}@CYs(_8-a`|PeI0r_105<7TRjSnt0(*m(!`CBu-n<_rq7k@Hx#? zCW^-}XAC?4S&F^>7cMc1#9APV=CK5bwtnFj%ChwG>^XfwjK}@%jx{SCcpRrYtnyJZ zXN+TyBwl4U1SmSlTOs>AwV`VecUy=%d#I6xYxyQ<?BA&_}UDl_*{t(Wz++KMF`cT1g6z+9vR}W;tBCS_~`BGZVGT8hA3d)*~ z-cP1q?lAo?0)h#zLoh8d@74@PcBpiqUP>}|)c{(9Jcq?(2E39cNLa+XPKD9Aj1l{v z5mN_2nwl+kP%@;emSRA(hE%Roxu(t}-le9rif0M4cit=BOB*p}sb&;{`isz|Z+Bi9 z6}9u5c`u^imQ24fFk#J~L>ApJE`MhNefXg$RwujQhG-|8F(yI?{xw|hFkYI7&;XT8>N3O|&s0-*u`+{4@a1%B?_XSogau}n$k z?Y^9$3lFaf&A-KcKPrPEm#{L~IrB$1c%I89$l#|hD}hFJlzAVkrT1al!TYJ+N3$a{ z9=Qs7VI?-rJYgSp^>yj^8xmvB4hajJF6A#2nlVW=HwkZa6c?6-5TYffGu16mi9km$}C&vl2y2laRtrX=#kAILq}2g9|y`|~?uNo9sO(xnF(bBC=<(y;k< zuDmSffJc5n_J&PsMBNi4%1-c_d?FoHJvm1*f&U2~|B0|_q>%KpT**2K4!wYby}5Ya z$HG*RSoJFK1$<(S0hW8gcIk&7=dGEav!;yP?_1Z2N!~$jSlz)ZeL2vuyWvmq`2J>I zD*mGme-)SkcH+N_z0=zh#X_VbS&Qd{t<2h<5CouM4|5?_O!#tLMXwv4n+Ucy``wrU zJ{o%U8-|EKqSy>0C^C8XTenEFS$m{w9`< zJ&Q26k<=zM?RH_>Myvryr!PjAhb@`yqq3BcFGXGvR-QFMmqFPR^_5Yh<(FeL0BqR^?SA5R3%G)e>%k&9xFh(&}GI5snYJXk}Wn*;;4s? z8K!LVFAw>|@QIr;n*F3?X4d2M&}wY7-h}2BFD~OSwyz=nddt=LylaR0T%-veag|&T z?SSan#Ap1BX>Ek0KSLG1nAM5;dV=)5*9-s8s$62ZE^3-r_5poqa$ts*L^jIc;?Fec zv+RZrcA_0S$voXyN$tc-_>o*fyIqn(F_OYgvO9eGMww(wyj1%#=D+JykRX90N%SJW zgyf#Qw@o%+2sE&Y*sZxqV zr>ktNB9>(Sm``mHk6T-eny$ma1+W1x`|`k~D>9rc|fwppn}GXoJ~My0V24F&#zRSQIa zk*hDPw=~WS zO5Q4_63Lu>c_Ct3PkHrURRoND4A;2RyGjB-#F<9f%YWz-spn|o?eN#=-tZ_-SdRr`J4PCBf;y*M$a$Gcds^5 z>F(XaGKuYhnIHJ8Ju5Vl*a9YvP+Drr1w^XHBZ2eQh+GyXvfM|&XY;GtZUfgh%z;bo z5raTLMelyvYD|mT7;AH(cKgCv85e^d6m7+auoj$5*F7FMdm9WFvl>j4GSRbnfO5%0 z*F0L;Bj$??$JD5)Nhhdccz8nbrkG>dd%g~^03rGvd%kj^7PAi$g+bz)AR&qFkf;M( zaUIvGzE|Q^p8=t<GNpRH`(j-+z)>Nw>3P|4GmxZpDTWq*CB&%wF)9Qzj7Kn6Q&CFk zqt!ikqVCG>M|Lc&=iQlOLs%*DRJFF!_8sh%h$h|q5qI!AHy2Q>?}ycdBmbungl6wA zG6&Ax|H;Gr7n#RaBz5>q3JxZ1L|m=D#tSqLE9|rJ$2qn9JW9Y=e;|FA8NO!E{F_=v zmIw*<7XV@*w2&1V&9P-W0hR}ZeX%`^$!mzMF_Tm6}3I^nS)*Jk%rwdxJMOA!!fwb!t^5>7&;Q%?dZEncQu5OioZX$<5g?y~J zmmuEc&<_7^)~Oj|RN*bqN8g;7AnvblMvo@j5Opv0W-epO>(f*YSOn1fO+LCak7OSA zclCWX_F>r`fJSt>|1i0IzP0}+v-7i(3v-)JY$$E>FOiUNr?q2 zw%X*RCo`FpICqC$Us*J97B7Xw-ck>r`b{>;rZWZAMS7G@4r3D!93Or-@i|<(o?L#7 zUq9xTfdZgC42YI^A{vv!??Mo6M$i>#2odcSdFsgUJS!CjG;*=L@0MFSP&JiyZOzqz zi*o&`x{SB^3T4vOer^0U#yt8v@0O+NZ|4DJ+3}-T^|}5K5~e7T@&-I3L;CgwiY++h z&k$^_C^(f)R;0wLdVJ&fUNGkx80(L~npdb^I}Me_F)b%UNAL~)VNbmyI`CD}Uv_)! zbG89D$hyG6*Z4L~of%KcS4)CqWwS(kQ-z4jt{~tQ{9nb80 zN7Ju|?TS%O;$RKEZSru>UGR%#5qH*hk5T)WVPby_b@wR#R7n5IugR+w1qHDSQeBU1 zqAYmTH&q*dC(CiiuI;W!4{)fmZgd*I6@z8In z_D9?QJr<*aE+^C9)JoGME}BxjgHZX02z&=0ql7c`70)9s&7!jliC3vgXd= z##pN$#~|=j`|lsLwg*zrtg?;eqiRFhoO$W#;)%FeOoDcse{q1JZ`&cGwM4Ob$6QVSr^*wk;6cSC2jeAy0;dnpYm|OnY zdT$E&3Nb+U{WWge{Hk8dukf7XnA*9Evb!gO>++CeNZvjd@o}ls6NS7(i8JXSttciF z^jP#i=26x3P~R_z;g>>$n05&vJ~9V@;bWH9z~yHJe_qV@AJ@)AouKs9Y$S*KVtLDQ z-2b#5OL9Hc`Ea^i@T|jF?4{3@8^p#EsiBv?c#aDHG#zqwS^d_zieC{EI`Lc@?@#%k z6t%9kIK~=F=VEzkn zMbbv0+X!US)`{jfNKgGoP1;Ob91?DIvC5a>FO3;BS|(+{Eh+ z2*N2yeENm+Wx2QqMIFW8^DK)OwW}_rrkvmz2Uvmk86zv6FZIcOpGCBBKGF&mitdJ`zkBRf@-qGCMWgWn!KWC1XtaX5B zWF=emX?}z#B6X5h-t6rrp0&@tpw97@P28NNhsI<|xx$iqN;A__9JTv;z+l+|e0YQ* z_kLX+Q(?h??OBd7`H_5V@&>rLh0uXdti#cg7&?x~i^9%Fy=HC`Krwe{k0j|shMbH> zOkVQQOK?$~HZeEz)FASM58$(*Sv8yKKPJ5C7aWJeGIDu$6W?w9+lN2<#|jj#UJ5#w zeQER_-=(kdn2;}jJbrZOCn>=nI)DA?u&4rX@J1GGDtx7;Uoeh02 z$wWfrL3?baNeOj~`N4}cs<8@^j}Lglb^7@!Lz@^+lhwJyykc{+d0VKfO+DZC8R$`G6n zqrs9}BCLgkSPV)BeU>Nh)}R!m4br$=QF4+S^H$@Ye74Yn@D0d~9$x=a=Oq-3Rj{JS_ z*rT8f9l~Vk{gGcrf6P$>EQiIH1I{CC%6O+9D?@BdD>1`O-j}n;q1SFg*izNFU%~k} zq}+dHcO3Wm($Fxn|Dz2G0mJ;tYFW1nMGWE+@8&|e2K;Dh2&nHSed?S$tIO?QpeOl) zEYU_d0@k(!?_-cfJZVu~_n2Y+b0HtOW%!}LiP<49sw$jLhyIFCc&9;^ZS}dSY!y`q zYvTObn|7P;e99_2%D6X1TZj6#@F>vUSlnCK+U-7`$abA?*nHxY;ASJ!(46WQ5TnMb zkuzyr@y2mCK@lOw>F1PqDzc3>&pC5b_EGt|4)RohICX~OoX+nND&H4)&Sh?P6lh&<30t-S8}9TVxQPfzaY1(dBErev9y9JEF}D`PH2p>oQ|O9NYZNpQqczpA@EAd zGmMDkO;r%*XoEWmBFJ~ch*crs3}NY{G-kNIf!aCfsM)}|$r}7+L;k#xu@4o5Obvd1 z1b4E}@u_ZHn^*}i24v^Wb?O1+%L(5!yj!av+c6}6-y(hn#X%VxD->i0w!W zL9xoYY>aV1n&gPYG%U}dqrs@CSwRVJ+J^fVrru{v%R`|!;_$0yammhf!gokta|CET zJt8)Mn1msT>FgzD0)AtZoe}0ohUI055zqc#Y`q6mQ$gD`N+0so3#=q!^z1x zYbG<#?Ag!W7RV2eGX?2Qg6D7iLeERFUpz+^unrx=)ERQgwopIu%D6#c`XX&O%t8TK zp;c|R&9%hzBE*E?An}=U>RZmwR~FM&)w=#*#$}}%^mJWHjgE>F%^9|nQ8nMr4$&%Z zYYJx*Z;|^x2v?_LaHVt4XoK#GvRI_ilQpQog{SX5!Pqc;J3|#DdG4zwenId4#r4U# zKr*N)CK-7r55zxrR_+}a>8qpgS-}JAG=91_#kCT58Za{UMG{DwLWdiN(Pyfx0MipA zCZ)41C`O)(sG28Fs8{fprj;%j85x-Kkjyyp=s;dH&9wux_9!-tSPy~Rjt{(C)txBg zbzjXql`07Q&MWD74a);$%(hw$fD;#W?++X(nI+|WciczBka%aM1?0 z=821LeW32_pt83KBH2k?7d6j_wlIhwGcLGt>Fow3KOMb3j!jb6r8D64e(?GTlAis7 zxK%+#*Qia+x)F1+|6Vqd-hHWNR3IA?MDFhO^5F0ljrgrvDQxL_L+x5l_ty(asW@XM zoTs#}SZQKcG^gc5!(T8u$7jE5bE}~G{z(A%@mNJLL%<&r<@jg_>{dmge=aCUr8tp( z$2O64K>YY=m;81&z~LShN;Jt(Jb$DE70Af^tG&`6sqsWQYviEHYiZyXSgxc4xHCn!bAUP!*0KLpRhd^3_B6<{^gaaTbw2xnN1CNT7&1Pe736$MI(z5bFJ zhw%kT#ljh!=~hcR&qLpRy+{qFRir0*Wldg4N(e*%-Vl@lg&WhyWaoSnGJ6m8d0uh-zCBx|79n^gOH z_9IgnZ649bG~^vI#iu*aw|p5!tX5l{-`yQ%H^>?@9;Rc&_Y2T*Yo7ZA4oq_n!C>m`kH={Rk)5aojhg^bh`Z3rp^l+|iz{XVd3F{j`TNdX_h?9~5K184*PfgsR=Yguy>A9}8|3jqCnMc9A%MfFa7B`E$&d}mP>457^+ z8^U;3P-k6TYj%&8w(CJFN!uJ@lzMt9umM9zOfD$j*%Mbb>V>IBMW-faVXKv&fYr;4 z-`hC+)GL}LY@{TRgLATf+T8#`H1V0Oy1uvb$`@>Pebp4a;@$A2NX3hN3#`|~8XtQL z7DvK-kqRNqYWE%rl$)`(9z`iTbPcVV$jN`YYd80)rVfIO*Ph6itS>fI#v5bHRRpML z+^J|*^+)_XWk}Iw>!;VboFaTL)8ZEd0I$Cdk+{bDtumYZBdo!&=-C5sbA;;P@@Pk% zc?$C%r-!UpF8%C8<;czVg9RQ`FW{_LO$W{)y<`=srm zFHky!PyW<5w|W(!Cs8w&t&S1w9zUQb4);xUo`5)CV8*hyK_TGNs&p>lLhg7iA6v8e zpdd0fcarPQ$Jku0e&X_mY`G4?z;=1_xKrO@0*_G(vU(ikN<%G{+a2O&VR3@c!YK;W z>r#}QQ>eRT7oTegJ`4~aMv1bGXNmFNsuH^UW{1DdMcNSC2C$`qdL%RmZpR zjSIvYj#PiWjX#Dk9WdGET1R|Bdq@?Hl+AB`3g^R3GWi`n}c@Wv&`naJwow4!$(ZMo%bFM zN11(GXmWkA$Kfo|hGC(cx*B3Vufz^BZ4%JZ&kqdy(jwJF*S490epR!LWO|bNK#8}U z;-YZgETQ6wS|e9G2_6z?QDMMnJC^hxFo|ET9oUspIcqL;ndO{h^TmC?~Rmmnw{;S zn7977MP0-W(_YgLJ}B<*1!;t(K6|u3U{<*ZJ^g^C=|1 z>5s19?L!96TN}-c8ll({)%T$e9V7dy;+KtKTlx=FWmmm(7LM?@B9gVY*wYDIkHC+F zgMJ*-y)83;tQPG2UZx#D#mA%vM(&CY7oY5BLxgrjL1>Ro?52clmgyW5b z{&h;O6kST2yd*lrcpCk~=OQXor#D+p?|!mXzEO4YD>~s3=IKZ`wr4VxbIqwfMw-Oi zZ{A%v7cchJw@D9jM|c2)|BzdD_4t%Qq-X^4!f*b<@;!dvx0%s5$+#q*E^R5ZY%v!; zcQ!NnGI1T7UkuoD;b|NEi+Lu_%7J|5@0C7tw6A*YQLy&LjLYnI6+|4)M`IXorSBM3 zsY~h*hIW1vrxf8o-|yu3cH&=&!6-_1ORZ(Bx| z*$r~e@Y>#2PWN*W2wGTL_Cj+%uPrR5Z;HOs(2sfJFkirq#*3a$xxdMd4rkE|TlLPU z%0|{$f{)(M6MF(8^(2_`G+rQk#ZjBb7grZjvPO z3KLzt38=dhl8vei4ip~AvJ~a>9G2Q5A$4L~o4d^!q#GSfzqqfwk5UD;Sj<|hah+e2 z%ZWeZrLkM|8~hKTmjyaDl?!*qp%}zV*$t+iO`OLh?Xjy!Yqj%cgpEpWfb6wnI!)%5 zI+pq0RsH7>FZrAGN;lRV+M!j~&EHU7;oFsz63;%r#Ad_ghCFi}HY+QW3iP@EVVyt= zjHYmU!O9iSO+t>b>8I$=*@@_hiN|I#Z*a!(dQUEWpLdO#;`HTxX29H2>eYQ!nUjdd zEztLlx_AF-P+039GoH=y(hfg!pRhhw5mol)h`VPKgIH|TFb_Y-WexBSn*4>1=qRYY z+jPGpC+B#wA5Xd0XW*XAts!`bDHKPq8QJpkNO`VFuY0T)LUl7q;1twQD3A=tsj7 zFs39A`D~ZLwrFVYoJ#gR&imxHB;Lryi$l45_i{Pbd9Srx9epk2gJG^~7s?NJwHT9q zOm^ciy~QNO>bK0Uj9-J?nQD`>s)c10Xf3fWvCx@`mgXC|p{s16=+OGlV{+Kn%ZXXP z^Ee-8IS8m)_-~{Ql{fclGK)pwZz&&j6;=r^e73g>uuRNKtH1e#N(yys7jYzmt?(ce?xC9~-GAsS5jGa}tO*c&h)vyRUv+G{iYI z|LNZ+rIzS%vf@d`9x&}K)z3xM8ofrRpw5R!hbzqc%e8t#?#ZFBeNf2HtZ^=gZ44vW z{@Y{v?hru8Qhnd36f={oGu?9HeNEs%Kq03`n8H@uwB7iNMj(=L+&TtV(xzf8uVeb2 z$gFG)RYZ70vNtxdk4!tDuqQ>}o-9k#H!@{Iqp+aDZ_I&PMU`VGnW5S0*@^Q+D@5De z@Gly0SxXUQC3<%FD4vldhwQ1>T_m->r=ofEh(aT^*q`;OH%yYxayrJdWn*dOy1So` z8XfH+h)|CNZ~N#){4h2v)^0%PN8Xc-G>nWy-&=$?seG}V6uIr(8Zlq?K!s(PY~Z2U zuR~vL{)%#{xrKHzmA0Uor-la$?{jMF5Q853&oa)$-#_;qkn|+9{vml zm}XNQvlTbdd{&CDU+rLLrZ4G*qlA;Dapt*1*|Bw7fp39S_V}Uv>(q}Q%;B~?eaX!7 zx#!dhEX=A7gc1`O-xLRnA2+5v?>vg!xY z0%yJWHxjXWsAgX0_}UKunUao15P|FesLqc!o)BAH6rL($xv+pk>eHMvWFiR+=4ug% zN>~>5<<0Y(YXVQh?pE1}rs2s~nr4us7ZS}k2^DY}u-(sGk77C{aZRZVt#BDuL7Yln zl5UzP{OTut3{#aN{2T2{Tc0vGxU(8}P?4CY=Ei5sOYE0^hW(YWmx$E51wPZ1KlWOM3EY2Yq-?@r zOMnAE#(4{5*B6m0XPnGr?28rT5*VHop4Eu^8B5rr)F6#fKfhGu=9axUo#Fa3mbVw* zycDpItGM`*ZVdf297)oLAu@n`z8w26CUKu)cbPb7!#kwa-SCxa9vpZi?YwBs0-I~- zT0SxDDfFSAU$_Of+jQCr=jk#_cxUMh=E&O65CbIrxFdPaTf;GYEIdKT)B-79&z*~kxv>f9&cj1T?Zp+*}Rz4OF*nbq)0BA?GCO_qiUu7 z#-LKHM^t@&(~=-2_K)$P;zGChGrGJpF=LMl*%!0Q-p{G8B=KF#xNz0kQdn$qGMg$L zGgz_~v8}HDq9~uS>jF+kycFu}QUEcp!{MK-KM0`?3o!a|X@MxdSn88|_Uu{qWP(fa z#dzVuv;~s7Q(>oKaga7^`IyU+gye^(AHKwC3+P-Jn5qHw{dO%E-w3fgCSF8!WPb&K zA1mI)R2QM<3|3uSeZuiA#{KgBGkJM)izd8M$Hb2*1lswqdO#LXutp`aECkkpy}Ne% zq@cbY!{T5jz(&hIjyKWz@R_XRb{C`jTSRky9~yRox3MZRaK7}2)i2fC&iAYkj6{e+PZ|06OI z_tTFHj|_`rD+4dm1eLFbv5MY%c3s_D23w^B=$ST_+OX{r=CK9OXJWYL^V6H2l;_0B zuV-^IvIeaPXZI%J;?qsw>D|UxqAckLt}pv%IMNKlRw{kyXtMA~0iG;7of=Dy%`qD7 zS^uMGrSy)W__Bv&P(3LwJb=tK$d4o$u4WZG zwsk+HO6N4sd^aIfL(+IShHx)~LntQ$mGyT6P4#px_?vZY?{jf!4dub_Ofu)`5O0?5 zb}&Fxtgn70MuqHP{;=vj`14JD_+0*LRCrd3+_?p!G$D=ds9b`{EkjRPlWCYemA>@N%1M@G2AA^MP&vS{tJ40%Q_jfBe?e|U)7%)@ET_WEE3aPS{I%>@~TPlwXzu3eSA zi3BOi=MU{ECu@DSp=U&s)j?iqKKF!I7jTtRBtgKfQ1x|X(${nwVgdlX+5x~TptVx( zl^bpEfd{;iT$6`NIYrG1KI9xzFt7gP~BcANL zCTfLPsl>(~V;Q{7P=+lF6zqH8jQ)R_VbTD6+RmyMSzKH=i;k)!bbcV31mCbUFE8dC zRsX2@HO@Z3%;N|BQPAd=rd{(JoJnKE2;X@)ndz$JdyEY%qg&JS*>FqGo~_cCOZ2!B zQvT+aj8?H{$KkN4eBYlIx^>`--2S10eP*;q+b~JoDd6<|m(-LJ4p+a3{jY4IGgt%n zI$Jf4d8C$Hwl>&dGZ7 z;b^wBrK9r*z1?m#W7_&jNrKgEWq)%;sH*+}6gLa(0sT{)DWvPSrPYk;`xBDd+s638 z+Ih*R0}l`t9jw~_;|_2@7%UKN+Z}M$cqHO^ow^<)D3{exHE@a6wt|naJy$cL{yppK zCVv0?x7wnrzI0{KkuluUBk1jH^H$1OwsT6Gi;dCPIRP2X90y+4c2B#$aBY?y8U5sj zaS#KG_J(FGoR;D=`zclG_RrImsYas);0lZWF)(fx7~vl1N0aSrHm>;xCRKPJZ6_R! zhTY?&kMeaCeoKUf9Kaqrl><9N_+R5y&G|-8=NH=L5Rxfr=o-J=1GIs5;Sv1Hu*K}1 zRqreT+k~Prc#ZAqQkeKW*SGt{g?HoMfOK>9eePiU(0a4`!|iX7+EAlMNZiWms}^Wb8_r zWksb$%Z#S8X~QsnK;nMt&@y4sm@l;TxYQ4atP+Y^BVy%hm0t2NohNBb>zWw>+Yt@_ z0?>!c4xI6|Z^sv}Z&2tWpIV%d+>FxK#q&44cuU>dC35c72g=-PqT+@hHh)av-Y|wE zi}=+dCMo*KE7d4XUkF2eGIip{;lOlV9bsvyfsWfu)~;M~b6)elZBk-Kp>=m?O&XB> z+?gCv`T&mMVS($#nR;=1@6E1ze^w^iwHfpCU*qDnQ$syf;;&hLbZd43GXYb>3KlDe zC`@UP=Ip{-SEd_R^9o-ZK8X8H6f3NJC%JJj5Zz(1a*yHl!S3bi*+)wxuWsUZ2bU2E zUWU67CQaXWqTW>6U0&%Jc%zc1Cah+8oC{GT$}YalFrs1+elU4cQ~#3rrA}Cn7>UoK zi}c;nOE%>I#Tu#azLNf9*NS5$n~FL)!XnrDlx}{;^0L!7RNzKxt-y8s{0E&A_k{So z2R_**gKwi}^cDK3DbcgPhRc9%+c^>P-yT4S-Q-&!t%u_tJ-o^(;MIfu<#;-%`s0>W z!rT&|7_+%$RW8RFca~6i`8~2(=Xhzvv9vu@cr@vcfznkYAhEqkaXDKm>0jn)J1~|4 znWxq8>`~i+XK4%FQIXcZcliGpAKIe}Yb0&j&vpO-=+F3P4%y_ensk?i0%L_vatG3x z?%RnjvX9g*IG}eUDf!s(Pec}XgGB4x4I_(R!~FqZ;SScqt+>(em#WKcWph%Egx%vY z$SKxf%1O!h;fb-IfUMv>fsc?!^Ij8`X`s40|2&r|$bZ3YX4 z($Oqrc!t+Q+V>`fqz&{f#NV-6=I>UQA=-a6EH&KcDUcRc$>t)G%n(&O6g+mT;y@w;iA*M{406$Q6d=PqLDMZYT~)) zUzC)!C|d8Mf|aak=DE$q4HxL{*B_z`9S6GnGYY-8G=0P73p4ukc^=f`XhE^2Q0&KXqCKXMlT?rdjD=EO6>X` zGEaYhX%gGfJo0pNhTHlB*lk<072;e^Up%S=<;|G=AgRWdjx(!;^zjhT9;IC4P~nUT zTh80jUynw;_rCku)|eAAJ*=1(Ti*DG1|hW@CE((4a;wd6-)u21=% z=#$TDSRQBIcsY_icOO8#B%{~Xx!3DEXVM)fyLtL+%o@QeFt2+R*(u4;jq+{2^3;`} zQT>uynGyVn@VUdt7kWbGclAOg!HpZ|7K~pe1B(So*>Ae}mtL@Y>{~!ZW%>-AX4jO% z`|00yu??kB653Z~$|KGLgBMN8zlju9;3@p@neoC=mFrQ1m7F>X(v`Bbo^AeBbvNE; zRp5NNZX8lRIGsbjOfcY^?gxs$kXReiqarMnWFZ9ZH8jX`n#F%Ok7*Y!XhB+wK4YH@ zhTm1Up#Kzd`!{49_lkdh#xuvYqB>&U%Qr@Cj?~J*f}TA+@Zp`4{mPXzPLaUR8uu+QR0a&i`6ggdt=TH%)m87e_RyJl!-2SI(QLEhjd0%^ zAwqL=63PN8gZG*|_##uz6Oe0duUIivP-uL`MM=o(N)w{5#kk8yKIms=78A0koqn_x zbk1j{sr&O-9xke2aAeP4lWne-_cn+rBW}jzkoCs<`Soe@ts91O+=BZ0HX{s&?)c0R zflzWDEbnm0L(b&7-RZ$Wy7wJ}_ST(4&8t#TK_(pi@^UgV*=7#yKD4!a9jXJG;Jj|B z$u9Y_Z@=`RB?-i1Yc(_3F4P$IVxE|yNBP_y)!yl*!3&)y3-T%WeyG}t7KaC@5Agt1s$??GNufe!^c9U zu=9K5u0q9_Zol zq@S{TJ{p4E8nqq7)Ms!Dd5a{Cn_l$HcQxm$tQe)c@ynuDq>}RVbmkS8RXqm$>s`Q_ zN5C4*zU4;XG2FKJHaqj9^z}bE0BRsb9WYUX71*b<>-$(`JQlSLjOibWR}P%N zDWfW&tArR|^>)odr%;^eAPy3z8MRN->Yt0U{tL;lQ1cJRT;x3u_qLOAtLZ+SnT*%L%hk8+A? z)FgXdlHr+`AJ9D8d$YSf#B;x1mHAk>`tINxrK7>ZOs(vV-%y2|zW$mgM8Z?fdF`5d z2=s#>>yXQ0p3GJ3CZJF^c`ny=f!3^Yda0>9aQG5hHtcJ zzm%yg1nIC6)|q&Zf)YW3mG`-$qiCoq?O|>kLa*|b_(@$#-$6Q6xH-=$RIxHSffA8_ zf-52zzW3-ZmfdS4Sc1SaSZYWpYNu&!ZhKabJwZQmyRi_hZS6Caq4z8fNrUeemfC=n{13^+YI;aVGYIA`((gBXLff;J3U@Z zf7#O2dfwy@kMB8ytBu}q^2-P_x(bG^@s_1G!L;tf#&m2+y@3z`>95PL8MMk%Z5y7l zWjk)@qRP^X@T&!^WC#v1Br)j&~pT25)DZdj*I*f z3J6A+BuI!ofr-SmurrQ*c3JMeIb6YVR9ux3u2#07c-g0^Up}(2bcs}&!)3_NHg@Bh z@o@UP7*H6{Q~>iH*lnm@zC3YfwlC~p&{@}(>`In^_*s|!v=blH3fppAUM%GZGZ@v* z88>del0J*EkmEV_(c1JV)_ISHhH-y;JQ}_!7~S!nDdZeskVQhD{LA5$Pa_2d=zcA1 zR0bEJAloTdehm+Jiqw83nQg+)6$fbKy#);;_=f}f&vzpum;|F~cWCnHa=&M~DT%dS8=iDdfQ*+%PM6Bu`R@>(PMBD>igKZMA}0naQ!MTvqcHwVEggNiVjAZ=JX4 z;@K=M-c(VZv0I*)7$lHm5%+RzMk-MjdRAvJ<>AWKujqvbn61GT+-P6tK-`>fph}AK zG3WTrAsm{qMGfZQC@muyFHS}Y2P*?S0@uZQh=wcYJc`?Kq8>_qew{Eir&*;Bz$+o#he&qd&80Agxd+4^C!}$Fbn%jqNG}V?}!1kEj z3*Sr3YP!|!BeDfJdH4&cLeY2I;e`0w?3A{%0Mw;>U!`a^0@BBy{~q+KB#jFVaw@As;E$SqlP{>p?hA@ZI3*4d-lr49SJ z(V;|E>l+U*H5E@by=kT3X9&e#3G?^d^58`V7(0i*EMV}zWlc38d6hsO)xT*F{tg{c zL93u3+G}(5_)F_mO4bt~nfULM_!@ZouL`aMluq6~AHo%Y7`q2E`q@w#=}Jlo$J!LM zp7^w*CBdh{;I52N+D%LM4WSp>rBD%@#bHz;=7#MDYLBy0ETcb6_?cJo?zIJV$MVi8 z6LXE>RqrFTIQj}?9-lSFL$PzVS`%=^Y#NC=(UfgXqfjP3?C8TQ2_=9A4-0!=bJw!w z+B6n$UGb37Jo=E!$QXfrIJ}mjWjdg3{MN2>ITJ22w6y0iEarYLz%)4%>;C{V5e+#LJoZ-S>L&&cq%B&m`6ZTpbL^xRCDa#q_*2bU)F%Qg?E zbJCv?dNonz@LZ5M6P0%!4b3n*%C1Yb1IyBALWoZfV<`D4=~R%%5;u8OZ786E<}ayM zKV>9aVueJ~AJF9ZM5udprID)Rq)ld_Idjh%d={m{fiA*tm0(*@~t zX8yz;*_5wp=|BI7$-J0e@1c&6E`{nC7usciLv~l{+4PG527?21*qiKP-6zQ3m($*M zo`83a$s+b5I5rxuK-8HlH|KR}WPV4xkhLaBk=OW%`02(=VPg1VjC}9R2|N%9!|Z!- z5U<9SkU}1t@Fmr}l=nA8wn>C|XT;I}YdVxP^nze=sS`D6GZA3Zy?d_WwWe^4i)2MDG^=r~4$@OFjWbvh=eg=qA z;O834U=mD^M3;1%0tDvYf-UvP5?`JP7ninHGq2N?4>DOI)mv!drXKjRLxG&vAtgy~ z`N^K!Y}Owbl~B>^tjaO{^VLsF>AGBA-x2SX;K=L$tTV6LbqTtJmEaf+CI+&xMv{G@z(ivgO z7#P9|9nrO*z0^j$&7xYTeV&OmNM1h)J8K#-{?`2j-h4+kMzv(ZtMYM%cUC)&(hlt^=nmwuWdvDyIC7sV)nWiD$gZ0jlR3VEKi6%a9*=% zVdt?UTvCIA&w3a`?(dT1BpB#&>oEN!jp%VkRO=R^(%GjK^Kj^Tqnk$$3<_T)#C3>D zot1+5*7Qz2bM-IH?2T8BN*yB#F6OX_G#Re-?V(A}JA8<+l1n1Dl!m0)a)TD-)xWZO zQ1~OKlrfl7OHm_Hq~t*ddagYW&C{Vn7=8eHsziS{Ax48F0q#_v{Zel@;q50l?Nckn z_WZ7+7|mTHhgSR~1KGaxJygzOA5Zd;AynvfC8@gnp%thXXx01FLeW5(81`~TrrmAf zDNB1~Rz6!DO7u4vqP>Z7%Lw02>=Y~xQ{Tx*GOW-V@(T0wy?xX@b)2(BC*)BX9Gr7q zUzkp0=Xh!w3LRsH2F;f)>GStwQNN1(psFJK}27qIXs zCraeU-(q>7pLgAKk4fqZs2vpY?o(*W%9p}Lv`ax5O3|tG{2SQvhW!T0S@pf33t=K^ zUeS#sx>!1hRkca?LVI5%yzKZIH5rt6IWL-|i40{`?I^0T97Q+f+WvmreHQzR5Gw0i zMiiIJ?c?$ZdSPvn0T@x{N*xWqg>HP>z+V=Iey7Z?&Hqguq;mbcfCsMm|3;%_s`}Ai zc7gn+A>RS-{{p22r+R)K{F;eZid~6?c~#9+ONYnOT~XFQfnOc#%)_5v(;v@n@W#s- z0cXbiTSMm+czTSwe~g}8t(QViOCFH_&~}O@O(LAovmC^L548mf*C(4Ax<8N3jIq~d zA2choxmTi_6`{M25iyKJKyTAkpy|?Y&L8}7ELz#gt(8SX0sn=cR!iEi zJCGXRe4v;#7dQ>*NKSgh;ghv_O2cnT^X7WV9t8#$nBuS~K7Bvza%+}7j4M#%h0y() zF&1_OUDYLCnnyV$tv~Exb%G4fzdXB1!t1f%)K*e1slQr9&RpWMqxmHG(+T@?(=}o#-*L_`Y<6$ zy^SV`Y$35w{(j?X2+u=p2#aO`nJ|FqAj`spsTdn7vc0fd$wn5rDoJeZ#>@X)G8RP z+;NG8wMy<}s~o7SDlIH8dtX3_*t|J~_5?s}03~Vrk}jv&In$Rv>jx_0pMT}%v5_*^ z@1SvUxi@uRraQVbJF&m)ZcvJ+x!i;Kt&ij9Vd9Q8&b#>g_dr*Fpv%n#(9JLe)-3y2>%_eNbfGc#pa<>!_mE;mV+k2T^MJ z+zngITC82FQLK-ISMJ1|4Z)@^Vj=GHM}Oy##t!^9n+1O9=#-u)*{7uGi_dnWZgE5= zJjT)44Wz^ebVLljc5Hs&==RmN$q(BkQP89yHyP4j0!RSuGs*e;XaKRi#lO(59-vJ0 z5!XUnNt#c%iAxshV=@=Ta%f~Oq>C43r3e+|HcMMj);b!M)UtRGNnz~t$?%-KwQ_<8 z>&ygQ5FXec;nd%Z^=#J#i}O+$KCm%O=OB1Oru+>bLNsoVd~-Y{?iq>gKyt~TJ=t=L zomNrB%K}BMj3_CpVmQ4KS1)&zxmRCz7I;NW2&B2Y=VghGV)?uj;(-}PQAefPjJ%)w zlt6qJB!}rsMaf9Utoe#cdVwKSeOBqQN7@2M5@MMPePMo@vFKN#LY%;myi?wNCf z<$*>UYJXNxtkC1C@1uThS>@r+m7<0;JH|4rkks`)oWru?rVl8;@M zXmqb!GxA}^Tz5#8>B>#KP5HCQ_HQYY8vSIp5ku&^%uSntS9EV~tw;Wn`f|PX{;M0w zDEY=~u9(sz%;HO5Zot>z+Lv_I7>&(kBb9L0bK6-r_sqeRl(mjp=%he`BDSeq z==Jj^*#a@%c$Gn}umh3?U_vwGe0cJqmf_8E|7T=i$Fc)t^bO*Xcz;T?Z2qEysa&nB_fg7@gU+2JoL0p<$-$@)t zr`&)xS(^0;HdOVgMKh9$k|BFRoa5r+(;ajUKkC{tE`x4clhZDA2HaVaaL$4VTuCat zwd~j<-MGM^W%}$Sx>+4r(E~B2wpEmTS%xpPvg1 z2vEm=OSniD8c8@2ND=uWTGbL@2$u!A-O9g#x*EuXw_G-*J|rsahWfuHcg*+nH(BJF zhh!n_(cM zodPzF5J=K0c?HE7(*?PW6dsO4Mb{Zo(zEBb_u3<$%r5-YQVO?ZS_E#i<==>2U10PD z+m42eB^ReQ$pxyi7Quy*y5X z)C^0|g_%F;4>>i%f8(nTD{GzSjcn1Tn+W(s(PY{70ocSLjx(Px4I?qP+uthc^Y&v3 zujY#ZSG)diaBp08s4SBCh`t=hxPm`kP9QIjkgAs74=FP51qvE20P^qcR=oaf4L0zr z`j~T?Kr0xiL0(N$#-U8}#{H-3`kRXe?dJp zqm)T(jh{+?c6ua!+-*!bD$iZ>DaPs6fhXCTqcYU(xO0BK@QEB>)E2wI=c7$xusFqS zL%1Qx`Z^5E%iq==)!KoXrd0L6R~E4R<$m1x^QD4Q*!L@&%8U_K4<8W^KYBn}{r2yk zMjks-Vk1n(UV6V$faP+(=3((xD(pbtP<`PH>>PajH;DlE+83_Kkn|Zk^f?xaGIMiT zSZP_jvYMTcQ3L53gRm4IFX#DprQj0I{%+cHzN?JGxcm`GPJ1T6e|OHSqGHBm&(&au z2pt}oD-#@=9X6(<%c2)^Z%)XxB8lcWouq6Nkr1mFXX>T)wXQ1E#(OtGonJ6KHO7}_ z$m`VRLF54pClwIw`*e!dVT1(1HiR>mdK!?2b2+trpgn3o5DTfZSwQAj={~Zstt20?m4I{z}T;u8jHk@?HmQ2&ElvdgVvlHXD^QqG&oQp|0If5 z{GSz0eM7U z^WIQd?j;|3!`%@T$@*EkBrgy4;Ss^$qwoYG$;N~c&K|C$W*GP5XLoZ=1jn#Vhx5`= zTqFw&JFX!Y|7v8mPiWaMj6vaqHaqumh6_4s9??OVKzOp-ni++bh6m9ofKl(i9aohT zb1NbWG{WdSZE@V$EkOiG>ht#70X=#eS-`R`)wV3D+Jkbo(GI=fDAjUu90qe1vGv z9O81QKKA;(!2)CzYPuUYZw3HAVa^)?K>+;q@4*PXZpB1-|E(8@`JZ}fp#1kqMa8P& z3%qj11o*}Ldry^~1pMudiHZ%UqGJ4e4Ex{z1BYLtq5@HXPrU=mc2ra%u>VsA>N&*z zQ|8M2&oNA^|0#2S_|Gw%2LGvN&;L(7m(+ji`QramrnmjCV{HDb9`v6wINipk!X><D-ki7o zxuuzJ_%t!#jZ4KwuTIVPRaDFG3izto`BDhAbyMTqncbf(as^`ql5b0?;CjLDbJN4Q z+-aUp-yhHm6NqZqhd6!TbebVNqZ~_-ZX0vXd>>6ZZ=xntIq@xA4{|{zzUGbBVj6L{ zNFT;xLPPy7gm3|c1OSu*L>Dn?s`CvpAeZO)n;u6db8^6bbE8vO+a5S@adJRYqHysPLIea&Af--?}}f0gH(<3>>CZig3fXI6K#-T z&iTJ2whX_Xk7B*7J2eGW#TiY;SB^}KkofjL^kw^gQRk?B`WLSJR9C3Qd1(O??Q#JOr$~fY`cFUOo<0mCQ(TLIFQ+(2Q zoykpXSv$C|_NoY*!4Gxkoz5u;Sk&j2RJI_jVE9~*i6^KtgO4{b0y_F(g`OP+o*`WF!Hhit zAxv3?!P*mNwRVRvzCL3bHf-MWG!j-%;}WV!u=+!{uuVX!Z9?TYuCidLNYVoKY0U0U zc1$9L?Lwky7r)<_hAzA9HG0xvo^Qv;i;sh%Yb?&9%FRyUy5b0~OylLcZGnjq1v~Sg zQha$kVfBEnY+`U{chTH3r*uxTDDUNFpr!Knmynsh;XR`RWQ64$g^@5N^8{!lDEii( z_zeZ-wT@+Zb+=a10SQY3vC+DOGM{34T3H2(iOd?G;WddJa{1jBP~z`6&uiaMTh>18 zT)TFNzSX!qCtgRY_7x<%o2#vymcY@oEK}QxbyB}N?#@~yzGzoy+ymD zJ^PE#=9;wI-jdk{W;PtdLyP?6FE_Kco6(o|5r|919?H!W>Ech2cOOJCYs04?A+%F| zI7sL8z>3G>D7Il5x_sFDP;Uj{0XpJkJ4;(Q5D}?95dKiDeJVx2X+pkP5wS(G? z?E59vW$S!zj-4f$-HiBTQzT4WzhQIp)Z@%ZOsUkbghIH>eqyysy{F>MB(BJLa%+() z1nXJy{24;4B$lKP7?UB;g}D^AOUK8AT9~Ek8_nZ_W(ENYor;_JN|>C=day6UvZ!Kq zdA&_NnXd<#tmy4xD0BSu?pN3&rtrcB=qP5zgL_(~BEsuAsb!5slq9qyX+LdCz^uwA zkcigD4?t~6{DB?KvC=j}^xNVO8ZZG04nJ#K*{$6$ZE|nOd}T&{b@2`MrIWQ0Y9?RO z&e&f9GJ;!PQHt*X&e6SlQs=$raeIgI*yU2`M?B!&x9a>B;DA+JAxYt?m zHAfl1tYAAD{_5L5e-ZQMJW&InLB279fy*E2uii9ExQUs(3DiDew z(yKxUM2a9?I#L3NN++NwMM#7wh=4%_0*FGSnkYTeLh=sm{lE4;=Q_{(EtxfI)~q%2 zyW89a5>Prh-KBj;FP4)a8-I96bvDf91IQCrMWl`XOD91=5LSyUT%2euCGpz(hmg9=+r)cWoqxS;N>TC>i0h0Ze?)u$rfP;u(f>dnBm{_1qOFkggT@i%YHA8mq>Az2Qyy+-n^ja&V;Zg@^$BIntlA-PePn zP7cJiJPMjhh)IoyoXlj(wV5qsbF?+?i_`8mdzYHvGue4T)5F)D{m1c{-IX92xic;O z1<=~pG5)EHhiX+dCQl#-UMMl0mY6kDyiBHY>(&H@s8-^ng|Gz!iZ{AH-ieRnJu4d~ z*MbpJs!ZADyqbgHygd==*?wDTi5z7gti)z=B(jiAB<_0!s?3GS9p4(k4pYB`_E4WG zd@iGP=H8R361@c9ke@!eyj|>}yXb5Q;ce^2+7Gt@6Sh)-|PPc?}n{4LEO_4%uZs;L6-Nn>PC|-=)tw)8Q6rD?~lqJ#h(q$K+f@B5oLuBJsskizy^%zmR{>A z<8=0xH+Y6mRht}QEj-njVPx5p&Z(fvztS9YSZStG=C{zPh%A?ns<*7Ds$>TiN7HK` zGvsSF7~vBk15|OYX#~+Z zZ`83Ov~Zo^#Cf#{F&1PYC+H zfnePtQ=1$1)TzqI@)~x>;9_Ce{Z zjHAs6<4Gx+LL3(tTbym_6)A6HWveXF3!Agb#WoI_sYZGilAL#HWh!yOX3>{ofAIyb zs5E9)o!_YpDsmH|3SXW*=G#{?9akUMHEJePv;5WU_mKk!4nEmem}@{aj__F1yLsEXOdX0v=YLjhag6PAOiF=T(e+Jj?yzOcj@&jh=z)UD|1o zUg*c`18L@--F;7^4{l7nfkhN1taI`xHeK%zZGV4WXY>gnPraJzL=1fpaM-{f^8%1!`-Zac|r$EcM4Y4$I5Lt2)d$ zx-uSuGFpgE_Jp#=3%3O6Klu3`dyEM)r^MN~-Eu5VVq#s81(JGVxjVZj>)1)<`zz0p zdOnM-0kWI5al%@olowT;7DKsah!{l9N#)QtJbD%^5iP-7k~t1JC8>vEFCHQKH4U3nYtKAn=k0V;&Y*=s8~ys&z!$-*cy@n}N8iroTJN4LutlyJsPvqO zt0y25Z@oVbGn2A7m^;MzrzJ8q_&mw%HYB^!uSy3IzRki@naTD&0dqRq{!$p5FxV*} z9iA@u4BFFvrksnMzM?Tq>l90%F%h)%6F0V)Q?L@OAYC^jg3IkOqN*1j&_G%kH(*zR z9&yP;etL5NEj-PDWkL%$IHB_5{SRjnZcF}&Ka_yhNh@L?3O#l(CGD7D8x`Sd!s6EW zk=X}X@5mA-MGuNM*WosMOS+D7yzvy@lGl8M+hBL=M~n8^_vb|QYkJd(M;zu=#l-fy zBw`suPJJLaxqoEgRr<)jltCDhW#Mu#VigmY?!5Oit#mk^J7_{pz_p#oXJprT*W&Uy zA|g|@N$ydoHhE1aEzsZYZa`1dxKsJ2MUpP1EIC9JFlPcGuXz`V5 z?fjH-L#2y43c)J#I%h2mbKSzvbjnB)cC-K7dU`IQJb`huR>WJ;Yv`kO&9S^@>SmDT zhiH~kqub(#MpZ93WmGLv3U~Fj)n(Ju6A?thj+nEam2hHdYsg#CsN;sx^!C!z?fovv z_doDA6QH7Io2xGHl*7uQMbe{kq-)Pg6w=f~S18VbmJqpeX5Hrt0kJYM=l#@k?$q{h z3y@m0u8U^F<1otu?Vsv+bf#k63~hg>T%2{t!V_i;W(1DAh!2ep7E!q$zkl5F-bm#7 zK{|@;(3=BRlE>MJIyYGus}x5E1U2~|sr1fI${Xv#HS40TWjxH$7Z;rCm-5|6;S@O7 zSj%`w88@YSWK^=u0yfKdu=g&z;TM_Sch}u*Tt3+Np85*yPV7HNNG!G4Za8bG|l2}i{#c9@(t|IkNr0C~` zLWkUqO=L=y-|x z*rCnH)~fMO5<=TB`8Q|g@vAH)}E{AVG6dQX2XYKAhP|%t}%rY9`L2Lp)-ARoGbNzIq@g(FW+ya zO)m2`51?5~!Kk}?7LAj@q5+H14(eHQNtA)ypZX#fxhANTMK90{fj_ouNT~EKY~bBo zp6EKl5OLT`eC?GF{GpTNu{A0`^#O)df+4vXG=OS5_^J#OKme1D2 zo=&mzYz~fCks&AZB?9S5lj%S=;i~dbM

$aGh;0d?Mq)$HVPB#uunWIYS-M`?)P9 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�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 buffer = renderer->add_uniform_buffer(ubo_info); ``` +### Shader Templates + +Shader templates allow the definition of a shader source code with placeholders that can be replaced at +load- or run-time. Templates are help if only specific parts of the shader code are supposed to +be configurable. This may be used, for example, to alter certain code path of the built-in shaders +without recompiling the engine. + +An example shader template looks like this: + +```glsl +#version 330 + +in vec2 tex_coord; +out vec4 frag_color; + +// PLACEHOLDER: uniform_color + +void main() { + // PLACEHOLDER: alpha + frag_color = vec4(col.xyz, alpha); +} +``` + +Placeholders are inserted as comments into the GLSL source. Every placeholder has an ID for +referencing. For these IDs, *snippets* can be defined to insert code in place of the placeholder +comment. Placeholders can be placed at any position in the template, so they can be used to insert +other statements than the control code, including unforms, input/output variables, functions and more. + +The snippets for the above example may look like this: + +`uniform_color.snippet` + +```glsl +uniform vec4 col; +``` + +`alpha.snippet` + +```glsl +float alpha = 0.5; +``` + +In the renderer, shader templates can be created using the `renderer::resources::ShaderTemplate` class. + +```cpp +util::Path template_path = shaderdir / "example_template.frag.glsl"; +resources::ShaderTemplate template(template_path); +``` + +After loading the template, snippets for the placeholders can be added. These are either loaded +as single files or from a directory. + +```cpp +util::Path snippets_path = shaderdir / "example_snippets"; +template.load_snippets(snippets_path); +``` + +or + +```cpp +util::Path snippets_path = shaderdir / "example_snippets"; +template.add_snippet(snippets_path / "uniform_color.snippet"); +template.add_snippet(snippets_path / "alpha.snippet"); +``` + +If snippets have been loaded for all placeholder IDs, the shader template can generate the final +shader source code as `renderer::resources::ShaderSource`. This can then be used to create the actual +`renderer::ShaderProgram` uploaded to the GPU. + +```cpp +resources::ShaderSource source = template.generate_source(); +ShaderProgram prog = = renderer->add_shader({source}); +``` + + ## Thread-safety This level might or might not be threadsafe depending on the concrete backend. The OpenGL version is, in typical GL fashion, so not-threadsafe it's almost anti-threadsafe. All code must be executed sequentially on a dedicated window thread, the same one on which the window and renderer were initially created. The plan for the Vulkan version is to make it at least independent of thread-local storage and hopefully completely threadsafe. diff --git a/libopenage/renderer/resources/shader_template.h b/libopenage/renderer/resources/shader_template.h index 210061ef0c..3e6656629e 100644 --- a/libopenage/renderer/resources/shader_template.h +++ b/libopenage/renderer/resources/shader_template.h @@ -64,6 +64,8 @@ class ShaderTemplate { size_t length; }; + /// All placeholders found in the template + /// precomputed on creation std::vector placeholders; }; } // namespace world From e4e0916f4f719cbf087010ffc365f484f7807edb Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 14 May 2025 08:59:22 +0200 Subject: [PATCH 765/771] renderer: Fix CI complaints. --- libopenage/renderer/stages/world/render_stage.cpp | 2 +- libopenage/renderer/stages/world/render_stage.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index d8d93279af..eec13b602b 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/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" diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index f1256f3b57..7a0fe02a4c 100644 --- a/libopenage/renderer/stages/world/render_stage.h +++ b/libopenage/renderer/stages/world/render_stage.h @@ -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. #pragma once From 927f547d4985cba8e172c9492273b34537571c56 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 15 Sep 2024 06:26:21 +0200 Subject: [PATCH 766/771] doc: UML changes for nyan data API v0.6.0. doc: UML changes for ApplyEffect. doc: UML changes for Task nodes. doc: UMl changes for NextCommand condition. doc: UMl changes for Ranged property. doc: UMl changes for ApplyEffect. --- doc/nyan/aoe2_nyan_tree.svg | 7602 ++++++++++++++++++----------------- doc/nyan/aoe2_nyan_tree.uxf | 6937 +++++++++++++++++--------------- 2 files changed, 7502 insertions(+), 7037 deletions(-) diff --git a/doc/nyan/aoe2_nyan_tree.svg b/doc/nyan/aoe2_nyan_tree.svg index 27360da9cf..ba6d22e8d4 100644 --- a/doc/nyan/aoe2_nyan_tree.svg +++ b/doc/nyan/aoe2_nyan_tree.svg @@ -1,7 +1,7 @@ -AbilityUsableability : AbilityMoveToTargetPathTypeClearCommandQueueNextCommandMovePopCommandQueueNextCommandIdleTaskTasknext : Nodetask : TaskTargetInRangeability : AbilityRangedmin_range : floatmax_range : floatApplyEffectMoveIdleCommandNextCommandnext : dict(Command, Node)SwitchConditionXORSwitchGateswitch : SwitchConditiondefault : NodePathTypeNextCommandcommand : CommandCommandInQueueConditionnext : NodeCommandInQueueWaitAbilityWaittime : floatEventUnit behaviour graphActivitygraph : ActivityAbilitynext : Nodeability : abstract(Ability)ability : AbilityXOREventGatenext : dict(Event, Node)XORGatenext : orderedset(Condition)default : NodeEndActivitystart : StartStartnext : NodeNodeConstructablestarting_progress : intconstruction_progress : set(Progress)TransformCarryRestockHarvestConstructAll ingame objectsare game entitiesProjectileBarracksSwordsmanRelicTreeFalseTruePatchPropertyPrioritypriority : intChainedBatchOrderedBatchUnorderedBatchChancechance : floatPrioritypriority : intBatchPropertyEffectBatcheffects : set(DiscreteEffect)properties : dict(BatchProperty, BatchProperty) = {}OwnsGameEntitygame_entity : GameEntityStateChangeActivestate_change : StateChangerResetResetProgressPropertyResistancePropertyAreaEffectrange : floatdropoff : DropoffTypeDiplomaticstances : set(DiplomaticStance)Costcost : CostEffectPropertyflacflacresistanceeffectMultipliermultiplier : floatModifierPropertyLockPoolslots : intLocklock_pools : set(LockPool)Locklock_pool : LockPoolAbilityPropertyMULTIXORXORNOTSUBSETMAXsize : intLogicGateinputs : set(LogicElement)BuySellExchangeModefee_multiplier : floatExchangeResourcesresource_a : Resourceresource_b : Resourceexchange_rate : ExchangeRateexchange_modes : set(ExchangeMode)ExchangeRatebase_price : floatprice_adjust : optional(dict(ExchangeMode, PriceMode)) = Noneprice_pool : optional(PricePool) = NoneAnyTransformPoolInternalDropSiteupdate_time : floatResourceContainerresource : Resourcemax_amount : intcarry_progress : set(Progress)ResourceStoragecontainers : set(ResourceContainer)MiscVariantElevationDifferenceHighmin_elevation_difference : optional(float) = NoneElevationDifferenceHighmin_elevation_difference : optional(float) = NoneAttributeAboveValueattribute : Attributethreshold : floatAttributeBelowPercentageattribute : Attributethreshold : floatAnimationOverlayoverlays : set(Animation)AnyAnyAnyTechTypeReplacegame_entities : set(GameEntity)Guardrange : floatPalettepalette : fileAttributeAbovePercentageattribute : Attributethreshold : floatNyanPatchStackedstack_limit : intSelectionBoxMatchToSpriteRectanglewidth : floatheight : floatMeanDistributionTypeDetectCloak (SWGB)range : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Hitboxradius_x : floatradius_y : floatradius_z : floatProjectileHitTerrainProjectilePassThroughpass_through_range : intAttributeBelowValueattribute : Attributethreshold : floatTimertime : floatResourceSpotsDepletedonly_enabled : boolSelfAnyLiteralScopestances : set(DiplomaticStance)SUBSETMINsize : intORANDLogicElementonly_once : boolResearchablesexclude : set(ResearchableTech)Creatablesexclude : set(CreatableGameEntity)ProductionModeProductionQueuesize : intproduction_modes : set(ProductionMode)OwnStoragecontainer : EntityContainerNoStackLinearshift_x : intshift_y : intscale_factor : floatHyperbolicshift_x : intshift_y : intscale_factor : floatCalculationTypeStackedstack_limit : intcalculation_type : CalculationTypedistribution_type : DistributionTypeTerrainTypeMostHerdingLongestTimeInRangeClosestHerdingHerdableModeShadowTimeRelativeProgressIncreaseTimeRelativeProgressDecreaseTimeRelativeProgressIncreaseTimeRelativeProgressDecreaseAttributeChangeAdaptiveArrearAdvancePaymentModeResearchAttributeCostattributes : set(Attribute)researchables : set(ResearchableTech)CreationAttributeCostattributes : set(Attribute)creatables : set(CreatableGameEntity)AttributeCostamount : set(AttributeAmount)ResourceCostamount : set(ResourceAmount)Costpayment_mode : PaymentModeUnconditionalUnconditionalTimeRelativeProgressChangeTimeRelativeAttributeChangePricePoolDynamicchange_value : floatmin_price : floatmax_price : floatFixedPriceModeDepositResourcesOnProgressprogress_type : ProgressTyperesources : set(Resource)affected_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Attributename : TranslatedStringabbreviation : TranslatedStringOverlayTerrainterrain_overlay : TerrainTerrainRequirementallowed_types : set(TerrainType)blacklisted_terrains : set(Terrain)Terrainterrain : TerrainStateChangestate_change : StateChangerAoE1TradeRouteexchange_resources : set(Resource)trade_amount : intProgressTypeTimeRelativeProgressChangetype : ProgressTypeFlatAttributeIncreaseFlatAttributeDecreaseTimeRelativeAttributeChangetype : AttributeChangeTypeTimeRelativeProgressChangetype : ProgressTypetotal_change_time : floatTimeRelativeAttributeIncreaseTimeRelativeAttributeDecreaseTimeRelativeAttributeChangetype : AttributeChangeTypetotal_change_time : floatignore_protection : set(ProtectingAttribute)ProgressStatusprogress_type : ProgressTypeprogress : floatRefundOnConditioncondition : set(LogicElement)refund_amount : set(ResourceAmount)AnimationOverrideoverrides : set(AnimationOverride)ExecutionSoundsounds : set(Sound)InverseLinearAdjacentTilesVariantnorth : optional(GameEntity)north_east : optional(GameEntity)east : optional(GameEntity)south_east : optional(GameEntity)south : optional(GameEntity)south_west : optional(GameEntity)west : optional(GameEntity)north_west : optional(GameEntity)Placetile_snap_distance : floatclearance_size_x : floatclearance_size_y : floatallow_rotation : boolmax_elevation_difference : intEjectPlacementModeSendToContainerTypeLureTypeDiplomaticLineOfSightdiplomatic_stance : DiplomaticStanceTerrainterrain : TerrainTerrainterrain : TerrainNormalInContainerDiscreteEffectcontainers : set(EntityContainer)ability : ApplyDiscreteEffectInContainerContinuousEffectcontainers : set(EntityContainer)ability : ApplyContinuousEffectStateChangerenable_abilities : set(Ability)disable_abilities : set(Ability)enable_modifiers : set(Modifier)disable_modifiers : set(Modifier)transform_pool : optional(TransformPool) = Nonepriority : intopenage nyan data API v0.5.0openage nyan data API v0.6.0GameEntityScopeaffected_types : set(GameEntityType)blacklisted_entities : set(GameEntity)StandardRegenerateResourceSpotrate : ResourceRateresource_spot : ResourceSpotAoE2ProjectileAmountprovider_abilities : set(ApplyDiscreteEffect)receiver_abilities : set(ApplyDiscreteEffect)change_types : set(AttributeChangeType)Orange elements:Effects/Resistances that canbe applied on other gameentitiesRevealline_of_sight : floataffected_types : set(GameEntityType)blacklisted_entities : set(GameEntity)ResearchTimeresearchables : set(ResearchableTech)StorageElementCapacitystorage_element : StorageElementDefinitionEntityContainerCapacitycontainer : EntityContainerHerdrange : floatstrength : intallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)InstantTechResearchtech : Techcondition : set(LogicElement)ResearchResourceCostresources : set(Resource)researchables : set(ResearchableTech)CreationResourceCostresources : set(Resource)creatables : set(CreatableGameEntity)CreationTimecreatables : set(CreatableGameEntity)ReloadTimeGatheringEfficiencyresource_spot : ResourceSpotAbsoluteProjectileAmountamount : floatStrayMoveModeAttackMoveModifierScopeExpectedPositionCurrentPositionTargetModeSendToContainertype : SendToContainerTypesearch_range : floatignore_containers : set(EntityContainer)SendToContainertype : SendToContainerTypestorages : set(EntityContainer)Scopedstances : set(DiplomaticStance)scope : ModifierScopeGameEntityFormationformation : Formationsubformation : SubformationSubformationordering_priority : intFormationsubformations : set(Subformation)FlatAttributeIncreaseFlatAttributeDecreaseFlatAttributeIncreaseFlatAttributeDecreaseFlatAttributeIncreaseFlatAttributeDecreaseFlatAttributeIncreaseFlatAttributeDecreaseAoE2TradeRouteTradeRoutetrade_resource : Resourcestart_trade_post : GameEntityend_trade_post : GameEntityTechtypes : set(TechType)name : TranslatedStringdescription : TranslatedMarkupFilelong_description : TranslatedMarkupFileupdates : orderedset(Patch)FallbackLinearNoDropoffDropoffTypeDiplomaticstances : set(DiplomaticStance)AnimationOverrideability : AnimatedAbilityanimations : set(Animation)priority : intCostcost : CostEntityContainerallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)storage_element_defs : set(StorageElementDefinition)slots : intcarry_progress : set(Progress)Visibilityvisible_in_fog : boolTurnturn_speed : floatLanguageietf_string : textLanguageSoundPairlanguage : Languagesound : SoundLanguageMarkupPairlanguage : Languagemarkup_file : fileLanguageTextPairlanguage : Languagestring : textLuretype : LureTypeLuretype : LureTypedestination : set(GameEntity)min_distance_to_destination : floatElevationDifferenceLowmin_elevation_difference : optional(float) = NoneSelfDiplomaticstances : set(DiplomaticStance)DiplomaticStanceExitContainerallowed_containers : set(EntityContainer)EnterContainerallowed_containers : set(EntityContainer)allowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)RangedDiscreteEffectmin_range : intmax_range : intHuntConvertTypeConvertSelfDestructConvert (Ranged)RangedContinuousEffectmin_range : intmax_range : intSelfDestructMakeHarvestableresource_spot : ResourceSpotresist_condition : set(LogicElement)MakeHarvestableresource_spot : ResourceSpotGatherauto_resume : boolresume_search_range : floattargets : set(ResourceSpot)gather_rate : ResourceRatecontainer : ResourceContainerRepairMonkHealMonkHeal (Ranged)ApplyContinuousEffecteffects : set(ContinuousEffect)application_delay : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)ApplyDiscreteEffectbatches : set(EffectBatch)reload_time : floatapplication_delay : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)FlatAttributeChangetype : AttributeChangeTypeblock_rate : AttributeRateContinuousResistanceAttributeRatetype : Attributerate : floatResourceRatetype : Resourcerate : floatDiscreteResistanceDiscreteEffectFlatAttributeChangeignore_protection : set(ProtectingAttribute)ContinuousEffectAoE2Convertprotection_round_recharge_time : floatAoE2Convertskip_guaranteed_rounds : intskip_protected_rounds : intConvertchance_resist : floatConvertcost_fail : optional(Cost) = NoneAttributeChangeTypeFlatAttributeChangetype : AttributeChangeTypeblock_value : AttributeAmountFlatAttributeChangetype : AttributeChangeTypemin_change_value : optional(AttributeAmount) = Nonemax_change_value : optional(AttributeAmount) = Nonechange_value : AttributeAmountignore_protection : set(ProtectingAttribute)Effectors' sideResistors' sideEffectproperties : dict(EffectProperty, EffectProperty) = {}Resistanceproperties : dict(ResistanceProperty, ResistanceProperty) = {}HealthGameEntityTypeAttributeAmounttype : Attributeamount : intAccuracyaccuracy : floataccuracy_dispersion : floatdispersion_dropoff : DropOffTypetarget_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Cloak (SWGB)interrupted_by : set(Ability)interrupt_cooldown : floatLineOfSightrange : floatFaithShield (SWGB)ProtectingAttributeprotects : AttributeAttributeSettingstarting_value : intTerrainOverlayterrain_overlay : TerrainAnimatedoverrides : set(AnimationOverride)Terrainsprite : fileCreatableGameEntityplacement_modes : set(PlacementMode)UseContingentamount : set(ResourceAmount)ProvideContingentamount : set(ResourceAmount)ResourceContingentmin_amount : intmax_amount : intLiteralscope : LiteralScopeProjectileunignore_entities : set(GameEntity)AttributeChangeTrackerattribute : Attributechange_progress : set(Progress)Collisionhitbox : HitboxNamedlong_description : TranslatedMarkupFileDropResourcescontainers : set(ResourceContainer)search_range : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Herdableadjacent_discover_range : floatmode : HerdableModeStopPathablehitbox : Hitboxpath_costs : dict(PathType, int)Foundationfoundation_terrain : TerrainPerspectiveVariantangle : intRestockamount : intPassiveStandGroundDefensiveAggressiveTradePosttrade_routes : set(TradeRoute)Followrange : floatPatrolGameEntityStancetype_preference : orderedset(GameEntityType)GameEntityStancestances: set(GameEntityStance)SendBackToTaskblacklisted_entities : set(GameEntity)TransferStoragestorage_element : GameEntitysource_container : EntityContainertarget_container : EntityContainerGameEntityProgressstatus : ProgressStatusTechResearchedtech : TechVariantchanges : orderedset(Patch)priority : intActiveTransformTotransform_progress : set(Progress)Selectableselection_box : SelectionBoxRallyPointAoE2 specific objectsimplementation)implementation)Basic nyan API objectsElevationDifferenceLowmin_elevation_difference : optional(float) = NoneFlyoverrelative_angle : floatflyover_types : set(GameEntityType)blacklisted_entities : set(GameEntity)Animationsprite : fileAnimationOverridesanimation.If min_projectiles is greater thanthe number of Projectiles inprojectiles, the last projectilein the orderedset should be usedbut AoM had more.Stores what happens aftera percentage ofconstruction, damage,transformation, etc. isreachedProgressright_boundary : floatgame_setup patches the uniquefeatures into the objects(graphics, techs, boni, abilities,etc.)RemoveStoragestorage_elements : set(GameEntity)CollectStoragestorage_elements : set(GameEntity)StorageElementDefinitionstorage_element : GameEntityelements_per_slot : intconflicts : set(StorageElementDefinition)state_change : optional(StateChanger) = NoneStoragecontainer : EntityContainerempty_condition : set(LogicElement)RandomVariantchance_share : floatResearchresearchables : set(ResearchableTech)Tradetrade_routes : set(TradeRoute)container : ResourceContainerCreatecreatables : set(CreatableGameEntity)RelicBonusGatheringRateresource_spot : ResourceSpotMoveSpeedAttributeSettingsValueattribute : AttributeFeitoriaBonusFoodAmountWoodAmountStoneAmountGoldAmountResourceAmounttype : Resourceamount : intContinuousResourcerates : set(ResourceRate)patched.IdlePlayerSetupgame_setup : orderedset(Patch)Despawnstate_change : optional(StateChanger) = NoneTauntactivation_message : textdisplay_message : TranslatedStringsound : SoundLiveattributes : set(AttributeSetting)Harvestableharvestable_by_default : boolPassiveTransformTotransform_progress : set(Progress)Cheatactivation_message : textchanges : orderedset(Patch)Flyheight : floatResistanceresistances : set(Resistance)ShootProjectilemax_projectiles : intmin_range : intmax_range : intreload_time : floatspawn_delay : floatprojectile_delay : floatrequire_turning : boolmanual_aiming_allowed : boolspawning_area_offset_x : floatspawning_area_offset_y : floatspawning_area_offset_z : floatspawning_area_width : floatspawning_area_height : floatspawning_area_randomness : floatallowed_types : set(GameEntityType)blacklisted_entities : set(GameEntity)RegenerateAttributerate : AttributeRateFormationformations : set(GameEntityFormation)Movepath_type : PathTypeCommandSoundsounds : set(Sound)Animatedanimations : set(Animation)Abilityproperties : dict(AbilityProperty, AbilityProperty) = {}Modpriority : intpatches : orderedset(Patch)Patchproperties : dict(PatchProperty, PatchProperty) = {}patch : NyanPatchModifierproperties : dict(ModifierProperty, ModifierProperty) = {}Soundplay_delay : floatsounds : orderedset(file)TerrainAmbientobject : GameEntitymax_density : intTerrainpath_costs : dict(PathType, int)ResearchableTechcondition : set(LogicElement)TranslatedSoundtranslations : set(LanguageSoundPair)TranslatedMarkupFiletranslations : set(LanguageMarkupPair)TranslatedObjectTranslatedStringtranslations : set(LanguageTextPair)Resourcename : TranslatedStringmax_storage : intResourceSpotdecay_rate : floatDropSiteaccepts_from : set(ResourceContainer)GameEntityvariants : set(Variant)Object - 10 + 9 UMLClass - 1190 - 3600 - 100 - 60 + 1080 + 3249 + 90 + 54 *Object* @@ -41,10 +41,10 @@ bg=red UMLClass - 410 - 3580 - 300 - 120 + 378 + 3231 + 270 + 108 *GameEntity* @@ -59,10 +59,10 @@ variants : set(Variant) UMLClass - 4930 - 3130 - 290 - 80 + 4446 + 2826 + 261 + 72 *DropSite* bg=green @@ -74,10 +74,10 @@ accepts_from : set(ResourceContainer) Relation - 700 - 3620 - 510 - 30 + 639 + 3267 + 459 + 27 lt=<<- 490.0;10.0;10.0;10.0 @@ -85,10 +85,10 @@ accepts_from : set(ResourceContainer) UMLClass - 850 - 3120 - 300 - 120 + 774 + 2817 + 270 + 108 *ResourceSpot* @@ -103,10 +103,10 @@ decay_rate : float UMLClass - 910 - 3300 - 180 - 80 + 828 + 2979 + 162 + 72 *Resource* bg=pink @@ -119,10 +119,10 @@ max_storage : int UMLClass - 0 - 2670 - 270 - 80 + 9 + 2412 + 243 + 72 *TranslatedString* bg=pink @@ -134,10 +134,10 @@ translations : set(LanguageTextPair) UMLClass - 370 - 2830 - 150 - 60 + 342 + 2556 + 135 + 54 *TranslatedObject* @@ -147,10 +147,10 @@ bg=pink UMLClass - 290 - 2670 - 290 - 80 + 270 + 2412 + 261 + 72 *TranslatedMarkupFile* bg=pink @@ -162,10 +162,10 @@ translations : set(LanguageMarkupPair) UMLClass - 600 - 2670 - 280 - 80 + 549 + 2412 + 252 + 72 *TranslatedSound* bg=pink @@ -177,10 +177,10 @@ translations : set(LanguageSoundPair) Relation - 430 - 2740 - 30 - 110 + 396 + 2475 + 27 + 99 lt=<<- 10.0;90.0;10.0;10.0 @@ -188,10 +188,10 @@ translations : set(LanguageSoundPair) Relation - 120 - 2740 - 340 - 60 + 117 + 2475 + 306 + 54 lt=- 320.0;40.0;10.0;40.0;10.0;10.0 @@ -199,10 +199,10 @@ translations : set(LanguageSoundPair) Relation - 430 - 2740 - 330 - 60 + 396 + 2475 + 297 + 54 lt=- 10.0;40.0;310.0;40.0;310.0;10.0 @@ -210,10 +210,10 @@ translations : set(LanguageSoundPair) UMLClass - 3260 - 2420 - 320 - 130 + 2943 + 2187 + 288 + 117 *ResearchableTech* bg=pink @@ -229,10 +229,10 @@ condition : set(LogicElement) UMLClass - 1330 - 3710 - 320 - 150 + 1206 + 3348 + 288 + 135 *Terrain* bg=pink @@ -249,10 +249,10 @@ path_costs : dict(PathType, int) UMLClass - 1530 - 3900 - 190 - 80 + 1386 + 3519 + 171 + 72 *TerrainAmbient* bg=pink @@ -265,10 +265,10 @@ max_density : int UMLClass - 1820 - 3250 - 190 - 80 + 1647 + 2934 + 171 + 72 *Sound* bg=pink @@ -281,10 +281,10 @@ sounds : orderedset(file) UMLClass - 1600 - 1250 - 360 - 80 + 1449 + 1134 + 324 + 72 *Modifier* bg=pink @@ -296,10 +296,10 @@ properties : dict(ModifierProperty, ModifierProperty) = {} UMLClass - 1330 - 3530 - 340 - 80 + 1206 + 3186 + 306 + 72 *Patch* bg=pink @@ -312,10 +312,10 @@ patch : NyanPatch UMLClass - 1820 - 3510 - 210 - 80 + 1647 + 3168 + 189 + 72 *Mod* bg=pink @@ -328,10 +328,10 @@ patches : orderedset(Patch) Relation - 1280 - 3620 - 2550 - 30 + 1161 + 3267 + 2295 + 27 lt=<<- 10.0;10.0;2530.0;10.0 @@ -339,10 +339,10 @@ patches : orderedset(Patch) UMLClass - 3810 - 3600 - 340 - 90 + 3438 + 3249 + 306 + 81 *Ability* bg=pink @@ -354,10 +354,10 @@ properties : dict(AbilityProperty, AbilityProperty) = {} UMLClass - 3620 - 4010 - 180 - 80 + 3267 + 3618 + 162 + 72 *Animated* bg=pink @@ -369,10 +369,10 @@ animations : set(Animation) UMLClass - 3620 - 3920 - 180 - 80 + 3267 + 3537 + 162 + 72 *CommandSound* bg=pink @@ -385,10 +385,10 @@ sounds : set(Sound) UMLClass - 4310 - 3840 - 180 - 100 + 3888 + 3465 + 162 + 90 *Move* bg=green @@ -402,10 +402,10 @@ path_type : PathType UMLClass - 4070 - 4280 - 290 - 80 + 3672 + 3861 + 261 + 72 *Formation* bg=green @@ -417,10 +417,10 @@ formations : set(GameEntityFormation) UMLClass - 4860 - 4320 - 180 - 80 + 4383 + 3897 + 162 + 72 *RegenerateAttribute* bg=green @@ -432,10 +432,10 @@ rate : AttributeRate UMLClass - 5960 - 1950 - 320 - 340 + 5373 + 1764 + 288 + 279 *ShootProjectile* bg=green @@ -444,8 +444,6 @@ bg=green projectiles : orderedset(GameEntity) min_projectiles : int max_projectiles : int -min_range : int -max_range : int // time until the ability can be used again reload_time : float // time to wait between the start of the ability @@ -469,10 +467,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 4830 - 4520 - 210 - 80 + 4356 + 4077 + 189 + 72 *Resistance* bg=green @@ -484,10 +482,10 @@ resistances : set(Resistance) UMLClass - 4090 - 3840 - 160 - 80 + 3690 + 3465 + 144 + 72 *Fly* bg=green @@ -499,10 +497,10 @@ height : float Relation - 510 - 2850 - 750 - 770 + 468 + 2574 + 675 + 693 lt=<<- 730.0;750.0;730.0;10.0;10.0;10.0 @@ -510,10 +508,10 @@ height : float UMLClass - 2110 - 3060 - 190 - 80 + 1908 + 2763 + 171 + 72 *Cheat* bg=pink @@ -526,10 +524,10 @@ changes : orderedset(Patch) Relation - 1080 - 3340 - 180 - 30 + 981 + 3015 + 162 + 27 lt=- 160.0;10.0;10.0;10.0 @@ -537,10 +535,10 @@ changes : orderedset(Patch) Relation - 990 - 3230 - 30 - 90 + 900 + 2916 + 27 + 81 lt=<. 10.0;70.0;10.0;10.0 @@ -548,10 +546,10 @@ changes : orderedset(Patch) Relation - 1140 - 3150 - 120 - 30 + 1035 + 2844 + 108 + 27 lt=- 10.0;10.0;100.0;10.0 @@ -559,10 +557,10 @@ changes : orderedset(Patch) Relation - 1580 - 3850 - 30 - 70 + 1431 + 3474 + 27 + 63 lt=<. 10.0;50.0;10.0;10.0 @@ -570,10 +568,10 @@ changes : orderedset(Patch) Relation - 2040 - 3180 - 90 - 30 + 1845 + 2871 + 81 + 27 lt=- 10.0;10.0;70.0;10.0 @@ -581,10 +579,10 @@ changes : orderedset(Patch) Relation - 1470 - 3620 - 30 - 110 + 1332 + 3267 + 27 + 99 lt=- 10.0;10.0;10.0;90.0 @@ -592,10 +590,10 @@ changes : orderedset(Patch) UMLClass - 6560 - 3590 - 290 - 110 + 5913 + 3240 + 261 + 99 *PassiveTransformTo* bg=green @@ -610,10 +608,10 @@ transform_progress : set(Progress) UMLClass - 4570 - 2890 - 300 - 140 + 4122 + 2610 + 270 + 126 *Harvestable* bg=green @@ -629,10 +627,10 @@ harvestable_by_default : bool UMLClass - 4950 - 4630 - 240 - 80 + 4464 + 4176 + 216 + 72 *Live* bg=green @@ -644,10 +642,10 @@ attributes : set(AttributeSetting) UMLClass - 2110 - 3150 - 250 - 100 + 1908 + 2844 + 225 + 90 *Taunt* bg=pink @@ -661,10 +659,10 @@ sound : Sound Relation - 1910 - 3580 - 30 - 70 + 1728 + 3231 + 27 + 63 lt=- 10.0;10.0;10.0;50.0 @@ -672,10 +670,10 @@ sound : Sound Relation - 1770 - 1320 - 30 - 2330 + 1602 + 1197 + 27 + 2097 lt=- 10.0;10.0;10.0;2310.0 @@ -683,10 +681,10 @@ sound : Sound Relation - 1470 - 3600 - 30 - 50 + 1332 + 3249 + 27 + 45 lt=- 10.0;10.0;10.0;30.0 @@ -694,10 +692,10 @@ sound : Sound Relation - 2040 - 3090 - 90 - 30 + 1845 + 2790 + 81 + 27 lt=- 70.0;10.0;10.0;10.0 @@ -705,10 +703,10 @@ sound : Sound UMLClass - 6420 - 3740 - 310 - 110 + 5787 + 3375 + 279 + 99 *Despawn* bg=green @@ -723,10 +721,10 @@ state_change : optional(StateChanger) = None UMLClass - 630 - 4380 - 310 - 160 + 576 + 3951 + 279 + 144 *PlayerSetup* bg=pink @@ -744,10 +742,10 @@ game_setup : orderedset(Patch) Relation - 770 - 3620 - 30 - 780 + 702 + 3267 + 27 + 702 lt=- 10.0;760.0;10.0;10.0 @@ -755,10 +753,10 @@ game_setup : orderedset(Patch) Relation - 1720 - 3620 - 30 - 420 + 1557 + 3267 + 27 + 378 lt=- 10.0;10.0;10.0;400.0 @@ -766,10 +764,10 @@ game_setup : orderedset(Patch) UMLClass - 6420 - 3670 - 120 - 60 + 5787 + 3312 + 108 + 54 *Idle* @@ -779,10 +777,10 @@ bg=green UMLNote - 1550 - 1350 - 210 - 160 + 1404 + 1224 + 189 + 144 Modifier should only be used in cases where Patches don't @@ -798,10 +796,10 @@ bg=blue UMLClass - 1660 - 1100 - 220 - 80 + 1503 + 999 + 198 + 72 *ContinuousResource* bg=yellow @@ -813,10 +811,10 @@ rates : set(ResourceRate) UMLClass - 910 - 3430 - 180 - 80 + 828 + 3096 + 162 + 72 *ResourceAmount* bg=pink @@ -829,10 +827,10 @@ amount : int UMLClass - 530 - 3410 - 120 - 60 + 486 + 3078 + 108 + 54 *GoldAmount* @@ -841,10 +839,10 @@ amount : int UMLClass - 530 - 3480 - 120 - 60 + 486 + 3141 + 108 + 54 *StoneAmount* @@ -853,10 +851,10 @@ amount : int UMLClass - 530 - 3340 - 120 - 60 + 486 + 3015 + 108 + 54 *WoodAmount* @@ -865,10 +863,10 @@ amount : int UMLClass - 530 - 3270 - 120 - 60 + 486 + 2952 + 108 + 54 *FoodAmount* @@ -877,10 +875,10 @@ amount : int Relation - 1080 - 3460 - 180 - 30 + 981 + 3123 + 162 + 27 lt=- 160.0;10.0;10.0;10.0 @@ -888,10 +886,10 @@ amount : int Relation - 990 - 3370 - 30 - 80 + 900 + 3042 + 27 + 72 lt=<. 10.0;10.0;10.0;60.0 @@ -899,10 +897,10 @@ amount : int Relation - 660 - 3460 - 270 - 30 + 603 + 3123 + 243 + 27 lt=<<- 250.0;10.0;10.0;10.0 @@ -910,10 +908,10 @@ amount : int Relation - 640 - 3290 - 50 - 100 + 585 + 2970 + 45 + 90 lt=- 10.0;10.0;30.0;10.0;30.0;80.0 @@ -921,10 +919,10 @@ amount : int Relation - 640 - 3360 - 50 - 100 + 585 + 3033 + 45 + 90 lt=- 10.0;10.0;30.0;10.0;30.0;80.0 @@ -932,10 +930,10 @@ amount : int Relation - 640 - 3450 - 50 - 80 + 585 + 3114 + 45 + 72 lt=- 10.0;60.0;30.0;60.0;30.0;10.0 @@ -943,10 +941,10 @@ amount : int Relation - 640 - 3430 - 50 - 50 + 585 + 3096 + 45 + 45 lt=- 10.0;10.0;30.0;10.0;30.0;30.0 @@ -954,10 +952,10 @@ amount : int Relation - 1770 - 1170 - 30 - 100 + 1602 + 1062 + 27 + 90 lt=<<- 10.0;80.0;10.0;10.0 @@ -965,10 +963,10 @@ amount : int UMLClass - 1630 - 980 - 120 - 60 + 1476 + 891 + 108 + 54 *FeitoriaBonus* @@ -977,10 +975,10 @@ amount : int Relation - 1680 - 1030 - 120 - 90 + 1521 + 936 + 108 + 81 lt=<<- 100.0;70.0;100.0;40.0;10.0;40.0;10.0;10.0 @@ -988,10 +986,10 @@ amount : int UMLClass - 970 - 1010 - 190 - 80 + 882 + 918 + 171 + 72 *AttributeSettingsValue* bg=yellow @@ -1003,10 +1001,10 @@ attribute : Attribute UMLClass - 1020 - 940 - 140 - 60 + 927 + 855 + 126 + 54 *MoveSpeed* @@ -1016,10 +1014,10 @@ bg=yellow UMLClass - 940 - 690 - 220 - 80 + 855 + 630 + 198 + 72 *GatheringRate* bg=yellow @@ -1031,10 +1029,10 @@ resource_spot : ResourceSpot Relation - 1200 - 480 - 600 - 760 + 1089 + 441 + 540 + 684 lt=- 580.0;740.0;10.0;740.0;10.0;10.0 @@ -1042,10 +1040,10 @@ resource_spot : ResourceSpot Relation - 1150 - 960 - 80 - 30 + 1044 + 873 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -1053,10 +1051,10 @@ resource_spot : ResourceSpot Relation - 1150 - 720 - 80 - 30 + 1044 + 657 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -1064,10 +1062,10 @@ resource_spot : ResourceSpot UMLClass - 1810 - 980 - 120 - 60 + 1638 + 891 + 108 + 54 *RelicBonus* @@ -1076,10 +1074,10 @@ resource_spot : ResourceSpot Relation - 1770 - 1030 - 120 - 60 + 1602 + 936 + 108 + 54 lt=- 10.0;40.0;100.0;40.0;100.0;10.0 @@ -1087,10 +1085,10 @@ resource_spot : ResourceSpot UMLClass - 3610 - 2580 - 280 - 80 + 3258 + 2331 + 252 + 72 *Create* bg=green @@ -1102,10 +1100,10 @@ creatables : set(CreatableGameEntity) UMLClass - 4370 - 2670 - 230 - 80 + 3942 + 2412 + 207 + 72 *Trade* bg=green @@ -1119,10 +1117,10 @@ container : ResourceContainer UMLClass - 3610 - 2420 - 280 - 80 + 3258 + 2187 + 252 + 72 *Research* bg=green @@ -1134,10 +1132,10 @@ researchables : set(ResearchableTech) UMLClass - 370 - 3770 - 150 - 80 + 342 + 3402 + 135 + 72 *RandomVariant* @@ -1149,10 +1147,10 @@ chance_share : float Relation - 750 - 3800 - 50 - 30 + 684 + 3429 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -1160,10 +1158,10 @@ chance_share : float UMLClass - 4620 - 1640 - 260 - 90 + 4167 + 1485 + 234 + 81 *Storage* bg=green @@ -1177,10 +1175,10 @@ empty_condition : set(LogicElement) UMLClass - 4980 - 1480 - 310 - 120 + 4491 + 1341 + 279 + 108 *StorageElementDefinition* bg=pink @@ -1195,10 +1193,10 @@ state_change : optional(StateChanger) = None Relation - 4740 - 1600 - 30 - 60 + 4275 + 1449 + 27 + 54 lt=<. 10.0;10.0;10.0;40.0 @@ -1206,10 +1204,10 @@ state_change : optional(StateChanger) = None UMLClass - 4420 - 1970 - 300 - 90 + 3987 + 1782 + 270 + 81 *CollectStorage* bg=green @@ -1222,10 +1220,10 @@ storage_elements : set(GameEntity) UMLClass - 4420 - 1870 - 300 - 90 + 3987 + 1692 + 270 + 81 *RemoveStorage* bg=green @@ -1238,10 +1236,10 @@ storage_elements : set(GameEntity) UMLNote - 720 - 4550 - 220 - 80 + 657 + 4104 + 198 + 72 game_setup patches the unique features into the objects @@ -1253,10 +1251,10 @@ bg=blue UMLClass - 2030 - 4780 - 370 - 110 + 1836 + 4311 + 333 + 99 *Progress* bg=pink @@ -1272,10 +1270,10 @@ right_boundary : float Relation - 2140 - 3620 - 30 - 1180 + 1935 + 3267 + 27 + 1062 lt=- 10.0;10.0;10.0;1160.0 @@ -1283,10 +1281,10 @@ right_boundary : float UMLNote - 2160 - 4680 - 190 - 90 + 1953 + 4221 + 171 + 81 Stores what happens after a percentage of @@ -1299,10 +1297,10 @@ bg=blue UMLNote - 2580 - 4220 - 200 - 90 + 2331 + 3807 + 180 + 81 In AoE2 there is only one HarvestProgress State @@ -1314,10 +1312,10 @@ bg=blue UMLNote - 6290 - 2050 - 240 - 90 + 5670 + 1854 + 216 + 81 If min_projectiles is greater than the number of Projectiles in @@ -1329,10 +1327,10 @@ bg=blue UMLNote - 1770 - 3760 - 130 - 120 + 1602 + 3393 + 117 + 108 Abilities and StorageElements @@ -1347,10 +1345,10 @@ bg=blue UMLNote - 4930 - 3460 - 210 - 100 + 4446 + 3123 + 189 + 90 Villager Gather abilities can override the graphics of @@ -1363,10 +1361,10 @@ bg=blue UMLClass - 2110 - 3330 - 190 - 80 + 1908 + 3006 + 171 + 72 *Animation* bg=pink @@ -1378,10 +1376,10 @@ sprite : file Relation - 2000 - 3280 - 70 - 30 + 1809 + 2961 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -1389,10 +1387,10 @@ sprite : file UMLClass - 440 - 240 - 250 - 90 + 405 + 225 + 225 + 81 *Flyover* bg=yellow @@ -1406,10 +1404,10 @@ blacklisted_entities : set(GameEntity) Relation - 710 - 320 - 370 - 30 + 648 + 297 + 333 + 27 lt=- 350.0;10.0;10.0;10.0 @@ -1417,10 +1415,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 370 - 340 - 320 - 70 + 342 + 315 + 288 + 63 *ElevationDifferenceLow* bg=yellow @@ -1432,10 +1430,10 @@ min_elevation_difference : optional(float) = None Relation - 680 - 270 - 60 - 30 + 621 + 252 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -1443,10 +1441,10 @@ min_elevation_difference : optional(float) = None Relation - 680 - 370 - 60 - 30 + 621 + 342 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -1454,10 +1452,10 @@ min_elevation_difference : optional(float) = None UMLNote - 0 - 100 - 190 - 70 + 9 + 99 + 171 + 63 Pink elements: @@ -1469,10 +1467,10 @@ bg=pink UMLNote - 0 - 180 - 190 - 70 + 9 + 171 + 171 + 63 Green elements: @@ -1484,10 +1482,10 @@ bg=green UMLNote - 0 - 260 - 190 - 70 + 9 + 243 + 171 + 63 Yellow elements: @@ -1499,10 +1497,10 @@ bg=yellow UMLNote - 0 - 440 - 190 - 70 + 9 + 405 + 171 + 63 White elements: @@ -1512,10 +1510,10 @@ AoE2 specific objects UMLClass - 3770 - 2730 - 120 - 60 + 3402 + 2466 + 108 + 54 *RallyPoint* @@ -1525,10 +1523,10 @@ bg=green UMLClass - 6130 - 3860 - 230 - 90 + 5526 + 3483 + 207 + 81 *Selectable* bg=green @@ -1540,10 +1538,10 @@ selection_box : SelectionBox UMLClass - 3770 - 4490 - 330 - 100 + 3402 + 4050 + 297 + 90 *ActiveTransformTo* bg=green @@ -1557,10 +1555,10 @@ transform_progress : set(Progress) UMLClass - 550 - 3770 - 210 - 80 + 504 + 3402 + 189 + 72 *Variant* bg=pink @@ -1573,10 +1571,10 @@ priority : int Relation - 510 - 3800 - 60 - 30 + 468 + 3429 + 54 + 27 lt=<<- 40.0;10.0;10.0;10.0 @@ -1584,10 +1582,10 @@ priority : int UMLClass - 1570 - 4260 - 210 - 80 + 1422 + 3843 + 189 + 72 *TechResearched* bg=pink @@ -1599,10 +1597,10 @@ tech : Tech UMLClass - 1550 - 4350 - 230 - 90 + 1404 + 3924 + 207 + 81 *GameEntityProgress* bg=pink @@ -1615,10 +1613,10 @@ status : ProgressStatus Relation - 1790 - 4220 - 30 - 900 + 1620 + 3807 + 27 + 810 lt=<<- 10.0;10.0;10.0;880.0 @@ -1626,10 +1624,10 @@ status : ProgressStatus UMLClass - 4460 - 1760 - 260 - 100 + 4023 + 1593 + 234 + 90 *TransferStorage* bg=green @@ -1643,10 +1641,10 @@ target_container : EntityContainer Relation - 1720 - 3680 - 70 - 30 + 1557 + 3321 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -1654,10 +1652,10 @@ target_container : EntityContainer UMLClass - 4780 - 1970 - 320 - 90 + 4311 + 1782 + 288 + 81 *SendBackToTask* bg=green @@ -1670,10 +1668,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 4070 - 4370 - 250 - 80 + 3672 + 3942 + 225 + 72 *GameEntityStance* bg=green @@ -1685,10 +1683,10 @@ stances: set(GameEntityStance) UMLClass - 4400 - 4380 - 340 - 90 + 3969 + 3951 + 306 + 81 *GameEntityStance* bg=pink @@ -1702,10 +1700,10 @@ type_preference : orderedset(GameEntityType) Relation - 4310 - 4400 - 110 - 30 + 3888 + 3969 + 99 + 27 lt=<. 90.0;10.0;10.0;10.0 @@ -1713,10 +1711,10 @@ type_preference : orderedset(GameEntityType) UMLClass - 4600 - 3840 - 110 - 60 + 4149 + 3465 + 99 + 54 *Patrol* @@ -1726,10 +1724,10 @@ bg=pink UMLClass - 4600 - 3910 - 160 - 80 + 4149 + 3528 + 144 + 72 *Follow* bg=pink @@ -1741,10 +1739,10 @@ range : float Relation - 4550 - 3800 - 30 - 410 + 4104 + 3429 + 27 + 369 lt=<<- 10.0;10.0;10.0;390.0 @@ -1752,10 +1750,10 @@ range : float Relation - 4550 - 3940 - 70 - 30 + 4104 + 3555 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -1763,10 +1761,10 @@ range : float UMLClass - 4370 - 2580 - 290 - 80 + 3942 + 2331 + 261 + 72 *TradePost* bg=green @@ -1778,10 +1776,10 @@ trade_routes : set(TradeRoute) UMLClass - 4360 - 4490 - 100 - 60 + 3933 + 4050 + 90 + 54 *Aggressive* @@ -1791,10 +1789,10 @@ bg=pink UMLClass - 4360 - 4560 - 100 - 60 + 3933 + 4113 + 90 + 54 *Defensive* @@ -1804,10 +1802,10 @@ bg=pink UMLClass - 4340 - 4630 - 120 - 60 + 3915 + 4176 + 108 + 54 *StandGround* @@ -1817,10 +1815,10 @@ bg=pink UMLClass - 4360 - 4700 - 100 - 60 + 3933 + 4239 + 90 + 54 *Passive* @@ -1830,10 +1828,10 @@ bg=pink Relation - 4450 - 4580 - 60 - 30 + 4014 + 4131 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -1841,10 +1839,10 @@ bg=pink Relation - 4480 - 4460 - 30 - 290 + 4041 + 4023 + 27 + 261 lt=<<- 10.0;10.0;10.0;270.0 @@ -1852,10 +1850,10 @@ bg=pink Relation - 4450 - 4650 - 60 - 30 + 4014 + 4194 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -1863,10 +1861,10 @@ bg=pink Relation - 4450 - 4720 - 60 - 30 + 4014 + 4257 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -1874,10 +1872,10 @@ bg=pink UMLClass - 4600 - 3040 - 270 - 140 + 4149 + 2745 + 243 + 126 *Restock* bg=green @@ -1894,10 +1892,10 @@ amount : int UMLClass - 370 - 3860 - 170 - 80 + 342 + 3483 + 153 + 72 *PerspectiveVariant* @@ -1909,10 +1907,10 @@ angle : int Relation - 530 - 3840 - 140 - 80 + 486 + 3465 + 126 + 72 lt=<<- 120.0;10.0;120.0;60.0;10.0;60.0 @@ -1920,10 +1918,10 @@ angle : int UMLClass - 6010 - 4760 - 220 - 80 + 5418 + 4293 + 198 + 72 *Foundation* bg=green @@ -1935,10 +1933,10 @@ foundation_terrain : Terrain UMLClass - 5100 - 4320 - 220 - 80 + 4599 + 3897 + 198 + 72 *Pathable* bg=green @@ -1951,10 +1949,10 @@ path_costs : dict(PathType, int) UMLClass - 6420 - 3530 - 120 - 60 + 5787 + 3186 + 108 + 54 // Returns unit to Idle state @@ -1965,10 +1963,10 @@ bg=green UMLClass - 6420 - 3930 - 320 - 100 + 5787 + 3546 + 288 + 90 *Herdable* bg=green @@ -1981,10 +1979,10 @@ mode : HerdableMode UMLClass - 4930 - 3220 - 260 - 110 + 4446 + 2907 + 234 + 99 *DropResources* bg=green @@ -2000,10 +1998,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 4750 - 4210 - 290 - 100 + 4284 + 3798 + 261 + 90 *Named* bg=green @@ -2017,10 +2015,10 @@ long_description : TranslatedMarkupFile UMLClass - 5100 - 4420 - 220 - 80 + 4599 + 3987 + 198 + 72 *Collision* bg=green @@ -2032,10 +2030,10 @@ hitbox : Hitbox UMLClass - 5700 - 4760 - 250 - 80 + 5139 + 4293 + 225 + 72 *AttributeChangeTracker* bg=green @@ -2048,10 +2046,10 @@ change_progress : set(Progress) UMLClass - 5960 - 1780 - 320 - 150 + 5373 + 1611 + 288 + 135 *Projectile* bg=green @@ -2070,10 +2068,10 @@ unignore_entities : set(GameEntity) UMLClass - 1610 - 4150 - 210 - 80 + 1458 + 3744 + 189 + 72 *Literal* bg=pink @@ -2085,10 +2083,10 @@ scope : LiteralScope UMLClass - 700 - 3300 - 180 - 80 + 639 + 2979 + 162 + 72 *ResourceContingent* bg=pink @@ -2101,10 +2099,10 @@ max_amount : int Relation - 870 - 3330 - 60 - 30 + 792 + 3006 + 54 + 27 lt=<<- 40.0;10.0;10.0;10.0 @@ -2112,10 +2110,10 @@ max_amount : int UMLClass - 4930 - 2950 - 250 - 80 + 4446 + 2664 + 225 + 72 *ProvideContingent* bg=green @@ -2127,10 +2125,10 @@ amount : set(ResourceAmount) UMLClass - 4930 - 3040 - 250 - 80 + 4446 + 2745 + 225 + 72 *UseContingent* bg=green @@ -2142,10 +2140,10 @@ amount : set(ResourceAmount) UMLClass - 3260 - 2560 - 320 - 160 + 2943 + 2313 + 288 + 144 *CreatableGameEntity* bg=pink @@ -2164,10 +2162,10 @@ placement_modes : set(PlacementMode) Relation - 3570 - 2610 - 60 - 30 + 3222 + 2358 + 54 + 27 lt=<. 10.0;10.0;40.0;10.0 @@ -2175,10 +2173,10 @@ placement_modes : set(PlacementMode) UMLClass - 2110 - 3510 - 190 - 80 + 1908 + 3168 + 171 + 72 *Terrain* bg=pink @@ -2190,10 +2188,10 @@ sprite : file Relation - 2040 - 3090 - 30 - 560 + 1845 + 2790 + 27 + 504 lt=- 10.0;10.0;10.0;540.0 @@ -2201,10 +2199,10 @@ sprite : file Relation - 2040 - 3360 - 90 - 30 + 1845 + 3033 + 81 + 27 lt=- 70.0;10.0;10.0;10.0 @@ -2212,10 +2210,10 @@ sprite : file UMLClass - 2460 - 4660 - 230 - 80 + 2223 + 4203 + 207 + 72 *Animated* bg=pink @@ -2228,10 +2226,10 @@ overrides : set(AnimationOverride) UMLClass - 2460 - 4840 - 190 - 80 + 2223 + 4365 + 171 + 72 *TerrainOverlay* bg=pink @@ -2245,10 +2243,10 @@ terrain_overlay : Terrain Relation - 2420 - 4690 - 60 - 30 + 2187 + 4230 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -2256,10 +2254,10 @@ terrain_overlay : Terrain Relation - 2420 - 4870 - 60 - 30 + 2187 + 4392 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -2267,10 +2265,10 @@ terrain_overlay : Terrain UMLClass - 4960 - 4770 - 220 - 110 + 4473 + 4302 + 198 + 99 *AttributeSetting* bg=pink @@ -2285,10 +2283,10 @@ starting_value : int UMLClass - 4960 - 5050 - 220 - 80 + 4473 + 4554 + 198 + 72 *ProtectingAttribute* bg=pink @@ -2300,10 +2298,10 @@ protects : Attribute Relation - 5060 - 4700 - 30 - 90 + 4563 + 4239 + 27 + 81 lt=<. 10.0;70.0;10.0;10.0 @@ -2311,10 +2309,10 @@ protects : Attribute Relation - 5060 - 5010 - 30 - 60 + 4563 + 4518 + 27 + 54 lt=<<- 10.0;10.0;10.0;40.0 @@ -2322,10 +2320,10 @@ protects : Attribute UMLClass - 5000 - 5170 - 150 - 60 + 4509 + 4662 + 135 + 54 *Shield (SWGB)* @@ -2334,10 +2332,10 @@ protects : Attribute Relation - 5060 - 5120 - 30 - 70 + 4563 + 4617 + 27 + 63 lt=<<- 10.0;10.0;10.0;50.0 @@ -2345,10 +2343,10 @@ protects : Attribute UMLClass - 5240 - 4940 - 100 - 60 + 4725 + 4455 + 90 + 54 *Faith* @@ -2357,10 +2355,10 @@ protects : Attribute Relation - 5180 - 4950 - 80 - 30 + 4671 + 4464 + 72 + 27 lt=<<- 10.0;10.0;60.0;10.0 @@ -2368,10 +2366,10 @@ protects : Attribute UMLClass - 4860 - 4410 - 180 - 80 + 4383 + 3978 + 162 + 72 *LineOfSight* bg=green @@ -2383,10 +2381,10 @@ range : float UMLClass - 5100 - 4210 - 210 - 80 + 4599 + 3798 + 189 + 72 *Cloak (SWGB)* bg=green @@ -2399,10 +2397,10 @@ interrupt_cooldown : float UMLClass - 6330 - 1740 - 270 - 130 + 5706 + 1575 + 243 + 117 *Accuracy* bg=pink @@ -2419,10 +2417,10 @@ blacklisted_entities : set(GameEntity) Relation - 6270 - 1780 - 80 - 30 + 5652 + 1611 + 72 + 27 lt=<. 60.0;10.0;10.0;10.0 @@ -2430,10 +2428,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 4720 - 4890 - 160 - 80 + 4257 + 4410 + 144 + 72 *AttributeAmount* bg=pink @@ -2446,10 +2444,10 @@ amount : int Relation - 4870 - 4940 - 100 - 30 + 4392 + 4455 + 90 + 27 lt=<. 80.0;10.0;10.0;10.0 @@ -2457,10 +2455,10 @@ amount : int UMLClass - 750 - 3550 - 140 - 60 + 684 + 3204 + 126 + 54 *GameEntityType* @@ -2470,10 +2468,10 @@ bg=pink Relation - 700 - 3580 - 70 - 30 + 639 + 3231 + 63 + 27 lt=<. 50.0;10.0;10.0;10.0 @@ -2481,10 +2479,10 @@ bg=pink UMLClass - 5240 - 5010 - 100 - 60 + 4725 + 4518 + 90 + 54 *Health* @@ -2493,10 +2491,10 @@ bg=pink Relation - 5210 - 4950 - 50 - 110 + 4698 + 4464 + 45 + 99 lt=- 10.0;10.0;10.0;90.0;30.0;90.0 @@ -2504,10 +2502,10 @@ bg=pink Relation - 810 - 3600 - 30 - 50 + 738 + 3249 + 27 + 45 lt=- 10.0;30.0;10.0;10.0 @@ -2515,10 +2513,10 @@ bg=pink Relation - 960 - 3620 - 30 - 2130 + 873 + 3267 + 27 + 1917 lt=- 10.0;2110.0;10.0;10.0 @@ -2526,10 +2524,10 @@ bg=pink Relation - 960 - 5720 - 420 - 60 + 873 + 5157 + 378 + 54 lt=- 10.0;10.0;400.0;10.0;400.0;40.0 @@ -2537,10 +2535,10 @@ bg=pink UMLClass - 1180 - 5760 - 390 - 80 + 1071 + 5193 + 351 + 72 *Resistance* bg=pink @@ -2552,10 +2550,10 @@ properties : dict(ResistanceProperty, ResistanceProperty) = {} UMLClass - 400 - 5760 - 330 - 80 + 369 + 5193 + 297 + 72 *Effect* bg=pink @@ -2567,10 +2565,10 @@ properties : dict(EffectProperty, EffectProperty) = {} Relation - 560 - 5720 - 430 - 60 + 513 + 5157 + 387 + 54 lt=- 410.0;10.0;10.0;10.0;10.0;40.0 @@ -2578,10 +2576,10 @@ properties : dict(EffectProperty, EffectProperty) = {} UMLNote - 1210 - 5680 - 140 - 40 + 1098 + 5121 + 126 + 36 Resistors' side bg=blue @@ -2590,10 +2588,10 @@ bg=blue UMLNote - 570 - 5680 - 140 - 40 + 522 + 5121 + 126 + 36 Effectors' side bg=blue @@ -2602,10 +2600,10 @@ bg=blue UMLClass - 670 - 5990 - 350 - 130 + 612 + 5400 + 315 + 117 *FlatAttributeChange* bg=pink @@ -2621,10 +2619,10 @@ ignore_protection : set(ProtectingAttribute) UMLClass - 1480 - 5990 - 250 - 90 + 1341 + 5400 + 225 + 81 *FlatAttributeChange* bg=pink @@ -2637,10 +2635,10 @@ block_value : AttributeAmount UMLClass - 1040 - 4770 - 190 - 60 + 945 + 4302 + 171 + 54 *AttributeChangeType* @@ -2650,10 +2648,10 @@ bg=pink UMLClass - 670 - 6390 - 300 - 130 + 612 + 5760 + 270 + 117 *Convert* bg=orange @@ -2669,10 +2667,10 @@ cost_fail : optional(Cost) = None UMLClass - 1480 - 6390 - 290 - 130 + 1341 + 5760 + 261 + 117 *Convert* bg=orange @@ -2685,10 +2683,10 @@ chance_resist : float UMLClass - 740 - 6530 - 240 - 90 + 675 + 5886 + 216 + 81 *AoE2Convert* bg=orange @@ -2701,10 +2699,10 @@ skip_protected_rounds : int UMLClass - 1530 - 6530 - 290 - 100 + 1386 + 5886 + 261 + 90 *AoE2Convert* bg=orange @@ -2723,10 +2721,10 @@ protection_round_recharge_time : float UMLClass - 320 - 5890 - 180 - 60 + 297 + 5310 + 162 + 54 *ContinuousEffect* @@ -2737,10 +2735,10 @@ bg=orange UMLClass - 170 - 5990 - 330 - 130 + 162 + 5400 + 297 + 117 *FlatAttributeChange* bg=pink @@ -2756,10 +2754,10 @@ ignore_protection : set(ProtectingAttribute) UMLClass - 670 - 5890 - 170 - 60 + 612 + 5310 + 153 + 54 *DiscreteEffect* @@ -2769,10 +2767,10 @@ bg=orange UMLClass - 1480 - 5890 - 180 - 60 + 1341 + 5310 + 162 + 54 *DiscreteResistance* @@ -2782,10 +2780,10 @@ bg=orange UMLClass - 910 - 3520 - 180 - 80 + 828 + 3177 + 162 + 72 *ResourceRate* bg=pink @@ -2798,10 +2796,10 @@ rate : float Relation - 1080 - 3550 - 180 - 30 + 981 + 3204 + 162 + 27 lt=- 160.0;10.0;10.0;10.0 @@ -2809,10 +2807,10 @@ rate : float UMLClass - 4720 - 4980 - 160 - 80 + 4257 + 4491 + 144 + 72 *AttributeRate* bg=pink @@ -2825,10 +2823,10 @@ rate : float Relation - 560 - 5830 - 30 - 60 + 513 + 5256 + 27 + 54 lt=<<- 10.0;10.0;10.0;40.0 @@ -2836,10 +2834,10 @@ rate : float Relation - 400 - 5860 - 190 - 50 + 369 + 5283 + 171 + 45 lt=- 170.0;10.0;10.0;10.0;10.0;30.0 @@ -2847,10 +2845,10 @@ rate : float Relation - 560 - 5860 - 210 - 50 + 513 + 5283 + 189 + 45 lt=- 10.0;10.0;190.0;10.0;190.0;30.0 @@ -2858,10 +2856,10 @@ rate : float Relation - 1170 - 5860 - 200 - 50 + 1062 + 5283 + 180 + 45 lt=- 180.0;10.0;10.0;10.0;10.0;30.0 @@ -2869,10 +2867,10 @@ rate : float Relation - 1350 - 5830 - 30 - 60 + 1224 + 5256 + 27 + 54 lt=<<- 10.0;10.0;10.0;40.0 @@ -2880,10 +2878,10 @@ rate : float Relation - 1340 - 5860 - 240 - 50 + 1215 + 5283 + 216 + 45 lt=- 10.0;10.0;220.0;10.0;220.0;30.0 @@ -2891,10 +2889,10 @@ rate : float UMLClass - 1090 - 5890 - 180 - 60 + 990 + 5310 + 162 + 54 *ContinuousResistance* @@ -2904,10 +2902,10 @@ bg=orange Relation - 960 - 4790 - 100 - 30 + 873 + 4320 + 90 + 27 lt=- 10.0;10.0;80.0;10.0 @@ -2915,10 +2913,10 @@ bg=orange UMLClass - 1060 - 5990 - 210 - 90 + 963 + 5400 + 189 + 81 *FlatAttributeChange* bg=pink @@ -2931,10 +2929,10 @@ block_rate : AttributeRate Relation - 630 - 5910 - 60 - 140 + 576 + 5328 + 54 + 126 lt=<<- 40.0;10.0;10.0;10.0;10.0;120.0;40.0;120.0 @@ -2942,10 +2940,10 @@ block_rate : AttributeRate Relation - 490 - 5910 - 60 - 140 + 450 + 5328 + 54 + 126 lt=<<- 10.0;10.0;40.0;10.0;40.0;120.0;10.0;120.0 @@ -2953,10 +2951,10 @@ block_rate : AttributeRate Relation - 630 - 6320 - 60 - 130 + 576 + 5697 + 54 + 117 lt=- 10.0;10.0;10.0;110.0;40.0;110.0 @@ -2964,10 +2962,10 @@ block_rate : AttributeRate Relation - 710 - 6510 - 50 - 80 + 648 + 5868 + 45 + 72 lt=<<- 10.0;10.0;10.0;60.0;30.0;60.0 @@ -2975,10 +2973,10 @@ block_rate : AttributeRate Relation - 1500 - 6510 - 50 - 80 + 1359 + 5868 + 45 + 72 lt=<<- 10.0;10.0;10.0;60.0;30.0;60.0 @@ -2986,10 +2984,10 @@ block_rate : AttributeRate Relation - 1440 - 6020 - 60 - 330 + 1305 + 5427 + 54 + 297 lt=- 10.0;10.0;10.0;310.0;40.0;310.0 @@ -2997,10 +2995,10 @@ block_rate : AttributeRate Relation - 1440 - 5910 - 60 - 140 + 1305 + 5328 + 54 + 126 lt=<<- 40.0;10.0;10.0;10.0;10.0;120.0;40.0;120.0 @@ -3008,10 +3006,10 @@ block_rate : AttributeRate Relation - 1260 - 5910 - 60 - 140 + 1143 + 5328 + 54 + 126 lt=<<- 10.0;10.0;40.0;10.0;40.0;120.0;10.0;120.0 @@ -3019,10 +3017,10 @@ block_rate : AttributeRate UMLClass - 6220 - 2740 - 320 - 140 + 5607 + 2475 + 288 + 126 *ApplyDiscreteEffect* bg=green @@ -3042,10 +3040,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 6220 - 2560 - 320 - 120 + 5607 + 2313 + 288 + 108 *ApplyContinuousEffect* bg=green @@ -3062,22 +3060,22 @@ blacklisted_entities : set(GameEntity) UMLClass - 6600 - 2460 - 140 - 60 + 5634 + 2214 + 135 + 54 -*MonkHeal* +*MonkHeal (Ranged)* UMLClass - 6600 - 2590 - 110 - 60 + 5949 + 2340 + 99 + 54 *Repair* @@ -3086,10 +3084,10 @@ blacklisted_entities : set(GameEntity) Relation - 630 - 6020 - 60 - 330 + 576 + 5427 + 54 + 297 lt=- 10.0;10.0;10.0;310.0;40.0;310.0 @@ -3097,10 +3095,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 4600 - 3190 - 270 - 130 + 4149 + 2880 + 243 + 117 *Gather* bg=green @@ -3116,10 +3114,10 @@ container : ResourceContainer UMLClass - 670 - 6290 - 260 - 80 + 612 + 5670 + 234 + 72 *MakeHarvestable* bg=orange @@ -3131,10 +3129,10 @@ resource_spot : ResourceSpot UMLClass - 1480 - 6290 - 340 - 80 + 1341 + 5670 + 306 + 72 *MakeHarvestable* bg=orange @@ -3147,37 +3145,21 @@ resist_condition : set(LogicElement) Relation - 6440 - 2480 - 180 - 30 + 5688 + 2259 + 27 + 72 lt=<<- - 10.0;10.0;160.0;10.0 - - - UMLClass - - 6220 - 2450 - 230 - 80 - - *RangedContinuousEffect* -bg=green - --- -min_range : int -max_range : int - + 10.0;60.0;10.0;10.0 Relation - 6530 - 2610 - 90 - 30 + 5886 + 2358 + 81 + 27 lt=<<- 10.0;10.0;70.0;10.0 @@ -3185,10 +3167,10 @@ max_range : int UMLClass - 6600 - 2750 - 140 - 60 + 5949 + 2484 + 126 + 54 *SelfDestruct* @@ -3198,10 +3180,10 @@ max_range : int Relation - 6530 - 2770 - 90 - 30 + 5886 + 2502 + 81 + 27 lt=<<- 10.0;10.0;70.0;10.0 @@ -3209,22 +3191,22 @@ max_range : int UMLClass - 6260 - 3040 - 130 - 60 + 5634 + 2646 + 135 + 54 -*Convert* +*Convert (Ranged)* Relation - 6310 - 2980 - 30 - 80 + 5688 + 2592 + 27 + 72 lt=<<- 10.0;10.0;10.0;60.0 @@ -3232,10 +3214,10 @@ max_range : int UMLClass - 1040 - 4840 - 120 - 60 + 945 + 4365 + 108 + 54 *ConvertType* @@ -3245,10 +3227,10 @@ bg=pink Relation - 960 - 4860 - 100 - 30 + 873 + 4383 + 90 + 27 lt=- 10.0;10.0;80.0;10.0 @@ -3256,10 +3238,10 @@ bg=pink Relation - 1440 - 6320 - 60 - 140 + 1305 + 5697 + 54 + 126 lt=- 10.0;10.0;10.0;120.0;40.0;120.0 @@ -3267,10 +3249,10 @@ bg=pink UMLClass - 6600 - 2820 - 120 - 60 + 5949 + 2547 + 108 + 54 *Hunt* @@ -3279,37 +3261,10 @@ bg=pink UMLClass - 6220 - 2910 - 200 - 80 - - *RangedDiscreteEffect* -bg=green - --- -min_range : int -max_range : int - - - - Relation - - 6310 - 2870 - 30 - 60 - - lt=<<- - 10.0;10.0;10.0;40.0 - - - UMLClass - - 4780 - 1760 - 320 - 100 + 4311 + 1593 + 288 + 90 *EnterContainer* bg=green @@ -3323,10 +3278,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 4780 - 1870 - 260 - 90 + 4311 + 1692 + 234 + 81 *ExitContainer* bg=green @@ -3338,10 +3293,10 @@ allowed_containers : set(EntityContainer) UMLClass - 2110 - 3260 - 160 - 60 + 1908 + 2943 + 144 + 54 *DiplomaticStance* @@ -3351,10 +3306,10 @@ bg=pink Relation - 2040 - 3280 - 90 - 30 + 1845 + 2961 + 81 + 27 lt=- 70.0;10.0;10.0;10.0 @@ -3362,10 +3317,10 @@ bg=pink UMLClass - 3620 - 4100 - 220 - 80 + 3267 + 3699 + 198 + 72 *Diplomatic* bg=pink @@ -3377,10 +3332,10 @@ stances : set(DiplomaticStance) Relation - 1050 - 30 - 180 - 480 + 954 + 36 + 162 + 432 lt=- 160.0;460.0;10.0;460.0;10.0;10.0 @@ -3388,10 +3343,10 @@ stances : set(DiplomaticStance) UMLClass - 2480 - 3320 - 100 - 60 + 2241 + 2997 + 90 + 54 *Self* @@ -3401,10 +3356,10 @@ bg=pink Relation - 2260 - 3280 - 290 - 30 + 2043 + 2961 + 261 + 27 lt=<<- 10.0;10.0;270.0;10.0 @@ -3412,10 +3367,10 @@ bg=pink Relation - 1200 - 200 - 320 - 310 + 1089 + 189 + 288 + 279 lt=- 300.0;10.0;170.0;10.0;170.0;290.0;10.0;290.0 @@ -3423,10 +3378,10 @@ bg=pink UMLClass - 1530 - 180 - 320 - 80 + 1386 + 171 + 288 + 72 *ElevationDifferenceLow* bg=yellow @@ -3438,10 +3393,10 @@ min_elevation_difference : optional(float) = None UMLClass - 230 - 6290 - 270 - 100 + 216 + 5670 + 243 + 90 *Lure* bg=orange @@ -3457,10 +3412,10 @@ min_distance_to_destination : float Relation - 490 - 6020 - 60 - 320 + 450 + 5427 + 54 + 288 lt=- 40.0;10.0;40.0;300.0;10.0;300.0 @@ -3468,10 +3423,10 @@ min_distance_to_destination : float UMLClass - 1020 - 6290 - 250 - 100 + 927 + 5670 + 225 + 90 *Lure* bg=orange @@ -3484,10 +3439,10 @@ type : LureType Relation - 1260 - 6020 - 60 - 320 + 1143 + 5427 + 54 + 288 lt=- 40.0;10.0;40.0;300.0;10.0;300.0 @@ -3495,10 +3450,10 @@ type : LureType UMLClass - 50 - 2540 - 160 - 80 + 54 + 2295 + 144 + 72 *LanguageTextPair* bg=pink @@ -3511,10 +3466,10 @@ string : text UMLClass - 340 - 2540 - 200 - 80 + 315 + 2295 + 180 + 72 *LanguageMarkupPair* bg=pink @@ -3527,10 +3482,10 @@ markup_file : file UMLClass - 660 - 2540 - 180 - 80 + 603 + 2295 + 162 + 72 *LanguageSoundPair* bg=pink @@ -3543,10 +3498,10 @@ sound : Sound Relation - 120 - 2610 - 30 - 80 + 117 + 2358 + 27 + 72 lt=<. 10.0;60.0;10.0;10.0 @@ -3554,10 +3509,10 @@ sound : Sound Relation - 430 - 2610 - 30 - 80 + 396 + 2358 + 27 + 72 lt=<. 10.0;60.0;10.0;10.0 @@ -3565,10 +3520,10 @@ sound : Sound Relation - 730 - 2610 - 30 - 80 + 666 + 2358 + 27 + 72 lt=<. 10.0;60.0;10.0;10.0 @@ -3576,10 +3531,10 @@ sound : Sound UMLClass - 570 - 2900 - 170 - 80 + 522 + 2619 + 153 + 72 *Language* bg=pink @@ -3591,10 +3546,10 @@ ietf_string : text Relation - 640 - 2850 - 30 - 70 + 585 + 2574 + 27 + 63 lt=- 10.0;50.0;10.0;10.0 @@ -3602,10 +3557,10 @@ ietf_string : text UMLClass - 4090 - 3930 - 160 - 80 + 3690 + 3546 + 144 + 72 *Turn* bg=green @@ -3617,10 +3572,10 @@ turn_speed : float UMLClass - 5100 - 4520 - 160 - 80 + 4599 + 4077 + 144 + 72 *Visibility* bg=green @@ -3632,10 +3587,10 @@ visible_in_fog : bool UMLClass - 4570 - 1480 - 350 - 130 + 4122 + 1341 + 315 + 117 *EntityContainer* bg=pink @@ -3651,10 +3606,10 @@ carry_progress : set(Progress) Relation - 4910 - 1530 - 90 - 30 + 4428 + 1386 + 81 + 27 lt=<. 70.0;10.0;10.0;10.0 @@ -3662,10 +3617,10 @@ carry_progress : set(Progress) UMLClass - 1230 - 5280 - 220 - 80 + 1116 + 4761 + 198 + 72 *Cost* bg=pink @@ -3677,10 +3632,10 @@ cost : Cost Relation - 1180 - 5400 - 70 - 30 + 1071 + 4869 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -3688,10 +3643,10 @@ cost : Cost UMLClass - 1770 - 3650 - 240 - 100 + 1602 + 3294 + 216 + 90 *AnimationOverride* bg=pink @@ -3705,10 +3660,10 @@ priority : int UMLClass - 1530 - 3400 - 220 - 80 + 1386 + 3069 + 198 + 72 *Diplomatic* bg=pink @@ -3720,10 +3675,10 @@ stances : set(DiplomaticStance) UMLClass - 740 - 4700 - 160 - 60 + 675 + 4239 + 144 + 54 *DropoffType* @@ -3733,10 +3688,10 @@ bg=pink Relation - 670 - 4720 - 90 - 30 + 612 + 4257 + 81 + 27 lt=<<- 70.0;10.0;10.0;10.0 @@ -3744,10 +3699,10 @@ bg=pink Relation - 670 - 4720 - 60 - 100 + 612 + 4257 + 54 + 90 lt=- 40.0;10.0;40.0;80.0;10.0;80.0 @@ -3755,10 +3710,10 @@ bg=pink Relation - 670 - 4650 - 60 - 100 + 612 + 4194 + 54 + 90 lt=- 40.0;80.0;40.0;10.0;10.0;10.0 @@ -3766,10 +3721,10 @@ bg=pink UMLClass - 520 - 4630 - 160 - 60 + 477 + 4176 + 144 + 54 *NoDropoff* @@ -3779,10 +3734,10 @@ bg=pink UMLClass - 520 - 4700 - 160 - 60 + 477 + 4239 + 144 + 54 *Linear* @@ -3792,10 +3747,10 @@ bg=pink UMLClass - 1280 - 4770 - 120 - 60 + 1161 + 4302 + 108 + 54 // This type is _only_ evaluated if all FlatAttributeChange Effects with other types are outside of the // range defined in the FlatAttributeChange Effect with type Fallback. @@ -3809,10 +3764,10 @@ bg=pink Relation - 1220 - 4790 - 80 - 30 + 1107 + 4320 + 72 + 27 lt=<<- 10.0;10.0;60.0;10.0 @@ -3820,10 +3775,10 @@ bg=pink Relation - 3570 - 2440 - 60 - 30 + 3222 + 2205 + 54 + 27 lt=<. 10.0;10.0;40.0;10.0 @@ -3831,10 +3786,10 @@ bg=pink UMLClass - 1040 - 4100 - 270 - 130 + 945 + 3699 + 243 + 117 *Tech* bg=pink @@ -3850,10 +3805,10 @@ updates : orderedset(Patch) Relation - 1230 - 3650 - 30 - 470 + 1116 + 3294 + 27 + 423 lt=<<- 10.0;10.0;10.0;450.0 @@ -3861,10 +3816,10 @@ updates : orderedset(Patch) UMLClass - 4730 - 2590 - 240 - 100 + 4266 + 2340 + 216 + 90 // Determines traded resource and resource amount *TradeRoute* @@ -3879,10 +3834,10 @@ end_trade_post : GameEntity Relation - 4590 - 2680 - 210 - 60 + 4140 + 2421 + 189 + 54 lt=<. 190.0;10.0;190.0;40.0;10.0;40.0 @@ -3890,10 +3845,10 @@ end_trade_post : GameEntity UMLClass - 5040 - 2590 - 170 - 60 + 4545 + 2340 + 153 + 54 *AoE2TradeRoute* @@ -3904,10 +3859,10 @@ bg=pink UMLClass - 720 - 6140 - 220 - 60 + 657 + 5535 + 198 + 54 *FlatAttributeDecrease* @@ -3917,10 +3872,10 @@ bg=orange UMLClass - 720 - 6210 - 220 - 60 + 657 + 5598 + 198 + 54 *FlatAttributeIncrease* @@ -3930,10 +3885,10 @@ bg=orange Relation - 690 - 6110 - 50 - 80 + 630 + 5508 + 45 + 72 lt=->> 30.0;60.0;10.0;60.0;10.0;10.0 @@ -3941,10 +3896,10 @@ bg=orange Relation - 690 - 6160 - 50 - 100 + 630 + 5553 + 45 + 90 lt=- 30.0;80.0;10.0;80.0;10.0;10.0 @@ -3952,10 +3907,10 @@ bg=orange UMLClass - 1540 - 6140 - 220 - 60 + 1395 + 5535 + 198 + 54 *FlatAttributeDecrease* @@ -3965,10 +3920,10 @@ bg=orange UMLClass - 1540 - 6210 - 220 - 60 + 1395 + 5598 + 198 + 54 *FlatAttributeIncrease* @@ -3978,10 +3933,10 @@ bg=orange Relation - 1500 - 6070 - 30 - 190 + 1359 + 5472 + 27 + 171 lt=<<- 10.0;10.0;10.0;170.0 @@ -3989,10 +3944,10 @@ bg=orange Relation - 1500 - 6160 - 60 - 30 + 1359 + 5553 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -4000,10 +3955,10 @@ bg=orange UMLClass - 230 - 6140 - 220 - 60 + 216 + 5535 + 198 + 54 *FlatAttributeDecrease* @@ -4013,10 +3968,10 @@ bg=orange UMLClass - 230 - 6210 - 220 - 60 + 216 + 5598 + 198 + 54 *FlatAttributeIncrease* @@ -4026,10 +3981,10 @@ bg=orange Relation - 440 - 6160 - 50 - 100 + 405 + 5553 + 45 + 90 lt=- 10.0;80.0;30.0;80.0;30.0;10.0 @@ -4037,10 +3992,10 @@ bg=orange Relation - 440 - 6110 - 50 - 80 + 405 + 5508 + 45 + 72 lt=->> 10.0;60.0;30.0;60.0;30.0;10.0 @@ -4048,10 +4003,10 @@ bg=orange UMLClass - 1020 - 6140 - 220 - 60 + 927 + 5535 + 198 + 54 *FlatAttributeDecrease* @@ -4061,10 +4016,10 @@ bg=orange UMLClass - 1020 - 6210 - 220 - 60 + 927 + 5598 + 198 + 54 *FlatAttributeIncrease* @@ -4074,10 +4029,10 @@ bg=orange Relation - 1230 - 6160 - 50 - 100 + 1116 + 5553 + 45 + 90 lt=- 10.0;80.0;30.0;80.0;30.0;10.0 @@ -4085,10 +4040,10 @@ bg=orange Relation - 1230 - 6070 - 50 - 120 + 1116 + 5472 + 45 + 108 lt=->> 10.0;100.0;30.0;100.0;30.0;10.0 @@ -4096,10 +4051,10 @@ bg=orange UMLClass - 1490 - 3160 - 250 - 80 + 1350 + 2853 + 225 + 72 *Formation* bg=pink @@ -4111,10 +4066,10 @@ subformations : set(Subformation) Relation - 1730 - 3190 - 70 - 30 + 1566 + 2880 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -4122,10 +4077,10 @@ subformations : set(Subformation) UMLClass - 1540 - 3270 - 180 - 80 + 1395 + 2952 + 162 + 72 *Subformation* bg=pink @@ -4137,10 +4092,10 @@ ordering_priority : int UMLClass - 4400 - 4280 - 220 - 80 + 3969 + 3861 + 198 + 72 *GameEntityFormation* bg=pink @@ -4153,10 +4108,10 @@ subformation : Subformation Relation - 4350 - 4310 - 70 - 30 + 3924 + 3888 + 63 + 27 lt=<. 50.0;10.0;10.0;10.0 @@ -4164,10 +4119,10 @@ subformation : Subformation Relation - 1710 - 3300 - 90 - 30 + 1548 + 2979 + 81 + 27 lt=- 10.0;10.0;70.0;10.0 @@ -4175,10 +4130,10 @@ subformation : Subformation Relation - 1620 - 3230 - 30 - 60 + 1467 + 2916 + 27 + 54 lt=<. 10.0;40.0;10.0;10.0 @@ -4186,10 +4141,10 @@ subformation : Subformation UMLClass - 1400 - 1660 - 210 - 90 + 1269 + 1503 + 189 + 81 *Scoped* bg=pink @@ -4203,24 +4158,13 @@ stances : set(DiplomaticStance) scope : ModifierScope - - Relation - - 6330 - 2520 - 30 - 60 - - lt=<<- - 10.0;40.0;10.0;10.0 - UMLClass - 670 - 6640 - 250 - 100 + 612 + 5985 + 225 + 90 *SendToContainer* bg=orange @@ -4233,10 +4177,10 @@ storages : set(EntityContainer) Relation - 630 - 6420 - 60 - 280 + 576 + 5787 + 54 + 252 lt=- 10.0;10.0;10.0;260.0;40.0;260.0 @@ -4244,10 +4188,10 @@ storages : set(EntityContainer) UMLClass - 1480 - 6640 - 260 - 100 + 1341 + 5985 + 234 + 90 *SendToContainer* bg=orange @@ -4261,10 +4205,10 @@ ignore_containers : set(EntityContainer) Relation - 1440 - 6430 - 60 - 270 + 1305 + 5796 + 54 + 243 lt=- 10.0;10.0;10.0;250.0;40.0;250.0 @@ -4272,10 +4216,10 @@ ignore_containers : set(EntityContainer) Relation - 3880 - 2750 - 60 - 870 + 3501 + 2484 + 54 + 783 lt=<<- 40.0;850.0;40.0;10.0;10.0;10.0 @@ -4283,10 +4227,10 @@ ignore_containers : set(EntityContainer) Relation - 3880 - 2610 - 60 - 170 + 3501 + 2358 + 54 + 153 lt=- 40.0;150.0;40.0;10.0;10.0;10.0 @@ -4294,10 +4238,10 @@ ignore_containers : set(EntityContainer) Relation - 3880 - 2450 - 60 - 190 + 3501 + 2214 + 54 + 171 lt=- 40.0;170.0;40.0;10.0;10.0;10.0 @@ -4305,10 +4249,10 @@ ignore_containers : set(EntityContainer) Relation - 4140 - 3620 - 2440 - 30 + 3735 + 3267 + 2196 + 27 lt=<<- 10.0;10.0;2420.0;10.0 @@ -4316,21 +4260,21 @@ ignore_containers : set(EntityContainer) Relation - 3570 - 3700 - 30 - 550 + 3222 + 3339 + 27 + 576 lt=<<- - 10.0;10.0;10.0;530.0 + 10.0;10.0;10.0;620.0 Relation - 3570 - 3950 - 70 - 30 + 3222 + 3564 + 63 + 27 lt=- 50.0;10.0;10.0;10.0 @@ -4338,10 +4282,10 @@ ignore_containers : set(EntityContainer) Relation - 3570 - 4040 - 70 - 30 + 3222 + 3645 + 63 + 27 lt=- 50.0;10.0;10.0;10.0 @@ -4349,10 +4293,10 @@ ignore_containers : set(EntityContainer) Relation - 3910 - 3680 - 30 - 830 + 3528 + 3321 + 27 + 747 lt=<<- 10.0;10.0;10.0;810.0 @@ -4360,10 +4304,10 @@ ignore_containers : set(EntityContainer) Relation - 4020 - 4310 - 70 - 120 + 3627 + 3888 + 63 + 108 lt=- 10.0;10.0;10.0;100.0;50.0;100.0 @@ -4371,10 +4315,10 @@ ignore_containers : set(EntityContainer) Relation - 3910 - 4310 - 180 - 30 + 3528 + 3888 + 162 + 27 lt=- 10.0;10.0;160.0;10.0 @@ -4382,10 +4326,10 @@ ignore_containers : set(EntityContainer) Relation - 4240 - 3870 - 60 - 120 + 3825 + 3492 + 54 + 108 lt=- 10.0;100.0;40.0;100.0;40.0;10.0 @@ -4393,10 +4337,10 @@ ignore_containers : set(EntityContainer) Relation - 4240 - 3620 - 60 - 280 + 3825 + 3267 + 54 + 252 lt=- 10.0;260.0;40.0;260.0;40.0;10.0 @@ -4404,10 +4348,10 @@ ignore_containers : set(EntityContainer) Relation - 4270 - 3870 - 60 - 30 + 3852 + 3492 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -4415,10 +4359,10 @@ ignore_containers : set(EntityContainer) Relation - 4650 - 2610 - 100 - 30 + 4194 + 2358 + 90 + 27 lt=<. 80.0;10.0;10.0;10.0 @@ -4426,10 +4370,10 @@ ignore_containers : set(EntityContainer) Relation - 4960 - 2610 - 100 - 30 + 4473 + 2358 + 90 + 27 lt=<<- 10.0;10.0;80.0;10.0 @@ -4437,10 +4381,10 @@ ignore_containers : set(EntityContainer) Relation - 5000 - 2610 - 30 - 120 + 4509 + 2358 + 27 + 108 lt=- 10.0;10.0;10.0;100.0 @@ -4448,10 +4392,10 @@ ignore_containers : set(EntityContainer) Relation - 3910 - 2500 - 480 - 30 + 3528 + 2259 + 432 + 27 lt=- 460.0;10.0;10.0;10.0 @@ -4459,10 +4403,10 @@ ignore_containers : set(EntityContainer) Relation - 4330 - 2500 - 60 - 140 + 3906 + 2259 + 54 + 126 lt=- 40.0;120.0;10.0;120.0;10.0;10.0 @@ -4470,10 +4414,10 @@ ignore_containers : set(EntityContainer) Relation - 4330 - 2610 - 60 - 120 + 3906 + 2358 + 54 + 108 lt=- 40.0;100.0;10.0;100.0;10.0;10.0 @@ -4481,10 +4425,10 @@ ignore_containers : set(EntityContainer) Relation - 4870 - 4990 - 100 - 30 + 4392 + 4500 + 90 + 27 lt=<. 80.0;10.0;10.0;10.0 @@ -4492,10 +4436,10 @@ ignore_containers : set(EntityContainer) Relation - 5060 - 3620 - 30 - 1030 + 4563 + 3267 + 27 + 927 lt=- 10.0;10.0;10.0;1010.0 @@ -4503,10 +4447,10 @@ ignore_containers : set(EntityContainer) Relation - 5030 - 4240 - 60 - 30 + 4536 + 3825 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4514,10 +4458,10 @@ ignore_containers : set(EntityContainer) Relation - 5030 - 4350 - 60 - 30 + 4536 + 3924 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4525,10 +4469,10 @@ ignore_containers : set(EntityContainer) Relation - 5060 - 4450 - 60 - 30 + 4563 + 4014 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4536,10 +4480,10 @@ ignore_containers : set(EntityContainer) Relation - 5030 - 4550 - 60 - 30 + 4536 + 4104 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4547,10 +4491,10 @@ ignore_containers : set(EntityContainer) Relation - 5060 - 4550 - 60 - 30 + 4563 + 4104 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4558,10 +4502,10 @@ ignore_containers : set(EntityContainer) Relation - 5030 - 4440 - 60 - 30 + 4536 + 4005 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -4569,10 +4513,10 @@ ignore_containers : set(EntityContainer) Relation - 5060 - 4240 - 60 - 30 + 4563 + 3825 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4580,10 +4524,10 @@ ignore_containers : set(EntityContainer) Relation - 5970 - 3620 - 30 - 1200 + 5382 + 3267 + 27 + 1080 lt=- 10.0;10.0;10.0;1180.0 @@ -4591,10 +4535,10 @@ ignore_containers : set(EntityContainer) Relation - 5940 - 4790 - 60 - 30 + 5355 + 4320 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4602,10 +4546,10 @@ ignore_containers : set(EntityContainer) Relation - 5970 - 4790 - 60 - 30 + 5382 + 4320 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4613,10 +4557,10 @@ ignore_containers : set(EntityContainer) Relation - 5060 - 4350 - 60 - 30 + 4563 + 3924 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4624,10 +4568,10 @@ ignore_containers : set(EntityContainer) Relation - 6380 - 3550 - 60 - 100 + 5751 + 3204 + 54 + 90 lt=- 40.0;10.0;10.0;10.0;10.0;80.0 @@ -4635,10 +4579,10 @@ ignore_containers : set(EntityContainer) Relation - 6380 - 3620 - 30 - 370 + 5751 + 3267 + 27 + 333 lt=- 10.0;350.0;10.0;10.0 @@ -4646,10 +4590,10 @@ ignore_containers : set(EntityContainer) Relation - 6380 - 3960 - 60 - 30 + 5751 + 3573 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4657,10 +4601,10 @@ ignore_containers : set(EntityContainer) Relation - 6180 - 2670 - 210 - 980 + 5571 + 2412 + 189 + 882 lt=- 190.0;10.0;190.0;40.0;10.0;40.0;10.0;960.0 @@ -4668,10 +4612,10 @@ ignore_containers : set(EntityContainer) Relation - 6530 - 2840 - 90 - 30 + 5886 + 2565 + 81 + 27 lt=<<- 10.0;10.0;70.0;10.0 @@ -4679,10 +4623,10 @@ ignore_containers : set(EntityContainer) Relation - 6360 - 2700 - 30 - 60 + 5733 + 2439 + 27 + 54 lt=- 10.0;10.0;10.0;40.0 @@ -4690,10 +4634,10 @@ ignore_containers : set(EntityContainer) Relation - 4890 - 2950 - 30 - 700 + 4410 + 2664 + 27 + 630 lt=- 10.0;680.0;10.0;10.0 @@ -4701,10 +4645,10 @@ ignore_containers : set(EntityContainer) Relation - 4890 - 2980 - 60 - 30 + 4410 + 2691 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -4712,10 +4656,10 @@ ignore_containers : set(EntityContainer) Relation - 4860 - 2950 - 60 - 30 + 4383 + 2664 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -4723,10 +4667,10 @@ ignore_containers : set(EntityContainer) Relation - 4860 - 3070 - 60 - 30 + 4383 + 2772 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -4734,10 +4678,10 @@ ignore_containers : set(EntityContainer) Relation - 4890 - 3070 - 60 - 30 + 4410 + 2772 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -4745,10 +4689,10 @@ ignore_containers : set(EntityContainer) Relation - 4890 - 3160 - 60 - 30 + 4410 + 2853 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -4756,10 +4700,10 @@ ignore_containers : set(EntityContainer) Relation - 4890 - 3250 - 60 - 30 + 4410 + 2934 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -4767,10 +4711,10 @@ ignore_containers : set(EntityContainer) Relation - 4860 - 3220 - 60 - 30 + 4383 + 2907 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -4778,10 +4722,10 @@ ignore_containers : set(EntityContainer) Relation - 4740 - 1720 - 1000 - 1930 + 4275 + 1557 + 900 + 1737 lt=- 980.0;1910.0;980.0;360.0;10.0;360.0;10.0;10.0 @@ -4789,10 +4733,10 @@ ignore_containers : set(EntityContainer) Relation - 5710 - 2070 - 270 - 30 + 5148 + 1872 + 243 + 27 lt=- 10.0;10.0;250.0;10.0 @@ -4800,10 +4744,10 @@ ignore_containers : set(EntityContainer) Relation - 5920 - 1870 - 60 - 230 + 5337 + 1692 + 54 + 207 lt=- 40.0;10.0;10.0;10.0;10.0;210.0 @@ -4811,10 +4755,10 @@ ignore_containers : set(EntityContainer) Relation - 4740 - 1790 - 60 - 30 + 4275 + 1620 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4822,10 +4766,10 @@ ignore_containers : set(EntityContainer) Relation - 4740 - 1900 - 60 - 30 + 4275 + 1719 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4833,10 +4777,10 @@ ignore_containers : set(EntityContainer) Relation - 4740 - 1990 - 60 - 30 + 4275 + 1800 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4844,10 +4788,10 @@ ignore_containers : set(EntityContainer) Relation - 4710 - 1990 - 60 - 30 + 4248 + 1800 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4855,10 +4799,10 @@ ignore_containers : set(EntityContainer) Relation - 4710 - 1900 - 60 - 30 + 4248 + 1719 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4866,10 +4810,10 @@ ignore_containers : set(EntityContainer) Relation - 4710 - 1790 - 60 - 30 + 4248 + 1620 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -4877,10 +4821,10 @@ ignore_containers : set(EntityContainer) UMLClass - 6330 - 1890 - 140 - 60 + 5706 + 1710 + 126 + 54 *TargetMode* @@ -4890,10 +4834,10 @@ bg=pink Relation - 6270 - 1910 - 80 - 30 + 5652 + 1728 + 72 + 27 lt=<. 60.0;10.0;10.0;10.0 @@ -4901,10 +4845,10 @@ bg=pink UMLClass - 6540 - 1890 - 160 - 60 + 5895 + 1710 + 144 + 54 *CurrentPosition* @@ -4914,10 +4858,10 @@ bg=pink Relation - 6460 - 1910 - 100 - 30 + 5823 + 1728 + 90 + 27 lt=<<- 10.0;10.0;80.0;10.0 @@ -4925,10 +4869,10 @@ bg=pink UMLClass - 6540 - 1960 - 160 - 60 + 5895 + 1773 + 144 + 54 *ExpectedPosition* @@ -4938,10 +4882,10 @@ bg=pink Relation - 6500 - 1910 - 60 - 100 + 5859 + 1728 + 54 + 90 lt=- 10.0;10.0;10.0;80.0;40.0;80.0 @@ -4949,10 +4893,10 @@ bg=pink UMLClass - 1200 - 1670 - 140 - 60 + 1089 + 1512 + 126 + 54 *ModifierScope* @@ -4962,10 +4906,10 @@ bg=pink Relation - 1330 - 1690 - 90 - 30 + 1206 + 1530 + 81 + 27 lt=<. 10.0;10.0;70.0;10.0 @@ -4973,10 +4917,10 @@ bg=pink UMLClass - 4600 - 4090 - 110 - 60 + 4149 + 3690 + 99 + 54 *AttackMove* @@ -4986,10 +4930,10 @@ bg=pink Relation - 4550 - 4110 - 70 - 30 + 4104 + 3708 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -4997,10 +4941,10 @@ bg=pink UMLClass - 4510 - 3750 - 120 - 60 + 4068 + 3384 + 108 + 54 *MoveMode* @@ -5010,10 +4954,10 @@ bg=pink Relation - 4390 - 3770 - 140 - 90 + 3960 + 3402 + 126 + 81 lt=<. 120.0;10.0;10.0;10.0;10.0;70.0 @@ -5021,10 +4965,10 @@ bg=pink Relation - 1490 - 290 - 60 - 30 + 1350 + 270 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -5032,10 +4976,10 @@ bg=pink UMLClass - 1530 - 270 - 130 - 60 + 1386 + 252 + 117 + 54 *Stray* @@ -5045,10 +4989,10 @@ bg=yellow UMLClass - 2390 - 770 - 300 - 80 + 2160 + 702 + 270 + 72 *AbsoluteProjectileAmount* bg=yellow @@ -5060,10 +5004,10 @@ amount : float Relation - 1770 - 440 - 590 - 800 + 1602 + 405 + 531 + 720 lt=- 10.0;780.0;570.0;780.0;570.0;10.0 @@ -5071,10 +5015,10 @@ amount : float UMLClass - 920 - 780 - 240 - 80 + 837 + 711 + 216 + 72 *GatheringEfficiency* bg=yellow @@ -5086,10 +5030,10 @@ resource_spot : ResourceSpot Relation - 1150 - 810 - 80 - 30 + 1044 + 738 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -5097,10 +5041,10 @@ resource_spot : ResourceSpot UMLClass - 1020 - 870 - 140 - 60 + 927 + 792 + 126 + 54 *ReloadTime* @@ -5110,10 +5054,10 @@ bg=yellow Relation - 1150 - 890 - 80 - 30 + 1044 + 810 + 72 + 27 lt=- 10.0;10.0;60.0;10.0 @@ -5121,10 +5065,10 @@ bg=yellow UMLClass - 1260 - 1010 - 280 - 80 + 1143 + 918 + 252 + 72 *CreationTime* bg=yellow @@ -5136,10 +5080,10 @@ creatables : set(CreatableGameEntity) Relation - 1200 - 1040 - 80 - 30 + 1089 + 945 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -5147,10 +5091,10 @@ creatables : set(CreatableGameEntity) UMLClass - 1260 - 920 - 300 - 80 + 1143 + 837 + 270 + 72 *CreationResourceCost* bg=yellow @@ -5164,10 +5108,10 @@ creatables : set(CreatableGameEntity) Relation - 1200 - 960 - 80 - 30 + 1089 + 873 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -5175,10 +5119,10 @@ creatables : set(CreatableGameEntity) UMLClass - 1260 - 650 - 290 - 80 + 1143 + 594 + 261 + 72 *ResearchResourceCost* bg=yellow @@ -5192,10 +5136,10 @@ researchables : set(ResearchableTech) Relation - 1200 - 680 - 80 - 30 + 1089 + 621 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -5203,10 +5147,10 @@ researchables : set(ResearchableTech) Relation - 2330 - 800 - 80 - 30 + 2106 + 729 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -5214,10 +5158,10 @@ researchables : set(ResearchableTech) UMLClass - 2390 - 680 - 220 - 80 + 2160 + 621 + 198 + 72 // Immediately unlocks a Tech as soon as the requirements are fulfilled *InstantTechResearch* @@ -5231,10 +5175,10 @@ condition : set(LogicElement) Relation - 2330 - 710 - 80 - 30 + 2106 + 648 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -5242,16 +5186,15 @@ condition : set(LogicElement) UMLClass - 4930 - 3340 - 250 - 110 + 4446 + 3015 + 225 + 99 *Herd* bg=green -- -range : float strength : int allowed_types : set(GameEntityType) blacklisted_entities : set(GameEntity) @@ -5260,10 +5203,10 @@ blacklisted_entities : set(GameEntity) Relation - 4890 - 3370 - 60 - 30 + 4410 + 3042 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -5271,10 +5214,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 880 - 510 - 280 - 80 + 801 + 468 + 252 + 72 *EntityContainerCapacity* bg=yellow @@ -5287,10 +5230,10 @@ container : EntityContainer UMLClass - 870 - 600 - 290 - 80 + 792 + 549 + 261 + 72 *StorageElementCapacity* bg=yellow @@ -5303,10 +5246,10 @@ storage_element : StorageElementDefinition Relation - 1150 - 540 - 80 - 30 + 1044 + 495 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -5314,10 +5257,10 @@ storage_element : StorageElementDefinition Relation - 1150 - 630 - 80 - 30 + 1044 + 576 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -5325,10 +5268,10 @@ storage_element : StorageElementDefinition UMLClass - 1260 - 740 - 290 - 80 + 1143 + 675 + 261 + 72 *ResearchTime* bg=yellow @@ -5341,10 +5284,10 @@ researchables : set(ResearchableTech) Relation - 1200 - 770 - 80 - 30 + 1089 + 702 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -5352,10 +5295,10 @@ researchables : set(ResearchableTech) UMLClass - 2390 - 500 - 280 - 100 + 2160 + 459 + 252 + 90 // Reveal area around listed units *Reveal* @@ -5370,10 +5313,10 @@ blacklisted_entities : set(GameEntity) Relation - 2330 - 530 - 80 - 30 + 2106 + 486 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -5381,10 +5324,10 @@ blacklisted_entities : set(GameEntity) UMLNote - 0 - 340 - 190 - 90 + 9 + 315 + 171 + 81 Orange elements: @@ -5397,10 +5340,10 @@ bg=orange UMLClass - 2390 - 860 - 320 - 100 + 2160 + 783 + 288 + 90 // The change values and fire rate of the provider and receiver are compared (divided) // with the result being added to the projectile amount of the receiver @@ -5419,10 +5362,10 @@ change_types : set(AttributeChangeType) UMLClass - 4640 - 3420 - 230 - 80 + 4185 + 3087 + 207 + 72 *RegenerateResourceSpot* bg=green @@ -5435,10 +5378,10 @@ resource_spot : ResourceSpot Relation - 4860 - 3450 - 60 - 30 + 4383 + 3114 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -5446,10 +5389,10 @@ resource_spot : ResourceSpot UMLClass - 1150 - 1760 - 120 - 60 + 1044 + 1593 + 108 + 54 // Only affect yourself (default for modifiers in GameEntity) @@ -5460,10 +5403,10 @@ bg=pink Relation - 1260 - 1720 - 70 - 100 + 1143 + 1557 + 63 + 90 lt=<<- 50.0;10.0;50.0;80.0;10.0;80.0 @@ -5471,10 +5414,10 @@ bg=pink UMLClass - 1020 - 1830 - 250 - 80 + 927 + 1656 + 225 + 72 // Affect all game entities in the list *GameEntityScope* @@ -5488,10 +5431,10 @@ blacklisted_entities : set(GameEntity) Relation - 1260 - 1790 - 70 - 90 + 1143 + 1620 + 63 + 81 lt=- 10.0;70.0;50.0;70.0;50.0;10.0 @@ -5499,21 +5442,21 @@ blacklisted_entities : set(GameEntity) Text - 0 - 0 - 230 - 30 + 9 + 9 + 207 + 27 - openage nyan data API v0.5.0 + openage nyan data API v0.6.0 UMLClass - 1420 - 3010 - 320 - 140 + 1287 + 2718 + 288 + 126 *StateChanger* bg=pink @@ -5531,10 +5474,10 @@ priority : int Relation - 1730 - 3060 - 70 - 30 + 1566 + 2763 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -5542,10 +5485,10 @@ priority : int UMLClass - 1980 - 500 - 310 - 90 + 1791 + 459 + 279 + 81 // Apply effects when in a container *InContainerContinuousEffect* @@ -5559,10 +5502,10 @@ ability : ApplyContinuousEffect Relation - 2280 - 530 - 80 - 30 + 2061 + 486 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -5570,10 +5513,10 @@ ability : ApplyContinuousEffect UMLClass - 1980 - 610 - 310 - 90 + 1791 + 558 + 279 + 81 // Apply effects when in a container *InContainerDiscreteEffect* @@ -5587,10 +5530,10 @@ ability : ApplyDiscreteEffect Relation - 2280 - 640 - 80 - 30 + 2061 + 585 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -5598,10 +5541,10 @@ ability : ApplyDiscreteEffect UMLClass - 4600 - 4160 - 110 - 60 + 4149 + 3753 + 99 + 54 *Normal* @@ -5611,10 +5554,10 @@ bg=pink Relation - 4550 - 4180 - 70 - 30 + 4104 + 3771 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -5622,10 +5565,10 @@ bg=pink UMLClass - 520 - 150 - 170 - 80 + 477 + 144 + 153 + 72 *Terrain* bg=yellow @@ -5637,10 +5580,10 @@ terrain : Terrain Relation - 680 - 180 - 60 - 30 + 621 + 171 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -5648,10 +5591,10 @@ terrain : Terrain UMLClass - 1530 - 0 - 170 - 80 + 1386 + 9 + 153 + 72 *Terrain* bg=yellow @@ -5663,10 +5606,10 @@ terrain : Terrain Relation - 1490 - 30 - 30 - 360 + 1350 + 36 + 27 + 324 lt=- 10.0;340.0;10.0;10.0 @@ -5674,10 +5617,10 @@ terrain : Terrain UMLClass - 2390 - 410 - 280 - 80 + 2160 + 378 + 252 + 72 // Reveal area around listed units *DiplomaticLineOfSight* @@ -5690,10 +5633,10 @@ diplomatic_stance : DiplomaticStance Relation - 2330 - 440 - 80 - 30 + 2106 + 405 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -5701,10 +5644,10 @@ diplomatic_stance : DiplomaticStance UMLClass - 1040 - 4910 - 120 - 60 + 945 + 4428 + 108 + 54 *LureType* @@ -5714,10 +5657,10 @@ bg=pink Relation - 960 - 4930 - 100 - 30 + 873 + 4446 + 90 + 27 lt=- 10.0;10.0;80.0;10.0 @@ -5725,10 +5668,10 @@ bg=pink UMLClass - 1040 - 4980 - 200 - 60 + 945 + 4491 + 180 + 54 *SendToContainerType* @@ -5738,10 +5681,10 @@ bg=pink Relation - 960 - 5000 - 100 - 30 + 873 + 4509 + 90 + 27 lt=- 10.0;10.0;80.0;10.0 @@ -5749,10 +5692,10 @@ bg=pink UMLClass - 3450 - 2760 - 140 - 60 + 3114 + 2493 + 126 + 54 *PlacementMode* @@ -5762,10 +5705,10 @@ bg=pink Relation - 3490 - 2710 - 30 - 70 + 3150 + 2448 + 27 + 63 lt=<. 10.0;50.0;10.0;10.0 @@ -5773,10 +5716,10 @@ bg=pink UMLClass - 3570 - 3070 - 110 - 60 + 3222 + 2772 + 99 + 54 *Eject* @@ -5786,10 +5729,10 @@ bg=pink UMLClass - 3570 - 2840 - 210 - 130 + 3222 + 2565 + 189 + 117 *Place* bg=pink @@ -5805,10 +5748,10 @@ max_elevation_difference : int Relation - 3520 - 2810 - 30 - 390 + 3177 + 2538 + 27 + 351 lt=<<- 10.0;10.0;10.0;370.0 @@ -5816,10 +5759,10 @@ max_elevation_difference : int Relation - 3520 - 2870 - 70 - 30 + 3177 + 2592 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -5827,10 +5770,10 @@ max_elevation_difference : int UMLClass - 370 - 3950 - 260 - 180 + 342 + 3564 + 234 + 162 *AdjacentTilesVariant* @@ -5849,10 +5792,10 @@ north_west : optional(GameEntity) Relation - 620 - 3890 - 50 - 120 + 567 + 3510 + 45 + 108 lt=- 30.0;10.0;30.0;100.0;10.0;100.0 @@ -5860,10 +5803,10 @@ north_west : optional(GameEntity) UMLClass - 520 - 4770 - 160 - 60 + 477 + 4302 + 144 + 54 *InverseLinear* @@ -5873,10 +5816,10 @@ bg=pink UMLClass - 3620 - 3830 - 180 - 80 + 3267 + 3456 + 162 + 72 *ExecutionSound* bg=pink @@ -5889,10 +5832,10 @@ sounds : set(Sound) Relation - 3570 - 3860 - 70 - 30 + 3222 + 3483 + 63 + 27 lt=- 50.0;10.0;10.0;10.0 @@ -5900,10 +5843,10 @@ sounds : set(Sound) Relation - 3570 - 3770 - 70 - 30 + 3222 + 3402 + 63 + 27 lt=- 50.0;10.0;10.0;10.0 @@ -5911,10 +5854,10 @@ sounds : set(Sound) UMLClass - 3620 - 3740 - 230 - 80 + 3267 + 3375 + 207 + 72 *AnimationOverride* bg=pink @@ -5926,10 +5869,10 @@ overrides : set(AnimationOverride) UMLClass - 1980 - 410 - 310 - 80 + 1791 + 378 + 279 + 72 *RefundOnCondition* bg=yellow @@ -5942,10 +5885,10 @@ refund_amount : set(ResourceAmount) Relation - 2280 - 440 - 80 - 30 + 2061 + 405 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -5953,10 +5896,10 @@ refund_amount : set(ResourceAmount) UMLClass - 1290 - 4410 - 230 - 80 + 1170 + 3978 + 207 + 72 *ProgressStatus* bg=pink @@ -5969,10 +5912,10 @@ progress : float Relation - 1510 - 4420 - 60 - 30 + 1368 + 3987 + 54 + 27 lt=<. 10.0;10.0;40.0;10.0 @@ -5980,10 +5923,10 @@ progress : float UMLClass - 170 - 6410 - 330 - 110 + 162 + 5778 + 297 + 99 *TimeRelativeAttributeChange* bg=pink @@ -5997,10 +5940,10 @@ ignore_protection : set(ProtectingAttribute) Relation - 490 - 6310 - 60 - 170 + 450 + 5688 + 54 + 153 lt=- 40.0;10.0;40.0;150.0;10.0;150.0 @@ -6008,10 +5951,10 @@ ignore_protection : set(ProtectingAttribute) UMLClass - 200 - 6540 - 260 - 60 + 189 + 5895 + 234 + 54 *TimeRelativeAttributeDecrease* @@ -6021,10 +5964,10 @@ bg=orange UMLClass - 200 - 6610 - 260 - 60 + 189 + 5958 + 234 + 54 *TimeRelativeAttributeIncrease* @@ -6034,10 +5977,10 @@ bg=orange Relation - 450 - 6560 - 50 - 100 + 414 + 5913 + 45 + 90 lt=- 10.0;80.0;30.0;80.0;30.0;10.0 @@ -6045,10 +5988,10 @@ bg=orange Relation - 450 - 6510 - 50 - 80 + 414 + 5868 + 45 + 72 lt=->> 10.0;60.0;30.0;60.0;30.0;10.0 @@ -6056,10 +5999,10 @@ bg=orange UMLClass - 250 - 6690 - 250 - 90 + 234 + 6030 + 225 + 81 *TimeRelativeProgressChange* bg=pink @@ -6072,10 +6015,10 @@ total_change_time : float Relation - 490 - 6450 - 60 - 300 + 450 + 5814 + 54 + 270 lt=- 40.0;10.0;40.0;280.0;10.0;280.0 @@ -6083,10 +6026,10 @@ total_change_time : float UMLClass - 1020 - 6410 - 250 - 90 + 927 + 5778 + 225 + 81 *TimeRelativeAttributeChange* bg=pink @@ -6098,10 +6041,10 @@ type : AttributeChangeType Relation - 1260 - 6310 - 60 - 160 + 1143 + 5688 + 54 + 144 lt=- 40.0;10.0;40.0;140.0;10.0;140.0 @@ -6109,10 +6052,10 @@ type : AttributeChangeType UMLClass - 1020 - 6540 - 220 - 60 + 927 + 5895 + 198 + 54 *FlatAttributeDecrease* @@ -6122,10 +6065,10 @@ bg=orange UMLClass - 1020 - 6610 - 220 - 60 + 927 + 5958 + 198 + 54 *FlatAttributeIncrease* @@ -6135,10 +6078,10 @@ bg=orange Relation - 1230 - 6560 - 50 - 100 + 1116 + 5913 + 45 + 90 lt=- 10.0;80.0;30.0;80.0;30.0;10.0 @@ -6146,10 +6089,10 @@ bg=orange Relation - 1230 - 6490 - 50 - 100 + 1116 + 5850 + 45 + 90 lt=->> 10.0;80.0;30.0;80.0;30.0;10.0 @@ -6157,10 +6100,10 @@ bg=orange UMLClass - 1020 - 6690 - 250 - 90 + 927 + 6030 + 225 + 81 *TimeRelativeProgressChange* bg=pink @@ -6173,10 +6116,10 @@ type : ProgressType Relation - 1260 - 6440 - 60 - 310 + 1143 + 5805 + 54 + 279 lt=- 40.0;10.0;40.0;290.0;10.0;290.0 @@ -6184,10 +6127,10 @@ type : ProgressType UMLClass - 2220 - 4180 - 120 - 60 + 2007 + 3771 + 108 + 54 *ProgressType* @@ -6197,10 +6140,10 @@ bg=pink Relation - 2140 - 4200 - 100 - 30 + 1935 + 3789 + 90 + 27 lt=- 10.0;10.0;80.0;10.0 @@ -6208,10 +6151,10 @@ bg=pink UMLClass - 5040 - 2670 - 270 - 80 + 4545 + 2412 + 243 + 72 *AoE1TradeRoute* bg=pink @@ -6224,10 +6167,10 @@ trade_amount : int UMLClass - 2460 - 4930 - 210 - 80 + 2223 + 4446 + 189 + 72 *StateChange* bg=pink @@ -6239,10 +6182,10 @@ state_change : StateChanger Relation - 2420 - 4960 - 60 - 30 + 2187 + 4473 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -6250,10 +6193,10 @@ state_change : StateChanger UMLClass - 2460 - 4750 - 150 - 80 + 2223 + 4284 + 135 + 72 *Terrain* bg=pink @@ -6267,10 +6210,10 @@ terrain : Terrain Relation - 2420 - 4780 - 60 - 30 + 2187 + 4311 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -6278,10 +6221,10 @@ terrain : Terrain Relation - 5970 - 4700 - 60 - 30 + 5382 + 4239 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -6289,10 +6232,10 @@ terrain : Terrain UMLClass - 6010 - 4670 - 260 - 80 + 5418 + 4212 + 234 + 72 *TerrainRequirement* bg=green @@ -6305,10 +6248,10 @@ blacklisted_terrains : set(Terrain) UMLClass - 6010 - 4570 - 250 - 90 + 5418 + 4122 + 225 + 81 *OverlayTerrain* bg=green @@ -6320,10 +6263,10 @@ terrain_overlay : Terrain UMLClass - 4950 - 4930 - 240 - 90 + 4464 + 4446 + 216 + 81 *Attribute* bg=pink @@ -6337,10 +6280,10 @@ abbreviation : TranslatedString Relation - 5060 - 4870 - 30 - 80 + 4563 + 4392 + 27 + 72 lt=<. 10.0;60.0;10.0;10.0 @@ -6348,10 +6291,10 @@ abbreviation : TranslatedString UMLClass - 1980 - 720 - 310 - 120 + 1791 + 657 + 279 + 108 *DepositResourcesOnProgress* bg=yellow @@ -6366,10 +6309,10 @@ blacklisted_entities : set(GameEntity) Relation - 2280 - 750 - 80 - 30 + 2061 + 684 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -6377,10 +6320,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 4410 - 2130 - 140 - 60 + 3978 + 1926 + 126 + 54 *PriceMode* @@ -6390,10 +6333,10 @@ bg=pink Relation - 4470 - 2400 - 30 - 80 + 4032 + 2169 + 27 + 72 lt=<. 10.0;10.0;10.0;60.0 @@ -6401,10 +6344,10 @@ bg=pink Relation - 4540 - 2150 - 170 - 30 + 4095 + 1944 + 153 + 27 lt=<<- 10.0;10.0;150.0;10.0 @@ -6412,10 +6355,10 @@ bg=pink UMLClass - 4690 - 2130 - 110 - 60 + 4230 + 1926 + 99 + 54 *Fixed* @@ -6425,10 +6368,10 @@ bg=pink Relation - 3520 - 3090 - 70 - 30 + 3177 + 2790 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -6436,10 +6379,10 @@ bg=pink Relation - 5000 - 2700 - 60 - 30 + 4509 + 2439 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -6447,10 +6390,10 @@ bg=pink Relation - 4650 - 2150 - 30 - 100 + 4194 + 1944 + 27 + 90 lt=- 10.0;10.0;10.0;80.0 @@ -6458,10 +6401,10 @@ bg=pink Relation - 4650 - 2220 - 60 - 30 + 4194 + 2007 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -6469,10 +6412,10 @@ bg=pink UMLClass - 4690 - 2200 - 150 - 100 + 4230 + 1989 + 135 + 90 *Dynamic* bg=pink @@ -6486,10 +6429,10 @@ max_price : float UMLClass - 4770 - 2330 - 120 - 60 + 4302 + 2106 + 108 + 54 *PricePool* @@ -6499,10 +6442,10 @@ bg=pink Relation - 4700 - 2350 - 90 - 30 + 4239 + 2124 + 81 + 27 lt=<. 70.0;10.0;10.0;10.0 @@ -6510,10 +6453,10 @@ bg=pink UMLClass - 750 - 80 - 250 - 60 + 684 + 81 + 225 + 54 *TimeRelativeAttributeChange* @@ -6523,10 +6466,10 @@ bg=yellow Relation - 990 - 100 - 90 - 30 + 900 + 99 + 81 + 27 lt=- 70.0;10.0;10.0;10.0 @@ -6534,10 +6477,10 @@ bg=yellow UMLClass - 770 - 10 - 230 - 60 + 702 + 18 + 207 + 54 *TimeRelativeProgressChange* @@ -6547,10 +6490,10 @@ bg=yellow Relation - 990 - 30 - 90 - 30 + 900 + 36 + 81 + 27 lt=- 70.0;10.0;10.0;10.0 @@ -6558,10 +6501,10 @@ bg=yellow UMLClass - 540 - 500 - 150 - 60 + 495 + 459 + 135 + 54 *Unconditional* @@ -6571,10 +6514,10 @@ bg=yellow Relation - 710 - 180 - 30 - 370 + 648 + 171 + 27 + 333 lt=- 10.0;10.0;10.0;350.0 @@ -6582,10 +6525,10 @@ bg=yellow UMLClass - 1530 - 340 - 170 - 60 + 1386 + 315 + 153 + 54 *Unconditional* @@ -6595,10 +6538,10 @@ bg=yellow Relation - 1490 - 360 - 60 - 30 + 1350 + 333 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -6606,10 +6549,10 @@ bg=yellow UMLClass - 1010 - 2740 - 240 - 80 + 918 + 2475 + 216 + 72 *Cost* bg=pink @@ -6621,10 +6564,10 @@ payment_mode : PaymentMode UMLClass - 1310 - 2690 - 230 - 80 + 1188 + 2430 + 207 + 72 *ResourceCost* bg=pink @@ -6636,10 +6579,10 @@ amount : set(ResourceAmount) UMLClass - 1310 - 2790 - 230 - 80 + 1188 + 2520 + 207 + 72 *AttributeCost* bg=pink @@ -6651,10 +6594,10 @@ amount : set(AttributeAmount) Relation - 1240 - 2770 - 70 - 30 + 1125 + 2502 + 63 + 27 lt=<<- 10.0;10.0;50.0;10.0 @@ -6662,10 +6605,10 @@ amount : set(AttributeAmount) Relation - 1280 - 2720 - 50 - 70 + 1161 + 2457 + 45 + 63 lt=- 10.0;50.0;10.0;10.0;30.0;10.0 @@ -6673,10 +6616,10 @@ amount : set(AttributeAmount) Relation - 1280 - 2760 - 50 - 90 + 1161 + 2493 + 45 + 81 lt=- 10.0;10.0;10.0;70.0;30.0;70.0 @@ -6684,10 +6627,10 @@ amount : set(AttributeAmount) UMLClass - 1260 - 830 - 300 - 80 + 1143 + 756 + 270 + 72 *CreationAttributeCost* bg=yellow @@ -6701,10 +6644,10 @@ creatables : set(CreatableGameEntity) Relation - 1200 - 860 - 80 - 30 + 1089 + 783 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -6712,10 +6655,10 @@ creatables : set(CreatableGameEntity) Relation - 1150 - 1040 - 80 - 30 + 1044 + 945 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -6723,10 +6666,10 @@ creatables : set(CreatableGameEntity) UMLClass - 1260 - 560 - 290 - 80 + 1143 + 513 + 261 + 72 *ResearchAttributeCost* bg=yellow @@ -6740,10 +6683,10 @@ researchables : set(ResearchableTech) Relation - 1200 - 590 - 80 - 30 + 1089 + 540 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -6751,10 +6694,10 @@ researchables : set(ResearchableTech) Relation - 1120 - 2810 - 30 - 70 + 1017 + 2538 + 27 + 63 lt=- 10.0;50.0;10.0;10.0 @@ -6762,10 +6705,10 @@ researchables : set(ResearchableTech) UMLClass - 1060 - 2640 - 140 - 60 + 963 + 2385 + 126 + 54 *PaymentMode* @@ -6775,10 +6718,10 @@ bg=pink Relation - 1120 - 2690 - 30 - 70 + 1017 + 2430 + 27 + 63 lt=<. 10.0;10.0;10.0;50.0 @@ -6786,10 +6729,10 @@ bg=pink UMLClass - 1000 - 2420 - 110 - 60 + 909 + 2187 + 99 + 54 *Advance* @@ -6799,10 +6742,10 @@ bg=pink UMLClass - 1000 - 2560 - 110 - 60 + 909 + 2313 + 99 + 54 *Arrear* @@ -6812,10 +6755,10 @@ bg=pink UMLClass - 1000 - 2490 - 110 - 60 + 909 + 2250 + 99 + 54 *Adaptive* @@ -6825,10 +6768,10 @@ bg=pink Relation - 1140 - 2370 - 30 - 290 + 1035 + 2142 + 27 + 261 lt=<<- 10.0;270.0;10.0;10.0 @@ -6836,10 +6779,10 @@ bg=pink Relation - 1100 - 2580 - 70 - 30 + 999 + 2331 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -6847,10 +6790,10 @@ bg=pink Relation - 1100 - 2510 - 70 - 30 + 999 + 2268 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -6858,10 +6801,10 @@ bg=pink Relation - 1100 - 2440 - 70 - 30 + 999 + 2205 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -6869,10 +6812,10 @@ bg=pink UMLClass - 2450 - 4010 - 160 - 60 + 2214 + 3618 + 144 + 54 *AttributeChange* @@ -6882,10 +6825,10 @@ bg=pink Relation - 2330 - 4200 - 110 - 30 + 2106 + 3789 + 99 + 27 lt=<<- 10.0;10.0;90.0;10.0 @@ -6893,10 +6836,10 @@ bg=pink UMLClass - 200 - 6800 - 260 - 60 + 189 + 6129 + 234 + 54 *TimeRelativeProgressDecrease* @@ -6906,10 +6849,10 @@ bg=orange UMLClass - 200 - 6870 - 260 - 60 + 189 + 6192 + 234 + 54 *TimeRelativeProgressIncrease* @@ -6919,10 +6862,10 @@ bg=orange Relation - 450 - 6820 - 50 - 100 + 414 + 6147 + 45 + 90 lt=- 10.0;80.0;30.0;80.0;30.0;10.0 @@ -6930,10 +6873,10 @@ bg=orange Relation - 450 - 6770 - 50 - 80 + 414 + 6102 + 45 + 72 lt=->> 10.0;60.0;30.0;60.0;30.0;10.0 @@ -6941,10 +6884,10 @@ bg=orange UMLClass - 980 - 6800 - 260 - 60 + 891 + 6129 + 234 + 54 *TimeRelativeProgressDecrease* @@ -6954,10 +6897,10 @@ bg=orange UMLClass - 980 - 6870 - 260 - 60 + 891 + 6192 + 234 + 54 *TimeRelativeProgressIncrease* @@ -6967,10 +6910,10 @@ bg=orange Relation - 1230 - 6820 - 50 - 100 + 1116 + 6147 + 45 + 90 lt=- 10.0;80.0;30.0;80.0;30.0;10.0 @@ -6978,10 +6921,10 @@ bg=orange Relation - 1230 - 6770 - 50 - 80 + 1116 + 6102 + 45 + 72 lt=->> 10.0;60.0;30.0;60.0;30.0;10.0 @@ -6989,10 +6932,10 @@ bg=orange UMLClass - 1000 - 2350 - 110 - 60 + 909 + 2124 + 99 + 54 *Shadow* @@ -7002,10 +6945,10 @@ bg=pink Relation - 1100 - 2370 - 70 - 30 + 999 + 2142 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -7013,10 +6956,10 @@ bg=pink UMLClass - 6510 - 4060 - 160 - 60 + 5868 + 3663 + 144 + 54 *HerdableMode* @@ -7026,10 +6969,10 @@ bg=pink Relation - 6580 - 4020 - 30 - 60 + 5931 + 3627 + 27 + 54 lt=<. 10.0;40.0;10.0;10.0 @@ -7037,10 +6980,10 @@ bg=pink UMLClass - 6600 - 4150 - 160 - 60 + 5949 + 3744 + 144 + 54 *ClosestHerding* @@ -7050,10 +6993,10 @@ bg=pink Relation - 6550 - 4110 - 30 - 230 + 5904 + 3708 + 27 + 207 lt=<<- 10.0;10.0;10.0;210.0 @@ -7061,10 +7004,10 @@ bg=pink Relation - 6550 - 4170 - 70 - 30 + 5904 + 3762 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -7072,10 +7015,10 @@ bg=pink UMLClass - 6600 - 4220 - 200 - 60 + 5949 + 3807 + 180 + 54 *LongestTimeInRange* @@ -7085,10 +7028,10 @@ bg=pink UMLClass - 6600 - 4290 - 160 - 60 + 5949 + 3870 + 144 + 54 *MostHerding* @@ -7098,10 +7041,10 @@ bg=pink Relation - 6550 - 4240 - 70 - 30 + 5904 + 3825 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -7109,10 +7052,10 @@ bg=pink Relation - 6550 - 4310 - 70 - 30 + 5904 + 3888 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -7120,10 +7063,10 @@ bg=pink UMLClass - 1400 - 3900 - 120 - 60 + 1269 + 3519 + 108 + 54 *TerrainType* @@ -7133,10 +7076,10 @@ bg=pink Relation - 1450 - 3850 - 30 - 70 + 1314 + 3474 + 27 + 63 lt=<. 10.0;50.0;10.0;10.0 @@ -7144,10 +7087,10 @@ bg=pink UMLClass - 1230 - 5370 - 260 - 90 + 1116 + 4842 + 234 + 81 *Stacked* bg=pink @@ -7162,10 +7105,10 @@ distribution_type : DistributionType Relation - 1180 - 5310 - 70 - 30 + 1071 + 4788 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -7173,10 +7116,10 @@ distribution_type : DistributionType UMLClass - 1620 - 5390 - 160 - 60 + 1467 + 4860 + 144 + 54 *CalculationType* @@ -7190,10 +7133,10 @@ bg=pink Relation - 1480 - 5410 - 160 - 30 + 1341 + 4878 + 144 + 27 lt=<. 140.0;10.0;10.0;10.0 @@ -7201,10 +7144,10 @@ bg=pink UMLClass - 1700 - 5650 - 160 - 100 + 1539 + 5094 + 144 + 90 *Hyperbolic* bg=pink @@ -7221,10 +7164,10 @@ scale_factor : float Relation - 1660 - 5440 - 30 - 260 + 1503 + 4905 + 27 + 234 lt=<<- 10.0;10.0;10.0;240.0 @@ -7232,10 +7175,10 @@ scale_factor : float Relation - 1500 - 6230 - 60 - 30 + 1359 + 5616 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -7243,10 +7186,10 @@ scale_factor : float Relation - 1660 - 5670 - 60 - 30 + 1503 + 5112 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -7254,10 +7197,10 @@ scale_factor : float UMLClass - 1700 - 5540 - 160 - 100 + 1539 + 4995 + 144 + 90 *Linear* bg=pink @@ -7274,10 +7217,10 @@ scale_factor : float Relation - 1660 - 5560 - 60 - 30 + 1503 + 5013 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -7285,10 +7228,10 @@ scale_factor : float UMLClass - 1700 - 5470 - 120 - 60 + 1539 + 4932 + 108 + 54 *NoStack* @@ -7301,10 +7244,10 @@ bg=pink Relation - 1660 - 5490 - 60 - 30 + 1503 + 4950 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -7312,10 +7255,10 @@ bg=pink UMLClass - 3570 - 3140 - 190 - 80 + 3222 + 2835 + 171 + 72 *OwnStorage* bg=pink @@ -7327,10 +7270,10 @@ container : EntityContainer Relation - 3520 - 3170 - 70 - 30 + 3177 + 2862 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -7338,10 +7281,10 @@ container : EntityContainer UMLClass - 3580 - 2330 - 310 - 80 + 3231 + 2106 + 279 + 72 *ProductionQueue* bg=green @@ -7354,10 +7297,10 @@ production_modes : set(ProductionMode) Relation - 3880 - 2360 - 60 - 120 + 3501 + 2133 + 54 + 108 lt=- 40.0;100.0;40.0;10.0;10.0;10.0 @@ -7365,10 +7308,10 @@ production_modes : set(ProductionMode) UMLClass - 3670 - 2230 - 160 - 60 + 3312 + 2016 + 144 + 54 *ProductionMode* @@ -7378,10 +7321,10 @@ bg=pink Relation - 3740 - 2280 - 30 - 70 + 3375 + 2061 + 27 + 63 lt=<. 10.0;10.0;10.0;50.0 @@ -7389,10 +7332,10 @@ bg=pink UMLClass - 3940 - 2310 - 270 - 80 + 3555 + 2088 + 243 + 72 *Creatables* bg=pink @@ -7404,10 +7347,10 @@ exclude : set(CreatableGameEntity) Relation - 3820 - 2250 - 140 - 30 + 3447 + 2034 + 126 + 27 lt=<<- 10.0;10.0;120.0;10.0 @@ -7415,10 +7358,10 @@ exclude : set(CreatableGameEntity) UMLClass - 3940 - 2220 - 240 - 80 + 3555 + 2007 + 216 + 72 *Researchables* bg=pink @@ -7430,10 +7373,10 @@ exclude : set(ResearchableTech) Relation - 3900 - 2250 - 60 - 120 + 3519 + 2034 + 54 + 108 lt=- 10.0;10.0;10.0;100.0;40.0;100.0 @@ -7441,10 +7384,10 @@ exclude : set(ResearchableTech) UMLClass - 1640 - 4020 - 200 - 80 + 1485 + 3627 + 180 + 72 *LogicElement* bg=pink @@ -7456,10 +7399,10 @@ only_once : bool UMLClass - 1990 - 4120 - 100 - 60 + 1800 + 3717 + 90 + 54 *AND* @@ -7469,10 +7412,10 @@ bg=pink Relation - 1960 - 4090 - 30 - 320 + 1773 + 3690 + 27 + 288 lt=<<- 10.0;10.0;10.0;300.0 @@ -7480,10 +7423,10 @@ bg=pink UMLClass - 1990 - 4190 - 100 - 60 + 1800 + 3780 + 90 + 54 *OR* @@ -7493,10 +7436,10 @@ bg=pink UMLClass - 1990 - 4260 - 140 - 80 + 1800 + 3843 + 126 + 72 *SUBSETMIN* bg=pink @@ -7508,10 +7451,10 @@ size : int Relation - 1940 - 4210 - 50 - 30 + 1755 + 3798 + 45 + 27 lt=- 30.0;10.0;10.0;10.0 @@ -7519,10 +7462,10 @@ size : int Relation - 1960 - 4210 - 50 - 30 + 1773 + 3798 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -7530,10 +7473,10 @@ size : int Relation - 1960 - 4140 - 50 - 30 + 1773 + 3735 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -7541,10 +7484,10 @@ size : int Relation - 6350 - 3890 - 60 - 30 + 5724 + 3510 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -7552,10 +7495,10 @@ size : int Relation - 6380 - 3790 - 60 - 30 + 5751 + 3420 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -7563,10 +7506,10 @@ size : int Relation - 6380 - 3690 - 60 - 30 + 5751 + 3330 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -7574,10 +7517,10 @@ size : int UMLClass - 1340 - 4150 - 210 - 80 + 1215 + 3744 + 189 + 72 *LiteralScope* bg=pink @@ -7589,10 +7532,10 @@ stances : set(DiplomaticStance) Relation - 1540 - 4180 - 90 - 30 + 1395 + 3771 + 81 + 27 lt=<. 10.0;10.0;70.0;10.0 @@ -7600,10 +7543,10 @@ stances : set(DiplomaticStance) UMLClass - 1310 - 4250 - 100 - 60 + 1188 + 3834 + 90 + 54 *Any* @@ -7614,10 +7557,10 @@ bg=pink Relation - 1420 - 4220 - 30 - 150 + 1287 + 3807 + 27 + 135 lt=<<- 10.0;10.0;10.0;130.0 @@ -7625,10 +7568,10 @@ bg=pink UMLClass - 1310 - 4320 - 100 - 60 + 1188 + 3897 + 90 + 54 *Self* @@ -7638,10 +7581,10 @@ bg=pink Relation - 1400 - 4340 - 50 - 30 + 1269 + 3915 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -7649,10 +7592,10 @@ bg=pink Relation - 1400 - 4270 - 50 - 30 + 1269 + 3852 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -7660,10 +7603,10 @@ bg=pink Relation - 1770 - 4290 - 50 - 30 + 1602 + 3870 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -7671,10 +7614,10 @@ bg=pink Relation - 1770 - 4380 - 50 - 30 + 1602 + 3951 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -7682,10 +7625,10 @@ bg=pink UMLClass - 1560 - 4450 - 220 - 80 + 1413 + 4014 + 198 + 72 *ResourceSpotsDepleted* bg=pink @@ -7697,10 +7640,10 @@ only_enabled : bool UMLClass - 1560 - 4540 - 220 - 80 + 1413 + 4095 + 198 + 72 *Timer* bg=pink @@ -7712,10 +7655,10 @@ time : float Relation - 1770 - 4480 - 50 - 30 + 1602 + 4041 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -7723,10 +7666,10 @@ time : float Relation - 1770 - 4570 - 50 - 30 + 1602 + 4122 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -7734,10 +7677,10 @@ time : float UMLClass - 1560 - 4630 - 220 - 80 + 1413 + 4176 + 198 + 72 *AttributeBelowValue* bg=pink @@ -7750,10 +7693,10 @@ threshold : float UMLClass - 1560 - 4900 - 220 - 80 + 1413 + 4419 + 198 + 72 *ProjectilePassThrough* bg=pink @@ -7765,10 +7708,10 @@ pass_through_range : int UMLClass - 1590 - 4990 - 190 - 60 + 1440 + 4500 + 171 + 54 *ProjectileHitTerrain* @@ -7778,10 +7721,10 @@ bg=pink Relation - 1770 - 4660 - 50 - 30 + 1602 + 4203 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -7789,10 +7732,10 @@ bg=pink Relation - 1770 - 4930 - 50 - 30 + 1602 + 4446 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -7800,10 +7743,10 @@ bg=pink Relation - 1770 - 5010 - 50 - 30 + 1602 + 4518 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -7811,10 +7754,10 @@ bg=pink UMLClass - 5380 - 4420 - 140 - 100 + 4851 + 3987 + 126 + 90 *Hitbox* bg=pink @@ -7828,10 +7771,10 @@ radius_z : float Relation - 5310 - 4450 - 90 - 30 + 4788 + 4014 + 81 + 27 lt=<. 70.0;10.0;10.0;10.0 @@ -7839,16 +7782,15 @@ radius_z : float UMLClass - 5100 - 4100 - 270 - 100 + 4599 + 3699 + 243 + 90 *DetectCloak (SWGB)* bg=green -- -range : float allowed_types : set(GameEntityType) blacklisted_entities : set(GameEntity) @@ -7856,10 +7798,10 @@ blacklisted_entities : set(GameEntity) Relation - 5060 - 4130 - 60 - 30 + 4563 + 3726 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -7867,10 +7809,10 @@ blacklisted_entities : set(GameEntity) UMLClass - 1420 - 5490 - 160 - 60 + 1287 + 4950 + 144 + 54 *DistributionType* @@ -7886,10 +7828,10 @@ bg=pink Relation - 1360 - 5450 - 80 - 90 + 1233 + 4914 + 72 + 81 lt=<. 60.0;70.0;10.0;70.0;10.0;10.0 @@ -7897,10 +7839,10 @@ bg=pink UMLClass - 1510 - 5570 - 100 - 60 + 1368 + 5022 + 90 + 54 *Mean* @@ -7910,10 +7852,10 @@ bg=pink Relation - 1470 - 5540 - 30 - 80 + 1332 + 4995 + 27 + 72 lt=<<- 10.0;10.0;10.0;60.0 @@ -7921,10 +7863,10 @@ bg=pink Relation - 1470 - 5590 - 60 - 30 + 1332 + 5040 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -7932,10 +7874,10 @@ bg=pink UMLClass - 6250 - 4080 - 140 - 90 + 5634 + 3681 + 126 + 81 *Rectangle* bg=pink @@ -7948,10 +7890,10 @@ height : float UMLClass - 6070 - 4080 - 140 - 60 + 5472 + 3681 + 126 + 54 *MatchToSprite* @@ -7961,10 +7903,10 @@ bg=pink UMLClass - 6160 - 3980 - 140 - 60 + 5553 + 3591 + 126 + 54 *SelectionBox* @@ -7974,10 +7916,10 @@ bg=pink Relation - 6220 - 3940 - 30 - 60 + 5607 + 3555 + 27 + 54 lt=<. 10.0;40.0;10.0;10.0 @@ -7985,10 +7927,10 @@ bg=pink Relation - 6260 - 4030 - 30 - 70 + 5643 + 3636 + 27 + 63 lt=<<- 10.0;10.0;10.0;50.0 @@ -7996,10 +7938,10 @@ bg=pink Relation - 6180 - 4030 - 30 - 70 + 5571 + 3636 + 27 + 63 lt=<<- 10.0;10.0;10.0;50.0 @@ -8007,10 +7949,10 @@ bg=pink UMLClass - 1440 - 1760 - 170 - 80 + 1305 + 1593 + 153 + 72 *Stacked* bg=pink @@ -8022,10 +7964,10 @@ stack_limit : int Relation - 1650 - 1620 - 30 - 290 + 1494 + 1467 + 27 + 261 lt=<<- 10.0;10.0;10.0;270.0 @@ -8033,10 +7975,10 @@ stack_limit : int Relation - 5970 - 4600 - 60 - 30 + 5382 + 4149 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -8044,10 +7986,10 @@ stack_limit : int UMLClass - 1290 - 3320 - 120 - 60 + 1170 + 2997 + 108 + 54 *NyanPatch* @@ -8057,10 +7999,10 @@ bg=pink Relation - 1230 - 3340 - 80 - 30 + 1116 + 3015 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -8068,10 +8010,10 @@ bg=pink UMLClass - 1820 - 4630 - 220 - 80 + 1647 + 4176 + 198 + 72 *AttributeAbovePercentage* bg=pink @@ -8084,10 +8026,10 @@ threshold : float Relation - 1770 - 4750 - 50 - 30 + 1602 + 4284 + 45 + 27 lt=- 30.0;10.0;10.0;10.0 @@ -8095,10 +8037,10 @@ threshold : float Relation - 2040 - 3540 - 90 - 30 + 1845 + 3195 + 81 + 27 lt=- 70.0;10.0;10.0;10.0 @@ -8106,10 +8048,10 @@ threshold : float UMLClass - 2110 - 3420 - 190 - 80 + 1908 + 3087 + 171 + 72 *Palette* bg=pink @@ -8121,10 +8063,10 @@ palette : file Relation - 2040 - 3450 - 90 - 30 + 1845 + 3114 + 81 + 27 lt=- 70.0;10.0;10.0;10.0 @@ -8132,10 +8074,10 @@ palette : file Relation - 4550 - 3860 - 70 - 30 + 4104 + 3483 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -8143,10 +8085,10 @@ palette : file UMLClass - 4600 - 4000 - 160 - 80 + 4149 + 3609 + 144 + 72 *Guard* bg=pink @@ -8158,10 +8100,10 @@ range : float Relation - 4550 - 4030 - 70 - 30 + 4104 + 3636 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -8169,10 +8111,10 @@ range : float UMLClass - 3570 - 2980 - 210 - 80 + 3222 + 2691 + 189 + 72 *Replace* bg=pink @@ -8184,10 +8126,10 @@ game_entities : set(GameEntity) Relation - 3520 - 3010 - 70 - 30 + 3177 + 2718 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -8195,10 +8137,10 @@ game_entities : set(GameEntity) UMLClass - 1070 - 4000 - 120 - 60 + 972 + 3609 + 108 + 54 *TechType* @@ -8208,10 +8150,10 @@ bg=pink Relation - 1180 - 4020 - 80 - 30 + 1071 + 3627 + 72 + 27 lt=- 10.0;10.0;60.0;10.0 @@ -8219,10 +8161,10 @@ bg=pink Relation - 1120 - 4050 - 30 - 70 + 1017 + 3654 + 27 + 63 lt=<. 10.0;10.0;10.0;50.0 @@ -8230,10 +8172,10 @@ bg=pink UMLClass - 1090 - 3910 - 100 - 60 + 990 + 3528 + 90 + 54 *Any* @@ -8243,10 +8185,10 @@ bg=pink Relation - 1130 - 3960 - 30 - 60 + 1026 + 3573 + 27 + 54 lt=<<- 10.0;40.0;10.0;10.0 @@ -8254,10 +8196,10 @@ bg=pink UMLClass - 1410 - 3990 - 100 - 60 + 1278 + 3600 + 90 + 54 *Any* @@ -8267,10 +8209,10 @@ bg=pink Relation - 1450 - 3950 - 30 - 60 + 1314 + 3564 + 27 + 54 lt=<<- 10.0;10.0;10.0;40.0 @@ -8278,10 +8220,10 @@ bg=pink UMLClass - 720 - 3480 - 100 - 60 + 657 + 3141 + 90 + 54 *Any* @@ -8291,10 +8233,10 @@ bg=pink Relation - 810 - 3500 - 70 - 70 + 738 + 3159 + 63 + 63 lt=<<- 50.0;50.0;50.0;10.0;10.0;10.0 @@ -8302,10 +8244,10 @@ bg=pink UMLClass - 2460 - 4570 - 230 - 80 + 2223 + 4122 + 207 + 72 *AnimationOverlay* bg=pink @@ -8318,10 +8260,10 @@ overlays : set(Animation) Relation - 2420 - 4600 - 60 - 30 + 2187 + 4149 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -8329,10 +8271,10 @@ overlays : set(Animation) UMLClass - 1820 - 4540 - 220 - 80 + 1647 + 4095 + 198 + 72 *AttributeBelowPercentage* bg=pink @@ -8345,10 +8287,10 @@ threshold : float UMLClass - 1560 - 4720 - 220 - 80 + 1413 + 4257 + 198 + 72 *AttributeAboveValue* bg=pink @@ -8361,10 +8303,10 @@ threshold : float Relation - 1790 - 4570 - 50 - 30 + 1620 + 4122 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -8372,10 +8314,10 @@ threshold : float Relation - 1790 - 4660 - 50 - 30 + 1620 + 4203 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -8383,10 +8325,10 @@ threshold : float UMLClass - 370 - 420 - 320 - 70 + 342 + 387 + 288 + 63 *ElevationDifferenceHigh* bg=yellow @@ -8398,10 +8340,10 @@ min_elevation_difference : optional(float) = None Relation - 680 - 450 - 60 - 30 + 621 + 414 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -8409,10 +8351,10 @@ min_elevation_difference : optional(float) = None Relation - 680 - 520 - 60 - 30 + 621 + 477 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -8420,10 +8362,10 @@ min_elevation_difference : optional(float) = None UMLClass - 1530 + 1386 90 - 320 - 80 + 288 + 72 *ElevationDifferenceHigh* bg=yellow @@ -8435,10 +8377,10 @@ min_elevation_difference : optional(float) = None Relation - 1490 - 120 - 60 - 30 + 1350 + 117 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -8446,10 +8388,10 @@ min_elevation_difference : optional(float) = None UMLClass - 370 - 4140 - 130 - 60 + 342 + 3735 + 117 + 54 *MiscVariant* @@ -8459,10 +8401,10 @@ bg=pink Relation - 490 - 3970 - 180 - 220 + 450 + 3582 + 162 + 198 lt=- 160.0;10.0;160.0;200.0;10.0;200.0 @@ -8470,10 +8412,10 @@ bg=pink UMLClass - 4630 - 3330 - 240 - 80 + 4176 + 3006 + 216 + 72 *ResourceStorage* bg=green @@ -8485,10 +8427,10 @@ containers : set(ResourceContainer) Relation - 4860 - 3360 - 60 - 30 + 4383 + 3033 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -8496,10 +8438,10 @@ containers : set(ResourceContainer) UMLClass - 4360 - 3330 - 240 - 100 + 3933 + 3006 + 216 + 90 *ResourceContainer* bg=pink @@ -8513,10 +8455,10 @@ carry_progress : set(Progress) Relation - 4590 - 3360 - 60 - 30 + 4140 + 3033 + 54 + 27 lt=<. 10.0;10.0;40.0;10.0 @@ -8524,10 +8466,10 @@ carry_progress : set(Progress) UMLClass - 4390 - 3460 - 180 - 80 + 3960 + 3123 + 162 + 72 *InternalDropSite* bg=pink @@ -8539,10 +8481,10 @@ update_time : float Relation - 4470 - 3420 - 30 - 60 + 4032 + 3087 + 27 + 54 lt=<<- 10.0;10.0;10.0;40.0 @@ -8550,10 +8492,10 @@ update_time : float UMLClass - 1260 - 3010 - 140 - 60 + 1143 + 2718 + 126 + 54 *TransformPool* @@ -8563,10 +8505,10 @@ bg=pink Relation - 1390 - 3030 - 50 - 30 + 1260 + 2736 + 45 + 27 lt=<. 10.0;10.0;30.0;10.0 @@ -8574,10 +8516,10 @@ bg=pink Relation - 4450 - 4510 - 60 - 30 + 4014 + 4068 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -8585,10 +8527,10 @@ bg=pink UMLClass - 2360 - 3320 - 100 - 60 + 2133 + 2997 + 90 + 54 *Any* @@ -8598,10 +8540,10 @@ bg=pink Relation - 2400 - 3280 - 30 - 60 + 2169 + 2961 + 27 + 54 lt=- 10.0;40.0;10.0;10.0 @@ -8609,10 +8551,10 @@ bg=pink Relation - 2520 - 3280 - 30 - 60 + 2277 + 2961 + 27 + 54 lt=- 10.0;40.0;10.0;10.0 @@ -8620,10 +8562,10 @@ bg=pink UMLClass - 4300 - 2310 - 410 - 100 + 3879 + 2088 + 369 + 90 *ExchangeRate* bg=pink @@ -8637,10 +8579,10 @@ price_pool : optional(PricePool) = None UMLClass - 4370 - 2460 - 260 - 110 + 3942 + 2223 + 234 + 99 *ExchangeResources* bg=green @@ -8655,10 +8597,10 @@ exchange_modes : set(ExchangeMode) Relation - 4470 - 2180 - 30 - 150 + 4032 + 1971 + 27 + 135 lt=<. 10.0;10.0;10.0;130.0 @@ -8666,10 +8608,10 @@ exchange_modes : set(ExchangeMode) UMLClass - 4670 - 2470 - 160 - 80 + 4212 + 2232 + 144 + 72 *ExchangeMode* bg=pink @@ -8681,10 +8623,10 @@ fee_multiplier : float Relation - 4620 - 2500 - 70 - 30 + 4167 + 2259 + 63 + 27 lt=<. 50.0;10.0;10.0;10.0 @@ -8692,10 +8634,10 @@ fee_multiplier : float UMLClass - 4910 - 2440 - 100 - 60 + 4428 + 2205 + 90 + 54 *Sell* @@ -8705,10 +8647,10 @@ bg=pink UMLClass - 4910 - 2510 - 100 - 60 + 4428 + 2268 + 90 + 54 *Buy* @@ -8718,10 +8660,10 @@ bg=pink Relation - 4820 - 2480 - 110 - 30 + 4347 + 2241 + 99 + 27 lt=<<- 10.0;10.0;90.0;10.0 @@ -8729,10 +8671,10 @@ bg=pink Relation - 4820 - 2510 - 110 - 30 + 4347 + 2268 + 99 + 27 lt=<<- 10.0;10.0;90.0;10.0 @@ -8740,10 +8682,10 @@ bg=pink Relation - 1710 - 4090 - 30 - 80 + 1548 + 3690 + 27 + 72 lt=<<- 10.0;10.0;10.0;60.0 @@ -8751,10 +8693,10 @@ bg=pink Relation - 1830 - 4050 - 70 - 30 + 1656 + 3654 + 63 + 27 lt=<<- 10.0;10.0;50.0;10.0 @@ -8762,10 +8704,10 @@ bg=pink UMLClass - 1880 - 4020 - 190 - 80 + 1701 + 3627 + 171 + 72 *LogicGate* bg=pink @@ -8777,10 +8719,10 @@ inputs : set(LogicElement) UMLClass - 1990 - 4350 - 140 - 80 + 1800 + 3924 + 126 + 72 *SUBSETMAX* bg=pink @@ -8792,10 +8734,10 @@ size : int Relation - 1960 - 4380 - 50 - 30 + 1773 + 3951 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -8803,10 +8745,10 @@ size : int UMLClass - 1850 - 4120 - 100 - 60 + 1674 + 3717 + 90 + 54 *NOT* @@ -8816,10 +8758,10 @@ bg=pink Relation - 1940 - 4140 - 50 - 30 + 1755 + 3735 + 45 + 27 lt=- 30.0;10.0;10.0;10.0 @@ -8827,10 +8769,10 @@ bg=pink UMLClass - 1850 - 4190 - 100 - 60 + 1674 + 3780 + 90 + 54 *XOR* @@ -8840,10 +8782,10 @@ bg=pink Relation - 1960 - 4290 - 50 - 30 + 1773 + 3870 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -8851,10 +8793,10 @@ bg=pink UMLClass - 1850 - 4260 - 100 - 60 + 1674 + 3843 + 90 + 54 *MULTIXOR* @@ -8864,10 +8806,10 @@ bg=pink Relation - 1940 - 4280 - 50 - 30 + 1755 + 3861 + 45 + 27 lt=- 30.0;10.0;10.0;10.0 @@ -8875,10 +8817,10 @@ bg=pink UMLClass - 3510 - 3650 - 150 - 60 + 3168 + 3294 + 135 + 54 *AbilityProperty* @@ -8888,10 +8830,10 @@ bg=pink Relation - 3570 - 3620 - 30 - 50 + 3222 + 3267 + 27 + 45 lt=- 10.0;10.0;10.0;30.0 @@ -8899,10 +8841,10 @@ bg=pink Relation - 3570 - 4130 - 70 - 30 + 3222 + 3726 + 63 + 27 lt=- 50.0;10.0;10.0;10.0 @@ -8910,10 +8852,10 @@ bg=pink UMLClass - 3620 - 4190 - 220 - 80 + 3267 + 3780 + 180 + 72 *Lock* bg=pink @@ -8925,10 +8867,10 @@ lock_pool : LockPool Relation - 3570 - 4220 - 70 - 30 + 3222 + 3807 + 63 + 27 lt=- 50.0;10.0;10.0;10.0 @@ -8936,10 +8878,10 @@ lock_pool : LockPool UMLClass - 5100 - 4010 - 190 - 80 + 4599 + 3618 + 171 + 72 *Lock* bg=green @@ -8951,10 +8893,10 @@ lock_pools : set(LockPool) Relation - 5060 - 4040 - 60 - 30 + 4563 + 3645 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -8962,10 +8904,10 @@ lock_pools : set(LockPool) UMLClass - 5330 - 4010 - 150 - 80 + 4806 + 3618 + 135 + 72 *LockPool* bg=pink @@ -8977,10 +8919,10 @@ slots : int Relation - 5280 - 4040 - 70 - 30 + 4761 + 3645 + 63 + 27 lt=<. 50.0;10.0;10.0;10.0 @@ -8988,10 +8930,10 @@ slots : int UMLClass - 1550 - 1570 - 160 - 60 + 1404 + 1422 + 144 + 54 *ModifierProperty* @@ -9001,10 +8943,10 @@ bg=pink Relation - 1700 - 1590 - 100 - 30 + 1539 + 1440 + 90 + 27 lt=- 80.0;10.0;10.0;10.0 @@ -9012,10 +8954,10 @@ bg=pink Relation - 1600 - 1690 - 80 - 30 + 1449 + 1530 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -9023,10 +8965,10 @@ bg=pink Relation - 1600 - 1790 - 80 - 30 + 1449 + 1620 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -9034,10 +8976,10 @@ bg=pink UMLClass - 1440 - 1850 - 170 - 80 + 1305 + 1674 + 153 + 72 *Multiplier* bg=pink @@ -9049,10 +8991,10 @@ multiplier : float Relation - 1600 - 1880 - 80 - 30 + 1449 + 1701 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -9060,10 +9002,10 @@ multiplier : float Relation - 2330 - 890 - 80 - 30 + 2106 + 810 + 72 + 27 lt=- 60.0;10.0;10.0;10.0 @@ -9071,10 +9013,10 @@ multiplier : float UMLNote - 1080 - 450 - 60 - 30 + 981 + 414 + 54 + 27 effect bg=blue @@ -9083,10 +9025,10 @@ bg=blue UMLNote - 1270 - 450 - 90 - 30 + 1152 + 414 + 81 + 27 resistance bg=blue @@ -9095,10 +9037,10 @@ bg=blue UMLNote - 730 - 290 - 60 - 30 + 666 + 270 + 54 + 27 flac bg=blue @@ -9107,10 +9049,10 @@ bg=blue UMLNote - 1390 - 170 - 60 - 30 + 1260 + 162 + 54 + 27 flac bg=blue @@ -9119,10 +9061,10 @@ bg=blue UMLClass - 690 - 5490 - 140 - 60 + 630 + 4950 + 126 + 54 *EffectProperty* @@ -9132,10 +9074,10 @@ bg=pink Relation - 820 - 5510 - 170 - 30 + 747 + 4968 + 153 + 27 lt=- 10.0;10.0;150.0;10.0 @@ -9143,10 +9085,10 @@ bg=pink Relation - 750 - 5130 - 30 - 380 + 684 + 4626 + 27 + 342 lt=<<- 10.0;360.0;10.0;10.0 @@ -9154,10 +9096,10 @@ bg=pink UMLClass - 520 - 5370 - 200 - 80 + 477 + 4842 + 180 + 72 *Cost* bg=pink @@ -9169,10 +9111,10 @@ cost : Cost UMLClass - 480 - 5280 - 240 - 80 + 441 + 4761 + 216 + 72 *Diplomatic* bg=pink @@ -9184,10 +9126,10 @@ stances : set(DiplomaticStance) Relation - 890 - 4720 - 100 - 30 + 810 + 4257 + 90 + 27 lt=- 10.0;10.0;80.0;10.0 @@ -9195,10 +9137,10 @@ stances : set(DiplomaticStance) UMLClass - 550 - 5190 - 170 - 80 + 504 + 4680 + 153 + 72 *AreaEffect* bg=pink @@ -9211,10 +9153,10 @@ dropoff : DropoffType Relation - 710 - 5220 - 70 - 30 + 648 + 4707 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -9222,10 +9164,10 @@ dropoff : DropoffType Relation - 710 - 5310 - 70 - 30 + 648 + 4788 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -9233,10 +9175,10 @@ dropoff : DropoffType Relation - 710 - 5400 - 70 - 30 + 648 + 4869 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -9244,10 +9186,10 @@ dropoff : DropoffType UMLClass - 1120 - 5490 - 150 - 60 + 1017 + 4950 + 135 + 54 *ResistanceProperty* @@ -9257,10 +9199,10 @@ bg=pink Relation - 960 - 5510 - 180 - 30 + 873 + 4968 + 162 + 27 lt=- 10.0;10.0;160.0;10.0 @@ -9268,10 +9210,10 @@ bg=pink Relation - 1180 - 5310 - 30 - 200 + 1071 + 4788 + 27 + 180 lt=<<- 10.0;180.0;10.0;10.0 @@ -9279,10 +9221,10 @@ bg=pink Relation - 2420 - 4530 - 30 - 460 + 2187 + 4086 + 27 + 414 lt=<<- 10.0;10.0;10.0;440.0 @@ -9290,10 +9232,10 @@ bg=pink UMLClass - 2320 - 4480 - 160 - 60 + 2097 + 4041 + 144 + 54 *ProgressProperty* @@ -9304,10 +9246,10 @@ bg=pink Relation - 2140 - 4500 - 200 - 30 + 1935 + 4059 + 180 + 27 lt=- 10.0;10.0;180.0;10.0 @@ -9315,10 +9257,10 @@ bg=pink Relation - 1960 - 3740 - 30 - 80 + 1773 + 3375 + 27 + 72 lt=<<- 10.0;10.0;10.0;60.0 @@ -9326,10 +9268,10 @@ bg=pink UMLClass - 1920 - 3800 - 100 - 60 + 1737 + 3429 + 90 + 54 *Reset* @@ -9339,10 +9281,10 @@ bg=pink UMLClass - 1280 - 3080 - 100 - 60 + 1161 + 2781 + 90 + 54 *Reset* @@ -9352,10 +9294,10 @@ bg=pink Relation - 1370 - 3100 - 70 - 30 + 1242 + 2799 + 63 + 27 lt=<<- 50.0;10.0;10.0;10.0 @@ -9363,10 +9305,10 @@ bg=pink UMLClass - 1590 - 5060 - 190 - 80 + 1440 + 4563 + 171 + 72 *StateChangeActive* bg=pink @@ -9378,10 +9320,10 @@ state_change : StateChanger Relation - 1770 - 5090 - 50 - 30 + 1602 + 4590 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -9389,10 +9331,10 @@ state_change : StateChanger UMLClass - 1560 - 4810 - 220 - 80 + 1413 + 4338 + 198 + 72 *OwnsGameEntity* bg=pink @@ -9404,10 +9346,10 @@ game_entity : GameEntity Relation - 1770 - 4840 - 50 - 30 + 1602 + 4365 + 45 + 27 lt=- 30.0;10.0;10.0;10.0 @@ -9415,10 +9357,10 @@ game_entity : GameEntity UMLClass - 6520 - 2910 - 330 - 80 + 5877 + 2628 + 297 + 72 *EffectBatch* bg=pink @@ -9432,10 +9374,10 @@ properties : dict(BatchProperty, BatchProperty) = {} UMLClass - 6910 - 2920 - 150 - 60 + 6228 + 2637 + 135 + 54 *BatchProperty* @@ -9446,10 +9388,10 @@ bg=pink UMLClass - 6980 - 3010 - 170 - 80 + 6291 + 2718 + 153 + 72 *Priority* bg=pink @@ -9462,10 +9404,10 @@ priority : int Relation - 6940 - 2970 - 30 - 190 + 6255 + 2682 + 27 + 171 lt=<<- 10.0;10.0;10.0;170.0 @@ -9473,10 +9415,10 @@ priority : int Relation - 6940 - 3040 - 60 - 30 + 6255 + 2745 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -9484,10 +9426,10 @@ priority : int UMLClass - 6980 - 3100 - 170 - 80 + 6291 + 2799 + 153 + 72 *Chance* bg=pink @@ -9500,10 +9442,10 @@ chance : float Relation - 6940 - 3130 - 60 - 30 + 6255 + 2826 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -9511,10 +9453,10 @@ chance : float Relation - 6840 - 2940 - 90 - 30 + 6165 + 2655 + 81 + 27 lt=<. 70.0;10.0;10.0;10.0 @@ -9522,10 +9464,10 @@ chance : float Relation - 6460 - 2870 - 80 - 100 + 5823 + 2592 + 72 + 90 lt=<. 60.0;80.0;10.0;80.0;10.0;10.0 @@ -9533,10 +9475,10 @@ chance : float UMLClass - 6620 - 3010 - 150 - 60 + 5967 + 2718 + 135 + 54 *UnorderedBatch* @@ -9547,10 +9489,10 @@ bg=pink Relation - 6580 - 2980 - 30 - 220 + 5931 + 2691 + 27 + 198 lt=<<- 10.0;10.0;10.0;200.0 @@ -9558,10 +9500,10 @@ bg=pink Relation - 6580 - 3030 - 60 - 30 + 5931 + 2736 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -9569,10 +9511,10 @@ bg=pink UMLClass - 6620 - 3080 - 150 - 60 + 5967 + 2781 + 135 + 54 *OrderedBatch* @@ -9584,10 +9526,10 @@ bg=pink Relation - 6580 - 3100 - 60 - 30 + 5931 + 2799 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -9595,10 +9537,10 @@ bg=pink UMLClass - 6620 - 3150 - 150 - 60 + 5967 + 2844 + 135 + 54 *ChainedBatch* @@ -9609,10 +9551,10 @@ bg=pink Relation - 6580 - 3170 - 60 - 30 + 5931 + 2862 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -9620,10 +9562,10 @@ bg=pink UMLClass - 520 - 5100 - 200 - 80 + 477 + 4599 + 180 + 72 *Priority* bg=pink @@ -9635,10 +9577,10 @@ priority : int Relation - 710 - 5130 - 70 - 30 + 648 + 4626 + 63 + 27 lt=- 10.0;10.0;50.0;10.0 @@ -9646,10 +9588,10 @@ priority : int UMLClass - 1380 - 3410 - 120 - 60 + 1251 + 3078 + 108 + 54 *PatchProperty* @@ -9659,10 +9601,10 @@ bg=pink Relation - 1490 - 3430 - 60 - 30 + 1350 + 3096 + 54 + 27 lt=<<- 10.0;10.0;40.0;10.0 @@ -9670,10 +9612,10 @@ bg=pink Relation - 1350 - 3370 - 30 - 180 + 1224 + 3042 + 27 + 162 lt=<. 10.0;10.0;10.0;160.0 @@ -9681,10 +9623,10 @@ bg=pink Relation - 1430 - 3460 - 30 - 90 + 1296 + 3123 + 27 + 81 lt=<. 10.0;10.0;10.0;70.0 @@ -9692,10 +9634,10 @@ bg=pink Relation - 1610 - 4040 - 50 - 30 + 1458 + 3645 + 45 + 27 lt=<<- 30.0;10.0;10.0;10.0 @@ -9703,10 +9645,10 @@ bg=pink UMLClass - 1520 - 4070 - 100 - 60 + 1377 + 3672 + 90 + 54 *True* @@ -9716,10 +9658,10 @@ bg=pink UMLClass - 1520 - 4000 - 100 - 60 + 1377 + 3609 + 90 + 54 *False* @@ -9729,10 +9671,10 @@ bg=pink Relation - 1610 - 4070 - 50 - 30 + 1458 + 3672 + 45 + 27 lt=<<- 30.0;10.0;10.0;10.0 @@ -9740,10 +9682,10 @@ bg=pink Relation - 1490 - 30 - 60 - 30 + 1350 + 36 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -9751,10 +9693,10 @@ bg=pink Relation - 1490 - 210 - 60 - 30 + 1350 + 198 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -9762,10 +9704,10 @@ bg=pink UMLClass - 160 - 3630 - 120 - 60 + 153 + 3276 + 108 + 54 *Tree* @@ -9774,10 +9716,10 @@ bg=pink UMLClass - 160 - 3700 - 120 - 60 + 153 + 3339 + 108 + 54 *Relic* @@ -9786,10 +9728,10 @@ bg=pink Relation - 300 - 3620 - 130 - 30 + 279 + 3267 + 117 + 27 lt=<<- 110.0;10.0;10.0;10.0 @@ -9797,10 +9739,10 @@ bg=pink Relation - 300 - 3510 - 30 - 310 + 279 + 3168 + 27 + 279 lt=- 10.0;10.0;10.0;290.0 @@ -9808,10 +9750,10 @@ bg=pink Relation - 270 - 3720 - 60 - 30 + 252 + 3357 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -9819,10 +9761,10 @@ bg=pink UMLClass - 160 - 3560 - 120 - 60 + 153 + 3213 + 108 + 54 *Swordsman* @@ -9831,10 +9773,10 @@ bg=pink UMLClass - 160 - 3490 - 120 - 60 + 153 + 3150 + 108 + 54 *Barracks* @@ -9843,10 +9785,10 @@ bg=pink Relation - 270 - 3650 - 60 - 30 + 252 + 3294 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -9854,10 +9796,10 @@ bg=pink Relation - 270 - 3580 - 60 - 30 + 252 + 3231 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -9865,10 +9807,10 @@ bg=pink Relation - 270 - 3510 - 60 - 30 + 252 + 3168 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -9876,10 +9818,10 @@ bg=pink UMLClass - 160 - 3770 - 120 - 60 + 153 + 3402 + 108 + 54 *Projectile* @@ -9888,10 +9830,10 @@ bg=pink Relation - 270 - 3790 - 60 - 30 + 252 + 3420 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -9899,10 +9841,10 @@ bg=pink UMLNote - 10 - 3490 - 140 - 70 + 18 + 3150 + 126 + 63 All ingame objects are game entities @@ -9912,10 +9854,10 @@ bg=blue UMLClass - 2450 - 4150 - 120 - 60 + 2214 + 3744 + 108 + 54 *Construct* @@ -9925,10 +9867,10 @@ bg=pink UMLClass - 2450 - 4220 - 120 - 60 + 2214 + 3807 + 108 + 54 *Harvest* @@ -9938,10 +9880,10 @@ bg=pink UMLClass - 2450 - 4290 - 120 - 60 + 2214 + 3870 + 108 + 54 *Restock* @@ -9951,10 +9893,10 @@ bg=pink UMLClass - 2450 - 4080 - 120 - 60 + 2214 + 3681 + 108 + 54 *Carry* @@ -9964,10 +9906,10 @@ bg=pink UMLClass - 2450 - 4360 - 120 - 60 + 2214 + 3933 + 108 + 54 *Transform* @@ -9977,10 +9919,10 @@ bg=pink Relation - 2410 - 4030 - 30 - 380 + 2178 + 3636 + 27 + 342 lt=- 10.0;10.0;10.0;360.0 @@ -9988,10 +9930,10 @@ bg=pink Relation - 2410 - 4030 - 60 - 30 + 2178 + 3636 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -9999,10 +9941,10 @@ bg=pink Relation - 2410 - 4100 - 60 - 30 + 2178 + 3699 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -10010,10 +9952,10 @@ bg=pink Relation - 2410 - 4170 - 60 - 30 + 2178 + 3762 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -10021,10 +9963,10 @@ bg=pink Relation - 2410 - 4240 - 60 - 30 + 2178 + 3825 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -10032,10 +9974,10 @@ bg=pink Relation - 2410 - 4310 - 60 - 30 + 2178 + 3888 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -10043,10 +9985,10 @@ bg=pink Relation - 2410 - 4380 - 60 - 30 + 2178 + 3951 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -10054,10 +9996,10 @@ bg=pink UMLClass - 5700 - 4670 - 250 - 80 + 5139 + 4212 + 225 + 72 *Constructable* bg=green @@ -10070,10 +10012,10 @@ construction_progress : set(Progress) Relation - 5940 - 4700 - 60 - 30 + 5355 + 4239 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -10081,10 +10023,10 @@ construction_progress : set(Progress) UMLClass - 2200 - 2230 - 110 - 60 + 1989 + 2016 + 99 + 54 *Node* @@ -10094,21 +10036,21 @@ bg=pink Relation - 2250 - 2280 - 30 - 280 + 2034 + 2061 + 27 + 342 lt=<<- - 10.0;10.0;10.0;260.0 + 10.0;10.0;10.0;360.0 UMLClass - 2070 - 2320 - 160 - 80 + 1872 + 2097 + 144 + 72 *Start* bg=pink @@ -10120,10 +10062,10 @@ next : Node Relation - 2220 - 2350 - 60 - 30 + 2007 + 2124 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -10131,10 +10073,10 @@ next : Node UMLClass - 1850 - 2320 - 150 - 80 + 1674 + 2097 + 135 + 72 *Activity* bg=pink @@ -10146,10 +10088,10 @@ start : Start Relation - 1990 - 2350 - 100 - 30 + 1800 + 2124 + 90 + 27 lt=<. 80.0;10.0;10.0;10.0 @@ -10157,10 +10099,10 @@ start : Start UMLClass - 2120 - 2420 - 110 - 60 + 1917 + 2187 + 99 + 54 *End* @@ -10170,10 +10112,10 @@ bg=pink Relation - 2220 - 2440 - 60 - 30 + 2007 + 2205 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -10181,10 +10123,10 @@ bg=pink UMLClass - 2290 - 2320 - 220 - 80 + 2070 + 2097 + 198 + 72 *XORGate* bg=pink @@ -10197,10 +10139,10 @@ default : Node UMLClass - 2290 - 2410 - 220 - 80 + 2070 + 2259 + 198 + 72 *XOREventGate* bg=pink @@ -10212,26 +10154,26 @@ next : dict(Event, Node) UMLClass - 2060 - 2500 - 170 - 90 + 1863 + 2259 + 153 + 81 *Ability* bg=pink -- next : Node -ability : abstract(Ability) +ability : Ability Relation - 2250 - 2350 - 60 - 30 + 2034 + 2124 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -10239,10 +10181,10 @@ ability : abstract(Ability) Relation - 2250 - 2440 - 60 - 30 + 2034 + 2286 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -10250,10 +10192,10 @@ ability : abstract(Ability) Relation - 2220 - 2530 - 60 - 30 + 2007 + 2286 + 54 + 27 lt=- 40.0;10.0;10.0;10.0 @@ -10261,10 +10203,10 @@ ability : abstract(Ability) UMLClass - 3710 - 3360 - 180 - 90 + 3348 + 3033 + 162 + 81 *Activity* bg=green @@ -10276,10 +10218,10 @@ graph : Activity Relation - 3880 - 3390 - 60 - 30 + 3501 + 3060 + 54 + 27 lt=- 10.0;10.0;40.0;10.0 @@ -10287,10 +10229,10 @@ graph : Activity UMLNote - 1790 - 2270 - 160 - 30 + 1620 + 2052 + 144 + 27 Unit behaviour graph bg=blue @@ -10299,10 +10241,10 @@ bg=blue UMLClass - 2560 - 2420 - 110 - 60 + 2349 + 2268 + 99 + 54 *Event* @@ -10312,21 +10254,21 @@ bg=pink Relation - 2500 - 2440 - 80 - 30 + 2259 + 2286 + 108 + 27 lt=<. - 60.0;10.0;10.0;10.0 + 100.0;10.0;10.0;10.0 Relation - 2580 - 2470 - 30 - 240 + 2367 + 2313 + 27 + 216 lt=<<- 10.0;10.0;10.0;220.0 @@ -10334,10 +10276,10 @@ bg=pink UMLClass - 2610 - 2500 - 160 - 80 + 2394 + 2340 + 144 + 72 *Wait* bg=pink @@ -10349,10 +10291,10 @@ time : float UMLClass - 2610 - 2590 - 120 - 60 + 2394 + 2421 + 108 + 54 *WaitAbility* @@ -10362,10 +10304,10 @@ bg=pink Relation - 2580 - 2530 - 50 - 30 + 2367 + 2367 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -10373,10 +10315,10 @@ bg=pink Relation - 2580 - 2610 - 50 - 30 + 2367 + 2439 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -10384,10 +10326,10 @@ bg=pink UMLClass - 2610 - 2660 - 160 - 60 + 2394 + 2484 + 144 + 54 *CommandInQueue* @@ -10397,10 +10339,10 @@ bg=pink Relation - 2580 - 2680 - 50 - 30 + 2367 + 2502 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -10408,10 +10350,10 @@ bg=pink UMLClass - 2520 - 2070 - 140 - 80 + 2196 + 1665 + 126 + 72 *Condition* bg=pink @@ -10423,21 +10365,21 @@ next : Node Relation - 2450 - 2100 - 90 - 240 + 2142 + 1692 + 72 + 423 lt=<. - 70.0;10.0;10.0;10.0;10.0;220.0 + 60.0;10.0;10.0;10.0;10.0;450.0 UMLClass - 2610 - 2170 - 160 - 60 + 2277 + 1755 + 144 + 54 *CommandInQueue* @@ -10447,21 +10389,21 @@ bg=pink Relation - 2580 - 2140 - 30 - 220 + 2250 + 1728 + 27 + 306 lt=<<- - 10.0;10.0;10.0;200.0 + 10.0;10.0;10.0;320.0 Relation - 2580 - 2190 - 50 - 30 + 2250 + 1773 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -10469,23 +10411,25 @@ bg=pink UMLClass - 2610 - 2240 - 160 - 60 + 2277 + 1818 + 144 + 72 - -*NextCommandIdle* -bg=pink + *NextCommand* +bg=pink + +-- +command : Command Relation - 2580 - 2260 - 50 - 30 + 2250 + 1836 + 45 + 27 lt=- 10.0;10.0;30.0;10.0 @@ -10493,60 +10437,433 @@ bg=pink Relation - 1770 - 2350 - 100 - 30 + 1602 + 2124 + 90 + 27 lt=- 10.0;10.0;80.0;10.0 + + Relation + + 2250 + 1926 + 45 + 27 + + lt=- + 10.0;10.0;30.0;10.0 + UMLClass - 2610 - 2310 - 160 - 60 + 3915 + 3591 + 108 + 54 -*NextCommandMove* +*PathType* bg=pink Relation - 2580 - 2330 - 50 - 30 + 3960 + 3546 + 27 + 63 + + lt=<. + 10.0;50.0;10.0;10.0 + + + UMLClass + + 2070 + 2178 + 225 + 72 + + *XORSwitchGate* +bg=pink + +-- +switch : SwitchCondition +default : Node + + + + Relation + + 2034 + 2205 + 54 + 27 lt=- - 10.0;10.0;30.0;10.0 + 40.0;10.0;10.0;10.0 UMLClass - 4340 - 3980 - 120 - 60 + 2349 + 2187 + 126 + 54 -*PathType* +*SwitchCondition* bg=pink Relation - 4390 - 3930 - 30 - 70 + 2286 + 2205 + 81 + 27 + + lt=<. + 70.0;10.0;10.0;10.0 + + + UMLClass + + 2529 + 2178 + 180 + 72 + + *NextCommand* +bg=pink + +-- +next : dict(Command, Node) + + + + Relation + + 2466 + 2205 + 81 + 27 + + lt=<<- + 70.0;10.0;10.0;10.0 + + + UMLClass + + 1656 + 2466 + 99 + 54 + + +*Command* +bg=pink + + + + Relation + + 1602 + 2484 + 72 + 27 + + lt=- + 10.0;10.0;60.0;10.0 + + + UMLClass + + 1719 + 2601 + 99 + 54 + + +*Idle* +bg=pink + + + + UMLClass + + 1719 + 2664 + 99 + 54 + + +*Move* +bg=pink + + + + UMLClass + + 1719 + 2538 + 99 + 54 + + +*ApplyEffect* +bg=pink + + + + Relation + + 1683 + 2511 + 27 + 198 + + lt=<<- + 10.0;10.0;10.0;200.0 + + + Relation + + 1683 + 2556 + 54 + 27 + + lt=- + 10.0;10.0;40.0;10.0 + + + Relation + + 1683 + 2619 + 54 + 27 + + lt=- + 40.0;10.0;10.0;10.0 + + + Relation + + 1683 + 2682 + 54 + 27 + + lt=- + 10.0;10.0;40.0;10.0 + + + UMLClass + + 3267 + 3861 + 144 + 72 + + *Ranged* +bg=pink + +-- +min_range : float +max_range : float + + + + Relation + + 3222 + 3888 + 63 + 27 + + lt=- + 50.0;10.0;10.0;10.0 + + + UMLClass + + 2277 + 1899 + 162 + 72 + + *TargetInRange* +bg=pink + +-- +ability : Ability + + + + UMLClass + + 1863 + 2349 + 153 + 81 + + *Task* +bg=pink + +-- +next : Node +task : Task + + + + Relation + + 2007 + 2376 + 54 + 27 + + lt=- + 40.0;10.0;10.0;10.0 + + + UMLClass + + 1926 + 2466 + 90 + 54 + + +*Task* +bg=pink + + + + Relation + + 1962 + 2421 + 27 + 63 lt=<. 10.0;50.0;10.0;10.0 + + Relation + + 1962 + 2511 + 27 + 198 + + lt=<<- + 10.0;10.0;10.0;200.0 + + + UMLClass + + 1989 + 2538 + 162 + 54 + + +*PopCommandQueue* +bg=pink + + + + Relation + + 1962 + 2556 + 45 + 27 + + lt=- + 10.0;10.0;30.0;10.0 + + + UMLClass + + 1989 + 2601 + 162 + 54 + + +*ClearCommandQueue* +bg=pink + + + + Relation + + 1962 + 2619 + 45 + 27 + + lt=- + 10.0;10.0;30.0;10.0 + + + UMLClass + + 1989 + 2664 + 126 + 54 + + +*MoveToTarget* +bg=pink + + + + Relation + + 1962 + 2682 + 45 + 27 + + lt=- + 10.0;10.0;30.0;10.0 + + + UMLClass + + 2277 + 1980 + 162 + 72 + + *AbilityUsable* +bg=pink + +-- +ability : Ability + + + + Relation + + 2250 + 2007 + 45 + 27 + + lt=- + 10.0;10.0;30.0;10.0 + From fb8971e8d86575c261251ccb8439a715c7dcf07a Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 15 Sep 2024 06:26:43 +0200 Subject: [PATCH 767/771] doc: Document API changes for nyan data API v0.6.0. doc: Document API changes for ApplyEffect. doc: Document API changes for Task nodes. doc: Document API changes for Ranged property. doc: Document API changes for NextCommand condition. --- doc/nyan/api_reference/reference_ability.md | 68 +++----- doc/nyan/api_reference/reference_util.md | 169 ++++++++++++++++++-- 2 files changed, 178 insertions(+), 59 deletions(-) diff --git a/doc/nyan/api_reference/reference_ability.md b/doc/nyan/api_reference/reference_ability.md index dfddc1c82f..2bb20fa6f2 100644 --- a/doc/nyan/api_reference/reference_ability.md +++ b/doc/nyan/api_reference/reference_ability.md @@ -127,6 +127,26 @@ If the lock pool the ability with this property cannot become active. +## ability.property.type.Ranged + +```python +Ranged(AbilityProperty): + min_range : float + max_range : float +``` + +Abilities with this property can only be used within a specified range around the game entity. The property mostly affects abilities that are *targeted*, i.e. that are used on other game entities or locations in the game world. + +If the target of the ability is another game entity and said game entity has a `Collision` ability, the range check factors in the `Hitbox` boundaries of the targeted game entity when calculating the distance. + +Without this property, abilities behave as if `min_range` and `max_range` are `0.0`. + +**min_range** +Minimum distance to the target of the ability. + +**max_range** +Maximum distance to the target of the ability. + ## ability.type.ActiveTransformTo ```python @@ -327,16 +347,12 @@ Alters the abilities and modifiers of a game entity after the despawn condition ```python DetectCloak(Ability): - range : float allowed_types : set(children(GameEntityType)) blacklisted_entities : set(GameEntity) ``` Enables the game entity to decloak other game entities which use the `Cloak` ability. -**range** -Range around the game entity in which other game entities will be decloaked. - **allowed_types** Whitelist of game entity types that can be decloaked. @@ -543,7 +559,6 @@ Determines whether the resource spot is harvestable when it is created. If `True ```python Herd(Ability): - range : float strength : int allowed_types : set(children(GameEntityType)) blacklisted_entities : set(GameEntity) @@ -551,9 +566,6 @@ Herd(Ability): Allows a game entity to change the ownership of other game entities with the `Herdable` ability. -**range** -Minimum distance to a herdable game entity to make it change ownership. - **strength** Comparison value for situations when the game entity competes with other game entities for a herdable. The game entity with the highest `strength` value will always be prefered, even if other game entities fulfill the condition set by `mode` in `Herdable` better. @@ -787,38 +799,6 @@ RallyPoint(Ability): Allows a game entity to set a rally point on the map. Game entities spawned by the `Create` ability or ejected from a container will move to the rally point location. The rally point can be placed on another game entity. In that case, the game entities moving there will try to use an appropriate ability on it. -## ability.type.RangedContinuousEffect - -```python -RangedContinuousEffect(ApplyContinuousEffect): - min_range : int - max_range : int -``` - -Applies continuous effects on another game entity. This specialization of `ApplyContinuousEffect` allows ranged application. - -**min_range** -Minimum distance to target. - -**max_range** -Maximum distance to the target. - -## ability.type.RangedDiscreteEffect - -```python -RangedDiscreteEffect(ApplyDiscreteEffect): - min_range : int - max_range : int -``` - -Applies batches of discrete effects on another game entity. This specialization of `ApplyDiscreteEffect` allows ranged application. - -**min_range** -Minimum distance to target. - -**max_range** -Maximum distance to the target. - ## ability.type.RegenerateAttribute ```python @@ -966,8 +946,6 @@ ShootProjectile(Ability): projectiles : orderedset(GameEntity) min_projectiles : int max_projectiles : int - min_range : int - max_range : int reload_time : float spawn_delay : float projectile_delay : float @@ -994,12 +972,6 @@ Minimum amount of projectiles spawned. **max_projectiles** Maximum amount of projectiles spawned. -**min_range** -Minimum distance to the targeted game entity. - -**max_range** -Maximum distance to the targeted game entity. - **reload_time** Time until the ability can be used again in seconds. The timer starts after the *last* projectile has been fired. diff --git a/doc/nyan/api_reference/reference_util.md b/doc/nyan/api_reference/reference_util.md index dd9646e862..7667a1a10a 100644 --- a/doc/nyan/api_reference/reference_util.md +++ b/doc/nyan/api_reference/reference_util.md @@ -54,6 +54,18 @@ Generalization object for conditions that can be used in `XORGate` nodes. **node** Node that is visited when the condition is true. +## util.activity.condition.type.AbilityUsable + +```python +AbilityUsable(Condition): + ability : abstract(Ability) +``` + +Is true when an ability can be used by the game entity when the node is visited. + +**ability** +Ability definition used for the usability check. This can reference a specific ability of the game entity or an abstract API object from the `engine.ability.type` namespace. If a specific ability is referenced, the ability must be assigned to the game entity **and** be enabled for the check to pass. If an API object is referenced, at least one ability of the same type must be enabled for the check to pass. + ## util.activity.condition.type.CommandInQueue ```python @@ -61,25 +73,35 @@ CommandInQueue(Condition): pass ``` -Is true when the command queue is not empty when the node is visited. +Is true when the game entity's command queue is not empty when the node is visited. -## util.activity.condition.type.NextCommandIdle +## util.activity.condition.type.NextCommand ```python -NextCommandIdle(Condition): - pass +NextCommand(Condition): + command : children(Command) ``` -Is true when the next command in the queue is of type `Idle`. +Is true when the next command in the game entity's command queue is of a specific type. -## util.activity.condition.type.NextCommandMove +**command** +Command type checked by the condition. + +## util.activity.condition.type.TargetInRange ```python -NextCommandMove(Condition): - pass +TargetInRange(Condition): + ability : abstract(Ability) ``` -Is true when the next command in the queue is of type `Move`. +Is true when the target of the next command in the game entity's command queue is in range of an ability. + +**ability** +Ability definition used for the range check. + +This can reference a specific ability of the game entity or an abstract API object from the `engine.ability.type` namespace. If a specific ability is referenced, the ability must be assigned to the game entity and must be enabled. Otherwise, the range check fails. If an API object is referenced, the first active ability with the same type as the API object is executed. + +If the ability has the property `Ranged`, the attributes of this property are utilized for the range check calculations. If the ability does not have a `Ranged` property, the condition is only true when the game entity is at the same position as the target. ## util.activity.event.Event @@ -149,7 +171,7 @@ Next node in the activity graph. **ability** Ability that is executed. -This can reference a specific ability of the game entity or an abstract API object from the `engine.ability.type` namespace. If a specific ability is referenced, the ability must be assigned to the game entity and must not be disabled. Otherwise, the ability is not executed. If an API object is referenced, the first active ability with the same type as the API object is executed. +This can reference a specific ability of the game entity or an abstract API object from the `engine.ability.type` namespace. If a specific ability is referenced, the ability must be assigned to the game entity and must be enabled. Otherwise, the ability is not executed. If an API object is referenced, the first active ability with the same type as the API object is executed. ## util.activity.node.type.End @@ -172,6 +194,22 @@ Start of an activity. Does nothing but pointing to the next node. **next** Next node in the activity graph. +## util.activity.node.type.Task + +```python +Task(Node): + next : Node + task : children(Task) +``` + +Executes a task on the game entity when the node is visited. + +**next** +Next node in the activity graph. + +**task** +Task that is executed. + ## util.activity.node.type.XOREventGate ```python @@ -195,11 +233,84 @@ XORGate(Node): Gateway that branches the activity graph depending on the result of conditional queries. Queries are executed immediately when the node is visited. **next** -Mapping of conditional queries to the next node in the activity graph. The first query that evaluates to true is used to determine the next node. If no query evaluates to true, the `default` node is used. +Mapping of conditional queries to the next node in the activity graph. The first query that evaluates to true is used to determine the next node. If no query evaluates to true, the `default` node is used as fallback. **default** Default node that is used if no query evaluates to true. +## util.activity.node.type.XORSwitchGate + +```python +XORSwitchGate(Node): + switch : children(SwitchCondition) + default : Node +``` + +Gateway that branches the activity graph depending on the value of a runtime parameter. In comparison to `XORGate`, only one conditional query is done based on the value (similar to the behaviour of a [switch statement](https://en.wikipedia.org/wiki/Switch_statement)). The query is executed immediately when the node is visited. + +**switch** +Defines which runtime parameter is checked as well as the mapping of parameter value to the next node in the activity graph. If a value is encountered at query execution time that is not associated with a node, the `default` node is used as fallback. + +**default** +Default node that is used if a value does not have an associated node. + +## util.activity.switch_condition.SwitchCondition + +```python +SwitchCondition(Object): + pass +``` + +Generalization object for conditions that can be used in `XORSwitchGate` nodes. + +## util.activity.switch_condition.type.NextCommand + +```python +NextCommand(SwitchCondition): + next : dict(children(Command), Node) +``` + +Switches branches based on the type of command that is in the queue of the game entity. + +**next** +Mapping of command types to the next node in the activity graph. + +## util.activity.task.Task + +```python +Task(Object): + pass +``` + +Generalization object for tasks that can be used in `Task` nodes. + +## util.activity.task.type.ClearCommandQueue + +```python +ClearCommandQueue(Task): + pass +``` + +Clear the command queue of the game entity executing the activity. + +## util.activity.task.type.MoveToTarget + +```python +MoveToTarget(Task): + pass +``` + +Move to the current target of the game entity. The target may be a position or another game entity. If the game entity has no target at time of execution, the task is skipped. + +## util.activity.task.type.PopCommandQueue + +```python +PopCommandQueue(Task): + pass +``` + +Pop the front command from the command queue of the game entity executing the activity. + ## util.animation_override.AnimationOverride ```python @@ -420,6 +531,42 @@ The activation message that has to be typed into the chat console. **changes** Changes to API objects. +## util.command.Command + +```python +Command(Object): + pass +``` + +Generalization object for commands of a game entity. + +## util.command.type.ApplyEffect + +```python +ApplyEffect(Command): + pass +``` + +Game entity command for using the `ApplyEffect` ability. + +## util.command.type.Idle + +```python +Idle(Command): + pass +``` + +Game entity command for using the `Idle` ability. + +## util.command.type.Move + +```python +Move(Command): + pass +``` + +Game entity command for using the `Move` ability. + ## util.container_type.SendToContainerType ```python From 05ad1e772a7b53c21d12db867b781e281ec70085 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 18 May 2025 17:08:36 +0200 Subject: [PATCH 768/771] doc: Changelog for nyan data API v0.6.0. --- doc/changelogs/nyan_api/v0.6.0.md | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 doc/changelogs/nyan_api/v0.6.0.md diff --git a/doc/changelogs/nyan_api/v0.6.0.md b/doc/changelogs/nyan_api/v0.6.0.md new file mode 100644 index 0000000000..1372c5ef83 --- /dev/null +++ b/doc/changelogs/nyan_api/v0.6.0.md @@ -0,0 +1,43 @@ +# [0.6.0] - 2025-05-18 +All notable changes for version [v0.6.0] are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Added +### Ability module +- Add `Ranged(Property)` object; defines range of an ability; replaces various range members in `Ability` objects + +### Utility module +- Add `AbilityUsable(Condition)` object; checks if an ability can be used by an entity +- Add `NextCommand(Condition)` object; checks if a specific command is next in the command queue; replaces individual objects checking for specific commands +- Add `TargetInRange(Condition)` object; checks if the target of an entity is in range of a specific ability +- Add `Task(Node)` object; execute internal task when visiting node +- Add `Task(Object)` object +- Add `ClearCommandQueue(Task)` object; clears the command queue when visiting task node +- Add `PopCommandQueue(Task)` object; pops the front command in the command queue when visiting task node +- Add `MoveToTarget(Task)` object; move to the current target of the entity when visiting task node +- Add `XORSwitchGate(Node)` object; switch branches based on evaluating single value +- Add `SwitchCondition(Object)` object +- Add `NextCommand(SwitchCondition)` object; switch branches based on the next command in the command queue +- Add `Command(Object)` object; references internal commands of the engine +- Add `ApplyEffect(Command)` object +- Add `Idle(Command)` object +- Add `Move(Command)` object + +### Removed +### Ability module +- Remove `RangedContinuousEffect(Ability)` object; functionality superceded by `Ranged(Property)` object +- Remove `RangedDiscreteEffect(Ability)` object; functionality superceded by `Ranged(Property)` object +- Remove `range` member from `DetectCloak(Ability)`; functionality superceded by `Ranged(Property)` object +- Remove `range` member from `Herd(Ability)`; functionality superceded by `Ranged(Property)` object +- Remove `min_range` member from `ShootProjectile(Ability)`; functionality superceded by `Ranged(Property)` object +- Remove `max_range` member from `ShootProjectile(Ability)`; functionality superceded by `Ranged(Property)` object + +### Utility module +- Remove `NextCommandIdle(Condition)` object; functionality superceded by `NextCommand(Condition)` object +- Remove `NextCommandMove(Condition)` object; functionality superceded by `NextCommand(Condition)` object + +## Reference visualization + +* [Gamedata](https://github.com/SFTtech/openage/blob/927f547d4985cba8e172c9492273b34537571c56/doc/nyan/aoe2_nyan_tree.svg) From f8763a1d4df4c794b64b463c1a0e26ed3cd95349 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 19 May 2025 21:32:30 +0200 Subject: [PATCH 769/771] doc: Fix renderer demo run instructions. --- doc/code/renderer/demos.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/code/renderer/demos.md b/doc/code/renderer/demos.md index 3be974d628..82de8c4e86 100644 --- a/doc/code/renderer/demos.md +++ b/doc/code/renderer/demos.md @@ -24,7 +24,7 @@ The demo mostly follows the steps described in the [Level 1 Renderer - Basic Usa documentation. ```bash -./bin/run test --demo renderer.tests.renderer_demo 0 +cd bin && ./run test --demo renderer.tests.renderer_demo 0 ``` **Result:** @@ -38,7 +38,7 @@ This demo shows how simple *textured* meshes can be created and rendered. It als how to interact with the window and the renderer using window callbacks. ```bash -./bin/run test --demo renderer.tests.renderer_demo 1 +cd bin && ./run test --demo renderer.tests.renderer_demo 1 ``` **Controls:** @@ -56,7 +56,7 @@ In this demo, we show how animation and texture metadata files are parsed and us load and render the correct textures and animations for a mesh. ```bash -./bin/run test --demo renderer.tests.renderer_demo 2 +cd bin && ./run test --demo renderer.tests.renderer_demo 2 ``` **Controls:** @@ -76,7 +76,7 @@ This demo shows a minimal setup for the [Level 2 Renderer](level2.md) and how to with it. The demo also introduces the camera system and how to interact with it. ```bash -./bin/run test --demo renderer.tests.renderer_demo 3 +cd bin && ./run test --demo renderer.tests.renderer_demo 3 ``` **Controls:** @@ -95,7 +95,7 @@ This demos shows how animation frame timing works and how to control the animati with the engine's internal clock. ```bash -./bin/run test --demo renderer.tests.renderer_demo 4 +cd bin && ./run test --demo renderer.tests.renderer_demo 4 ``` **Controls:** @@ -115,7 +115,7 @@ This demo shows how to create [uniform buffers](level1.md#uniform-buffers) and h Additionally, uniform buffer usage for the camera system is demonstrated. ```bash -./bin/run test --demo renderer.tests.renderer_demo 5 +cd bin && ./run test --demo renderer.tests.renderer_demo 5 ``` **Controls:** @@ -132,7 +132,7 @@ Additionally, uniform buffer usage for the camera system is demonstrated. This demo shows how to use [frustum culling](level2.md#frustum-culling) in the renderer. ```bash -./bin/run test --demo renderer.tests.renderer_demo 6 +cd bin && ./run test --demo renderer.tests.renderer_demo 6 ``` **Controls:** @@ -144,12 +144,12 @@ This demo shows how to use [frustum culling](level2.md#frustum-culling) in the r ![Demo 6](/doc/code/renderer/images/demo_6.png) -### Demo 6 +### Demo 7 This demo shows how to use [shader templating](level1.md#shader-templates) in the renderer. ```bash -./bin/run test --demo renderer.tests.renderer_demo 6 +cd bin && ./run test --demo renderer.tests.renderer_demo 7 ``` **Result:** @@ -164,7 +164,7 @@ This demo shows how to use [shader templating](level1.md#shader-templates) in th This stresstest tests the performance when rendering an increasingly larger number of objects. ```bash -./bin/run test --demo renderer.tests.stresstest 0 +cd bin && ./run test --demo renderer.tests.renderer_stresstest 0 ``` **Result:** @@ -177,7 +177,7 @@ This stresstest tests the performance when [frustum culling](level2.md#frustum-c number of objects is rendered on the screen. ```bash -./bin/run test --demo renderer.tests.stresstest 1 +cd bin && ./run test --demo renderer.tests.renderer_stresstest 1 ``` **Result:** From 906e14bdcee9651d53a4110198aa7334b752a317 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Sat, 24 May 2025 17:44:45 +0100 Subject: [PATCH 770/771] build system: remove STANDALONE from cabchecksum.pyx --- openage/cabextract/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openage/cabextract/CMakeLists.txt b/openage/cabextract/CMakeLists.txt index 9a545259af..87f4fd36eb 100644 --- a/openage/cabextract/CMakeLists.txt +++ b/openage/cabextract/CMakeLists.txt @@ -1,6 +1,6 @@ add_cython_modules( lzxd.pyx - STANDALONE cabchecksum.pyx + cabchecksum.pyx ) add_py_modules( From b671dde2a118402dba911b2304b9fad01c1a0049 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 14 Jun 2025 00:21:35 +0200 Subject: [PATCH 771/771] buildsys: Renames 'codegen' target to 'cppgen'. Fixes a CMake warning because 'codegen' is a reserved name. --- Makefile | 6 +++++- buildsystem/codegen.cmake | 4 ++-- libopenage/CMakeLists.txt | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 4fd6e2fe5a..ad8ad7adf0 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,11 @@ libopenage: $(BUILDDIR) .PHONY: codegen codegen: $(BUILDDIR) - $(MAKE) $(MAKEARGS) -C $(BUILDDIR) codegen + $(MAKE) $(MAKEARGS) -C $(BUILDDIR) cppgen + +.PHONY: cppgen +cppgen: $(BUILDDIR) + $(MAKE) $(MAKEARGS) -C $(BUILDDIR) cppgen .PHONY: pxdgen pxdgen: $(BUILDDIR) diff --git a/buildsystem/codegen.cmake b/buildsystem/codegen.cmake index 068078bf6d..685dbf0463 100644 --- a/buildsystem/codegen.cmake +++ b/buildsystem/codegen.cmake @@ -1,4 +1,4 @@ -# Copyright 2014-2019 the openage authors. See copying.md for legal info. +# Copyright 2014-2025 the openage authors. See copying.md for legal info. # set CODEGEN_SCU_FILE to the absolute path to SCU file macro(get_codegen_scu_file) @@ -52,7 +52,7 @@ function(codegen_run) COMMENT "openage.codegen: generating c++ code" ) - add_custom_target(codegen + add_custom_target(cppgen DEPENDS "${CODEGEN_TIMEFILE}" ) diff --git a/libopenage/CMakeLists.txt b/libopenage/CMakeLists.txt index 030231baf7..30b7b23b01 100644 --- a/libopenage/CMakeLists.txt +++ b/libopenage/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2014-2019 the openage authors. See copying.md for legal info. +# Copyright 2014-2025 the openage authors. See copying.md for legal info. # main C++ library definitions. # dependency and source file setup for the resulting library. @@ -13,7 +13,7 @@ declare_binary(libopenage openage library allow_no_undefined) set_target_properties(libopenage PROPERTIES VERSION 0 AUTOMOC ON - AUTOGEN_TARGET_DEPENDS "codegen" + AUTOGEN_TARGET_DEPENDS "cppgen" ) ##################################################

EEL~#BuQV?; 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�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 Date: Sun, 28 Jul 2024 03:34:49 +0200 Subject: [PATCH 561/771] gamestate: Use nyan terrain definition for modpacks without terrain graphics. --- libopenage/gamestate/map.cpp | 32 ++++++-------- libopenage/gamestate/terrain_factory.cpp | 56 +++++++++++++++++------- libopenage/gamestate/terrain_tile.h | 4 +- 3 files changed, 54 insertions(+), 38 deletions(-) diff --git a/libopenage/gamestate/map.cpp b/libopenage/gamestate/map.cpp index beb02b5e8a..3d9af584cb 100644 --- a/libopenage/gamestate/map.cpp +++ b/libopenage/gamestate/map.cpp @@ -27,17 +27,15 @@ Map::Map(const std::shared_ptr &terrain) : util::Vector2s grid_size{this->terrain->get_row_size(), this->terrain->get_column_size()}; for (const auto &chunk : this->terrain->get_chunks()) { for (const auto &tile : chunk->get_tiles()) { - if (tile.terrain != std::nullopt) { - auto path_costs = api::APITerrain::get_path_costs(*tile.terrain); + auto path_costs = api::APITerrain::get_path_costs(tile.terrain); - for (const auto &path_cost : path_costs) { - if (not this->grid_lookup.contains(path_cost.first)) { - auto grid = std::make_shared(grid_idx, grid_size, side_length); - this->pathfinder->add_grid(grid); + for (const auto &path_cost : path_costs) { + if (not this->grid_lookup.contains(path_cost.first)) { + auto grid = std::make_shared(grid_idx, grid_size, side_length); + this->pathfinder->add_grid(grid); - this->grid_lookup.emplace(path_cost.first, grid_idx); - grid_idx += 1; - } + this->grid_lookup.emplace(path_cost.first, grid_idx); + grid_idx += 1; } } } @@ -48,17 +46,15 @@ Map::Map(const std::shared_ptr &terrain) : auto chunk_terrain = this->terrain->get_chunk(chunk_idx); for (size_t tile_idx = 0; tile_idx < chunk_terrain->get_tiles().size(); ++tile_idx) { auto tile = chunk_terrain->get_tile(tile_idx); - if (tile.terrain != std::nullopt) { - auto path_costs = api::APITerrain::get_path_costs(*tile.terrain); + auto path_costs = api::APITerrain::get_path_costs(tile.terrain); - for (const auto &path_cost : path_costs) { - auto grid_id = this->grid_lookup.at(path_cost.first); - auto grid = this->pathfinder->get_grid(grid_id); + for (const auto &path_cost : path_costs) { + auto grid_id = this->grid_lookup.at(path_cost.first); + auto grid = this->pathfinder->get_grid(grid_id); - auto sector = grid->get_sector(chunk_idx); - auto cost_field = sector->get_cost_field(); - cost_field->set_cost(tile_idx, path_cost.second); - } + auto sector = grid->get_sector(chunk_idx); + auto cost_field = sector->get_cost_field(); + cost_field->set_cost(tile_idx, path_cost.second); } } } diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp index 55c70b34cf..3f16a6acf1 100644 --- a/libopenage/gamestate/terrain_factory.cpp +++ b/libopenage/gamestate/terrain_factory.cpp @@ -26,15 +26,30 @@ static const std::vector test_terrain_paths = { "../test/textures/test_terrain2.terrain", }; -static const std::vector aoe1_test_terrain = {}; -static const std::vector de1_test_terrain = {}; +static const std::vector aoe1_test_terrain = { + "aoe1_base.data.terrain.forest.forest.Forest", + "aoe1_base.data.terrain.grass.grass.Grass", + "aoe1_base.data.terrain.dirt.dirt.Dirt", + "aoe1_base.data.terrain.water.water.Water", +}; +static const std::vector de1_test_terrain = { + "aoe1_base.data.terrain.desert.desert.Desert", + "aoe1_base.data.terrain.grass.grass.Grass", + "aoe1_base.data.terrain.dirt.dirt.Dirt", + "aoe1_base.data.terrain.water.water.Water", +}; static const std::vector aoe2_test_terrain = { "aoe2_base.data.terrain.foundation.foundation.Foundation", "aoe2_base.data.terrain.grass.grass.Grass", "aoe2_base.data.terrain.dirt.dirt.Dirt", "aoe2_base.data.terrain.water.water.Water", }; -static const std::vector de2_test_terrain = {}; +static const std::vector de2_test_terrain = { + "de2_base.data.terrain.foundation.foundation.Foundation", + "de2_base.data.terrain.grass.grass.Grass", + "de2_base.data.terrain.dirt.dirt.Dirt", + "de2_base.data.terrain.water.water.Water", +}; static const std::vector hd_test_terrain = { "hd_base.data.terrain.foundation.foundation.Foundation", "hd_base.data.terrain.grass.grass.Grass", @@ -47,10 +62,16 @@ static const std::vector swgb_test_terrain = { "swgb_base.data.terrain.desert0.desert0.Desert0", "swgb_base.data.terrain.water1.water1.Water1", }; -static const std::vector trial_test_terrain = {}; +static const std::vector trial_test_terrain = { + "trial_base.data.terrain.foundation.foundation.Foundation", + "trial_base.data.terrain.grass.grass.Grass", + "trial_base.data.terrain.dirt.dirt.Dirt", + "trial_base.data.terrain.water.water.Water", +}; // TODO: Remove hardcoded test texture references static std::vector test_terrains; // declare static so we only have to do this once +static bool has_graphics = false; void build_test_terrains(const std::shared_ptr &gstate) { auto modpack_ids = gstate->get_mod_manager()->get_load_order(); @@ -59,36 +80,43 @@ void build_test_terrains(const std::shared_ptr &gstate) { test_terrains.insert(test_terrains.end(), aoe1_test_terrain.begin(), aoe1_test_terrain.end()); + has_graphics = false; } else if (modpack_id == "de1_base") { test_terrains.insert(test_terrains.end(), de1_test_terrain.begin(), de1_test_terrain.end()); + has_graphics = false; } else if (modpack_id == "aoe2_base") { test_terrains.insert(test_terrains.end(), aoe2_test_terrain.begin(), aoe2_test_terrain.end()); + has_graphics = true; } else if (modpack_id == "de2_base") { test_terrains.insert(test_terrains.end(), de2_test_terrain.begin(), de2_test_terrain.end()); + has_graphics = false; } else if (modpack_id == "hd_base") { test_terrains.insert(test_terrains.end(), hd_test_terrain.begin(), hd_test_terrain.end()); + has_graphics = true; } else if (modpack_id == "swgb_base") { test_terrains.insert(test_terrains.end(), swgb_test_terrain.begin(), swgb_test_terrain.end()); + has_graphics = true; } else if (modpack_id == "trial_base") { test_terrains.insert(test_terrains.end(), trial_test_terrain.begin(), trial_test_terrain.end()); + has_graphics = true; } } } @@ -185,7 +213,7 @@ std::shared_ptr TerrainFactory::add_chunk(const std::shared_ptr terrain_obj; + nyan::Object terrain_obj; if (test_terrains.empty()) { build_test_terrains(gstate); } @@ -204,20 +232,14 @@ std::shared_ptr TerrainFactory::add_chunk(const std::shared_ptrget_db_view()->get_object(test_terrains.at(terrain_index)); - terrain_info_path = api::APITerrain::get_terrain_path(terrain_obj.value()); - tiles.push_back({terrain_obj, terrain_info_path, terrain_elevation_t::zero()}); - } - test_chunk_index += 1; - } - else { - // use a test texture - if (test_chunk_index >= test_terrain_paths.size()) { - test_chunk_index = 0; - } - terrain_info_path = test_terrain_paths.at(test_chunk_index); + if (has_graphics) { + terrain_info_path = api::APITerrain::get_terrain_path(terrain_obj); + } + else { + terrain_info_path = test_terrain_paths.at(test_chunk_index % test_terrain_paths.size()); + } - for (size_t i = 0; i < size[0] * size[1]; ++i) { tiles.push_back({terrain_obj, terrain_info_path, terrain_elevation_t::zero()}); } diff --git a/libopenage/gamestate/terrain_tile.h b/libopenage/gamestate/terrain_tile.h index 36ae11ddf3..6d6dd99b55 100644 --- a/libopenage/gamestate/terrain_tile.h +++ b/libopenage/gamestate/terrain_tile.h @@ -20,10 +20,8 @@ using terrain_elevation_t = util::FixedPoint; struct TerrainTile { /** * Terrain definition used by this tile. - * - * TODO: Make this non-optional once all modpacks support terrain graphics. */ - std::optional terrain; + nyan::Object terrain; /** * Path to the terrain asset used by this tile. From 347d586e41107d2f22e55c5f0501b4a7d0cafe4a Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 28 Jul 2024 14:46:53 +0200 Subject: [PATCH 562/771] presenter: Let camera look at map center on startup. --- libopenage/presenter/presenter.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index 62435074d4..33c798bcef 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -116,6 +116,7 @@ void Presenter::init_graphics(bool debug) { // Camera this->camera = std::make_shared(this->renderer, this->window->get_size()); + this->camera->look_at_coord(coord::scene3{10.0, 10.0, 0}); // Center camera on the map this->window->add_resize_callback([this](size_t w, size_t h, double /*scale*/) { this->camera->resize(w, h); }); From 170e43f099add0381dbfc00ec3bed3b63dd6b208 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 8 Sep 2024 01:29:22 +0200 Subject: [PATCH 563/771] gamestate: Make component classes 'final'. --- libopenage/gamestate/component/api/idle.h | 4 ++-- libopenage/gamestate/component/api/live.h | 2 +- libopenage/gamestate/component/api/move.h | 4 ++-- libopenage/gamestate/component/api/selectable.h | 4 ++-- libopenage/gamestate/component/api/turn.h | 4 ++-- libopenage/gamestate/component/internal/activity.h | 2 +- libopenage/gamestate/component/internal/command_queue.h | 4 ++-- libopenage/gamestate/component/internal/ownership.h | 2 +- libopenage/gamestate/component/internal/position.h | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/libopenage/gamestate/component/api/idle.h b/libopenage/gamestate/component/api/idle.h index f9d42e5330..e8b114ff67 100644 --- a/libopenage/gamestate/component/api/idle.h +++ b/libopenage/gamestate/component/api/idle.h @@ -1,4 +1,4 @@ -// 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 @@ -7,7 +7,7 @@ namespace openage::gamestate::component { -class Idle : public APIComponent { +class Idle final : public APIComponent { public: using APIComponent::APIComponent; diff --git a/libopenage/gamestate/component/api/live.h b/libopenage/gamestate/component/api/live.h index cf733d5199..2e1f5e41d5 100644 --- a/libopenage/gamestate/component/api/live.h +++ b/libopenage/gamestate/component/api/live.h @@ -14,7 +14,7 @@ namespace openage::gamestate::component { -class Live : public APIComponent { +class Live final : public APIComponent { public: using APIComponent::APIComponent; diff --git a/libopenage/gamestate/component/api/move.h b/libopenage/gamestate/component/api/move.h index 1cc6d65cf7..99d87b8d72 100644 --- a/libopenage/gamestate/component/api/move.h +++ b/libopenage/gamestate/component/api/move.h @@ -1,4 +1,4 @@ -// 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 @@ -8,7 +8,7 @@ namespace openage::gamestate::component { -class Move : public APIComponent { +class Move final : public APIComponent { public: using APIComponent::APIComponent; diff --git a/libopenage/gamestate/component/api/selectable.h b/libopenage/gamestate/component/api/selectable.h index d12e1522a7..b37f674444 100644 --- a/libopenage/gamestate/component/api/selectable.h +++ b/libopenage/gamestate/component/api/selectable.h @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #pragma once @@ -10,7 +10,7 @@ namespace openage::gamestate::component { -class Selectable : public APIComponent { +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 881bb05de9..9506bcd6db 100644 --- a/libopenage/gamestate/component/api/turn.h +++ b/libopenage/gamestate/component/api/turn.h @@ -1,4 +1,4 @@ -// 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 @@ -10,7 +10,7 @@ namespace openage::gamestate::component { -class Turn : public APIComponent { +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 16976b7e11..ae0fd73389 100644 --- a/libopenage/gamestate/component/internal/activity.h +++ b/libopenage/gamestate/component/internal/activity.h @@ -27,7 +27,7 @@ class Node; namespace component { -class Activity : public InternalComponent { +class Activity final : public InternalComponent { public: /** * Creates a new activity component. diff --git a/libopenage/gamestate/component/internal/command_queue.h b/libopenage/gamestate/component/internal/command_queue.h index ea0c26502a..a7905c4d24 100644 --- a/libopenage/gamestate/component/internal/command_queue.h +++ b/libopenage/gamestate/component/internal/command_queue.h @@ -1,4 +1,4 @@ -// 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 @@ -19,7 +19,7 @@ class EventLoop; namespace gamestate::component { -class CommandQueue : public InternalComponent { +class CommandQueue final : public InternalComponent { public: /** * Creates an Ownership component. diff --git a/libopenage/gamestate/component/internal/ownership.h b/libopenage/gamestate/component/internal/ownership.h index 8ac13a0ba7..ab4b30bed3 100644 --- a/libopenage/gamestate/component/internal/ownership.h +++ b/libopenage/gamestate/component/internal/ownership.h @@ -19,7 +19,7 @@ class EventLoop; namespace gamestate::component { -class Ownership : public InternalComponent { +class Ownership final : public InternalComponent { public: /** * Creates an Ownership component. diff --git a/libopenage/gamestate/component/internal/position.h b/libopenage/gamestate/component/internal/position.h index fc4808ed0f..ff4cab0da2 100644 --- a/libopenage/gamestate/component/internal/position.h +++ b/libopenage/gamestate/component/internal/position.h @@ -20,7 +20,7 @@ class EventLoop; namespace gamestate::component { -class Position : public InternalComponent { +class Position final : public InternalComponent { public: /** * Create a Position component. From 44952e5e646307da417d404c10e9bba4e12497f2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 8 Sep 2024 01:34:55 +0200 Subject: [PATCH 564/771] doc: Change recommended entrypoint for Windows build. --- doc/build_instructions/windows_msvc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/build_instructions/windows_msvc.md b/doc/build_instructions/windows_msvc.md index 5be6d9167c..af889d8b99 100644 --- a/doc/build_instructions/windows_msvc.md +++ b/doc/build_instructions/windows_msvc.md @@ -93,7 +93,7 @@ _Note:_ If you want to download and build Nyan automatically add `-DDOWNLOAD_NYA - If prebuilt QT6 was installed, the original location of QT6 DLLs is `\bin`. - Now, to run the openage: - - Open a CMD window in `\build\` and run `python -m openage game` + - Open a CMD window in `\build\` and run `python -m openage main` - Execute`\build\run.exe` every time after that and enjoy! ## Packaging From eb62b8ce432c3209cabdb4b52c8e2d24bd184c5b Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 8 Sep 2024 22:32:05 +0200 Subject: [PATCH 565/771] gamestate: Create grids for all existing path types. --- libopenage/gamestate/game.cpp | 2 +- libopenage/gamestate/map.cpp | 35 ++++++++++++++------------------ libopenage/gamestate/map.h | 5 ++++- libopenage/gamestate/terrain.cpp | 9 ++------ libopenage/gamestate/terrain.h | 13 +++--------- libopenage/pathfinding/grid.h | 4 ++-- 6 files changed, 27 insertions(+), 41 deletions(-) diff --git a/libopenage/gamestate/game.cpp b/libopenage/gamestate/game.cpp index db50f8cc72..e63499d769 100644 --- a/libopenage/gamestate/game.cpp +++ b/libopenage/gamestate/game.cpp @@ -143,7 +143,7 @@ void Game::generate_terrain(const std::shared_ptr &terrain_facto auto terrain = terrain_factory->add_terrain({20, 20}, {chunk0, chunk1, chunk2, chunk3}); - auto map = std::make_shared(terrain); + auto map = std::make_shared(this->state, terrain); this->state->set_map(map); } diff --git a/libopenage/gamestate/map.cpp b/libopenage/gamestate/map.cpp index 3d9af584cb..1de5b0fe27 100644 --- a/libopenage/gamestate/map.cpp +++ b/libopenage/gamestate/map.cpp @@ -5,6 +5,7 @@ #include #include "gamestate/api/terrain.h" +#include "gamestate/game_state.h" #include "gamestate/terrain.h" #include "gamestate/terrain_chunk.h" #include "pathfinding/cost_field.h" @@ -14,31 +15,25 @@ namespace openage::gamestate { -Map::Map(const std::shared_ptr &terrain) : +Map::Map(const std::shared_ptr &state, + const std::shared_ptr &terrain) : terrain{terrain}, pathfinder{std::make_shared()}, grid_lookup{} { - // Get grid types - // TODO: This should probably not be dependent on the existing tiles, but - // on all defined nyan PathType objects + // Create a grid for each path type + // TODO: This is non-deterministic because of the unordered set. Is this a problem? + auto nyan_db = state->get_db_view(); + std::unordered_set path_types = nyan_db->get_obj_children_all("engine.util.path_type.PathType"); size_t grid_idx = 0; auto chunk_size = this->terrain->get_chunk(0)->get_size(); - auto side_length = std::max(chunk_size[0], chunk_size[0]); - util::Vector2s grid_size{this->terrain->get_row_size(), this->terrain->get_column_size()}; - for (const auto &chunk : this->terrain->get_chunks()) { - for (const auto &tile : chunk->get_tiles()) { - auto path_costs = api::APITerrain::get_path_costs(tile.terrain); - - for (const auto &path_cost : path_costs) { - if (not this->grid_lookup.contains(path_cost.first)) { - auto grid = std::make_shared(grid_idx, grid_size, side_length); - this->pathfinder->add_grid(grid); - - this->grid_lookup.emplace(path_cost.first, grid_idx); - grid_idx += 1; - } - } - } + auto side_length = std::max(chunk_size[0], chunk_size[1]); + auto grid_size = this->terrain->get_chunks_size(); + for (const auto &path_type : path_types) { + auto grid = std::make_shared(grid_idx, grid_size, side_length); + this->pathfinder->add_grid(grid); + + this->grid_lookup.emplace(path_type, grid_idx); + grid_idx += 1; } // Set path costs diff --git a/libopenage/gamestate/map.h b/libopenage/gamestate/map.h index 4033373023..2817f74457 100644 --- a/libopenage/gamestate/map.h +++ b/libopenage/gamestate/map.h @@ -17,6 +17,7 @@ class Pathfinder; } // namespace path namespace gamestate { +class GameState; class Terrain; class Map { @@ -26,9 +27,11 @@ class Map { * * Initializes the pathfinder with the terrain path costs. * + * @param state Game state. * @param terrain Terrain object. */ - Map(const std::shared_ptr &terrain); + Map(const std::shared_ptr &state, + const std::shared_ptr &terrain); ~Map() = default; diff --git a/libopenage/gamestate/terrain.cpp b/libopenage/gamestate/terrain.cpp index e7d078f2c5..97c5ef5b51 100644 --- a/libopenage/gamestate/terrain.cpp +++ b/libopenage/gamestate/terrain.cpp @@ -78,14 +78,9 @@ const util::Vector2s &Terrain::get_size() const { return this->size; } -size_t Terrain::get_row_size() const { +const util::Vector2s Terrain::get_chunks_size() const { auto chunk_size = this->chunks[0]->get_size(); - return this->size[0] / chunk_size[0]; -} - -size_t Terrain::get_column_size() const { - auto chunk_size = this->chunks[0]->get_size(); - return this->size[1] / chunk_size[1]; + return {this->size[0] / chunk_size[0], this->size[1] / chunk_size[1]}; } void Terrain::add_chunk(const std::shared_ptr &chunk) { diff --git a/libopenage/gamestate/terrain.h b/libopenage/gamestate/terrain.h index 1a5db1bf95..d77eaf939c 100644 --- a/libopenage/gamestate/terrain.h +++ b/libopenage/gamestate/terrain.h @@ -52,18 +52,11 @@ class Terrain { const util::Vector2s &get_size() const; /** - * Get the size of a row in the terrain. + * Get the size of the terrain (in chunks). * - * @return Row size (width). + * @return Terrain chunk size (width x height). */ - size_t get_row_size() const; - - /** - * Get the size of a column in the terrain. - * - * @return Column size (height). - */ - size_t get_column_size() const; + const util::Vector2s get_chunks_size() const; /** * Add a chunk to the terrain. diff --git a/libopenage/pathfinding/grid.h b/libopenage/pathfinding/grid.h index 8f34a7643c..8f24743433 100644 --- a/libopenage/pathfinding/grid.h +++ b/libopenage/pathfinding/grid.h @@ -23,7 +23,7 @@ class Grid { * Create a new empty grid of width x height sectors with a specified size. * * @param id ID of the grid. - * @param size Size of the grid (width x height). + * @param size Size of the grid in sectors (width x height). * @param sector_size Side length of each sector. */ Grid(grid_id_t id, @@ -34,7 +34,7 @@ class Grid { * Create a grid of width x height sectors from a list of existing sectors. * * @param id ID of the grid. - * @param size Size of the grid (width x height). + * @param size Size of the grid in sectors (width x height). * @param sectors Existing sectors. */ Grid(grid_id_t id, From c6db4fbb304be2da72653c0a8c89092cac1c7962 Mon Sep 17 00:00:00 2001 From: edvin Date: Wed, 18 Sep 2024 14:13:55 +0200 Subject: [PATCH 566/771] renderer: Update syntax of GlUniformBuffer to match that of GlUniformInput --- libopenage/renderer/opengl/uniform_buffer.cpp | 51 +++++++++---------- libopenage/renderer/opengl/uniform_buffer.h | 34 ++++++------- libopenage/renderer/uniform_buffer.h | 32 ++++++------ libopenage/renderer/uniform_input.cpp | 34 ++++++------- 4 files changed, 75 insertions(+), 76 deletions(-) diff --git a/libopenage/renderer/opengl/uniform_buffer.cpp b/libopenage/renderer/opengl/uniform_buffer.cpp index 661f9e3173..3f16f1fdd6 100644 --- a/libopenage/renderer/opengl/uniform_buffer.cpp +++ b/libopenage/renderer/opengl/uniform_buffer.cpp @@ -80,8 +80,8 @@ std::shared_ptr GlUniformBuffer::new_unif_in() { return in; } -void GlUniformBuffer::set_unif(std::shared_ptr const &in, const char *unif, void const *val, GLenum type) { - auto unif_in = std::dynamic_pointer_cast(in); +void GlUniformBuffer::set_unif(UniformBufferInput &in, const char *unif, void const *val, GLenum type) { + auto &unif_in = dynamic_cast(in); auto uniform = this->uniforms.find(unif); ENSURE(uniform != std::end(this->uniforms), @@ -96,83 +96,82 @@ void GlUniformBuffer::set_unif(std::shared_ptr const &in, co ENSURE(size == unif_data.size, "Tried to set uniform " << unif << " to a value of the wrong size."); - auto update_off = unif_in->update_offs.find(unif); - if (update_off != std::end(unif_in->update_offs)) [[likely]] { // always used after the uniform value is written once - // already wrote to this uniform since last upload + auto update_off = unif_in.update_offs.find(unif); + if (update_off != std::end(unif_in.update_offs)) [[likely]] { // always used after the uniform value is written once // already wrote to this uniform since last upload size_t off = update_off->second; - memcpy(unif_in->update_data.data() + off, val, size); + memcpy(unif_in.update_data.data() + off, val, size); } else { // first time writing to this uniform since last upload, so // extend the buffer before storing the uniform value - size_t prev_size = unif_in->update_data.size(); - unif_in->update_data.resize(prev_size + size); - memcpy(unif_in->update_data.data() + prev_size, val, size); - unif_in->update_offs.emplace(unif, prev_size); + size_t prev_size = unif_in.update_data.size(); + unif_in.update_data.resize(prev_size + size); + memcpy(unif_in.update_data.data() + prev_size, val, size); + unif_in.update_offs.emplace(unif, prev_size); } } -void GlUniformBuffer::set_i32(std::shared_ptr const &in, const char *unif, int32_t val) { +void GlUniformBuffer::set_i32(UniformBufferInput &in, const char *unif, int32_t val) { this->set_unif(in, unif, &val, GL_INT); } -void GlUniformBuffer::set_u32(std::shared_ptr const &in, const char *unif, uint32_t val) { +void GlUniformBuffer::set_u32(UniformBufferInput &in, const char *unif, uint32_t val) { this->set_unif(in, unif, &val, GL_UNSIGNED_INT); } -void GlUniformBuffer::set_f32(std::shared_ptr const &in, const char *unif, float val) { +void GlUniformBuffer::set_f32(UniformBufferInput &in, const char *unif, float val) { this->set_unif(in, unif, &val, GL_FLOAT); } -void GlUniformBuffer::set_f64(std::shared_ptr const &in, const char *unif, double val) { +void GlUniformBuffer::set_f64(UniformBufferInput &in, const char *unif, double val) { this->set_unif(in, unif, &val, GL_DOUBLE); } -void GlUniformBuffer::set_bool(std::shared_ptr const &in, const char *unif, bool val) { +void GlUniformBuffer::set_bool(UniformBufferInput &in, const char *unif, bool val) { this->set_unif(in, unif, &val, GL_BOOL); } -void GlUniformBuffer::set_v2f32(std::shared_ptr const &in, const char *unif, Eigen::Vector2f const &val) { +void GlUniformBuffer::set_v2f32(UniformBufferInput &in, const char *unif, Eigen::Vector2f const &val) { this->set_unif(in, unif, val.data(), GL_FLOAT_VEC2); } -void GlUniformBuffer::set_v3f32(std::shared_ptr const &in, const char *unif, Eigen::Vector3f const &val) { +void GlUniformBuffer::set_v3f32(UniformBufferInput &in, const char *unif, Eigen::Vector3f const &val) { this->set_unif(in, unif, val.data(), GL_FLOAT_VEC3); } -void GlUniformBuffer::set_v4f32(std::shared_ptr const &in, const char *unif, Eigen::Vector4f const &val) { +void GlUniformBuffer::set_v4f32(UniformBufferInput &in, const char *unif, Eigen::Vector4f const &val) { this->set_unif(in, unif, val.data(), GL_FLOAT_VEC4); } -void GlUniformBuffer::set_v2i32(std::shared_ptr const &in, const char *unif, Eigen::Vector2i const &val) { +void GlUniformBuffer::set_v2i32(UniformBufferInput &in, const char *unif, Eigen::Vector2i const &val) { this->set_unif(in, unif, val.data(), GL_INT_VEC2); } -void GlUniformBuffer::set_v3i32(std::shared_ptr const &in, const char *unif, Eigen::Vector3i const &val) { +void GlUniformBuffer::set_v3i32(UniformBufferInput &in, const char *unif, Eigen::Vector3i const &val) { this->set_unif(in, unif, val.data(), GL_INT_VEC3); } -void GlUniformBuffer::set_v4i32(std::shared_ptr const &in, const char *unif, Eigen::Vector4i const &val) { +void GlUniformBuffer::set_v4i32(UniformBufferInput &in, const char *unif, Eigen::Vector4i const &val) { this->set_unif(in, unif, val.data(), GL_INT_VEC4); } -void GlUniformBuffer::set_v2ui32(std::shared_ptr const &in, const char *unif, Eigen::Vector2 const &val) { +void GlUniformBuffer::set_v2ui32(UniformBufferInput &in, const char *unif, Eigen::Vector2 const &val) { this->set_unif(in, unif, val.data(), GL_UNSIGNED_INT_VEC2); } -void GlUniformBuffer::set_v3ui32(std::shared_ptr const &in, const char *unif, Eigen::Vector3 const &val) { +void GlUniformBuffer::set_v3ui32(UniformBufferInput &in, const char *unif, Eigen::Vector3 const &val) { this->set_unif(in, unif, val.data(), GL_UNSIGNED_INT_VEC3); } -void GlUniformBuffer::set_v4ui32(std::shared_ptr const &in, const char *unif, Eigen::Vector4 const &val) { +void GlUniformBuffer::set_v4ui32(UniformBufferInput &in, const char *unif, Eigen::Vector4 const &val) { this->set_unif(in, unif, val.data(), GL_UNSIGNED_INT_VEC4); } -void GlUniformBuffer::set_m4f32(std::shared_ptr const &in, const char *unif, Eigen::Matrix4f const &val) { +void GlUniformBuffer::set_m4f32(UniformBufferInput &in, const char *unif, Eigen::Matrix4f const &val) { this->set_unif(in, unif, val.data(), GL_FLOAT_MAT4); } -void GlUniformBuffer::set_tex(std::shared_ptr const &in, const char *unif, std::shared_ptr const &val) { +void GlUniformBuffer::set_tex(UniformBufferInput &in, const char *unif, std::shared_ptr const &val) { auto tex = std::dynamic_pointer_cast(val); GLuint handle = tex->get_handle(); this->set_unif(in, unif, &handle, GL_SAMPLER_2D); diff --git a/libopenage/renderer/opengl/uniform_buffer.h b/libopenage/renderer/opengl/uniform_buffer.h index 6c7ca3d869..f1981fd19e 100644 --- a/libopenage/renderer/opengl/uniform_buffer.h +++ b/libopenage/renderer/opengl/uniform_buffer.h @@ -51,22 +51,22 @@ class GlUniformBuffer final : public UniformBuffer protected: std::shared_ptr new_unif_in() override; - void set_i32(std::shared_ptr const &, const char *, int32_t) override; - void set_u32(std::shared_ptr const &, const char *, uint32_t) override; - void set_f32(std::shared_ptr const &, const char *, float) override; - void set_f64(std::shared_ptr const &, const char *, double) override; - void set_bool(std::shared_ptr const &, const char *, bool) override; - void set_v2f32(std::shared_ptr const &, const char *, Eigen::Vector2f const &) override; - void set_v3f32(std::shared_ptr const &, const char *, Eigen::Vector3f const &) override; - void set_v4f32(std::shared_ptr const &, const char *, Eigen::Vector4f const &) override; - void set_v2i32(std::shared_ptr const &, const char *, Eigen::Vector2i const &) override; - void set_v3i32(std::shared_ptr const &, const char *, Eigen::Vector3i const &) override; - void set_v4i32(std::shared_ptr const &, const char *, Eigen::Vector4i const &) override; - void set_v2ui32(std::shared_ptr const &, const char *, Eigen::Vector2 const &) override; - void set_v3ui32(std::shared_ptr const &, const char *, Eigen::Vector3 const &) override; - void set_v4ui32(std::shared_ptr const &, const char *, Eigen::Vector4 const &) override; - void set_m4f32(std::shared_ptr const &, const char *, Eigen::Matrix4f const &) override; - void set_tex(std::shared_ptr const &, const char *, std::shared_ptr const &) override; + void set_i32(UniformBufferInput &in, const char *, int32_t) override; + void set_u32(UniformBufferInput &in, const char *, uint32_t) override; + void set_f32(UniformBufferInput &in, const char *, float) override; + void set_f64(UniformBufferInput &in, const char *, double) override; + void set_bool(UniformBufferInput &in, const char *, bool) override; + void set_v2f32(UniformBufferInput &in, const char *, Eigen::Vector2f const &) override; + void set_v3f32(UniformBufferInput &in, const char *, Eigen::Vector3f const &) override; + void set_v4f32(UniformBufferInput &in, const char *, Eigen::Vector4f const &) override; + void set_v2i32(UniformBufferInput &in, const char *, Eigen::Vector2i const &) override; + void set_v3i32(UniformBufferInput &in, const char *, Eigen::Vector3i const &) override; + void set_v4i32(UniformBufferInput &in, const char *, Eigen::Vector4i const &) override; + void set_v2ui32(UniformBufferInput &in, const char *, Eigen::Vector2 const &) override; + void set_v3ui32(UniformBufferInput &in, const char *, Eigen::Vector3 const &) override; + void set_v4ui32(UniformBufferInput &in, const char *, Eigen::Vector4 const &) override; + void set_m4f32(UniformBufferInput &in, const char *, Eigen::Matrix4f const &) override; + void set_tex(UniformBufferInput &in, const char *, std::shared_ptr const &) override; private: /** @@ -80,7 +80,7 @@ class GlUniformBuffer final : public UniformBuffer * @param val Pointer to the value to update the uniform with. * @param type Type of the uniform. */ - void set_unif(std::shared_ptr const &in, + void set_unif(UniformBufferInput &in, const char *name, void const *val, GLenum type); diff --git a/libopenage/renderer/uniform_buffer.h b/libopenage/renderer/uniform_buffer.h index 85040a4976..c91edf1132 100644 --- a/libopenage/renderer/uniform_buffer.h +++ b/libopenage/renderer/uniform_buffer.h @@ -50,22 +50,22 @@ class UniformBuffer : public std::enable_shared_from_this { protected: virtual std::shared_ptr new_unif_in() = 0; - virtual void set_i32(std::shared_ptr const &, const char *, int32_t) = 0; - virtual void set_u32(std::shared_ptr const &, const char *, uint32_t) = 0; - virtual void set_f32(std::shared_ptr const &, const char *, float) = 0; - virtual void set_f64(std::shared_ptr const &, const char *, double) = 0; - virtual void set_bool(std::shared_ptr const &, const char *, bool) = 0; - virtual void set_v2f32(std::shared_ptr const &, const char *, Eigen::Vector2f const &) = 0; - virtual void set_v3f32(std::shared_ptr const &, const char *, Eigen::Vector3f const &) = 0; - virtual void set_v4f32(std::shared_ptr const &, const char *, Eigen::Vector4f const &) = 0; - virtual void set_v2i32(std::shared_ptr const &, const char *, Eigen::Vector2i const &) = 0; - virtual void set_v3i32(std::shared_ptr const &, const char *, Eigen::Vector3i const &) = 0; - virtual void set_v4i32(std::shared_ptr const &, const char *, Eigen::Vector4i const &) = 0; - virtual void set_v2ui32(std::shared_ptr const &, const char *, Eigen::Vector2 const &) = 0; - virtual void set_v3ui32(std::shared_ptr const &, const char *, Eigen::Vector3 const &) = 0; - virtual void set_v4ui32(std::shared_ptr const &, const char *, Eigen::Vector4 const &) = 0; - virtual void set_m4f32(std::shared_ptr const &, const char *, Eigen::Matrix4f const &) = 0; - virtual void set_tex(std::shared_ptr const &, const char *, std::shared_ptr const &) = 0; + virtual void set_i32(UniformBufferInput &in, const char *, int32_t) = 0; + virtual void set_u32(UniformBufferInput &in, const char *, uint32_t) = 0; + virtual void set_f32(UniformBufferInput &in, const char *, float) = 0; + virtual void set_f64(UniformBufferInput &in, const char *, double) = 0; + virtual void set_bool(UniformBufferInput &in, const char *, bool) = 0; + virtual void set_v2f32(UniformBufferInput &in, const char *, Eigen::Vector2f const &) = 0; + virtual void set_v3f32(UniformBufferInput &in, const char *, Eigen::Vector3f const &) = 0; + virtual void set_v4f32(UniformBufferInput &in, const char *, Eigen::Vector4f const &) = 0; + virtual void set_v2i32(UniformBufferInput &in, const char *, Eigen::Vector2i const &) = 0; + virtual void set_v3i32(UniformBufferInput &in, const char *, Eigen::Vector3i const &) = 0; + virtual void set_v4i32(UniformBufferInput &in, const char *, Eigen::Vector4i const &) = 0; + virtual void set_v2ui32(UniformBufferInput &in, const char *, Eigen::Vector2 const &) = 0; + virtual void set_v3ui32(UniformBufferInput &in, const char *, Eigen::Vector3 const &) = 0; + virtual void set_v4ui32(UniformBufferInput &in, const char *, Eigen::Vector4 const &) = 0; + virtual void set_m4f32(UniformBufferInput &in, const char *, Eigen::Matrix4f const &) = 0; + virtual void set_tex(UniformBufferInput &in, const char *, std::shared_ptr const &) = 0; }; } // namespace openage::renderer diff --git a/libopenage/renderer/uniform_input.cpp b/libopenage/renderer/uniform_input.cpp index ee42202faf..b3abbe7ba8 100644 --- a/libopenage/renderer/uniform_input.cpp +++ b/libopenage/renderer/uniform_input.cpp @@ -157,71 +157,71 @@ UniformBufferInput::UniformBufferInput(std::shared_ptr const &buf void UniformBufferInput::update() {} void UniformBufferInput::update(const char *unif, int32_t val) { - this->buffer->set_i32(this->shared_from_this(), unif, val); + this->buffer->set_i32(*this, unif, val); } void UniformBufferInput::update(const char *unif, uint32_t val) { - this->buffer->set_u32(this->shared_from_this(), unif, val); + this->buffer->set_u32(*this, unif, val); } void UniformBufferInput::update(const char *unif, float val) { - this->buffer->set_f32(this->shared_from_this(), unif, val); + this->buffer->set_f32(*this, unif, val); } void UniformBufferInput::update(const char *unif, double val) { - this->buffer->set_f64(this->shared_from_this(), unif, val); + this->buffer->set_f64(*this, unif, val); } void UniformBufferInput::update(const char *unif, bool val) { - this->buffer->set_bool(this->shared_from_this(), unif, val); + this->buffer->set_bool(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector2f const &val) { - this->buffer->set_v2f32(this->shared_from_this(), unif, val); + this->buffer->set_v2f32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector3f const &val) { - this->buffer->set_v3f32(this->shared_from_this(), unif, val); + this->buffer->set_v3f32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector4f const &val) { - this->buffer->set_v4f32(this->shared_from_this(), unif, val); + this->buffer->set_v4f32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector2i const &val) { - this->buffer->set_v2i32(this->shared_from_this(), unif, val); + this->buffer->set_v2i32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector3i const &val) { - this->buffer->set_v3i32(this->shared_from_this(), unif, val); + this->buffer->set_v3i32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector4i const &val) { - this->buffer->set_v4i32(this->shared_from_this(), unif, val); + this->buffer->set_v4i32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector2 const &val) { - this->buffer->set_v2ui32(this->shared_from_this(), unif, val); + this->buffer->set_v2ui32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector3 const &val) { - this->buffer->set_v3ui32(this->shared_from_this(), unif, val); + this->buffer->set_v3ui32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector4 const &val) { - this->buffer->set_v4ui32(this->shared_from_this(), unif, val); + this->buffer->set_v4ui32(*this, unif, val); } void UniformBufferInput::update(const char *unif, std::shared_ptr const &val) { - this->buffer->set_tex(this->shared_from_this(), unif, val); + this->buffer->set_tex(*this, unif, val); } void UniformBufferInput::update(const char *unif, std::shared_ptr &val) { - this->buffer->set_tex(this->shared_from_this(), unif, val); + this->buffer->set_tex(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Matrix4f const &val) { - this->buffer->set_m4f32(this->shared_from_this(), unif, val); + this->buffer->set_m4f32(*this, unif, val); } } // namespace openage::renderer From 6256c66c6e35b9db7838012ea7d3a18a5b102dee Mon Sep 17 00:00:00 2001 From: Edvin Date: Wed, 18 Sep 2024 14:13:55 +0200 Subject: [PATCH 567/771] renderer: Update syntax of GlUniformBuffer to match that of GlUniformInput --- libopenage/renderer/opengl/uniform_buffer.cpp | 51 +++++++++---------- libopenage/renderer/opengl/uniform_buffer.h | 34 ++++++------- libopenage/renderer/uniform_buffer.h | 32 ++++++------ libopenage/renderer/uniform_input.cpp | 34 ++++++------- 4 files changed, 75 insertions(+), 76 deletions(-) diff --git a/libopenage/renderer/opengl/uniform_buffer.cpp b/libopenage/renderer/opengl/uniform_buffer.cpp index 661f9e3173..3f16f1fdd6 100644 --- a/libopenage/renderer/opengl/uniform_buffer.cpp +++ b/libopenage/renderer/opengl/uniform_buffer.cpp @@ -80,8 +80,8 @@ std::shared_ptr GlUniformBuffer::new_unif_in() { return in; } -void GlUniformBuffer::set_unif(std::shared_ptr const &in, const char *unif, void const *val, GLenum type) { - auto unif_in = std::dynamic_pointer_cast(in); +void GlUniformBuffer::set_unif(UniformBufferInput &in, const char *unif, void const *val, GLenum type) { + auto &unif_in = dynamic_cast(in); auto uniform = this->uniforms.find(unif); ENSURE(uniform != std::end(this->uniforms), @@ -96,83 +96,82 @@ void GlUniformBuffer::set_unif(std::shared_ptr const &in, co ENSURE(size == unif_data.size, "Tried to set uniform " << unif << " to a value of the wrong size."); - auto update_off = unif_in->update_offs.find(unif); - if (update_off != std::end(unif_in->update_offs)) [[likely]] { // always used after the uniform value is written once - // already wrote to this uniform since last upload + auto update_off = unif_in.update_offs.find(unif); + if (update_off != std::end(unif_in.update_offs)) [[likely]] { // always used after the uniform value is written once // already wrote to this uniform since last upload size_t off = update_off->second; - memcpy(unif_in->update_data.data() + off, val, size); + memcpy(unif_in.update_data.data() + off, val, size); } else { // first time writing to this uniform since last upload, so // extend the buffer before storing the uniform value - size_t prev_size = unif_in->update_data.size(); - unif_in->update_data.resize(prev_size + size); - memcpy(unif_in->update_data.data() + prev_size, val, size); - unif_in->update_offs.emplace(unif, prev_size); + size_t prev_size = unif_in.update_data.size(); + unif_in.update_data.resize(prev_size + size); + memcpy(unif_in.update_data.data() + prev_size, val, size); + unif_in.update_offs.emplace(unif, prev_size); } } -void GlUniformBuffer::set_i32(std::shared_ptr const &in, const char *unif, int32_t val) { +void GlUniformBuffer::set_i32(UniformBufferInput &in, const char *unif, int32_t val) { this->set_unif(in, unif, &val, GL_INT); } -void GlUniformBuffer::set_u32(std::shared_ptr const &in, const char *unif, uint32_t val) { +void GlUniformBuffer::set_u32(UniformBufferInput &in, const char *unif, uint32_t val) { this->set_unif(in, unif, &val, GL_UNSIGNED_INT); } -void GlUniformBuffer::set_f32(std::shared_ptr const &in, const char *unif, float val) { +void GlUniformBuffer::set_f32(UniformBufferInput &in, const char *unif, float val) { this->set_unif(in, unif, &val, GL_FLOAT); } -void GlUniformBuffer::set_f64(std::shared_ptr const &in, const char *unif, double val) { +void GlUniformBuffer::set_f64(UniformBufferInput &in, const char *unif, double val) { this->set_unif(in, unif, &val, GL_DOUBLE); } -void GlUniformBuffer::set_bool(std::shared_ptr const &in, const char *unif, bool val) { +void GlUniformBuffer::set_bool(UniformBufferInput &in, const char *unif, bool val) { this->set_unif(in, unif, &val, GL_BOOL); } -void GlUniformBuffer::set_v2f32(std::shared_ptr const &in, const char *unif, Eigen::Vector2f const &val) { +void GlUniformBuffer::set_v2f32(UniformBufferInput &in, const char *unif, Eigen::Vector2f const &val) { this->set_unif(in, unif, val.data(), GL_FLOAT_VEC2); } -void GlUniformBuffer::set_v3f32(std::shared_ptr const &in, const char *unif, Eigen::Vector3f const &val) { +void GlUniformBuffer::set_v3f32(UniformBufferInput &in, const char *unif, Eigen::Vector3f const &val) { this->set_unif(in, unif, val.data(), GL_FLOAT_VEC3); } -void GlUniformBuffer::set_v4f32(std::shared_ptr const &in, const char *unif, Eigen::Vector4f const &val) { +void GlUniformBuffer::set_v4f32(UniformBufferInput &in, const char *unif, Eigen::Vector4f const &val) { this->set_unif(in, unif, val.data(), GL_FLOAT_VEC4); } -void GlUniformBuffer::set_v2i32(std::shared_ptr const &in, const char *unif, Eigen::Vector2i const &val) { +void GlUniformBuffer::set_v2i32(UniformBufferInput &in, const char *unif, Eigen::Vector2i const &val) { this->set_unif(in, unif, val.data(), GL_INT_VEC2); } -void GlUniformBuffer::set_v3i32(std::shared_ptr const &in, const char *unif, Eigen::Vector3i const &val) { +void GlUniformBuffer::set_v3i32(UniformBufferInput &in, const char *unif, Eigen::Vector3i const &val) { this->set_unif(in, unif, val.data(), GL_INT_VEC3); } -void GlUniformBuffer::set_v4i32(std::shared_ptr const &in, const char *unif, Eigen::Vector4i const &val) { +void GlUniformBuffer::set_v4i32(UniformBufferInput &in, const char *unif, Eigen::Vector4i const &val) { this->set_unif(in, unif, val.data(), GL_INT_VEC4); } -void GlUniformBuffer::set_v2ui32(std::shared_ptr const &in, const char *unif, Eigen::Vector2 const &val) { +void GlUniformBuffer::set_v2ui32(UniformBufferInput &in, const char *unif, Eigen::Vector2 const &val) { this->set_unif(in, unif, val.data(), GL_UNSIGNED_INT_VEC2); } -void GlUniformBuffer::set_v3ui32(std::shared_ptr const &in, const char *unif, Eigen::Vector3 const &val) { +void GlUniformBuffer::set_v3ui32(UniformBufferInput &in, const char *unif, Eigen::Vector3 const &val) { this->set_unif(in, unif, val.data(), GL_UNSIGNED_INT_VEC3); } -void GlUniformBuffer::set_v4ui32(std::shared_ptr const &in, const char *unif, Eigen::Vector4 const &val) { +void GlUniformBuffer::set_v4ui32(UniformBufferInput &in, const char *unif, Eigen::Vector4 const &val) { this->set_unif(in, unif, val.data(), GL_UNSIGNED_INT_VEC4); } -void GlUniformBuffer::set_m4f32(std::shared_ptr const &in, const char *unif, Eigen::Matrix4f const &val) { +void GlUniformBuffer::set_m4f32(UniformBufferInput &in, const char *unif, Eigen::Matrix4f const &val) { this->set_unif(in, unif, val.data(), GL_FLOAT_MAT4); } -void GlUniformBuffer::set_tex(std::shared_ptr const &in, const char *unif, std::shared_ptr const &val) { +void GlUniformBuffer::set_tex(UniformBufferInput &in, const char *unif, std::shared_ptr const &val) { auto tex = std::dynamic_pointer_cast(val); GLuint handle = tex->get_handle(); this->set_unif(in, unif, &handle, GL_SAMPLER_2D); diff --git a/libopenage/renderer/opengl/uniform_buffer.h b/libopenage/renderer/opengl/uniform_buffer.h index 6c7ca3d869..f1981fd19e 100644 --- a/libopenage/renderer/opengl/uniform_buffer.h +++ b/libopenage/renderer/opengl/uniform_buffer.h @@ -51,22 +51,22 @@ class GlUniformBuffer final : public UniformBuffer protected: std::shared_ptr new_unif_in() override; - void set_i32(std::shared_ptr const &, const char *, int32_t) override; - void set_u32(std::shared_ptr const &, const char *, uint32_t) override; - void set_f32(std::shared_ptr const &, const char *, float) override; - void set_f64(std::shared_ptr const &, const char *, double) override; - void set_bool(std::shared_ptr const &, const char *, bool) override; - void set_v2f32(std::shared_ptr const &, const char *, Eigen::Vector2f const &) override; - void set_v3f32(std::shared_ptr const &, const char *, Eigen::Vector3f const &) override; - void set_v4f32(std::shared_ptr const &, const char *, Eigen::Vector4f const &) override; - void set_v2i32(std::shared_ptr const &, const char *, Eigen::Vector2i const &) override; - void set_v3i32(std::shared_ptr const &, const char *, Eigen::Vector3i const &) override; - void set_v4i32(std::shared_ptr const &, const char *, Eigen::Vector4i const &) override; - void set_v2ui32(std::shared_ptr const &, const char *, Eigen::Vector2 const &) override; - void set_v3ui32(std::shared_ptr const &, const char *, Eigen::Vector3 const &) override; - void set_v4ui32(std::shared_ptr const &, const char *, Eigen::Vector4 const &) override; - void set_m4f32(std::shared_ptr const &, const char *, Eigen::Matrix4f const &) override; - void set_tex(std::shared_ptr const &, const char *, std::shared_ptr const &) override; + void set_i32(UniformBufferInput &in, const char *, int32_t) override; + void set_u32(UniformBufferInput &in, const char *, uint32_t) override; + void set_f32(UniformBufferInput &in, const char *, float) override; + void set_f64(UniformBufferInput &in, const char *, double) override; + void set_bool(UniformBufferInput &in, const char *, bool) override; + void set_v2f32(UniformBufferInput &in, const char *, Eigen::Vector2f const &) override; + void set_v3f32(UniformBufferInput &in, const char *, Eigen::Vector3f const &) override; + void set_v4f32(UniformBufferInput &in, const char *, Eigen::Vector4f const &) override; + void set_v2i32(UniformBufferInput &in, const char *, Eigen::Vector2i const &) override; + void set_v3i32(UniformBufferInput &in, const char *, Eigen::Vector3i const &) override; + void set_v4i32(UniformBufferInput &in, const char *, Eigen::Vector4i const &) override; + void set_v2ui32(UniformBufferInput &in, const char *, Eigen::Vector2 const &) override; + void set_v3ui32(UniformBufferInput &in, const char *, Eigen::Vector3 const &) override; + void set_v4ui32(UniformBufferInput &in, const char *, Eigen::Vector4 const &) override; + void set_m4f32(UniformBufferInput &in, const char *, Eigen::Matrix4f const &) override; + void set_tex(UniformBufferInput &in, const char *, std::shared_ptr const &) override; private: /** @@ -80,7 +80,7 @@ class GlUniformBuffer final : public UniformBuffer * @param val Pointer to the value to update the uniform with. * @param type Type of the uniform. */ - void set_unif(std::shared_ptr const &in, + void set_unif(UniformBufferInput &in, const char *name, void const *val, GLenum type); diff --git a/libopenage/renderer/uniform_buffer.h b/libopenage/renderer/uniform_buffer.h index 85040a4976..c91edf1132 100644 --- a/libopenage/renderer/uniform_buffer.h +++ b/libopenage/renderer/uniform_buffer.h @@ -50,22 +50,22 @@ class UniformBuffer : public std::enable_shared_from_this { protected: virtual std::shared_ptr new_unif_in() = 0; - virtual void set_i32(std::shared_ptr const &, const char *, int32_t) = 0; - virtual void set_u32(std::shared_ptr const &, const char *, uint32_t) = 0; - virtual void set_f32(std::shared_ptr const &, const char *, float) = 0; - virtual void set_f64(std::shared_ptr const &, const char *, double) = 0; - virtual void set_bool(std::shared_ptr const &, const char *, bool) = 0; - virtual void set_v2f32(std::shared_ptr const &, const char *, Eigen::Vector2f const &) = 0; - virtual void set_v3f32(std::shared_ptr const &, const char *, Eigen::Vector3f const &) = 0; - virtual void set_v4f32(std::shared_ptr const &, const char *, Eigen::Vector4f const &) = 0; - virtual void set_v2i32(std::shared_ptr const &, const char *, Eigen::Vector2i const &) = 0; - virtual void set_v3i32(std::shared_ptr const &, const char *, Eigen::Vector3i const &) = 0; - virtual void set_v4i32(std::shared_ptr const &, const char *, Eigen::Vector4i const &) = 0; - virtual void set_v2ui32(std::shared_ptr const &, const char *, Eigen::Vector2 const &) = 0; - virtual void set_v3ui32(std::shared_ptr const &, const char *, Eigen::Vector3 const &) = 0; - virtual void set_v4ui32(std::shared_ptr const &, const char *, Eigen::Vector4 const &) = 0; - virtual void set_m4f32(std::shared_ptr const &, const char *, Eigen::Matrix4f const &) = 0; - virtual void set_tex(std::shared_ptr const &, const char *, std::shared_ptr const &) = 0; + virtual void set_i32(UniformBufferInput &in, const char *, int32_t) = 0; + virtual void set_u32(UniformBufferInput &in, const char *, uint32_t) = 0; + virtual void set_f32(UniformBufferInput &in, const char *, float) = 0; + virtual void set_f64(UniformBufferInput &in, const char *, double) = 0; + virtual void set_bool(UniformBufferInput &in, const char *, bool) = 0; + virtual void set_v2f32(UniformBufferInput &in, const char *, Eigen::Vector2f const &) = 0; + virtual void set_v3f32(UniformBufferInput &in, const char *, Eigen::Vector3f const &) = 0; + virtual void set_v4f32(UniformBufferInput &in, const char *, Eigen::Vector4f const &) = 0; + virtual void set_v2i32(UniformBufferInput &in, const char *, Eigen::Vector2i const &) = 0; + virtual void set_v3i32(UniformBufferInput &in, const char *, Eigen::Vector3i const &) = 0; + virtual void set_v4i32(UniformBufferInput &in, const char *, Eigen::Vector4i const &) = 0; + virtual void set_v2ui32(UniformBufferInput &in, const char *, Eigen::Vector2 const &) = 0; + virtual void set_v3ui32(UniformBufferInput &in, const char *, Eigen::Vector3 const &) = 0; + virtual void set_v4ui32(UniformBufferInput &in, const char *, Eigen::Vector4 const &) = 0; + virtual void set_m4f32(UniformBufferInput &in, const char *, Eigen::Matrix4f const &) = 0; + virtual void set_tex(UniformBufferInput &in, const char *, std::shared_ptr const &) = 0; }; } // namespace openage::renderer diff --git a/libopenage/renderer/uniform_input.cpp b/libopenage/renderer/uniform_input.cpp index ee42202faf..b3abbe7ba8 100644 --- a/libopenage/renderer/uniform_input.cpp +++ b/libopenage/renderer/uniform_input.cpp @@ -157,71 +157,71 @@ UniformBufferInput::UniformBufferInput(std::shared_ptr const &buf void UniformBufferInput::update() {} void UniformBufferInput::update(const char *unif, int32_t val) { - this->buffer->set_i32(this->shared_from_this(), unif, val); + this->buffer->set_i32(*this, unif, val); } void UniformBufferInput::update(const char *unif, uint32_t val) { - this->buffer->set_u32(this->shared_from_this(), unif, val); + this->buffer->set_u32(*this, unif, val); } void UniformBufferInput::update(const char *unif, float val) { - this->buffer->set_f32(this->shared_from_this(), unif, val); + this->buffer->set_f32(*this, unif, val); } void UniformBufferInput::update(const char *unif, double val) { - this->buffer->set_f64(this->shared_from_this(), unif, val); + this->buffer->set_f64(*this, unif, val); } void UniformBufferInput::update(const char *unif, bool val) { - this->buffer->set_bool(this->shared_from_this(), unif, val); + this->buffer->set_bool(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector2f const &val) { - this->buffer->set_v2f32(this->shared_from_this(), unif, val); + this->buffer->set_v2f32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector3f const &val) { - this->buffer->set_v3f32(this->shared_from_this(), unif, val); + this->buffer->set_v3f32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector4f const &val) { - this->buffer->set_v4f32(this->shared_from_this(), unif, val); + this->buffer->set_v4f32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector2i const &val) { - this->buffer->set_v2i32(this->shared_from_this(), unif, val); + this->buffer->set_v2i32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector3i const &val) { - this->buffer->set_v3i32(this->shared_from_this(), unif, val); + this->buffer->set_v3i32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector4i const &val) { - this->buffer->set_v4i32(this->shared_from_this(), unif, val); + this->buffer->set_v4i32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector2 const &val) { - this->buffer->set_v2ui32(this->shared_from_this(), unif, val); + this->buffer->set_v2ui32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector3 const &val) { - this->buffer->set_v3ui32(this->shared_from_this(), unif, val); + this->buffer->set_v3ui32(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Vector4 const &val) { - this->buffer->set_v4ui32(this->shared_from_this(), unif, val); + this->buffer->set_v4ui32(*this, unif, val); } void UniformBufferInput::update(const char *unif, std::shared_ptr const &val) { - this->buffer->set_tex(this->shared_from_this(), unif, val); + this->buffer->set_tex(*this, unif, val); } void UniformBufferInput::update(const char *unif, std::shared_ptr &val) { - this->buffer->set_tex(this->shared_from_this(), unif, val); + this->buffer->set_tex(*this, unif, val); } void UniformBufferInput::update(const char *unif, Eigen::Matrix4f const &val) { - this->buffer->set_m4f32(this->shared_from_this(), unif, val); + this->buffer->set_m4f32(*this, unif, val); } } // namespace openage::renderer From 623554ed6930a6f6ac76dac4b676a604bbbacb28 Mon Sep 17 00:00:00 2001 From: edvin Date: Thu, 19 Sep 2024 14:03:29 +0200 Subject: [PATCH 568/771] Added syntax changes to GlUniformBufferInput class. --- copying.md | 1 + libopenage/renderer/opengl/uniform_buffer.cpp | 47 ++++++++++++------- libopenage/renderer/opengl/uniform_buffer.h | 12 +++++ libopenage/renderer/opengl/uniform_input.cpp | 23 ++++++++- libopenage/renderer/opengl/uniform_input.h | 27 +++++++---- 5 files changed, 84 insertions(+), 26 deletions(-) diff --git a/copying.md b/copying.md index fb6501bab0..1002c8321a 100644 --- a/copying.md +++ b/copying.md @@ -154,6 +154,7 @@ _the openage authors_ are: | Haoyang Bi | AyiStar | ayistar à outlook dawt com | | Michael Seibt | RoboSchmied | github à roboschmie dawt de | | Nikhil Ghosh | NikhilGhosh75 | nghosh606 à gmail dawt com | +| Edvin Lindholm | EdvinLndh | edvinlndh à gmail dawt com | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. diff --git a/libopenage/renderer/opengl/uniform_buffer.cpp b/libopenage/renderer/opengl/uniform_buffer.cpp index 3f16f1fdd6..c153d1e080 100644 --- a/libopenage/renderer/opengl/uniform_buffer.cpp +++ b/libopenage/renderer/opengl/uniform_buffer.cpp @@ -33,6 +33,12 @@ GlUniformBuffer::GlUniformBuffer(const std::shared_ptr &context, this->bind(); glBufferData(GL_UNIFORM_BUFFER, this->data_size, NULL, usage); + uniform_id_t unif_id = 0; + for (auto &uniform : uniforms) { + this->uniforms_by_name.insert(std::make_pair(uniform.first, unif_id)); + unif_id += 1; + } + glBindBufferRange(GL_UNIFORM_BUFFER, this->binding_point, *this->handle, 0, this->data_size); log::log(MSG(dbg) << "Created OpenGL uniform buffer (size: " @@ -51,14 +57,17 @@ void GlUniformBuffer::set_binding_point(GLuint binding_point) { void GlUniformBuffer::update_uniforms(std::shared_ptr const &unif_in) { auto glunif_in = std::dynamic_pointer_cast(unif_in); - ENSURE(glunif_in->get_buffer() == this->shared_from_this(), "Uniform input passed to different buffer than it was created with."); + ENSURE(glunif_in->get_buffer().get() == this, "Uniform input passed to different buffer than it was created with."); this->bind(); + const auto &update_offs = glunif_in->update_offs; uint8_t const *data = glunif_in->update_data.data(); - for (auto const &pair : glunif_in->update_offs) { - uint8_t const *ptr = data + pair.second; - auto unif_def = this->uniforms[pair.first]; + for (auto const &pair : this->uniforms_by_name) { + auto id = pair.second; + auto offset = update_offs[id]; + uint8_t const *ptr = data + offset.offset; + auto unif_def = this->uniforms.find(pair.first)->second; auto loc = unif_def.offset; auto size = unif_def.size; @@ -66,6 +75,10 @@ void GlUniformBuffer::update_uniforms(std::shared_ptr const } } +const std::unordered_map &GlUniformBuffer::get_uniforms() const { + return this->uniforms; +} + bool GlUniformBuffer::has_uniform(const char *unif) { return this->uniforms.count(unif) != 0; } @@ -83,6 +96,10 @@ std::shared_ptr GlUniformBuffer::new_unif_in() { void GlUniformBuffer::set_unif(UniformBufferInput &in, const char *unif, void const *val, GLenum type) { auto &unif_in = dynamic_cast(in); + auto unif_id = this->uniforms_by_name.find(unif)->second; + ENSURE(unif_id < this->uniforms.size(), + "Tried to set uniform with invalid ID " << unif_id); + auto uniform = this->uniforms.find(unif); ENSURE(uniform != std::end(this->uniforms), "Tried to set uniform " << unif << " that does not exist in the shader program."); @@ -96,18 +113,16 @@ void GlUniformBuffer::set_unif(UniformBufferInput &in, const char *unif, void co ENSURE(size == unif_data.size, "Tried to set uniform " << unif << " to a value of the wrong size."); - auto update_off = unif_in.update_offs.find(unif); - if (update_off != std::end(unif_in.update_offs)) [[likely]] { // always used after the uniform value is written once // already wrote to this uniform since last upload - size_t off = update_off->second; - memcpy(unif_in.update_data.data() + off, val, size); - } - else { - // first time writing to this uniform since last upload, so - // extend the buffer before storing the uniform value - size_t prev_size = unif_in.update_data.size(); - unif_in.update_data.resize(prev_size + size); - memcpy(unif_in.update_data.data() + prev_size, val, size); - unif_in.update_offs.emplace(unif, prev_size); + auto &update_off = unif_in.update_offs[unif_id]; + auto offset = update_off.offset; + memcpy(unif_in.update_data.data() + offset, val, size); + if (not update_off.used) [[unlikely]] { // only true if the uniform value was not set before + auto lower_bound = std::lower_bound( + std::begin(unif_in.used_uniforms), + std::end(unif_in.used_uniforms), + unif_id); + unif_in.used_uniforms.insert(lower_bound, unif_id); + update_off.used = true; } } diff --git a/libopenage/renderer/opengl/uniform_buffer.h b/libopenage/renderer/opengl/uniform_buffer.h index f1981fd19e..0fdfc7309d 100644 --- a/libopenage/renderer/opengl/uniform_buffer.h +++ b/libopenage/renderer/opengl/uniform_buffer.h @@ -33,6 +33,13 @@ class GlUniformBuffer final : public UniformBuffer */ GLuint get_binding_point() const; + /** + * Get the uniform buffers uniforms. + * + * @return Uniforms in the shader program. + */ + const std::unordered_map &get_uniforms() const; + /** * Set the binding point of the buffer. * @@ -90,6 +97,11 @@ class GlUniformBuffer final : public UniformBuffer */ std::unordered_map uniforms; + /** + * Maps uniform names to their ID (the index in the uniform vector). + */ + std::unordered_map uniforms_by_name; + /** * Size of the buffer (in bytes). */ diff --git a/libopenage/renderer/opengl/uniform_input.cpp b/libopenage/renderer/opengl/uniform_input.cpp index 111b33b879..0121cc751a 100644 --- a/libopenage/renderer/opengl/uniform_input.cpp +++ b/libopenage/renderer/opengl/uniform_input.cpp @@ -4,6 +4,7 @@ #include "renderer/opengl/lookup.h" #include "renderer/opengl/shader_program.h" +#include "renderer/opengl/uniform_buffer.h" #include "renderer/opengl/util.h" @@ -28,7 +29,25 @@ GlUniformInput::GlUniformInput(const std::shared_ptr &prog) : this->update_data.resize(offset); } -GlUniformBufferInput::GlUniformBufferInput(std::shared_ptr const &buffer) : - UniformBufferInput{buffer} {} +GlUniformBufferInput::GlUniformBufferInput(const std::shared_ptr &buffer) : + UniformBufferInput{buffer} { + auto glBuf = std::dynamic_pointer_cast(buffer); + + auto uniforms = glBuf->get_uniforms(); + + // Reserve space for the used uniforms. + this->used_uniforms.reserve(uniforms.size()); + + // Calculate the byte-wise offsets of all uniforms. + size_t offset = 0; + this->update_offs.reserve(uniforms.size()); + for (auto &uniform : uniforms) { + this->update_offs.push_back({uniform.second.offset, false}); + offset += GL_UNIFORM_TYPE_SIZE.get(uniform.second.type); + } + + // Resize the update data buffer to the total size of all uniforms. + this->update_data.resize(offset); +} } // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/uniform_input.h b/libopenage/renderer/opengl/uniform_input.h index 8ca3ff02dd..961e12a2a5 100644 --- a/libopenage/renderer/opengl/uniform_input.h +++ b/libopenage/renderer/opengl/uniform_input.h @@ -18,6 +18,7 @@ class UniformBuffer; namespace opengl { class GlShaderProgram; +class GlUniformBuffer; /** * Describes OpenGL-specific uniform valuations. @@ -57,18 +58,28 @@ class GlUniformInput final : public UniformInput { * Describes OpenGL-specific uniform buffer valuations. */ class GlUniformBufferInput final : public UniformBufferInput { +private: + struct GlUniformOffset { + // Offset in the update_data buffer. + size_t offset; + /// Dtermine whether the uniform value has been set. + bool used; + }; + + public: - GlUniformBufferInput(std::shared_ptr const &); + GlUniformBufferInput(const std::shared_ptr &buffer); /** - * We store uniform updates lazily. They are only actually uploaded to GPU - * when a draw call is made. - * - * \p update_offs maps the uniform names to where their - * value is in \p update_data in terms of a byte-wise offset. This is only a partial - * valuation, so not all uniforms have to be present here. + * Store the IDs of the uniforms from the shader set by this uniform input. */ - std::unordered_map update_offs; + std::vector used_uniforms; + + /** + * Store offsets of uniforms in the update_data buffer and + * whether the uniform value has been set. + */ + std::vector update_offs; /** * Buffer containing untyped uniform update data. From de212cbe268df46d89ca11dd9c676c41b2772761 Mon Sep 17 00:00:00 2001 From: edvin Date: Sun, 22 Sep 2024 22:09:27 +0200 Subject: [PATCH 569/771] Changed uniforms attribute from unordered_map to vector --- libopenage/renderer/opengl/renderer.cpp | 17 +++++----- libopenage/renderer/opengl/shader_data.h | 8 ++++- libopenage/renderer/opengl/shader_program.cpp | 22 ++++++------- libopenage/renderer/opengl/uniform_buffer.cpp | 33 ++++++++++--------- libopenage/renderer/opengl/uniform_buffer.h | 6 ++-- libopenage/renderer/opengl/uniform_input.cpp | 4 +-- 6 files changed, 48 insertions(+), 42 deletions(-) diff --git a/libopenage/renderer/opengl/renderer.cpp b/libopenage/renderer/opengl/renderer.cpp index 48639dc368..33b734790f 100644 --- a/libopenage/renderer/opengl/renderer.cpp +++ b/libopenage/renderer/opengl/renderer.cpp @@ -86,7 +86,7 @@ std::shared_ptr GlRenderer::get_display_target() { std::shared_ptr GlRenderer::add_uniform_buffer(resources::UniformBufferInfo const &info) { auto inputs = info.get_inputs(); - std::unordered_map uniforms{}; + std::vector uniforms{}; size_t offset = 0; for (auto const &input : inputs) { auto type = GL_UBO_INPUT_TYPE.get(input.type); @@ -94,14 +94,13 @@ std::shared_ptr GlRenderer::add_uniform_buffer(resources::Uniform // align offset to the size of the type offset += offset % size; - - uniforms.emplace( - std::make_pair(input.name, - GlInBlockUniform{type, - offset, - resources::UniformBufferInfo::get_size(input, info.get_layout()), - resources::UniformBufferInfo::get_stride_size(input.type, info.get_layout()), - input.count})); + uniforms.push_back( + GlInBlockUniform{type, + offset, + resources::UniformBufferInfo::get_size(input, info.get_layout()), + resources::UniformBufferInfo::get_stride_size(input.type, info.get_layout()), + input.count, + input.name}); offset += size; } diff --git a/libopenage/renderer/opengl/shader_data.h b/libopenage/renderer/opengl/shader_data.h index 7b987fb6a7..d60d16cb85 100644 --- a/libopenage/renderer/opengl/shader_data.h +++ b/libopenage/renderer/opengl/shader_data.h @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -61,6 +62,11 @@ struct GlInBlockUniform { * Only relevant for arrays. The number of elements in the array. */ size_t count; + + /** + * Name of the block uniform. + */ + std::string name; }; /** @@ -78,7 +84,7 @@ struct GlUniformBlock { /** * Maps uniform names within this block to their descriptions. */ - std::unordered_map uniforms; + std::vector uniforms; /** * The binding point assigned to this block. diff --git a/libopenage/renderer/opengl/shader_program.cpp b/libopenage/renderer/opengl/shader_program.cpp index da24ac1596..395ee395c2 100644 --- a/libopenage/renderer/opengl/shader_program.cpp +++ b/libopenage/renderer/opengl/shader_program.cpp @@ -125,7 +125,7 @@ GlShaderProgram::GlShaderProgram(const std::shared_ptr &context, std::vector uniform_indices(val); glGetActiveUniformBlockiv(handle, i_unif_block, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, uniform_indices.data()); - std::unordered_map uniforms; + std::vector uniforms; for (GLuint const i_unif : uniform_indices) { in_block_unifs.insert(i_unif); @@ -152,14 +152,14 @@ GlShaderProgram::GlShaderProgram(const std::shared_ptr &context, // We do not need to handle sampler types here like in the uniform loop below, // because named blocks cannot contain samplers. - uniforms.insert(std::make_pair( - name.data(), + uniforms.push_back( GlInBlockUniform{ type, size_t(offset), size_t(count) * GL_UNIFORM_TYPE_SIZE.get(type), size_t(stride), - size_t(count)})); + size_t(count), + std::string(name.data())}); } // ENSURE(block_binding < caps.max_uniform_buffer_bindings, @@ -257,10 +257,10 @@ GlShaderProgram::GlShaderProgram(const std::shared_ptr &context, for (auto const &pair : this->uniform_blocks) { log::log(MSG(dbg) << "(" << pair.second.index << ") " << pair.first << " (size: " << pair.second.data_size << ") {"); - for (auto const &unif_pair : pair.second.uniforms) { - log::log(MSG(dbg) << "\t+" << unif_pair.second.offset - << " " << unif_pair.first << ": " - << GLSL_TYPE_NAME.get(unif_pair.second.type)); + for (auto const &unif : pair.second.uniforms) { + log::log(MSG(dbg) << "\t+" << unif.offset + << " " << unif.name << ": " + << GLSL_TYPE_NAME.get(unif.type)); } log::log(MSG(dbg) << "}"); } @@ -478,9 +478,9 @@ void GlShaderProgram::bind_uniform_buffer(const char *block_name, std::shared_pt auto &block = this->uniform_blocks[block_name]; // Check if the uniform buffer matches the block definition - for (auto const &pair : block.uniforms) { - ENSURE(gl_buffer->has_uniform(pair.first.c_str()), - "Uniform buffer does not contain uniform '" << pair.first << "' required by block " << block_name); + for (auto const &unif : block.uniforms) { + ENSURE(gl_buffer->has_uniform(unif.name.c_str()), + "Uniform buffer does not contain uniform '" << unif.name << "' required by block " << block_name); } block.binding_point = gl_buffer->get_binding_point(); diff --git a/libopenage/renderer/opengl/uniform_buffer.cpp b/libopenage/renderer/opengl/uniform_buffer.cpp index c153d1e080..cc3aa3d3dc 100644 --- a/libopenage/renderer/opengl/uniform_buffer.cpp +++ b/libopenage/renderer/opengl/uniform_buffer.cpp @@ -16,7 +16,7 @@ namespace openage::renderer::opengl { GlUniformBuffer::GlUniformBuffer(const std::shared_ptr &context, size_t size, - std::unordered_map uniforms, + std::vector uniforms, GLuint binding_point, GLenum usage) : GlSimpleObject(context, @@ -35,7 +35,7 @@ GlUniformBuffer::GlUniformBuffer(const std::shared_ptr &context, uniform_id_t unif_id = 0; for (auto &uniform : uniforms) { - this->uniforms_by_name.insert(std::make_pair(uniform.first, unif_id)); + this->uniforms_by_name.insert(std::make_pair(uniform.name, unif_id)); unif_id += 1; } @@ -62,25 +62,30 @@ void GlUniformBuffer::update_uniforms(std::shared_ptr const this->bind(); const auto &update_offs = glunif_in->update_offs; + const auto &used_uniforms = glunif_in->used_uniforms; + const auto &uniforms = this->uniforms; uint8_t const *data = glunif_in->update_data.data(); - for (auto const &pair : this->uniforms_by_name) { - auto id = pair.second; - auto offset = update_offs[id]; + + size_t unif_count = used_uniforms.size(); + for (size_t i = 0; i < unif_count; ++i) { + uniform_id_t unif_id = used_uniforms[i]; + auto offset = update_offs[unif_id]; + uint8_t const *ptr = data + offset.offset; - auto unif_def = this->uniforms.find(pair.first)->second; - auto loc = unif_def.offset; - auto size = unif_def.size; + auto &unif = uniforms[unif_id]; + auto loc = unif.offset; + auto size = unif.size; glBufferSubData(GL_UNIFORM_BUFFER, loc, size, ptr); } } -const std::unordered_map &GlUniformBuffer::get_uniforms() const { +const std::vector &GlUniformBuffer::get_uniforms() const { return this->uniforms; } -bool GlUniformBuffer::has_uniform(const char *unif) { - return this->uniforms.count(unif) != 0; +bool GlUniformBuffer::has_uniform(const char *name) { + return this->uniforms_by_name.contains(name); } void GlUniformBuffer::bind() const { @@ -100,11 +105,7 @@ void GlUniformBuffer::set_unif(UniformBufferInput &in, const char *unif, void co ENSURE(unif_id < this->uniforms.size(), "Tried to set uniform with invalid ID " << unif_id); - auto uniform = this->uniforms.find(unif); - ENSURE(uniform != std::end(this->uniforms), - "Tried to set uniform " << unif << " that does not exist in the shader program."); - - auto const &unif_data = uniform->second; + auto const &unif_data = this->uniforms[unif_id]; ENSURE(type == unif_data.type, "Tried to set uniform " << unif << " to a value of the wrong type."); diff --git a/libopenage/renderer/opengl/uniform_buffer.h b/libopenage/renderer/opengl/uniform_buffer.h index 0fdfc7309d..24fd1e23a2 100644 --- a/libopenage/renderer/opengl/uniform_buffer.h +++ b/libopenage/renderer/opengl/uniform_buffer.h @@ -22,7 +22,7 @@ class GlUniformBuffer final : public UniformBuffer public: GlUniformBuffer(const std::shared_ptr &context, size_t size, - std::unordered_map uniforms, + std::vector uniforms, GLuint binding_point = 0, GLenum usage = GL_DYNAMIC_DRAW); @@ -38,7 +38,7 @@ class GlUniformBuffer final : public UniformBuffer * * @return Uniforms in the shader program. */ - const std::unordered_map &get_uniforms() const; + const std::vector &get_uniforms() const; /** * Set the binding point of the buffer. @@ -95,7 +95,7 @@ class GlUniformBuffer final : public UniformBuffer /** * Uniform definitions inside the buffer. */ - std::unordered_map uniforms; + std::vector uniforms; /** * Maps uniform names to their ID (the index in the uniform vector). diff --git a/libopenage/renderer/opengl/uniform_input.cpp b/libopenage/renderer/opengl/uniform_input.cpp index 0121cc751a..38c63f2d66 100644 --- a/libopenage/renderer/opengl/uniform_input.cpp +++ b/libopenage/renderer/opengl/uniform_input.cpp @@ -42,8 +42,8 @@ GlUniformBufferInput::GlUniformBufferInput(const std::shared_ptr size_t offset = 0; this->update_offs.reserve(uniforms.size()); for (auto &uniform : uniforms) { - this->update_offs.push_back({uniform.second.offset, false}); - offset += GL_UNIFORM_TYPE_SIZE.get(uniform.second.type); + this->update_offs.push_back({uniform.offset, false}); + offset += GL_UNIFORM_TYPE_SIZE.get(uniform.type); } // Resize the update data buffer to the total size of all uniforms. From b3a21a31e05681ad029327affd54607caf617913 Mon Sep 17 00:00:00 2001 From: PRIYANKjakharia <167257749+PRIYANKjakharia@users.noreply.github.com> Date: Fri, 4 Oct 2024 21:15:09 +0530 Subject: [PATCH 570/771] Update README.md correction of a few grammatical errors and typos --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ccd2b66c0c..bf4c6c4702 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Goals ----- * Fully authentic look and feel - * This can only be approximated, since the behaviour of the original game is mostly undocumented, + * This can only be approximated since the behavior of the original game is mostly undocumented, and guessing/experimenting can only get you this close * We will not implement useless artificial limitations (max 30 selectable units...) * An easily-moddable content format: [**nyan** yet another notation](https://github.com/SFTtech/nyan) @@ -79,7 +79,7 @@ Current State of the Project **Important notice**: At the moment, "gameplay" is basically non-functional. We're implementing the internal game simulation (how units even do anything) with simplicity and extensibility in mind, so we had to get rid of the temporary (but kind of working) previous version. -With these changes we can (finally) actually make use of our converted asset packs and our nyan API! +With these changes, we can (finally) actually make use of our converted asset packs and our Nyan API! We're working day and night to make gameplay return\*. If you're interested, we wrote detailed explanations on our blog: [Part 1](https://blog.openage.dev/new-gamestate-2020.html), [Part 2](https://blog.openage.dev/engine-core-modules.html), [Monthly Devlog](https://blog.openage.dev/tag/news.html). @@ -99,16 +99,16 @@ If you're interested, we wrote detailed explanations on our blog: [Part 1](https Installation Packages --------------------- -There's many missing parts for an actually working game. +There are many missing parts for an actually working game. So if you "just wanna play", [you'll be disappointed](#current-state-of-the-project), unfortunately. -We strongly recommend to build the program from source to get the latest, greatest and shiniest project state :) +We strongly recommend building the program from source to get the latest, greatest, and shiniest project state :) -* For **Linux** check at [repology](https://repology.org/project/openage/versions) if your distribution has any packages available. Otherwise you need to build from source. +* For **Linux** check at [repology](https://repology.org/project/openage/versions) if your distribution has any packages available. Otherwise, you need to build from source. We don't release `*.deb`, `*.rpm`, Flatpak, snap or AppImage packages yet. * For **Windows** check our [release page](https://github.com/SFTtech/openage/releases) for the latest installer. - Otherwise, you need to build from source. + Otherwise, you need to build from the source. * For **macOS** we currently don't have any packages, you need to build from source. @@ -152,7 +152,7 @@ Contributing You might ask yourself now "Sounds cool, but how do I participate and ~~get famous~~ contribute useful features?". -Fortunately for you, there is a lot to do and we are very grateful for help. +Fortunately for you, there is a lot to do and we are very grateful for your help. ## Where do I start? @@ -160,7 +160,7 @@ Fortunately for you, there is a lot to do and we are very grateful for help. * **Ask us** in the [chat](https://matrix.to/#/#sfttech:matrix.org). Someone there could need help with something. * You can also **take the initiative** and fix a bug you found, create an issue for discussion or - implement a feature that we never though of, but always wanted. + implement a feature that we never thought of, but always wanted. ## Ok, I found something. What now? @@ -176,7 +176,7 @@ Fortunately for you, there is a lot to do and we are very grateful for help. ## How do I contribute my features/changes? * Read the **[contributing guide](/doc/contributing.md)**. -* You can upload work in progress (WIP) versions or drafts of your contribution to get feedback or support. +* You can upload work-in-progress (WIP) versions or drafts of your contribution to get feedback or support. * Tell us (again) when you want us to review your work. ## I want to help, but I'm not a programmer... @@ -186,7 +186,7 @@ just have to ask and we'll find something. Alternatively, lurking is also allowe ---- -Cheers, happy hecking! +Cheers, happy hacking! Development Process From 8bad4db7eede66b7dccd51049f98ab79601e934e Mon Sep 17 00:00:00 2001 From: PRIYANKjakharia <167257749+PRIYANKjakharia@users.noreply.github.com> Date: Sat, 5 Oct 2024 23:39:27 +0530 Subject: [PATCH 571/771] Update README.md Co-authored-by: Christoph Heine <6852422+heinezen@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bf4c6c4702..88c615cc50 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ just have to ask and we'll find something. Alternatively, lurking is also allowe ---- -Cheers, happy hacking! +Cheers, happy hecking! Development Process From e8bf59115dcf0a666be2d1f260dcdd379361ccc3 Mon Sep 17 00:00:00 2001 From: PRIYANKjakharia <167257749+PRIYANKjakharia@users.noreply.github.com> Date: Sat, 5 Oct 2024 23:39:34 +0530 Subject: [PATCH 572/771] Update README.md Co-authored-by: Christoph Heine <6852422+heinezen@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 88c615cc50..266cdfc5fa 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Current State of the Project **Important notice**: At the moment, "gameplay" is basically non-functional. We're implementing the internal game simulation (how units even do anything) with simplicity and extensibility in mind, so we had to get rid of the temporary (but kind of working) previous version. -With these changes, we can (finally) actually make use of our converted asset packs and our Nyan API! +With these changes, we can (finally) actually make use of our converted asset packs and our nyan API! We're working day and night to make gameplay return\*. If you're interested, we wrote detailed explanations on our blog: [Part 1](https://blog.openage.dev/new-gamestate-2020.html), [Part 2](https://blog.openage.dev/engine-core-modules.html), [Monthly Devlog](https://blog.openage.dev/tag/news.html). From 03c159202221d48482718f3027ae855d7768e78d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 6 Oct 2024 00:34:23 +0200 Subject: [PATCH 573/771] renderer: Fix tex unit assignment for std::optional type. --- libopenage/renderer/opengl/shader_program.cpp | 7 +++++-- libopenage/renderer/opengl/shader_program.h | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libopenage/renderer/opengl/shader_program.cpp b/libopenage/renderer/opengl/shader_program.cpp index 395ee395c2..4b46a8263c 100644 --- a/libopenage/renderer/opengl/shader_program.cpp +++ b/libopenage/renderer/opengl/shader_program.cpp @@ -424,8 +424,11 @@ void GlShaderProgram::update_uniforms(std::shared_ptr const &uni glBindTexture(GL_TEXTURE_2D, tex); // TODO: maybe call this at a more appropriate position glUniform1i(loc, tex_unit_id); - auto &tex_value = *this->textures_per_texunits[tex_unit_id]; - tex_value = tex; + ENSURE(tex_unit_id < this->textures_per_texunits.size(), + "Tried to assign texture to non-existant texture unit at index " + << tex_unit_id + << " (max: " << this->textures_per_texunits.size() << ")."); + this->textures_per_texunits[tex_unit_id] = tex; break; } default: diff --git a/libopenage/renderer/opengl/shader_program.h b/libopenage/renderer/opengl/shader_program.h index 5d31f2a071..e5c75374c3 100644 --- a/libopenage/renderer/opengl/shader_program.h +++ b/libopenage/renderer/opengl/shader_program.h @@ -195,6 +195,7 @@ class GlShaderProgram final : public ShaderProgram std::unordered_map attribs; /// Store which texture handles are currently bound to the shader's texture units. + /// A value of std::nullopt means the texture unit is unbound (no texture assigned). std::vector> textures_per_texunits; /// Whether this program has been validated. From ac2dbbdd97f56ff6df426a5158b0abca260fd7a1 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 11 Oct 2024 17:19:33 +0200 Subject: [PATCH 574/771] buildsys: Remove unused imports. --- openage/cppinterface/exctranslate.pyx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/openage/cppinterface/exctranslate.pyx b/openage/cppinterface/exctranslate.pyx index 9c6be25d36..3e5c96e5ba 100644 --- a/openage/cppinterface/exctranslate.pyx +++ b/openage/cppinterface/exctranslate.pyx @@ -1,4 +1,4 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. """ Provides the raise_py_exception and describe_py_exception callbacks for @@ -10,21 +10,14 @@ from cpython.exc cimport ( PyErr_Occurred, PyErr_Fetch, PyErr_NormalizeException, - PyErr_SetObject, - PyErr_Restore -) -from cpython.pystate cimport ( - PyThreadState, - PyThreadState_Get + PyErr_SetObject ) -from libcpp.string cimport string from libcpp cimport bool as cppbool -from libopenage.log.level cimport level, err as lvl_err from libopenage.log.message cimport message from libopenage.error.error cimport Error -from libopenage.error.backtrace cimport Backtrace, backtrace_symbol +from libopenage.error.backtrace cimport backtrace_symbol from libopenage.pyinterface.functional cimport Func1 from libopenage.pyinterface.pyexception cimport ( PyException, @@ -36,9 +29,8 @@ from libopenage.pyinterface.exctranslate cimport ( set_exc_translation_funcs ) -import importlib from ..testing.testing import TestError -from ..log import err, info +from ..log import info cdef extern from "Python.h": int PyException_SetTraceback(PyObject *ex, PyObject *tb) From ce1f7f809bf38d4b365d36f9886aca8fdc4452e7 Mon Sep 17 00:00:00 2001 From: heinezen Date: Fri, 11 Oct 2024 17:24:34 +0200 Subject: [PATCH 575/771] buildsys: Deactivate _PyTraceback_Add import. --- openage/cppinterface/exctranslate.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openage/cppinterface/exctranslate.pyx b/openage/cppinterface/exctranslate.pyx index 3e5c96e5ba..9d390eb463 100644 --- a/openage/cppinterface/exctranslate.pyx +++ b/openage/cppinterface/exctranslate.pyx @@ -35,8 +35,8 @@ from ..log import info cdef extern from "Python.h": int PyException_SetTraceback(PyObject *ex, PyObject *tb) -cdef extern from "traceback.h": - void _PyTraceback_Add(const char *funcname, const char *filename, int lineno) +# cdef extern from "traceback.h": +# void _PyTraceback_Add(const char *funcname, const char *filename, int lineno) cdef void PyTraceback_Add(const char *functionname, const char *filename, int lineno) noexcept with gil: @@ -47,7 +47,7 @@ cdef void PyTraceback_Add(const char *functionname, const char *filename, int li # possible since 3.4.3 due to http://bugs.python.org/issue24436. # the function will likely remain internal due to https://bugs.python.org/issue24743 - _PyTraceback_Add(functionname, filename, lineno) + # _PyTraceback_Add(functionname, filename, lineno) cdef class CPPMessageObject: From 98908b093d668372fa7ac12bb658ce7173ef81d8 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 12 Oct 2024 15:03:02 +0200 Subject: [PATCH 576/771] buildsys: Add comments explaining why _PyTraceback_Add is no longer used. --- openage/cppinterface/exctranslate.pyx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openage/cppinterface/exctranslate.pyx b/openage/cppinterface/exctranslate.pyx index 9d390eb463..ea99fc7042 100644 --- a/openage/cppinterface/exctranslate.pyx +++ b/openage/cppinterface/exctranslate.pyx @@ -35,6 +35,9 @@ from ..log import info cdef extern from "Python.h": int PyException_SetTraceback(PyObject *ex, PyObject *tb) +# _PyTraceback_Add has been made private in Python 3.13 +# see https://github.com/python/cpython/pull/108453 +# TODO: Find another solution to add tracebacks # cdef extern from "traceback.h": # void _PyTraceback_Add(const char *funcname, const char *filename, int lineno) @@ -42,11 +45,12 @@ cdef extern from "Python.h": cdef void PyTraceback_Add(const char *functionname, const char *filename, int lineno) noexcept with gil: """ Add a new traceback stack frame. - Redirects to Python's internal _PyTraceback_Add function. - """ - # possible since 3.4.3 due to http://bugs.python.org/issue24436. - # the function will likely remain internal due to https://bugs.python.org/issue24743 + Note: Currently does nothing, because _PyTraceback_Add is no longer + accessible since Python 3.13. + + TODO: Find another solution to add tracebacks. + """ # _PyTraceback_Add(functionname, filename, lineno) From 8d5629a291759343a40e3adf7274074f8ecd9e3d Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 14 Oct 2024 01:59:39 +0200 Subject: [PATCH 577/771] doc: Update macOS build instructions. --- doc/build_instructions/macos.md | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/doc/build_instructions/macos.md b/doc/build_instructions/macos.md index ba139c3b12..5290f8976a 100644 --- a/doc/build_instructions/macos.md +++ b/doc/build_instructions/macos.md @@ -6,14 +6,10 @@ ``` brew update-reset && brew update -brew tap homebrew/cask-fonts -brew install font-dejavu +brew install --cask font-dejavu brew install cmake python3 libepoxy freetype fontconfig harfbuzz opus opusfile qt6 libogg libpng toml11 eigen brew install llvm -pip3 install cython numpy mako lz4 pillow pygments toml - -# optional, for documentation generation -brew install doxygen +pip3 install --upgrade --break-system-packages cython numpy mako lz4 pillow pygments setuptools toml ``` You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies: @@ -22,7 +18,14 @@ You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/buildi brew install flex make ``` +Optionally, for documentation generation: + +``` +brew install doxygen +``` + ## Clone the repository + ``` git clone https://github.com/SFTtech/openage cd openage @@ -30,8 +33,18 @@ cd openage ## Building +We advise against using the clang version that comes with macOS (Apple Clang) as it notoriously out of date and often causes compilation errors. Use homebrew's clang if you don't want any trouble. You can pass the path of homebrew clang to the openage `configure` script which will generate the CMake files for building: + +``` +# on Intel macOS, llvm is by default in /usr/local/Cellar/llvm/bin/ +# on ARM macOS, llvm is by default in /opt/homebrew/Cellar/llvm/bin/ +CLANG_PATH="/bin/clang++" +./configure --compiler="$CLANG_PATH" --download-nyan +``` + +Afterwards, trigger the build using `make`: + ``` -./configure --compiler=$(which clang++) --mode=release --download-nyan make -j$(sysctl -n hw.ncpu) ``` @@ -40,7 +53,7 @@ make -j$(sysctl -n hw.ncpu) ## Running -`make run` or `./bin/run` launches the game. Try `./bin/run --help`! +`make run` or `cd bin && ./run` launches the game. Try `./run --help` if you don't know what to do! ## To create the documentation From a66e08b4d9604dccd0e36abae5e4fb2b5fbaafdb Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 14 Oct 2024 02:12:41 +0200 Subject: [PATCH 578/771] ci: Update macOS CI config. --- .github/workflows/macosx-ci.yml | 26 ++++++-------------------- doc/build_instructions/macos.md | 3 +-- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/.github/workflows/macosx-ci.yml b/.github/workflows/macosx-ci.yml index e30906228d..07ac79a5f0 100644 --- a/.github/workflows/macosx-ci.yml +++ b/.github/workflows/macosx-ci.yml @@ -5,7 +5,7 @@ jobs: # https://docs.github.com/en/actions/reference/software-installed-on-github-hosted-runners # we install stuff not already there macos-build: - runs-on: macOS-latest + runs-on: macos-latest steps: - name: Checkout sources uses: actions/checkout@v4 @@ -40,26 +40,14 @@ jobs: run: brew update-reset - name: Brew update run: brew update - - name: Install clang / LLVM 15.0.0 - run: | - set -x - brew install --force wget - mkdir -p /tmp/clang - cd /tmp/clang - wget https://github.com/llvm/llvm-project/releases/download/llvmorg-15.0.0/clang+llvm-15.0.0-x86_64-apple-darwin.tar.xz -O clang-15.0.0.tar.xz - ls - tar -xvf clang-15.0.0.tar.xz -C ~ - cd ~ - mv clang+llvm-15.0.0-x86_64-apple-darwin clang-15.0.0 - ~/clang-15.0.0/bin/clang++ --version - name: Brew install DeJaVu fonts run: brew install --cask font-dejavu - - name: Remove python's 2to3 link so that 'brew link' does not fail - run: rm /usr/local/bin/2to3* && rm /usr/local/bin/idle3* - name: Install environment helpers with homebrew run: brew install --force ccache - - name: Install dependencies with homebrew - run: brew install --force libepoxy freetype fontconfig harfbuzz opus opusfile qt6 libogg libpng toml11 eigen + - name: Install LLVM with homebrew + run: brew install --force llvm + - name: Install openage dependencies with homebrew + run: brew install --force cmake python3 libepoxy freetype fontconfig harfbuzz opus opusfile qt6 libogg libpng toml11 eigen - name: Install nyan dependencies with homebrew run: brew install --force flex make - name: Install python3 packages @@ -68,9 +56,7 @@ jobs: # numpy pulls gcc as dep? and pygments doesn't work. run: pip3 install --upgrade --break-system-packages cython numpy mako lz4 pillow pygments setuptools toml - name: Configure - run: | - CLANG_PATH="$HOME/clang-15.0.0/bin/clang++" - ./configure --compiler="$CLANG_PATH" --mode=debug --ccache --download-nyan + run: ./configure --compiler="$(brew --prefix llvm)/bin/clang++" --mode=release --ccache --download-nyan - name: Build run: make -j$(sysctl -n hw.logicalcpu) VERBOSE=1 - name: Test diff --git a/doc/build_instructions/macos.md b/doc/build_instructions/macos.md index 5290f8976a..c9946f2d39 100644 --- a/doc/build_instructions/macos.md +++ b/doc/build_instructions/macos.md @@ -38,8 +38,7 @@ We advise against using the clang version that comes with macOS (Apple Clang) as ``` # on Intel macOS, llvm is by default in /usr/local/Cellar/llvm/bin/ # on ARM macOS, llvm is by default in /opt/homebrew/Cellar/llvm/bin/ -CLANG_PATH="/bin/clang++" -./configure --compiler="$CLANG_PATH" --download-nyan +./configure --compiler="$(brew --prefix llvm)/bin/clang"" --download-nyan ``` Afterwards, trigger the build using `make`: From 656ce847a933e1a3b62e6233e4c31ed7579d0cf2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 14 Oct 2024 02:27:37 +0200 Subject: [PATCH 579/771] ci: Update versions of deprecated actions. --- .github/workflows/macosx-ci.yml | 4 ++-- .github/workflows/ubuntu-22.04.yml | 6 +++--- .github/workflows/windows-server-2019.yml | 6 +++--- .github/workflows/windows-server-2022.yml | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/macosx-ci.yml b/.github/workflows/macosx-ci.yml index 07ac79a5f0..2270121092 100644 --- a/.github/workflows/macosx-ci.yml +++ b/.github/workflows/macosx-ci.yml @@ -19,7 +19,7 @@ jobs: string(TIMESTAMP current_date "%Y-%m-%d-%H:%M:%S" UTC) message("timestamp=${current_date}" >> $GITHUB_OUTPUT) - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-deps with: path: | @@ -29,7 +29,7 @@ jobs: restore-keys: | ${{ runner.os }}-deps- - name: Cache ccache dir - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-ccache with: path: ~/Library/Caches/ccache diff --git a/.github/workflows/ubuntu-22.04.yml b/.github/workflows/ubuntu-22.04.yml index b6eaa70230..311524f799 100644 --- a/.github/workflows/ubuntu-22.04.yml +++ b/.github/workflows/ubuntu-22.04.yml @@ -16,7 +16,7 @@ jobs: sudo docker save openage-devenv:latest | gzip > /tmp/staging/devenv.tar.gz shell: bash - name: Publish the Docker image - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: devenv-image-compressed.tar.gz path: '/tmp/staging/devenv.tar.gz' @@ -32,7 +32,7 @@ jobs: run: mkdir -p /tmp/image shell: bash - name: Download devenv image - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: devenv-image-compressed.tar.gz path: '/tmp/image' @@ -47,7 +47,7 @@ jobs: mkdir -p /tmp/openage tar -czvf /tmp/openage/openage-build.tar.gz ./build - name: Publish build artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: openage-build.tar.gz path: '/tmp/openage/openage-build.tar.gz' diff --git a/.github/workflows/windows-server-2019.yml b/.github/workflows/windows-server-2019.yml index 7a2262c00f..df9d96284b 100644 --- a/.github/workflows/windows-server-2019.yml +++ b/.github/workflows/windows-server-2019.yml @@ -15,7 +15,7 @@ jobs: vswhere -latest shell: pwsh - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.9' architecture: 'x64' @@ -78,7 +78,7 @@ jobs: python -m openage --add-dll-search-path $DLL_PATH --version shell: pwsh - name: Publish build artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: build-files @@ -86,7 +86,7 @@ jobs: if-no-files-found: error retention-days: 30 - name: Publish packaged artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: package-files diff --git a/.github/workflows/windows-server-2022.yml b/.github/workflows/windows-server-2022.yml index f0172b103e..bbc73b78c2 100644 --- a/.github/workflows/windows-server-2022.yml +++ b/.github/workflows/windows-server-2022.yml @@ -15,7 +15,7 @@ jobs: vswhere -latest shell: pwsh - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.9' architecture: 'x64' @@ -78,7 +78,7 @@ jobs: python -m openage --add-dll-search-path $DLL_PATH --version shell: pwsh - name: Publish build artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: build-files @@ -86,7 +86,7 @@ jobs: if-no-files-found: error retention-days: 30 - name: Publish packaged artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: package-files From f9ec53b5c8357e75cc07bba17146c5a41c99e729 Mon Sep 17 00:00:00 2001 From: heinezen Date: Tue, 1 Oct 2024 16:30:39 +0200 Subject: [PATCH 580/771] renderer: Fix lock of resource reads in render entity. --- libopenage/renderer/stages/world/object.cpp | 6 +++++ .../renderer/stages/world/render_entity.cpp | 6 ++++- .../renderer/stages/world/render_entity.h | 27 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index d4629f866b..e7ffc24dde 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -63,6 +63,9 @@ void WorldObject::fetch_updates(const time::time_t &time) { // 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()); this->animation_info.sync(this->render_entity->get_animation_path(), std::function(const std::string &)>( @@ -79,6 +82,9 @@ void WorldObject::fetch_updates(const time::time_t &time) { this->last_update); this->angle.sync(this->render_entity->get_angle(), this->last_update); + // Unlock mutex of the render entity + read_lock.unlock(); + // Set self to changed so that world renderer can update the renderable this->changed = true; this->render_entity->clear_changed_flag(); diff --git a/libopenage/renderer/stages/world/render_entity.cpp b/libopenage/renderer/stages/world/render_entity.cpp index 6909a6554a..61101181c0 100644 --- a/libopenage/renderer/stages/world/render_entity.cpp +++ b/libopenage/renderer/stages/world/render_entity.cpp @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #include "render_entity.h" @@ -96,4 +96,8 @@ void WorldRenderEntity::clear_changed_flag() { this->changed = false; } +std::shared_lock WorldRenderEntity::get_read_lock() { + return std::shared_lock{this->mutex}; +} + } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/render_entity.h b/libopenage/renderer/stages/world/render_entity.h index 7c56635f89..e1161d8b88 100644 --- a/libopenage/renderer/stages/world/render_entity.h +++ b/libopenage/renderer/stages/world/render_entity.h @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -30,6 +31,8 @@ class WorldRenderEntity { /** * Update the render entity with information from the gamestate. * + * Updating the render entity with this method is thread-safe. + * * @param ref_id Game entity ID. * @param position Position of the game entity inside the game world. * @param angle Angle of the game entity inside the game world. @@ -47,6 +50,8 @@ class WorldRenderEntity { * * Update the render entity with information from the gamestate. * + * Updating the render entity with this method is thread-safe. + * * @param ref_id Game entity ID. * @param position Position of the game entity inside the game world. * @param animation_path Path to the animation definition. @@ -60,6 +65,8 @@ class WorldRenderEntity { /** * Get the ID of the corresponding game entity. * + * Accessing the game entity ID is thread-safe. + * * @return Game entity ID. */ uint32_t get_id(); @@ -67,6 +74,9 @@ class WorldRenderEntity { /** * 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(); @@ -74,6 +84,9 @@ class WorldRenderEntity { /** * 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(); @@ -81,6 +94,9 @@ class WorldRenderEntity { /** * 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(); @@ -88,6 +104,8 @@ class WorldRenderEntity { /** * Get the time of the last update. * + * Accessing the update time is thread-safe. + * * @return Time of last update. */ time::time_t get_update_time(); @@ -105,6 +123,15 @@ class WorldRenderEntity { */ void clear_changed_flag(); + /** + * Get a shared lock for thread-safe reading from the render entity. + * + * The caller is responsible for unlocking the mutex after reading. + * + * @return Lock for the render entity. + */ + std::shared_lock get_read_lock(); + private: /** * Flag for determining if the render entity has been updated by the From cfd835f5b476c8545577e013461d14ff9dd9d338 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 3 Oct 2024 13:58:18 +0200 Subject: [PATCH 581/771] renderer: Add RenderEntity base class for common methods. --- libopenage/renderer/stages/CMakeLists.txt | 4 + .../renderer/stages/hud/render_entity.cpp | 23 +---- .../renderer/stages/hud/render_entity.h | 42 +--------- libopenage/renderer/stages/render_entity.cpp | 37 +++++++++ libopenage/renderer/stages/render_entity.h | 83 +++++++++++++++++++ .../renderer/stages/terrain/render_entity.cpp | 15 +--- .../renderer/stages/terrain/render_entity.h | 36 +------- .../renderer/stages/world/render_entity.cpp | 27 +----- .../renderer/stages/world/render_entity.h | 57 +------------ 9 files changed, 135 insertions(+), 189 deletions(-) create mode 100644 libopenage/renderer/stages/render_entity.cpp create mode 100644 libopenage/renderer/stages/render_entity.h diff --git a/libopenage/renderer/stages/CMakeLists.txt b/libopenage/renderer/stages/CMakeLists.txt index c2ae5bf781..77d6faa091 100644 --- a/libopenage/renderer/stages/CMakeLists.txt +++ b/libopenage/renderer/stages/CMakeLists.txt @@ -1,3 +1,7 @@ +add_sources(libopenage + render_entity.cpp +) + add_subdirectory(camera/) add_subdirectory(hud/) add_subdirectory(screen/) diff --git a/libopenage/renderer/stages/hud/render_entity.cpp b/libopenage/renderer/stages/hud/render_entity.cpp index f8cf19e692..948b492fa5 100644 --- a/libopenage/renderer/stages/hud/render_entity.cpp +++ b/libopenage/renderer/stages/hud/render_entity.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 "render_entity.h" @@ -8,8 +8,7 @@ namespace openage::renderer::hud { HudDragRenderEntity::HudDragRenderEntity(const coord::input drag_start) : - changed{false}, - last_update{0.0}, + RenderEntity{}, drag_pos{nullptr, 0, "", nullptr, drag_start}, drag_start{drag_start} { } @@ -24,12 +23,6 @@ void HudDragRenderEntity::update(const coord::input drag_pos, this->changed = true; } -time::time_t HudDragRenderEntity::get_update_time() { - std::shared_lock lock{this->mutex}; - - return this->last_update; -} - const curve::Continuous &HudDragRenderEntity::get_drag_pos() { return this->drag_pos; } @@ -38,16 +31,4 @@ const coord::input &HudDragRenderEntity::get_drag_start() { return this->drag_start; } -bool HudDragRenderEntity::is_changed() { - std::shared_lock lock{this->mutex}; - - return this->changed; -} - -void HudDragRenderEntity::clear_changed_flag() { - std::unique_lock lock{this->mutex}; - - this->changed = false; -} - } // namespace openage::renderer::hud diff --git a/libopenage/renderer/stages/hud/render_entity.h b/libopenage/renderer/stages/hud/render_entity.h index e2e2da65fa..8ea1ac8d8a 100644 --- a/libopenage/renderer/stages/hud/render_entity.h +++ b/libopenage/renderer/stages/hud/render_entity.h @@ -3,12 +3,10 @@ #pragma once #include -#include -#include #include "coord/pixel.h" #include "curve/continuous.h" -#include "time/time.h" +#include "renderer/stages/render_entity.h" namespace openage::renderer::hud { @@ -16,7 +14,7 @@ namespace openage::renderer::hud { /** * Render entity for pushing drag selection updates to the HUD renderer. */ -class HudDragRenderEntity { +class HudDragRenderEntity final : public renderer::RenderEntity { public: /** * Create a new render entity for drag selection in the HUD. @@ -36,13 +34,6 @@ class HudDragRenderEntity { void update(const coord::input drag_pos, const time::time_t time = 0.0); - /** - * Get the time of the last update. - * - * @return Time of last update. - */ - time::time_t get_update_time(); - /** * Get the position of the dragged corner. * @@ -57,31 +48,7 @@ class HudDragRenderEntity { */ const coord::input &get_drag_start(); - /** - * Check whether the render entity has received new updates. - * - * @return true if updates have been received, else false. - */ - bool is_changed(); - - /** - * Clear the update flag by setting it to false. - */ - void clear_changed_flag(); - private: - /** - * Flag for determining if the render entity has been updated by the - * corresponding gamestate entity. Set to true every time \p update() - * is called. - */ - bool changed; - - /** - * Time of the last update call. - */ - time::time_t last_update; - /** * Position of the dragged corner. */ @@ -91,10 +58,5 @@ class HudDragRenderEntity { * Position of the start corner. */ coord::input drag_start; - - /** - * Mutex for protecting threaded access. - */ - std::shared_mutex mutex; }; } // namespace openage::renderer::hud diff --git a/libopenage/renderer/stages/render_entity.cpp b/libopenage/renderer/stages/render_entity.cpp new file mode 100644 index 0000000000..dc95de9a6d --- /dev/null +++ b/libopenage/renderer/stages/render_entity.cpp @@ -0,0 +1,37 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "render_entity.h" + +#include + + +namespace openage::renderer { + +RenderEntity::RenderEntity() : + changed{false}, + last_update{time::time_t::zero()} { +} + +time::time_t RenderEntity::get_update_time() { + std::shared_lock lock{this->mutex}; + + return this->last_update; +} + +bool RenderEntity::is_changed() { + std::shared_lock lock{this->mutex}; + + return this->changed; +} + +void RenderEntity::clear_changed_flag() { + std::unique_lock lock{this->mutex}; + + this->changed = false; +} + +std::shared_lock RenderEntity::get_read_lock() { + return std::shared_lock{this->mutex}; +} + +} // namespace openage::renderer diff --git a/libopenage/renderer/stages/render_entity.h b/libopenage/renderer/stages/render_entity.h new file mode 100644 index 0000000000..f441452968 --- /dev/null +++ b/libopenage/renderer/stages/render_entity.h @@ -0,0 +1,83 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include + +#include "time/time.h" + + +namespace openage::renderer { + +/** + * Interface for render entities that allow pushing updates from game simulation + * to renderer. + */ +class RenderEntity { +public: + ~RenderEntity() = default; + + /** + * Get the time of the last update. + * + * Accessing the update time is thread-safe. + * + * @return Time of last update. + */ + time::time_t get_update_time(); + + /** + * Check whether the render entity has received new updates from the + * gamestate. + * + * @return true if updates have been received, else false. + */ + bool is_changed(); + + /** + * Clear the update flag by setting it to false. + */ + void clear_changed_flag(); + + /** + * Get a shared lock for thread-safe reading from the render entity. + * + * The caller is responsible for unlocking the mutex after reading. + * + * @return Lock for the render entity. + */ + std::shared_lock get_read_lock(); + +protected: + /** + * Create a new render entity. + * + * Members are initialized to these values by default: + * - \p changed: false + * - \p last_update: time::time_t::zero() + * + * Declared as protected to prevent direct instantiation of this class. + */ + RenderEntity(); + + /** + * Flag for determining if the render entity has been updated by the + * corresponding gamestate entity. Set to true every time \p update() + * is called. + */ + bool changed; + + /** + * Time of the last update call. + */ + time::time_t last_update; + + /** + * Mutex for protecting threaded access. + */ + std::shared_mutex mutex; +}; +} // namespace openage::renderer diff --git a/libopenage/renderer/stages/terrain/render_entity.cpp b/libopenage/renderer/stages/terrain/render_entity.cpp index 7f2e6d825e..221884e943 100644 --- a/libopenage/renderer/stages/terrain/render_entity.cpp +++ b/libopenage/renderer/stages/terrain/render_entity.cpp @@ -10,10 +10,9 @@ namespace openage::renderer::terrain { TerrainRenderEntity::TerrainRenderEntity() : - changed{false}, + RenderEntity{}, size{0, 0}, tiles{}, - last_update{0.0}, terrain_paths{}, vertices{} // terrain_path{nullptr, 0}, @@ -132,16 +131,4 @@ const util::Vector2s &TerrainRenderEntity::get_size() { return this->size; } -bool TerrainRenderEntity::is_changed() { - std::shared_lock lock{this->mutex}; - - return this->changed; -} - -void TerrainRenderEntity::clear_changed_flag() { - std::unique_lock lock{this->mutex}; - - this->changed = false; -} - } // namespace openage::renderer::terrain diff --git a/libopenage/renderer/stages/terrain/render_entity.h b/libopenage/renderer/stages/terrain/render_entity.h index 4136c7c81a..70cd62e91a 100644 --- a/libopenage/renderer/stages/terrain/render_entity.h +++ b/libopenage/renderer/stages/terrain/render_entity.h @@ -2,8 +2,6 @@ #pragma once -#include -#include #include #include #include @@ -11,7 +9,7 @@ #include "coord/scene.h" #include "coord/tile.h" #include "curve/discrete.h" -#include "time/time.h" +#include "renderer/stages/render_entity.h" #include "util/vector.h" @@ -20,7 +18,7 @@ namespace openage::renderer::terrain { /** * Render entity for pushing updates to the Terrain renderer. */ -class TerrainRenderEntity { +class TerrainRenderEntity final : public renderer::RenderEntity { public: TerrainRenderEntity(); ~TerrainRenderEntity() = default; @@ -84,27 +82,7 @@ class TerrainRenderEntity { */ const util::Vector2s &get_size(); - /** - * Check whether the render entity has received new updates from the - * gamestate. - * - * @return true if updates have been received, else false. - */ - bool is_changed(); - - /** - * Clear the update flag by setting it to false. - */ - void clear_changed_flag(); - private: - /** - * Flag for determining if the render entity has been updated by the - * corresponding gamestate entity. Set to true every time \p update() - * is called. - */ - bool changed; - /** * Chunk dimensions (width x height). */ @@ -115,11 +93,6 @@ class TerrainRenderEntity { */ tiles_t tiles; - /** - * Time of the last update call. - */ - time::time_t last_update; - /** * Terrain texture paths used in \p tiles . */ @@ -129,10 +102,5 @@ class TerrainRenderEntity { * Terrain vertices (ingame coordinates). */ std::vector vertices; - - /** - * Mutex for protecting threaded access. - */ - std::shared_mutex mutex; }; } // namespace openage::renderer::terrain diff --git a/libopenage/renderer/stages/world/render_entity.cpp b/libopenage/renderer/stages/world/render_entity.cpp index 61101181c0..c56a7bb0a3 100644 --- a/libopenage/renderer/stages/world/render_entity.cpp +++ b/libopenage/renderer/stages/world/render_entity.cpp @@ -11,12 +11,11 @@ namespace openage::renderer::world { WorldRenderEntity::WorldRenderEntity() : - changed{false}, + RenderEntity{}, ref_id{0}, position{nullptr, 0, "", nullptr, SCENE_ORIGIN}, angle{nullptr, 0, "", nullptr, 0}, - animation_path{nullptr, 0}, - last_update{0.0} { + animation_path{nullptr, 0} { } void WorldRenderEntity::update(const uint32_t ref_id, @@ -78,26 +77,4 @@ const curve::Discrete &WorldRenderEntity::get_animation_path() { return this->animation_path; } -time::time_t WorldRenderEntity::get_update_time() { - std::shared_lock lock{this->mutex}; - - return this->last_update; -} - -bool WorldRenderEntity::is_changed() { - std::shared_lock lock{this->mutex}; - - return this->changed; -} - -void WorldRenderEntity::clear_changed_flag() { - std::unique_lock lock{this->mutex}; - - this->changed = false; -} - -std::shared_lock WorldRenderEntity::get_read_lock() { - return std::shared_lock{this->mutex}; -} - } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/render_entity.h b/libopenage/renderer/stages/world/render_entity.h index e1161d8b88..cd0f59d37a 100644 --- a/libopenage/renderer/stages/world/render_entity.h +++ b/libopenage/renderer/stages/world/render_entity.h @@ -3,19 +3,14 @@ #pragma once #include -#include -#include -#include #include -#include - #include "coord/phys.h" #include "coord/scene.h" #include "curve/continuous.h" #include "curve/discrete.h" #include "curve/segmented.h" -#include "time/time.h" +#include "renderer/stages/render_entity.h" namespace openage::renderer::world { @@ -23,7 +18,7 @@ namespace openage::renderer::world { /** * Render entity for pushing updates to the World renderer. */ -class WorldRenderEntity { +class WorldRenderEntity final : public renderer::RenderEntity { public: WorldRenderEntity(); ~WorldRenderEntity() = default; @@ -101,45 +96,7 @@ class WorldRenderEntity { */ const curve::Discrete &get_animation_path(); - /** - * Get the time of the last update. - * - * Accessing the update time is thread-safe. - * - * @return Time of last update. - */ - time::time_t get_update_time(); - - /** - * Check whether the render entity has received new updates from the - * gamestate. - * - * @return true if updates have been received, else false. - */ - bool is_changed(); - - /** - * Clear the update flag by setting it to false. - */ - void clear_changed_flag(); - - /** - * Get a shared lock for thread-safe reading from the render entity. - * - * The caller is responsible for unlocking the mutex after reading. - * - * @return Lock for the render entity. - */ - std::shared_lock get_read_lock(); - private: - /** - * Flag for determining if the render entity has been updated by the - * corresponding gamestate entity. Set to true every time \p update() - * is called. - */ - bool changed; - /** * ID of the game entity in the gamestate. */ @@ -159,15 +116,5 @@ class WorldRenderEntity { * Path to the animation definition file. */ curve::Discrete animation_path; - - /** - * Time of the last update call. - */ - time::time_t last_update; - - /** - * Mutex for protecting threaded access. - */ - std::shared_mutex mutex; }; } // namespace openage::renderer::world From d44f90329441f17c725bcb7751f566538843814c Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 3 Oct 2024 14:06:37 +0200 Subject: [PATCH 582/771] refactor: Shorten render entity class name in HUD render stage. --- libopenage/input/controller/hud/controller.cpp | 8 ++++---- libopenage/input/controller/hud/controller.h | 8 ++++---- libopenage/renderer/stages/hud/object.cpp | 4 ++-- libopenage/renderer/stages/hud/object.h | 6 +++--- libopenage/renderer/stages/hud/render_entity.cpp | 12 ++++++------ libopenage/renderer/stages/hud/render_entity.h | 6 +++--- libopenage/renderer/stages/hud/render_stage.cpp | 2 +- libopenage/renderer/stages/hud/render_stage.h | 4 ++-- 8 files changed, 25 insertions(+), 25 deletions(-) diff --git a/libopenage/input/controller/hud/controller.cpp b/libopenage/input/controller/hud/controller.cpp index b423ef79ae..af89923d1b 100644 --- a/libopenage/input/controller/hud/controller.cpp +++ b/libopenage/input/controller/hud/controller.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 "controller.h" @@ -27,11 +27,11 @@ bool Controller::process(const event_arguments &ev_args, return true; } -void Controller::set_drag_entity(const std::shared_ptr &entity) { +void Controller::set_drag_entity(const std::shared_ptr &entity) { this->drag_entity = entity; } -const std::shared_ptr &Controller::get_drag_entity() const { +const std::shared_ptr &Controller::get_drag_entity() const { return this->drag_entity; } @@ -39,7 +39,7 @@ void setup_defaults(const std::shared_ptr &ctx, const std::shared_ptr &hud_renderer) { binding_func_t drag_selection_init{[&](const event_arguments &args, const std::shared_ptr controller) { - auto render_entity = std::make_shared(args.mouse); + auto render_entity = std::make_shared(args.mouse); hud_renderer->add_drag_entity(render_entity); controller->set_drag_entity(render_entity); }}; diff --git a/libopenage/input/controller/hud/controller.h b/libopenage/input/controller/hud/controller.h index 60e9ff2bb8..44d64ee833 100644 --- a/libopenage/input/controller/hud/controller.h +++ b/libopenage/input/controller/hud/controller.h @@ -10,7 +10,7 @@ namespace openage { namespace renderer::hud { -class HudDragRenderEntity; +class DragRenderEntity; class HudRenderStage; } // namespace renderer::hud @@ -42,20 +42,20 @@ class Controller : public std::enable_shared_from_this { * * @param entity New render entity. */ - void set_drag_entity(const std::shared_ptr &entity); + void set_drag_entity(const std::shared_ptr &entity); /** * Get the render entity for the selection box. * * @return Render entity for the selection box. */ - const std::shared_ptr &get_drag_entity() const; + const std::shared_ptr &get_drag_entity() const; private: /** * Render entity for the selection box. */ - std::shared_ptr drag_entity; + std::shared_ptr drag_entity; }; /** diff --git a/libopenage/renderer/stages/hud/object.cpp b/libopenage/renderer/stages/hud/object.cpp index 2b53bd1b3b..a5ac552a78 100644 --- a/libopenage/renderer/stages/hud/object.cpp +++ b/libopenage/renderer/stages/hud/object.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 "object.h" @@ -21,7 +21,7 @@ HudDragObject::HudDragObject(const std::shared_ptr &entity) { +void HudDragObject::set_render_entity(const std::shared_ptr &entity) { this->render_entity = entity; this->fetch_updates(); } diff --git a/libopenage/renderer/stages/hud/object.h b/libopenage/renderer/stages/hud/object.h index fac062f18d..d81e51ed8d 100644 --- a/libopenage/renderer/stages/hud/object.h +++ b/libopenage/renderer/stages/hud/object.h @@ -26,7 +26,7 @@ class Animation2dInfo; } // namespace resources namespace hud { -class HudDragRenderEntity; +class DragRenderEntity; /** * Stores the state of a renderable object in the HUD render stage. @@ -46,7 +46,7 @@ class HudDragObject { * * @param entity New world render entity. */ - void set_render_entity(const std::shared_ptr &entity); + void set_render_entity(const std::shared_ptr &entity); /** * Set the current camera of the scene. @@ -147,7 +147,7 @@ class HudDragObject { /** * Source for positional and texture data. */ - std::shared_ptr render_entity; + std::shared_ptr render_entity; /** * Position of the dragged corner. diff --git a/libopenage/renderer/stages/hud/render_entity.cpp b/libopenage/renderer/stages/hud/render_entity.cpp index 948b492fa5..5aaea81a84 100644 --- a/libopenage/renderer/stages/hud/render_entity.cpp +++ b/libopenage/renderer/stages/hud/render_entity.cpp @@ -7,14 +7,14 @@ namespace openage::renderer::hud { -HudDragRenderEntity::HudDragRenderEntity(const coord::input drag_start) : - RenderEntity{}, +DragRenderEntity::DragRenderEntity(const coord::input drag_start) : + renderer::RenderEntity{}, drag_pos{nullptr, 0, "", nullptr, drag_start}, drag_start{drag_start} { } -void HudDragRenderEntity::update(const coord::input drag_pos, - const time::time_t time) { +void DragRenderEntity::update(const coord::input drag_pos, + const time::time_t time) { std::unique_lock lock{this->mutex}; this->drag_pos.set_insert(time, drag_pos); @@ -23,11 +23,11 @@ void HudDragRenderEntity::update(const coord::input drag_pos, this->changed = true; } -const curve::Continuous &HudDragRenderEntity::get_drag_pos() { +const curve::Continuous &DragRenderEntity::get_drag_pos() { return this->drag_pos; } -const coord::input &HudDragRenderEntity::get_drag_start() { +const coord::input &DragRenderEntity::get_drag_start() { return this->drag_start; } diff --git a/libopenage/renderer/stages/hud/render_entity.h b/libopenage/renderer/stages/hud/render_entity.h index 8ea1ac8d8a..bfc255814a 100644 --- a/libopenage/renderer/stages/hud/render_entity.h +++ b/libopenage/renderer/stages/hud/render_entity.h @@ -14,15 +14,15 @@ namespace openage::renderer::hud { /** * Render entity for pushing drag selection updates to the HUD renderer. */ -class HudDragRenderEntity final : public renderer::RenderEntity { +class DragRenderEntity final : public renderer::RenderEntity { public: /** * Create a new render entity for drag selection in the HUD. * * @param drag_start Position of the start corner. */ - HudDragRenderEntity(const coord::input drag_start); - ~HudDragRenderEntity() = default; + DragRenderEntity(const coord::input drag_start); + ~DragRenderEntity() = default; /** * Update the render entity with information from the gamestate diff --git a/libopenage/renderer/stages/hud/render_stage.cpp b/libopenage/renderer/stages/hud/render_stage.cpp index a55751ec5f..d5d7c5b83f 100644 --- a/libopenage/renderer/stages/hud/render_stage.cpp +++ b/libopenage/renderer/stages/hud/render_stage.cpp @@ -45,7 +45,7 @@ std::shared_ptr HudRenderStage::get_render_pass() { return this->render_pass; } -void HudRenderStage::add_drag_entity(const std::shared_ptr entity) { +void HudRenderStage::add_drag_entity(const std::shared_ptr entity) { std::unique_lock lock{this->mutex}; auto hud_object = std::make_shared(this->asset_manager); diff --git a/libopenage/renderer/stages/hud/render_stage.h b/libopenage/renderer/stages/hud/render_stage.h index f63662aa40..047aac6349 100644 --- a/libopenage/renderer/stages/hud/render_stage.h +++ b/libopenage/renderer/stages/hud/render_stage.h @@ -31,7 +31,7 @@ class AssetManager; namespace hud { class HudDragObject; -class HudDragRenderEntity; +class DragRenderEntity; /** * Renderer for the "Heads-Up Display" (HUD). @@ -71,7 +71,7 @@ class HudRenderStage { * * @param render_entity New render entity. */ - void add_drag_entity(const std::shared_ptr entity); + void add_drag_entity(const std::shared_ptr entity); /** * Remove the render object for drag selection. From 3f73385ba0bdbef0fbecc11a1f3cc2aca77452ac Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 3 Oct 2024 14:08:10 +0200 Subject: [PATCH 583/771] refactor: Shorten render entity class name in Terrain render stage. --- libopenage/gamestate/terrain_chunk.cpp | 2 +- libopenage/gamestate/terrain_chunk.h | 4 +-- libopenage/renderer/demo/demo_3.cpp | 2 +- libopenage/renderer/demo/stresstest_0.cpp | 2 +- libopenage/renderer/demo/stresstest_1.cpp | 2 +- libopenage/renderer/render_factory.cpp | 8 +++--- libopenage/renderer/render_factory.h | 6 ++-- libopenage/renderer/stages/terrain/chunk.cpp | 2 +- libopenage/renderer/stages/terrain/chunk.h | 6 ++-- libopenage/renderer/stages/terrain/model.cpp | 4 +-- libopenage/renderer/stages/terrain/model.h | 4 +-- .../renderer/stages/terrain/render_entity.cpp | 28 +++++++++---------- .../renderer/stages/terrain/render_entity.h | 6 ++-- .../renderer/stages/terrain/render_stage.cpp | 2 +- .../renderer/stages/terrain/render_stage.h | 6 ++-- 15 files changed, 42 insertions(+), 42 deletions(-) diff --git a/libopenage/gamestate/terrain_chunk.cpp b/libopenage/gamestate/terrain_chunk.cpp index e988260333..e7dadbaf09 100644 --- a/libopenage/gamestate/terrain_chunk.cpp +++ b/libopenage/gamestate/terrain_chunk.cpp @@ -18,7 +18,7 @@ TerrainChunk::TerrainChunk(const util::Vector2s size, } } -void TerrainChunk::set_render_entity(const std::shared_ptr &entity) { +void TerrainChunk::set_render_entity(const std::shared_ptr &entity) { this->render_entity = entity; } diff --git a/libopenage/gamestate/terrain_chunk.h b/libopenage/gamestate/terrain_chunk.h index 684d3af357..7939476adc 100644 --- a/libopenage/gamestate/terrain_chunk.h +++ b/libopenage/gamestate/terrain_chunk.h @@ -32,7 +32,7 @@ class TerrainChunk { * * @param entity New render entity. */ - void set_render_entity(const std::shared_ptr &entity); + void set_render_entity(const std::shared_ptr &entity); /** * Get the size of this terrain chunk. @@ -78,7 +78,7 @@ class TerrainChunk { /** * Render entity for pushing updates to the renderer. Can be \p nullptr. */ - std::shared_ptr render_entity; + std::shared_ptr render_entity; }; } // namespace openage::gamestate diff --git a/libopenage/renderer/demo/demo_3.cpp b/libopenage/renderer/demo/demo_3.cpp index 408e699196..9c294285f9 100644 --- a/libopenage/renderer/demo/demo_3.cpp +++ b/libopenage/renderer/demo/demo_3.cpp @@ -123,7 +123,7 @@ void renderer_demo_3(const util::Path &path) { // Fill a 10x10 terrain grid with height values auto terrain_size = util::Vector2s{10, 10}; - std::vector> tiles{}; + std::vector> tiles{}; tiles.reserve(terrain_size[0] * terrain_size[1]); for (size_t i = 0; i < terrain_size[0] * terrain_size[1]; ++i) { tiles.emplace_back(0.0f, "./textures/test_terrain.terrain"); diff --git a/libopenage/renderer/demo/stresstest_0.cpp b/libopenage/renderer/demo/stresstest_0.cpp index 76c53d7b21..fd7cdb467d 100644 --- a/libopenage/renderer/demo/stresstest_0.cpp +++ b/libopenage/renderer/demo/stresstest_0.cpp @@ -133,7 +133,7 @@ void renderer_stresstest_0(const util::Path &path) { // Fill a 10x10 terrain grid with height values auto terrain_size = util::Vector2s{10, 10}; - std::vector> tiles{}; + std::vector> tiles{}; tiles.reserve(terrain_size[0] * terrain_size[1]); for (size_t i = 0; i < terrain_size[0] * terrain_size[1]; ++i) { tiles.emplace_back(0.0f, "./textures/test_terrain.terrain"); diff --git a/libopenage/renderer/demo/stresstest_1.cpp b/libopenage/renderer/demo/stresstest_1.cpp index ac6177f819..c398a0e382 100644 --- a/libopenage/renderer/demo/stresstest_1.cpp +++ b/libopenage/renderer/demo/stresstest_1.cpp @@ -138,7 +138,7 @@ void renderer_stresstest_1(const util::Path &path) { // Fill a 10x10 terrain grid with height values auto terrain_size = util::Vector2s{10, 10}; - std::vector> tiles{}; + std::vector> tiles{}; tiles.reserve(terrain_size[0] * terrain_size[1]); for (size_t i = 0; i < terrain_size[0] * terrain_size[1]; ++i) { tiles.emplace_back(0.0f, "./textures/test_terrain.terrain"); diff --git a/libopenage/renderer/render_factory.cpp b/libopenage/renderer/render_factory.cpp index 6d5e276479..eedf46ca53 100644 --- a/libopenage/renderer/render_factory.cpp +++ b/libopenage/renderer/render_factory.cpp @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #include "render_factory.h" @@ -15,9 +15,9 @@ RenderFactory::RenderFactory(const std::shared_ptr world_renderer{world_renderer} { } -std::shared_ptr RenderFactory::add_terrain_render_entity(const util::Vector2s chunk_size, - const coord::tile_delta chunk_offset) { - auto entity = std::make_shared(); +std::shared_ptr RenderFactory::add_terrain_render_entity(const util::Vector2s chunk_size, + const coord::tile_delta chunk_offset) { + auto entity = std::make_shared(); this->terrain_renderer->add_render_entity(entity, chunk_size, chunk_offset.to_phys2().to_scene2()); return entity; diff --git a/libopenage/renderer/render_factory.h b/libopenage/renderer/render_factory.h index 26601bcf7f..cefc92ce77 100644 --- a/libopenage/renderer/render_factory.h +++ b/libopenage/renderer/render_factory.h @@ -11,7 +11,7 @@ namespace openage::renderer { namespace terrain { class TerrainRenderStage; -class TerrainRenderEntity; +class RenderEntity; } // namespace terrain namespace world { @@ -47,8 +47,8 @@ class RenderFactory { * * @return Render entity for pushing terrain updates. */ - std::shared_ptr add_terrain_render_entity(const util::Vector2s chunk_size, - const coord::tile_delta chunk_offset); + std::shared_ptr add_terrain_render_entity(const util::Vector2s chunk_size, + const coord::tile_delta chunk_offset); /** * Create a new world render entity and register it at the world renderer. diff --git a/libopenage/renderer/stages/terrain/chunk.cpp b/libopenage/renderer/stages/terrain/chunk.cpp index 12b207bdc9..e080ec523b 100644 --- a/libopenage/renderer/stages/terrain/chunk.cpp +++ b/libopenage/renderer/stages/terrain/chunk.cpp @@ -17,7 +17,7 @@ TerrainChunk::TerrainChunk(const std::shared_ptr &entity) { +void TerrainChunk::set_render_entity(const std::shared_ptr &entity) { this->render_entity = entity; } diff --git a/libopenage/renderer/stages/terrain/chunk.h b/libopenage/renderer/stages/terrain/chunk.h index b64133e5b6..b34a3db880 100644 --- a/libopenage/renderer/stages/terrain/chunk.h +++ b/libopenage/renderer/stages/terrain/chunk.h @@ -19,7 +19,7 @@ class AssetManager; namespace terrain { class TerrainRenderMesh; -class TerrainRenderEntity; +class RenderEntity; /** * Stores the state of a terrain chunk in the terrain render stage. @@ -44,7 +44,7 @@ class TerrainChunk { * @param size Size of the chunk in tiles. * @param offset Offset of the chunk from origin in tiles. */ - void set_render_entity(const std::shared_ptr &entity); + void set_render_entity(const std::shared_ptr &entity); /** * Fetch updates from the render entity. @@ -114,7 +114,7 @@ class TerrainChunk { * Source for ingame terrain coordinates. These coordinates are translated into * our render vertex mesh when \p update() is called. */ - std::shared_ptr render_entity; + std::shared_ptr render_entity; }; diff --git a/libopenage/renderer/stages/terrain/model.cpp b/libopenage/renderer/stages/terrain/model.cpp index 8d8cc4c727..30e235c1bd 100644 --- a/libopenage/renderer/stages/terrain/model.cpp +++ b/libopenage/renderer/stages/terrain/model.cpp @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #include "model.h" @@ -26,7 +26,7 @@ TerrainRenderModel::TerrainRenderModel(const std::shared_ptr &entity, +void TerrainRenderModel::add_chunk(const std::shared_ptr &entity, const util::Vector2s size, const coord::scene2_delta offset) { auto chunk = std::make_shared(this->asset_manager, size, offset); diff --git a/libopenage/renderer/stages/terrain/model.h b/libopenage/renderer/stages/terrain/model.h index c06ea351c0..951599855d 100644 --- a/libopenage/renderer/stages/terrain/model.h +++ b/libopenage/renderer/stages/terrain/model.h @@ -21,7 +21,7 @@ class AssetManager; } namespace terrain { -class TerrainRenderEntity; +class RenderEntity; class TerrainRenderMesh; class TerrainChunk; @@ -46,7 +46,7 @@ class TerrainRenderModel { * @param chunk_size Size of the chunk in tiles. * @param chunk_offset Offset of the chunk from origin in tiles. */ - void add_chunk(const std::shared_ptr &entity, + void add_chunk(const std::shared_ptr &entity, const util::Vector2s chunk_size, const coord::scene2_delta chunk_offset); diff --git a/libopenage/renderer/stages/terrain/render_entity.cpp b/libopenage/renderer/stages/terrain/render_entity.cpp index 221884e943..2471ecdb6d 100644 --- a/libopenage/renderer/stages/terrain/render_entity.cpp +++ b/libopenage/renderer/stages/terrain/render_entity.cpp @@ -9,8 +9,8 @@ namespace openage::renderer::terrain { -TerrainRenderEntity::TerrainRenderEntity() : - RenderEntity{}, +RenderEntity::RenderEntity() : + renderer::RenderEntity{}, size{0, 0}, tiles{}, terrain_paths{}, @@ -19,11 +19,11 @@ TerrainRenderEntity::TerrainRenderEntity() : { } -void TerrainRenderEntity::update_tile(const util::Vector2s size, - const coord::tile &pos, - const terrain_elevation_t elevation, - const std::string terrain_path, - const time::time_t time) { +void RenderEntity::update_tile(const util::Vector2s size, + const coord::tile &pos, + const terrain_elevation_t elevation, + const std::string terrain_path, + const time::time_t time) { std::unique_lock lock{this->mutex}; if (this->vertices.empty()) { @@ -51,9 +51,9 @@ void TerrainRenderEntity::update_tile(const util::Vector2s size, this->changed = true; } -void TerrainRenderEntity::update(const util::Vector2s size, - const tiles_t tiles, - const time::time_t time) { +void RenderEntity::update(const util::Vector2s size, + const tiles_t tiles, + const time::time_t time) { std::unique_lock lock{this->mutex}; // increase by 1 in every dimension because tiles @@ -107,25 +107,25 @@ void TerrainRenderEntity::update(const util::Vector2s size, this->changed = true; } -const std::vector &TerrainRenderEntity::get_vertices() { +const std::vector &RenderEntity::get_vertices() { std::shared_lock lock{this->mutex}; return this->vertices; } -const TerrainRenderEntity::tiles_t &TerrainRenderEntity::get_tiles() { +const RenderEntity::tiles_t &RenderEntity::get_tiles() { std::shared_lock lock{this->mutex}; return this->tiles; } -const std::unordered_set &TerrainRenderEntity::get_terrain_paths() { +const std::unordered_set &RenderEntity::get_terrain_paths() { std::shared_lock lock{this->mutex}; return this->terrain_paths; } -const util::Vector2s &TerrainRenderEntity::get_size() { +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 70cd62e91a..7b32c22401 100644 --- a/libopenage/renderer/stages/terrain/render_entity.h +++ b/libopenage/renderer/stages/terrain/render_entity.h @@ -18,10 +18,10 @@ namespace openage::renderer::terrain { /** * Render entity for pushing updates to the Terrain renderer. */ -class TerrainRenderEntity final : public renderer::RenderEntity { +class RenderEntity final : public renderer::RenderEntity { public: - TerrainRenderEntity(); - ~TerrainRenderEntity() = default; + RenderEntity(); + ~RenderEntity() = default; using terrain_elevation_t = util::FixedPoint; using tiles_t = std::vector>; diff --git a/libopenage/renderer/stages/terrain/render_stage.cpp b/libopenage/renderer/stages/terrain/render_stage.cpp index 740fe260f6..01376d886b 100644 --- a/libopenage/renderer/stages/terrain/render_stage.cpp +++ b/libopenage/renderer/stages/terrain/render_stage.cpp @@ -48,7 +48,7 @@ std::shared_ptr TerrainRenderStage::get_render_pass() { return this->render_pass; } -void TerrainRenderStage::add_render_entity(const std::shared_ptr entity, +void TerrainRenderStage::add_render_entity(const std::shared_ptr entity, const util::Vector2s chunk_size, const coord::scene2_delta chunk_offset) { std::unique_lock lock{this->mutex}; diff --git a/libopenage/renderer/stages/terrain/render_stage.h b/libopenage/renderer/stages/terrain/render_stage.h index 40caf2b309..6066dc90aa 100644 --- a/libopenage/renderer/stages/terrain/render_stage.h +++ b/libopenage/renderer/stages/terrain/render_stage.h @@ -32,7 +32,7 @@ class AssetManager; } namespace terrain { -class TerrainRenderEntity; +class RenderEntity; class TerrainRenderMesh; class TerrainRenderModel; @@ -73,7 +73,7 @@ class TerrainRenderStage { * * @param render_entity New render entity. */ - void add_render_entity(const std::shared_ptr entity, + void add_render_entity(const std::shared_ptr entity, const util::Vector2s chunk_size, const coord::scene2_delta chunk_offset); @@ -119,7 +119,7 @@ class TerrainRenderStage { /** * Engine interface for updating terrain draw information. */ - std::shared_ptr render_entity; + std::shared_ptr render_entity; /** * 3D model of the terrain. From b3295e34938a505e82bd762de45efcf542e07dc8 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 3 Oct 2024 14:08:49 +0200 Subject: [PATCH 584/771] refactor: Shorten render entity class name in world render stage. --- libopenage/gamestate/game_entity.cpp | 4 +-- libopenage/gamestate/game_entity.h | 6 ++-- libopenage/renderer/demo/stresstest_0.cpp | 2 +- libopenage/renderer/demo/stresstest_1.cpp | 2 +- libopenage/renderer/render_factory.cpp | 4 +-- libopenage/renderer/render_factory.h | 4 +-- libopenage/renderer/stages/world/object.cpp | 2 +- libopenage/renderer/stages/world/object.h | 6 ++-- .../renderer/stages/world/render_entity.cpp | 30 +++++++++---------- .../renderer/stages/world/render_entity.h | 6 ++-- .../renderer/stages/world/render_stage.cpp | 2 +- .../renderer/stages/world/render_stage.h | 4 +-- 12 files changed, 36 insertions(+), 36 deletions(-) diff --git a/libopenage/gamestate/game_entity.cpp b/libopenage/gamestate/game_entity.cpp index a4e18a29e3..421c2847e7 100644 --- a/libopenage/gamestate/game_entity.cpp +++ b/libopenage/gamestate/game_entity.cpp @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #include "game_entity.h" @@ -30,7 +30,7 @@ entity_id_t GameEntity::get_id() const { return this->id; } -void GameEntity::set_render_entity(const std::shared_ptr &entity) { +void GameEntity::set_render_entity(const std::shared_ptr &entity) { // TODO: Transfer state from old render entity to new one? this->render_entity = entity; diff --git a/libopenage/gamestate/game_entity.h b/libopenage/gamestate/game_entity.h index 9b30e8ec76..923aed2eab 100644 --- a/libopenage/gamestate/game_entity.h +++ b/libopenage/gamestate/game_entity.h @@ -14,7 +14,7 @@ namespace openage { namespace renderer::world { -class WorldRenderEntity; +class RenderEntity; } namespace gamestate { @@ -62,7 +62,7 @@ class GameEntity { * * @param entity New render entity. */ - void set_render_entity(const std::shared_ptr &entity); + void set_render_entity(const std::shared_ptr &entity); /** * Set the event manager of this entity. @@ -142,7 +142,7 @@ class GameEntity { /** * Render entity for pushing updates to the renderer. Can be \p nullptr. */ - std::shared_ptr render_entity; + std::shared_ptr render_entity; /** * Event manager. diff --git a/libopenage/renderer/demo/stresstest_0.cpp b/libopenage/renderer/demo/stresstest_0.cpp index fd7cdb467d..eae208c5d3 100644 --- a/libopenage/renderer/demo/stresstest_0.cpp +++ b/libopenage/renderer/demo/stresstest_0.cpp @@ -147,7 +147,7 @@ void renderer_stresstest_0(const util::Path &path) { terrain0->update(terrain_size, tiles); // World entities - std::vector> render_entities{}; + std::vector> render_entities{}; auto add_world_entity = [&](const coord::phys3 initial_pos, const time::time_t time) { const auto animation_path = "./textures/test_tank_mirrored.sprite"; diff --git a/libopenage/renderer/demo/stresstest_1.cpp b/libopenage/renderer/demo/stresstest_1.cpp index c398a0e382..9d2d43968b 100644 --- a/libopenage/renderer/demo/stresstest_1.cpp +++ b/libopenage/renderer/demo/stresstest_1.cpp @@ -151,7 +151,7 @@ void renderer_stresstest_1(const util::Path &path) { // send the terrain data to the terrain renderer terrain0->update(terrain_size, tiles); - std::vector> render_entities{}; + std::vector> render_entities{}; auto add_world_entity = [&](const coord::phys3 initial_pos, const time::time_t time) { const auto animation_path = "./textures/test_tank_mirrored.sprite"; diff --git a/libopenage/renderer/render_factory.cpp b/libopenage/renderer/render_factory.cpp index eedf46ca53..3dee6dd6ae 100644 --- a/libopenage/renderer/render_factory.cpp +++ b/libopenage/renderer/render_factory.cpp @@ -23,8 +23,8 @@ std::shared_ptr RenderFactory::add_terrain_render_entity( return entity; } -std::shared_ptr RenderFactory::add_world_render_entity() { - auto entity = std::make_shared(); +std::shared_ptr RenderFactory::add_world_render_entity() { + auto entity = std::make_shared(); this->world_renderer->add_render_entity(entity); return entity; diff --git a/libopenage/renderer/render_factory.h b/libopenage/renderer/render_factory.h index cefc92ce77..a211e3edf4 100644 --- a/libopenage/renderer/render_factory.h +++ b/libopenage/renderer/render_factory.h @@ -16,7 +16,7 @@ class RenderEntity; namespace world { class WorldRenderStage; -class WorldRenderEntity; +class RenderEntity; } // namespace world /** @@ -55,7 +55,7 @@ class RenderFactory { * * @return Render entity for pushing terrain updates. */ - std::shared_ptr add_world_render_entity(); + std::shared_ptr add_world_render_entity(); private: /** diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index e7ffc24dde..0e98a10d1f 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -43,7 +43,7 @@ WorldObject::WorldObject(const std::shared_ptr &entity) { +void WorldObject::set_render_entity(const std::shared_ptr &entity) { this->render_entity = entity; this->fetch_updates(); } diff --git a/libopenage/renderer/stages/world/object.h b/libopenage/renderer/stages/world/object.h index 1302733954..c5579f62a0 100644 --- a/libopenage/renderer/stages/world/object.h +++ b/libopenage/renderer/stages/world/object.h @@ -29,7 +29,7 @@ class Animation2dInfo; } // namespace resources namespace world { -class WorldRenderEntity; +class RenderEntity; /** * Stores the state of a renderable object in the World render stage. @@ -49,7 +49,7 @@ class WorldObject { * * @param entity New world render entity. */ - void set_render_entity(const std::shared_ptr &entity); + void set_render_entity(const std::shared_ptr &entity); /** * Fetch updates from the render entity. @@ -177,7 +177,7 @@ class WorldObject { * Entity that gets updates from the gamestate, e.g. the position and * requested animation data. */ - std::shared_ptr render_entity; + std::shared_ptr render_entity; /** * Reference ID for passing interaction with the graphic (e.g. mouse clicks) back to diff --git a/libopenage/renderer/stages/world/render_entity.cpp b/libopenage/renderer/stages/world/render_entity.cpp index c56a7bb0a3..7a8e145741 100644 --- a/libopenage/renderer/stages/world/render_entity.cpp +++ b/libopenage/renderer/stages/world/render_entity.cpp @@ -10,19 +10,19 @@ namespace openage::renderer::world { -WorldRenderEntity::WorldRenderEntity() : - RenderEntity{}, +RenderEntity::RenderEntity() : + renderer::RenderEntity{}, ref_id{0}, position{nullptr, 0, "", nullptr, SCENE_ORIGIN}, angle{nullptr, 0, "", nullptr, 0}, animation_path{nullptr, 0} { } -void WorldRenderEntity::update(const uint32_t ref_id, - const curve::Continuous &position, - const curve::Segmented &angle, - const std::string animation_path, - const time::time_t time) { +void RenderEntity::update(const uint32_t ref_id, + const curve::Continuous &position, + const curve::Segmented &angle, + const std::string animation_path, + const time::time_t time) { std::unique_lock lock{this->mutex}; this->ref_id = ref_id; @@ -40,10 +40,10 @@ void WorldRenderEntity::update(const uint32_t ref_id, this->last_update = time; } -void WorldRenderEntity::update(const uint32_t ref_id, - const coord::phys3 position, - const std::string animation_path, - const time::time_t time) { +void RenderEntity::update(const uint32_t ref_id, + const coord::phys3 position, + const std::string animation_path, + const time::time_t time) { std::unique_lock lock{this->mutex}; this->ref_id = ref_id; @@ -53,25 +53,25 @@ void WorldRenderEntity::update(const uint32_t ref_id, this->last_update = time; } -uint32_t WorldRenderEntity::get_id() { +uint32_t RenderEntity::get_id() { std::shared_lock lock{this->mutex}; return this->ref_id; } -const curve::Continuous &WorldRenderEntity::get_position() { +const curve::Continuous &RenderEntity::get_position() { std::shared_lock lock{this->mutex}; return this->position; } -const curve::Segmented &WorldRenderEntity::get_angle() { +const curve::Segmented &RenderEntity::get_angle() { std::shared_lock lock{this->mutex}; return this->angle; } -const curve::Discrete &WorldRenderEntity::get_animation_path() { +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 cd0f59d37a..ed9011b8a4 100644 --- a/libopenage/renderer/stages/world/render_entity.h +++ b/libopenage/renderer/stages/world/render_entity.h @@ -18,10 +18,10 @@ namespace openage::renderer::world { /** * Render entity for pushing updates to the World renderer. */ -class WorldRenderEntity final : public renderer::RenderEntity { +class RenderEntity final : public renderer::RenderEntity { public: - WorldRenderEntity(); - ~WorldRenderEntity() = default; + RenderEntity(); + ~RenderEntity() = default; /** * Update the render entity with information from the gamestate. diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 0434f3cf07..d8d93279af 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -50,7 +50,7 @@ std::shared_ptr WorldRenderStage::get_render_pass() { return this->render_pass; } -void WorldRenderStage::add_render_entity(const std::shared_ptr entity) { +void WorldRenderStage::add_render_entity(const std::shared_ptr entity) { std::unique_lock lock{this->mutex}; auto world_object = std::make_shared(this->asset_manager); diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index e6ae39c094..f1256f3b57 100644 --- a/libopenage/renderer/stages/world/render_stage.h +++ b/libopenage/renderer/stages/world/render_stage.h @@ -31,7 +31,7 @@ class AssetManager; } namespace world { -class WorldRenderEntity; +class RenderEntity; class WorldObject; /** @@ -74,7 +74,7 @@ class WorldRenderStage { * * @param render_entity New render entity. */ - void add_render_entity(const std::shared_ptr entity); + void add_render_entity(const std::shared_ptr entity); /** * Update the render entities and render positions. From 5e7a7683f9923291d3e08fce3b0668213ec4ff32 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 3 Oct 2024 14:37:16 +0200 Subject: [PATCH 585/771] renderer: Return copies instead of referenced in terrain render enity. Makes the updates thread-safe. --- libopenage/renderer/stages/terrain/chunk.cpp | 25 ++++++++++++------- libopenage/renderer/stages/terrain/chunk.h | 12 +++++++-- .../renderer/stages/terrain/render_entity.cpp | 12 ++++----- .../renderer/stages/terrain/render_entity.h | 20 ++++++++++++--- 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/libopenage/renderer/stages/terrain/chunk.cpp b/libopenage/renderer/stages/terrain/chunk.cpp index e080ec523b..58a95d2a95 100644 --- a/libopenage/renderer/stages/terrain/chunk.cpp +++ b/libopenage/renderer/stages/terrain/chunk.cpp @@ -31,11 +31,19 @@ void TerrainChunk::fetch_updates(const time::time_t & /* time */) { if (not this->render_entity->is_changed()) { return; } + + // 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(); + auto tiles = this->render_entity->get_tiles(); + auto heightmap_verts = this->render_entity->get_vertices(); + + // Recreate the mesh data // TODO: Change mesh instead of recreating it // TODO: Multiple meshes this->meshes.clear(); - for (const auto &terrain_path : this->render_entity->get_terrain_paths()) { - auto new_mesh = this->create_mesh(terrain_path); + for (const auto &terrain_path : terrain_paths) { + auto new_mesh = this->create_mesh(terrain_size, tiles, heightmap_verts, terrain_path); new_mesh->create_model_matrix(this->offset); this->meshes.push_back(new_mesh); } @@ -59,13 +67,12 @@ const std::vector> &TerrainChunk::get_meshes( return this->meshes; } -std::shared_ptr TerrainChunk::create_mesh(const std::string &texture_path) { - auto size = this->render_entity->get_size(); - auto v_width = size[0]; - auto v_height = size[1]; - - auto tiles = this->render_entity->get_tiles(); - auto heightmap_verts = this->render_entity->get_vertices(); +std::shared_ptr TerrainChunk::create_mesh(const util::Vector2s vert_size, + const RenderEntity::tiles_t &tiles, + const std::vector &heightmap_verts, + const std::string &texture_path) { + auto v_width = vert_size[0]; + auto v_height = vert_size[1]; // vertex data for the mesh std::vector mesh_verts{}; diff --git a/libopenage/renderer/stages/terrain/chunk.h b/libopenage/renderer/stages/terrain/chunk.h index b34a3db880..7d3d9fcc70 100644 --- a/libopenage/renderer/stages/terrain/chunk.h +++ b/libopenage/renderer/stages/terrain/chunk.h @@ -7,6 +7,7 @@ #include #include "coord/scene.h" +#include "renderer/stages/terrain/render_entity.h" #include "time/time.h" #include "util/vector.h" @@ -19,7 +20,6 @@ class AssetManager; namespace terrain { class TerrainRenderMesh; -class RenderEntity; /** * Stores the state of a terrain chunk in the terrain render stage. @@ -85,9 +85,17 @@ class TerrainChunk { /** * Create a terrain mesh from the data provided by the render entity. * + * @param vert_size Size of the terrain in vertices. + * @param tiles Data for each tile (elevation, terrain path). + * @param heightmap_verts Position of each vertex in the chunk. + * @param texture_path Path to the texture for the terrain. + * * @return New terrain mesh. */ - std::shared_ptr create_mesh(const std::string &texture_path); + std::shared_ptr create_mesh(const util::Vector2s vert_size, + const RenderEntity::tiles_t &tiles, + const std::vector &heightmap_verts, + const std::string &texture_path); /** * Size of the chunk in tiles (width x height). diff --git a/libopenage/renderer/stages/terrain/render_entity.cpp b/libopenage/renderer/stages/terrain/render_entity.cpp index 2471ecdb6d..2316f5dab7 100644 --- a/libopenage/renderer/stages/terrain/render_entity.cpp +++ b/libopenage/renderer/stages/terrain/render_entity.cpp @@ -14,9 +14,7 @@ RenderEntity::RenderEntity() : size{0, 0}, tiles{}, terrain_paths{}, - vertices{} -// terrain_path{nullptr, 0}, -{ + vertices{} { } void RenderEntity::update_tile(const util::Vector2s size, @@ -107,25 +105,25 @@ void RenderEntity::update(const util::Vector2s size, this->changed = true; } -const std::vector &RenderEntity::get_vertices() { +const std::vector RenderEntity::get_vertices() { std::shared_lock lock{this->mutex}; return this->vertices; } -const RenderEntity::tiles_t &RenderEntity::get_tiles() { +const RenderEntity::tiles_t RenderEntity::get_tiles() { std::shared_lock lock{this->mutex}; return this->tiles; } -const std::unordered_set &RenderEntity::get_terrain_paths() { +const std::unordered_set RenderEntity::get_terrain_paths() { std::shared_lock lock{this->mutex}; return this->terrain_paths; } -const util::Vector2s &RenderEntity::get_size() { +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 7b32c22401..0f726a2351 100644 --- a/libopenage/renderer/stages/terrain/render_entity.h +++ b/libopenage/renderer/stages/terrain/render_entity.h @@ -30,6 +30,8 @@ class RenderEntity final : public renderer::RenderEntity { * Update a single tile of the displayed terrain (chunk) with information from the * gamestate. * + * Updating the render entity with this method is thread-safe. + * * @param size Size of the terrain in tiles (width x length) * @param pos Position of the tile in the chunk. * @param elevation Height of terrain tile. @@ -46,6 +48,8 @@ class RenderEntity final : public renderer::RenderEntity { * Update the full grid of the displayed terrain (chunk) with information from the * gamestate. * + * Updating the render entity with this method is thread-safe. + * * @param size Size of the terrain in tiles (width x length) * @param tiles Animation data for each tile (elevation, terrain path). * @param time Simulation time of the update. @@ -57,30 +61,38 @@ 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(); + const std::vector get_vertices(); /** * Get the tiles of the terrain. * + * Accessing the terrain tiles is thread-safe. + * * @return Terrain tiles. */ - const tiles_t &get_tiles(); + const tiles_t get_tiles(); /** * 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(); + const std::unordered_set get_terrain_paths(); /** * 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. */ - const util::Vector2s &get_size(); + const util::Vector2s get_size(); private: /** From 8314b6cb110f1096c123a11681846520180d71b0 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 3 Oct 2024 14:48:09 +0200 Subject: [PATCH 586/771] renderer: Fix lock of resource reads in hud render entity. --- libopenage/renderer/stages/hud/object.cpp | 7 +++++++ libopenage/renderer/stages/hud/render_entity.cpp | 6 +++++- libopenage/renderer/stages/hud/render_entity.h | 9 ++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/libopenage/renderer/stages/hud/object.cpp b/libopenage/renderer/stages/hud/object.cpp index a5ac552a78..6e23b6663a 100644 --- a/libopenage/renderer/stages/hud/object.cpp +++ b/libopenage/renderer/stages/hud/object.cpp @@ -38,8 +38,15 @@ void HudDragObject::fetch_updates(const time::time_t &time) { // 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 */); + // Unlock the render entity mutex + read_lock.unlock(); + // Set self to changed so that world renderer can update the renderable this->changed = true; this->render_entity->clear_changed_flag(); diff --git a/libopenage/renderer/stages/hud/render_entity.cpp b/libopenage/renderer/stages/hud/render_entity.cpp index 5aaea81a84..2ff3f75649 100644 --- a/libopenage/renderer/stages/hud/render_entity.cpp +++ b/libopenage/renderer/stages/hud/render_entity.cpp @@ -24,10 +24,14 @@ void DragRenderEntity::update(const coord::input drag_pos, } const curve::Continuous &DragRenderEntity::get_drag_pos() { + std::shared_lock lock{this->mutex}; + return this->drag_pos; } -const coord::input &DragRenderEntity::get_drag_start() { +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 bfc255814a..9d15204386 100644 --- a/libopenage/renderer/stages/hud/render_entity.h +++ b/libopenage/renderer/stages/hud/render_entity.h @@ -28,6 +28,8 @@ class DragRenderEntity final : public renderer::RenderEntity { * Update the render entity with information from the gamestate * or input system. * + * Updating the render entity with this method is thread-safe. + * * @param drag_pos Position of the dragged corner. * @param time Current simulation time. */ @@ -37,6 +39,9 @@ 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(); @@ -44,9 +49,11 @@ 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(); + const coord::input get_drag_start(); private: /** From 535b6cbf8ea11bc59fd1f27f17617ed8c17cc8ee Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 31 Oct 2024 17:44:53 +0100 Subject: [PATCH 587/771] etc: clang-format rule for preprocessor indentation. --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index 9264f3bc0f..4191b164f7 100644 --- a/.clang-format +++ b/.clang-format @@ -81,7 +81,7 @@ IndentCaseBlocks: false IndentCaseLabels: false IndentExternBlock: NoIndent IndentGotoLabels: false -IndentPPDirectives: None +IndentPPDirectives: BeforeHash IndentWidth: 4 IndentWrappedFunctionNames: false # clang-format-16 InsertNewlineAtEOF: true From 3c47b65f5a8afdb69b01239993842c439ca3c08a Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 31 Oct 2024 18:05:49 +0100 Subject: [PATCH 588/771] refactor: Format cpp files with preprocessor code. --- libopenage/console/tests.cpp | 18 ++--- libopenage/engine/engine.h | 6 +- libopenage/error/handlers.cpp | 6 +- libopenage/error/stackanalyzer.cpp | 20 +++--- libopenage/event/demo/gui.cpp | 32 ++++----- libopenage/event/demo/gui.h | 18 ++--- libopenage/event/demo/main.cpp | 12 ++-- libopenage/event/demo/physics.cpp | 24 +++---- libopenage/log/level.h | 10 +-- libopenage/log/message.h | 6 +- libopenage/log/stdout_logsink.cpp | 6 +- libopenage/renderer/color.cpp | 13 ++-- .../guisys/private/opengl_debug_logger.cpp | 12 ++-- libopenage/renderer/vulkan/loader.cpp | 24 +++---- libopenage/util/compiler.cpp | 40 ++++++----- libopenage/util/fds.cpp | 47 ++++++------ libopenage/util/fds.h | 2 +- libopenage/util/fslike/directory.cpp | 72 ++++++++----------- libopenage/util/macro/concat.h | 22 +++--- libopenage/util/macro/loop.h | 22 +++--- libopenage/util/os.cpp | 45 ++++++------ libopenage/util/pty.h | 12 ++-- libopenage/util/strings.h | 4 +- libopenage/util/subprocess.cpp | 22 +++--- libopenage/util/thread_id.cpp | 20 +++--- libopenage/versions/versions.cpp | 4 +- 26 files changed, 253 insertions(+), 266 deletions(-) diff --git a/libopenage/console/tests.cpp b/libopenage/console/tests.cpp index 445fb71b30..7da8dd02f4 100644 --- a/libopenage/console/tests.cpp +++ b/libopenage/console/tests.cpp @@ -1,12 +1,12 @@ -// Copyright 2014-2019 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. -#include #include +#include #ifdef _MSC_VER -#define STDOUT_FILENO 1 + #define STDOUT_FILENO 1 #else -#include + #include #endif #include "../util/fds.h" #include "../util/pty.h" @@ -15,8 +15,8 @@ #include #include -#include "../log/log.h" #include "../error/error.h" +#include "../log/log.h" #include "buf.h" #include "console.h" @@ -51,7 +51,7 @@ void render() { void interactive() { - #ifndef _WIN32 +#ifndef _WIN32 console::Buf buf{{80, 25}, 1337, 80}; struct winsize ws; @@ -75,7 +75,7 @@ void interactive() { throw Error(MSG(err) << "execl(\"" << shell << "\", \"" << shell << "\", nullptr) failed: " << strerror(errno)); } default: - //we are the parent + // we are the parent break; } @@ -143,7 +143,7 @@ void interactive() { ssize_t retval = read(ptyin.fd, rdbuf, rdbuf_size); switch (retval) { case -1: - switch(errno) { + switch (errno) { case EIO: loop = false; break; @@ -175,7 +175,7 @@ void interactive() { // show cursor termout.puts("\x1b[?25h"); - #endif /* _WIN32 */ +#endif /* _WIN32 */ } diff --git a/libopenage/engine/engine.h b/libopenage/engine/engine.h index 4f96ae74bf..1fe4298061 100644 --- a/libopenage/engine/engine.h +++ b/libopenage/engine/engine.h @@ -11,7 +11,7 @@ // TODO: Remove custom jthread definition when clang/libc++ finally supports it #if __llvm__ -#if !__cpp_lib_jthread + #if !__cpp_lib_jthread namespace std { class jthread : public thread { public: @@ -27,9 +27,9 @@ class jthread : public thread { } }; } // namespace std -#endif + #endif #else -#include + #include #endif diff --git a/libopenage/error/handlers.cpp b/libopenage/error/handlers.cpp index f608b0e78f..b3e61450b2 100644 --- a/libopenage/error/handlers.cpp +++ b/libopenage/error/handlers.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. /* * This file holds handlers for std::terminate and SIGSEGV. @@ -17,9 +17,9 @@ #include #ifdef _MSC_VER -#include + #include #else -#include + #include #endif #include "util/init.h" diff --git a/libopenage/error/stackanalyzer.cpp b/libopenage/error/stackanalyzer.cpp index 878cb01025..cc04dae05d 100644 --- a/libopenage/error/stackanalyzer.cpp +++ b/libopenage/error/stackanalyzer.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "stackanalyzer.h" @@ -30,8 +30,8 @@ constexpr uint64_t base_skip_frames = 1; #if WITH_BACKTRACE -// use modern -#include + // use modern + #include namespace openage { namespace error { @@ -59,7 +59,7 @@ struct backtrace_state *bt_state; util::OnInit init_backtrace_state([]() { bt_state = backtrace_create_state( nullptr, // auto-determine filename - 1, // threaded + 1, // threaded backtrace_error_callback, nullptr // passed to the callback ); @@ -189,8 +189,8 @@ void StackAnalyzer::get_symbols(std::function cb #else // WITHOUT_BACKTRACE -#ifdef _WIN32 -#include + #ifdef _WIN32 + #include namespace openage { namespace error { @@ -208,10 +208,10 @@ void StackAnalyzer::analyze() { } // namespace error } // namespace openage -#else // not _MSC_VER + #else // not _MSC_VER -// use GNU's -#include + // use GNU's + #include namespace openage::error { @@ -256,7 +256,7 @@ void StackAnalyzer::analyze() { } // namespace openage::error -#endif // for _MSC_VER or GNU execinfo + #endif // for _MSC_VER or GNU execinfo namespace openage::error { diff --git a/libopenage/event/demo/gui.cpp b/libopenage/event/demo/gui.cpp index 100509c0f4..66d0bbf79d 100644 --- a/libopenage/event/demo/gui.cpp +++ b/libopenage/event/demo/gui.cpp @@ -1,25 +1,25 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. +// Copyright 2016-2024 the openage authors. See copying.md for legal info. #include "gui.h" // the gui requires ncurses. #if WITH_NCURSES -#include -#include -#include -#include -#ifdef __MINGW32__ -#include -#else -#include -#endif // __MINGW32__ -#include - -#include "curve/continuous.h" -#include "curve/discrete.h" -#include "event/demo/gamestate.h" -#include "util/fixed_point.h" + #include + #include + #include + #include + #ifdef __MINGW32__ + #include + #else + #include + #endif // __MINGW32__ + #include + + #include "curve/continuous.h" + #include "curve/discrete.h" + #include "event/demo/gamestate.h" + #include "util/fixed_point.h" namespace openage::event::demo { diff --git a/libopenage/event/demo/gui.h b/libopenage/event/demo/gui.h index de21a99d02..8e447d808f 100644 --- a/libopenage/event/demo/gui.h +++ b/libopenage/event/demo/gui.h @@ -1,18 +1,18 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once #include "config.h" #if WITH_NCURSES -#include -#include -#include -#include - -#include "event/demo/gamestate.h" -#include "time/time.h" -#include "util/vector.h" + #include + #include + #include + #include + + #include "event/demo/gamestate.h" + #include "time/time.h" + #include "util/vector.h" namespace openage::event::demo { diff --git a/libopenage/event/demo/main.cpp b/libopenage/event/demo/main.cpp index 3ccbed5ac6..f5100b2e82 100644 --- a/libopenage/event/demo/main.cpp +++ b/libopenage/event/demo/main.cpp @@ -1,4 +1,4 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. +// Copyright 2016-2024 the openage authors. See copying.md for legal info. #include "main.h" @@ -16,11 +16,11 @@ #include "renderer/gui/integration/public/gui_application_with_logger.h" #if WITH_NCURSES -#ifdef __MINGW32__ -#include -#else -#include -#endif // __MINGW32__ + #ifdef __MINGW32__ + #include + #else + #include + #endif // __MINGW32__ #endif diff --git a/libopenage/event/demo/physics.cpp b/libopenage/event/demo/physics.cpp index e27a51d962..e7e969a533 100644 --- a/libopenage/event/demo/physics.cpp +++ b/libopenage/event/demo/physics.cpp @@ -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. #include "physics.h" @@ -9,13 +9,13 @@ #include #if WITH_NCURSES -#ifdef __MINGW32__ -#include -#else -#include -#endif // __MINGW32__ + #ifdef __MINGW32__ + #include + #else + #include + #endif // __MINGW32__ -#include "gui.h" + #include "gui.h" #endif #include "error/error.h" @@ -377,7 +377,7 @@ void Physics::init(const std::shared_ptr &gstate, loop->create_event("demo.ball.reflect_panel", state->ball->position, state, now); // FIXME once "reset": deregister - //reset(state, mgr, now); + // reset(state, mgr, now); } void Physics::process_input(const std::shared_ptr &state, @@ -466,11 +466,11 @@ void Physics::process_input(const std::shared_ptr &state, Physics::reset(state, *mgr, now); break; - //if (player->state->get(now).state == PongEvent::LOST) { + // if (player->state->get(now).state == PongEvent::LOST) { // state->ball.position->set_last(now, state.display_boundary * 0.5); - //} - //update_ball(state, now, init_recursion_limit); - //break; + // } + // update_ball(state, now, init_recursion_limit); + // break; default: break; diff --git a/libopenage/log/level.h b/libopenage/log/level.h index ef7322ec63..0d35f163c5 100644 --- a/libopenage/log/level.h +++ b/libopenage/log/level.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -50,11 +50,11 @@ struct OAAPI level : util::Enum { level(); #ifdef __MINGW32__ -// Do not try to optimize these out even if it seems they are not used. -// Namely MIN that is not used within the library. -#define NOOPTIMIZE __attribute__((__used__)) + // Do not try to optimize these out even if it seems they are not used. + // Namely MIN that is not used within the library. + #define NOOPTIMIZE __attribute__((__used__)) #else -#define NOOPTIMIZE + #define NOOPTIMIZE #endif // _win32 static constexpr level_value MIN NOOPTIMIZE{{"min loglevel", -1000}, "5"}; diff --git a/libopenage/log/message.h b/libopenage/log/message.h index d46ed18c7d..ca534e2efe 100644 --- a/libopenage/log/message.h +++ b/libopenage/log/message.h @@ -21,11 +21,11 @@ #if defined(__GNUC__) -#define OPENAGE_FUNC_NAME __PRETTY_FUNCTION__ + #define OPENAGE_FUNC_NAME __PRETTY_FUNCTION__ #elif defined(_MSC_VER) -#define OPENAGE_FUNC_NAME __FUNCSIG__ + #define OPENAGE_FUNC_NAME __FUNCSIG__ #else -#define OPENAGE_FUNC_NAME __FUNCTION__ + #define OPENAGE_FUNC_NAME __FUNCTION__ #endif namespace openage { diff --git a/libopenage/log/stdout_logsink.cpp b/libopenage/log/stdout_logsink.cpp index 647af51c49..99daecf5cd 100644 --- a/libopenage/log/stdout_logsink.cpp +++ b/libopenage/log/stdout_logsink.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "stdout_logsink.h" @@ -7,8 +7,8 @@ #include #ifdef _MSC_VER -#define WIN32_LEAN_AND_MEAN -#include + #define WIN32_LEAN_AND_MEAN + #include #endif #include "log/level.h" diff --git a/libopenage/renderer/color.cpp b/libopenage/renderer/color.cpp index 7333dfb073..f03f883a0a 100644 --- a/libopenage/renderer/color.cpp +++ b/libopenage/renderer/color.cpp @@ -1,12 +1,11 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "color.h" namespace openage { namespace renderer { -Color::Color() - : +Color::Color() : r{0}, g{0}, b{0}, @@ -14,8 +13,7 @@ Color::Color() // Empty } -Color::Color(uint8_t r, uint8_t g, uint8_t b, uint8_t a) - : +Color::Color(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : r{r}, g{g}, b{b}, @@ -32,6 +30,7 @@ bool Color::operator!=(const Color &other) const { } Color Colors::WHITE = {255, 255, 255, 255}; -Color Colors::BLACK = { 0, 0, 0, 255}; +Color Colors::BLACK = {0, 0, 0, 255}; -}} // openage::renderer +} // namespace renderer +} // namespace openage diff --git a/libopenage/renderer/gui/guisys/private/opengl_debug_logger.cpp b/libopenage/renderer/gui/guisys/private/opengl_debug_logger.cpp index 61025528a6..0f565efaae 100644 --- a/libopenage/renderer/gui/guisys/private/opengl_debug_logger.cpp +++ b/libopenage/renderer/gui/guisys/private/opengl_debug_logger.cpp @@ -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. #include "opengl_debug_logger.h" @@ -7,11 +7,11 @@ #include #ifdef __APPLE__ -// from https://www.khronos.org/registry/OpenGL/api/GL/glext.h -#define GL_DEBUG_CALLBACK_FUNCTION 0x8244 -#define GL_DEBUG_OUTPUT_SYNCHRONOUS 0x8242 -#define GL_DEBUG_TYPE_ERROR 0x824C -#define GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR 0x824E + // from https://www.khronos.org/registry/OpenGL/api/GL/glext.h + #define GL_DEBUG_CALLBACK_FUNCTION 0x8244 + #define GL_DEBUG_OUTPUT_SYNCHRONOUS 0x8242 + #define GL_DEBUG_TYPE_ERROR 0x824C + #define GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR 0x824E #endif namespace qtgui { diff --git a/libopenage/renderer/vulkan/loader.cpp b/libopenage/renderer/vulkan/loader.cpp index fdeb6f8c94..2dcf6bd2fc 100644 --- a/libopenage/renderer/vulkan/loader.cpp +++ b/libopenage/renderer/vulkan/loader.cpp @@ -1,4 +1,4 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #include "loader.h" @@ -9,14 +9,14 @@ namespace openage { namespace renderer { namespace vulkan { -VlkLoader::VlkLoader() - : inited(false) {} +VlkLoader::VlkLoader() : + inited(false) {} void VlkLoader::init(VkInstance instance) { - #ifndef NDEBUG +#ifndef NDEBUG this->pCreateDebugReportCallbackEXT = PFN_vkCreateDebugReportCallbackEXT(vkGetInstanceProcAddr(instance, "vkCreateDebugReportCallbackEXT")); this->pDestroyDebugReportCallbackEXT = PFN_vkDestroyDebugReportCallbackEXT(vkGetInstanceProcAddr(instance, "vkDestroyDebugReportCallbackEXT")); - #endif +#endif this->inited = true; } @@ -24,10 +24,9 @@ void VlkLoader::init(VkInstance instance) { #ifndef NDEBUG VkResult VlkLoader::vkCreateDebugReportCallbackEXT( VkInstance instance, - const VkDebugReportCallbackCreateInfoEXT* pCreateInfo, - const VkAllocationCallbacks* pAllocator, - VkDebugReportCallbackEXT* pCallback -) { + const VkDebugReportCallbackCreateInfoEXT *pCreateInfo, + const VkAllocationCallbacks *pAllocator, + VkDebugReportCallbackEXT *pCallback) { if (!this->inited) { throw Error(MSG(err) << "Tried to request function from Vulkan extension loader before initializing it."); } @@ -42,8 +41,7 @@ VkResult VlkLoader::vkCreateDebugReportCallbackEXT( void VlkLoader::vkDestroyDebugReportCallbackEXT( VkInstance instance, VkDebugReportCallbackEXT callback, - const VkAllocationCallbacks* pAllocator -) { + const VkAllocationCallbacks *pAllocator) { if (!this->inited) { throw Error(MSG(err) << "Tried to request function from Vulkan extension loader before initializing it."); } @@ -54,4 +52,6 @@ void VlkLoader::vkDestroyDebugReportCallbackEXT( } #endif -}}} // openage::renderer::vulkan +} // namespace vulkan +} // namespace renderer +} // namespace openage diff --git a/libopenage/util/compiler.cpp b/libopenage/util/compiler.cpp index b854d91278..082240a704 100644 --- a/libopenage/util/compiler.cpp +++ b/libopenage/util/compiler.cpp @@ -1,22 +1,22 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "compiler.h" #ifndef _WIN32 -#include -#include + #include + #include #else -#define WIN32_LEAN_AND_MEAN -#include -#include + #define WIN32_LEAN_AND_MEAN + #include + #include #endif #include "strings.h" #include #include -#include #include +#include namespace openage { namespace util { @@ -34,7 +34,8 @@ std::string demangle(const char *symbol) { if (status != 0) { return symbol; - } else { + } + else { std::string result{buf}; free(buf); return result; @@ -78,12 +79,12 @@ std::optional symbol_name_win(const void *addr) { constexpr int buffer_size = sizeof(SYMBOL_INFO) + name_buffer_size * sizeof(char); std::array buffer; - SYMBOL_INFO *symbol_info = reinterpret_cast(buffer.data()); + SYMBOL_INFO *symbol_info = reinterpret_cast(buffer.data()); symbol_info->SizeOfStruct = sizeof(SYMBOL_INFO); symbol_info->MaxNameLen = name_buffer_size; - if (SymFromAddr(process_handle, reinterpret_cast(addr), nullptr, symbol_info)) { + if (SymFromAddr(process_handle, reinterpret_cast(addr), nullptr, symbol_info)) { return std::string(symbol_info->Name); } } @@ -92,7 +93,7 @@ std::optional symbol_name_win(const void *addr) { } -} +} // namespace #endif std::string symbol_name(const void *addr, bool require_exact_addr, bool no_pure_addrs) { @@ -100,8 +101,7 @@ std::string symbol_name(const void *addr, bool require_exact_addr, bool no_pure_ auto symbol_name_result = symbol_name_win(addr); - if (!initialized_symbol_handler_successfully || - !symbol_name_result.has_value()) { + if (!initialized_symbol_handler_successfully || !symbol_name_result.has_value()) { return no_pure_addrs ? "" : addr_to_string(addr); } @@ -113,8 +113,9 @@ std::string symbol_name(const void *addr, bool require_exact_addr, bool no_pure_ if (dladdr(addr, &addr_info) == 0) { // dladdr has... failed. return no_pure_addrs ? "" : addr_to_string(addr); - } else { - size_t symbol_offset = (size_t) addr - (size_t) addr_info.dli_saddr; + } + else { + size_t symbol_offset = (size_t)addr - (size_t)addr_info.dli_saddr; if (addr_info.dli_sname == nullptr or (symbol_offset != 0 and require_exact_addr)) { return no_pure_addrs ? "" : addr_to_string(addr); @@ -123,7 +124,8 @@ std::string symbol_name(const void *addr, bool require_exact_addr, bool no_pure_ if (symbol_offset == 0) { // this is our symbol name. return demangle(addr_info.dli_sname); - } else { + } + else { return util::sformat("%s+0x%lx", demangle(addr_info.dli_sname).c_str(), symbol_offset); @@ -138,7 +140,8 @@ bool is_symbol(const void *addr) { if (!initialized_symbol_handler_successfully) { return true; - } else { + } + else { return symbol_name_win(addr).has_value(); } @@ -154,4 +157,5 @@ bool is_symbol(const void *addr) { } -}} // openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/util/fds.cpp b/libopenage/util/fds.cpp index 468d3354bc..8d5188c10d 100644 --- a/libopenage/util/fds.cpp +++ b/libopenage/util/fds.cpp @@ -1,4 +1,4 @@ -// Copyright 2014-2019 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #include "fds.h" @@ -7,12 +7,12 @@ #include #include -#include #include "pty.h" +#include #ifdef _WIN32 -#include + #include #else -#include + #include #endif #include "unicode.h" @@ -28,10 +28,10 @@ FD::FD(int fd, bool set_nonblocking) { this->close_on_destroy = true; if (set_nonblocking) { - #ifndef _WIN32 +#ifndef _WIN32 int flags = ::fcntl(this->fd, F_GETFL, 0); ::fcntl(this->fd, F_SETFL, flags | O_NONBLOCK); - #endif +#endif } } @@ -64,31 +64,32 @@ int FD::putbyte(char c) { int FD::putcp(int cp) { char utf8buf[5]; if (util::utf8_encode(cp, utf8buf) == 0) { - //unrepresentable character (question mark in black rhombus) + // unrepresentable character (question mark in black rhombus) return this->puts("\uFFFD"); - } else { + } + else { return this->puts(utf8buf); } } int FD::printf(const char *format, ...) { const unsigned buf_size = 16; - char *buf = static_cast(malloc(sizeof(char) * buf_size)); + char *buf = static_cast(malloc(sizeof(char) * buf_size)); if (!buf) { return -1; } va_list vl; - //first, try to vsnprintf to a buffer of length 16 + // first, try to vsnprintf to a buffer of length 16 va_start(vl, format); unsigned len = vsnprintf(buf, buf_size, format, vl); va_end(vl); - //if that wasn't enough, allocate more memory and try again + // if that wasn't enough, allocate more memory and try again if (len >= buf_size) { char *oldbuf = buf; - buf = static_cast(realloc(oldbuf, sizeof(char) * (len + 1))); + buf = static_cast(realloc(oldbuf, sizeof(char) * (len + 1))); if (!buf) { free(oldbuf); return -1; @@ -99,42 +100,42 @@ int FD::printf(const char *format, ...) { va_end(vl); } - //output buf to the socket + // output buf to the socket int result = this->puts(buf); - //free the buffer + // free the buffer free(buf); return result; } void FD::setinputmodecanon() { - #ifndef _WIN32 +#ifndef _WIN32 if (::isatty(this->fd)) { - //get the terminal settings for stdin + // get the terminal settings for stdin ::tcgetattr(this->fd, &this->old_tio); - //backup old settings + // backup old settings struct termios new_tio = this->old_tio; - //disable buffered i/o (canonical mode) and local echo + // disable buffered i/o (canonical mode) and local echo new_tio.c_lflag &= (~ICANON & ~ECHO & ~ISIG); - //set the settings + // set the settings ::tcsetattr(this->fd, TCSANOW, &new_tio); this->restore_input_mode_on_destroy = true; } - #endif /* _WIN32 */ +#endif /* _WIN32 */ } void FD::restoreinputmode() { - #ifndef _WIN32 +#ifndef _WIN32 if (::isatty(this->fd)) { ::tcsetattr(this->fd, TCSANOW, &this->old_tio); this->restore_input_mode_on_destroy = false; } - #endif /* _WIN32 */ +#endif /* _WIN32 */ } -} // openage::util +} // namespace openage::util diff --git a/libopenage/util/fds.h b/libopenage/util/fds.h index 251ad0aec7..309384e1ad 100644 --- a/libopenage/util/fds.h +++ b/libopenage/util/fds.h @@ -5,7 +5,7 @@ #include #ifndef _WIN32 -#include + #include #endif namespace openage { diff --git a/libopenage/util/fslike/directory.cpp b/libopenage/util/fslike/directory.cpp index 22055e2c32..420757f3a6 100644 --- a/libopenage/util/fslike/directory.cpp +++ b/libopenage/util/fslike/directory.cpp @@ -1,11 +1,11 @@ -// Copyright 2017-2019 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #include "directory.h" // HACK: windows.h defines max and min as macros. This results in compile errors. #ifdef _WIN32 -// defining `NOMINMAX` disables the definition of those macros. -#define NOMINMAX + // defining `NOMINMAX` disables the definition of those macros. + #define NOMINMAX #endif #include @@ -17,34 +17,32 @@ #include #ifdef __APPLE__ -#include + #include #endif #ifdef _WIN32 -#include -#include -#include -// HACK: What the heck? I want the std::filesystem library! -#define O_NOCTTY 0 -#define O_NONBLOCK 0 -#define W_OK 2 + #include + #include + #include + // HACK: What the heck? I want the std::filesystem library! + #define O_NOCTTY 0 + #define O_NONBLOCK 0 + #define W_OK 2 #else // ! _MSC_VER -#include + #include #endif -#include "./native.h" #include "../file.h" #include "../filelike/native.h" #include "../misc.h" #include "../path.h" +#include "./native.h" namespace openage::util::fslike { -Directory::Directory(std::string basepath, bool create_if_missing) - : +Directory::Directory(std::string basepath, bool create_if_missing) : basepath{std::move(basepath)} { - if (create_if_missing) { this->mkdirs({}); } @@ -78,8 +76,7 @@ bool Directory::is_file(const Path::parts_t &parts) { auto stat_result = this->do_stat(parts); // test for regular file - if (std::get<1>(stat_result) == 0 and - S_ISREG(std::get<0>(stat_result).st_mode)) { + if (std::get<1>(stat_result) == 0 and S_ISREG(std::get<0>(stat_result).st_mode)) { return true; } @@ -91,8 +88,7 @@ bool Directory::is_dir(const Path::parts_t &parts) { auto stat_result = this->do_stat(parts); // test for regular file - if (std::get<1>(stat_result) == 0 and - S_ISDIR(std::get<0>(stat_result).st_mode)) { + if (std::get<1>(stat_result) == 0 and S_ISDIR(std::get<0>(stat_result).st_mode)) { return true; } @@ -104,9 +100,7 @@ bool Directory::writable(const Path::parts_t &parts) { Path::parts_t parts_test = parts; // try to find the first existing path-part - while (not (this->is_dir(parts_test) or - this->is_file(parts_test))) { - + while (not(this->is_dir(parts_test) or this->is_file(parts_test))) { if (parts_test.size() == 0) { throw Error{ERR << "file not found"}; } @@ -120,7 +114,6 @@ bool Directory::writable(const Path::parts_t &parts) { std::vector Directory::list(const Path::parts_t &parts) { - const std::string path = this->resolve(parts); std::vector ret; @@ -145,7 +138,6 @@ std::vector Directory::list(const Path::parts_t &parts) { bool Directory::mkdirs(const Path::parts_t &parts) { - Path::parts_t all_parts = util::split(this->basepath, PATHSEP); vector_extend(all_parts, parts); @@ -158,9 +150,7 @@ bool Directory::mkdirs(const Path::parts_t &parts) { struct stat buf; // it it exists already, try creating the next one - if (stat(dirpath.c_str(), &buf) == 0 and - S_ISDIR(buf.st_mode)) { - + if (stat(dirpath.c_str(), &buf) == 0 and S_ISDIR(buf.st_mode)) { continue; } @@ -184,40 +174,35 @@ bool Directory::mkdirs(const Path::parts_t &parts) { File Directory::open_r(const Path::parts_t &parts) { return File{ std::make_shared(this->resolve(parts), - filelike::Native::mode_t::R) - }; + filelike::Native::mode_t::R)}; } File Directory::open_w(const Path::parts_t &parts) { return File{ std::make_shared(this->resolve(parts), - filelike::Native::mode_t::W) - }; + filelike::Native::mode_t::W)}; } File Directory::open_rw(const Path::parts_t &parts) { return File{ std::make_shared(this->resolve(parts), - filelike::Native::mode_t::RW) - }; + filelike::Native::mode_t::RW)}; } File Directory::open_a(const Path::parts_t &parts) { return File{ std::make_shared(this->resolve(parts), - filelike::Native::mode_t::A) - }; + filelike::Native::mode_t::A)}; } File Directory::open_ar(const Path::parts_t &parts) { return File{ std::make_shared(this->resolve(parts), - filelike::Native::mode_t::AR) - }; + filelike::Native::mode_t::AR)}; } @@ -228,9 +213,9 @@ std::string Directory::get_native_path(const Path::parts_t &parts) { bool Directory::rename(const Path::parts_t &parts, const Path::parts_t &target_parts) { - return std::rename(this->resolve(parts).c_str(), - this->resolve(target_parts).c_str()) == 0; + this->resolve(target_parts).c_str()) + == 0; } @@ -245,9 +230,8 @@ bool Directory::touch(const Path::parts_t &parts) { // create the file if missing int fd = open( path.c_str(), - O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK, - 0666 - ); + O_WRONLY | O_CREAT | O_NOCTTY | O_NONBLOCK, + 0666); if (fd < 0) { return false; @@ -308,4 +292,4 @@ std::ostream &Directory::repr(std::ostream &stream) { return stream; } -} // openage::util::fslike +} // namespace openage::util::fslike diff --git a/libopenage/util/macro/concat.h b/libopenage/util/macro/concat.h index b8f7b746ca..dd057ccd9f 100644 --- a/libopenage/util/macro/concat.h +++ b/libopenage/util/macro/concat.h @@ -9,16 +9,16 @@ #define CONCAT_N(_1, _2, _3, NAME, ...) NAME #ifdef _MSC_VER -#define CONCAT_COUNT_ARGS_IMPL(args) CONCAT_N args -#define CONCAT_COUNT_ARGS(...) CONCAT_COUNT_ARGS_IMPL((__VA_ARGS__, 3, 2, 1)) -#define CONCAT_HELPER2(count) CONCAT_##count -#define CONCAT_HELPER1(count) CONCAT_HELPER2(count) -#define CONCAT_HELPER(count) CONCAT_HELPER1(count) -#define CONCAT_GLUE(x, y) x y -#define CONCAT(OP, ...) CONCAT_GLUE(CONCAT_HELPER(CONCAT_COUNT_ARGS(__VA_ARGS__)), (OP, __VA_ARGS__)) + #define CONCAT_COUNT_ARGS_IMPL(args) CONCAT_N args + #define CONCAT_COUNT_ARGS(...) CONCAT_COUNT_ARGS_IMPL((__VA_ARGS__, 3, 2, 1)) + #define CONCAT_HELPER2(count) CONCAT_##count + #define CONCAT_HELPER1(count) CONCAT_HELPER2(count) + #define CONCAT_HELPER(count) CONCAT_HELPER1(count) + #define CONCAT_GLUE(x, y) x y + #define CONCAT(OP, ...) CONCAT_GLUE(CONCAT_HELPER(CONCAT_COUNT_ARGS(__VA_ARGS__)), (OP, __VA_ARGS__)) #else -#define CONCAT(OP, ...) CONCAT_N(__VA_ARGS__, \ - CONCAT_3, \ - CONCAT_2, \ - CONCAT_1)(OP, __VA_ARGS__) + #define CONCAT(OP, ...) CONCAT_N(__VA_ARGS__, \ + CONCAT_3, \ + CONCAT_2, \ + CONCAT_1)(OP, __VA_ARGS__) #endif diff --git a/libopenage/util/macro/loop.h b/libopenage/util/macro/loop.h index 5b8dc59e8e..c868a12694 100644 --- a/libopenage/util/macro/loop.h +++ b/libopenage/util/macro/loop.h @@ -9,16 +9,16 @@ #define LOOP_N(_1, _2, _3, NAME, ...) NAME #ifdef _MSC_VER -#define LOOP_COUNT_ARGS_IMPL(args) LOOP_N args -#define LOOP_COUNT_ARGS(...) LOOP_COUNT_ARGS_IMPL((__VA_ARGS__, 3, 2, 1)) -#define LOOP_HELPER2(count) LOOP_##count -#define LOOP_HELPER1(count) LOOP_HELPER2(count) -#define LOOP_HELPER(count) LOOP_HELPER1(count) -#define LOOP_GLUE(x, y) x y -#define LOOP(MACRO, ...) LOOP_GLUE(LOOP_HELPER(LOOP_COUNT_ARGS(__VA_ARGS__)), (MACRO, __VA_ARGS__)) + #define LOOP_COUNT_ARGS_IMPL(args) LOOP_N args + #define LOOP_COUNT_ARGS(...) LOOP_COUNT_ARGS_IMPL((__VA_ARGS__, 3, 2, 1)) + #define LOOP_HELPER2(count) LOOP_##count + #define LOOP_HELPER1(count) LOOP_HELPER2(count) + #define LOOP_HELPER(count) LOOP_HELPER1(count) + #define LOOP_GLUE(x, y) x y + #define LOOP(MACRO, ...) LOOP_GLUE(LOOP_HELPER(LOOP_COUNT_ARGS(__VA_ARGS__)), (MACRO, __VA_ARGS__)) #else -#define LOOP(MACRO, ...) LOOP_N(__VA_ARGS__, \ - LOOP_3, \ - LOOP_2, \ - LOOP_1)(MACRO, __VA_ARGS__) + #define LOOP(MACRO, ...) LOOP_N(__VA_ARGS__, \ + LOOP_3, \ + LOOP_2, \ + LOOP_1)(MACRO, __VA_ARGS__) #endif diff --git a/libopenage/util/os.cpp b/libopenage/util/os.cpp index c3bbea2da6..ccc68d86c6 100644 --- a/libopenage/util/os.cpp +++ b/libopenage/util/os.cpp @@ -1,4 +1,4 @@ -// Copyright 2014-2019 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #include "os.h" @@ -7,16 +7,16 @@ #ifdef _WIN32 // TODO not yet implemented #else -#include + #include #endif #ifdef __APPLE__ -#include + #include #endif #ifdef __FreeBSD__ -#include -#include + #include + #include #endif #include "../log/log.h" @@ -77,8 +77,7 @@ std::string self_exec_filename() { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, - -1 - }; + -1}; while (true) { std::unique_ptr buf{new char[bufsize]}; @@ -103,23 +102,23 @@ int execute_file(const char *path, bool background) { // TODO some sort of shell-open, not yet implemented return -1; // failure #else - std::string runner = ""; -#ifdef __APPLE__ - runner = subprocess::which("open"); -#else - runner = subprocess::which("xdg-open"); - // fallback - if (runner.size() == 0) { - runner = subprocess::which("gnome-open"); - } -#endif - if (runner.size() == 0) { - log::log(MSG(err) << "could not locate file-opener"); - return -1; - } + std::string runner = ""; + #ifdef __APPLE__ + runner = subprocess::which("open"); + #else + runner = subprocess::which("xdg-open"); + // fallback + if (runner.size() == 0) { + runner = subprocess::which("gnome-open"); + } + #endif + if (runner.size() == 0) { + log::log(MSG(err) << "could not locate file-opener"); + return -1; + } - return subprocess::call({runner.c_str(), path, nullptr}, not background); + return subprocess::call({runner.c_str(), path, nullptr}, not background); #endif } -} // namespace openage +} // namespace openage::os diff --git a/libopenage/util/pty.h b/libopenage/util/pty.h index 0ae9e72d23..cb85f38865 100644 --- a/libopenage/util/pty.h +++ b/libopenage/util/pty.h @@ -3,14 +3,14 @@ #pragma once #ifdef __APPLE__ -#include + #include #elif defined(__FreeBSD__) -#include -#include -#include -#include + #include + #include + #include + #include #elif _WIN32 // TODO not yet implemented #else -#include + #include #endif diff --git a/libopenage/util/strings.h b/libopenage/util/strings.h index ad04e5267c..69f2759fb4 100644 --- a/libopenage/util/strings.h +++ b/libopenage/util/strings.h @@ -11,9 +11,9 @@ #include #if defined(__GNUC__) -#define ATTRIBUTE_FORMAT(i, j) __attribute__((format(printf, i, j))) + #define ATTRIBUTE_FORMAT(i, j) __attribute__((format(printf, i, j))) #else -#define ATTRIBUTE_FORMAT(i, j) + #define ATTRIBUTE_FORMAT(i, j) #endif namespace openage { diff --git a/libopenage/util/subprocess.cpp b/libopenage/util/subprocess.cpp index 98ae6720eb..c9a7670159 100644 --- a/libopenage/util/subprocess.cpp +++ b/libopenage/util/subprocess.cpp @@ -1,19 +1,19 @@ -// Copyright 2014-2019 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #include "subprocess.h" +#include #include #include -#include #ifdef _WIN32 // TODO not yet implemented #else -#include -#include -#include -#include -#include + #include + #include + #include + #include + #include #endif #include "../log/log.h" @@ -90,7 +90,7 @@ int call(const std::vector &argv, bool wait, const char *redirect_ if (redirect_stdout_to != nullptr) { replacement_stdout_fd = open( redirect_stdout_to, - O_WRONLY | O_CREAT|O_TRUNC|O_CLOEXEC, + O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); if (replacement_stdout_fd < 0) { @@ -215,8 +215,8 @@ int call(const std::vector &argv, bool wait, const char *redirect_ } if (child_errno > 0) { - log::log(MSG(err) << "execv has failed: " << strerror(child_errno)); - return -1; + log::log(MSG(err) << "execv has failed: " << strerror(child_errno)); + return -1; } } @@ -235,7 +235,7 @@ int call(const std::vector &argv, bool wait, const char *redirect_ // everything went well. return status; - #endif +#endif } } // namespace openage::subprocess diff --git a/libopenage/util/thread_id.cpp b/libopenage/util/thread_id.cpp index 0bcfc0abc9..3a178c441d 100644 --- a/libopenage/util/thread_id.cpp +++ b/libopenage/util/thread_id.cpp @@ -1,13 +1,13 @@ -// Copyright 2015-2018 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "thread_id.h" #include "config.h" #if HAVE_THREAD_LOCAL_STORAGE -#include + #include #else -#include + #include #endif namespace openage { @@ -23,9 +23,8 @@ namespace util { */ class ThreadIdSupplier { public: - ThreadIdSupplier() - : - val{ThreadIdSupplier::counting_id++} {} + ThreadIdSupplier() : + val{ThreadIdSupplier::counting_id++} {} const size_t val; @@ -42,12 +41,13 @@ std::atomic ThreadIdSupplier::counting_id{0}; #endif size_t get_current_thread_id() { - #if HAVE_THREAD_LOCAL_STORAGE +#if HAVE_THREAD_LOCAL_STORAGE static thread_local ThreadIdSupplier current_thread_id; return current_thread_id.val; - #else +#else return std::hash()(std::this_thread::get_id()); - #endif +#endif } -}} // namespace openage::util +} // namespace util +} // namespace openage diff --git a/libopenage/versions/versions.cpp b/libopenage/versions/versions.cpp index d2e1fd251a..29655b6db9 100644 --- a/libopenage/versions/versions.cpp +++ b/libopenage/versions/versions.cpp @@ -1,9 +1,9 @@ -// Copyright 2020-2023 the openage authors. See copying.md for legal info. +// Copyright 2020-2024 the openage authors. See copying.md for legal info. #include "versions.h" #ifdef __linux__ -#include + #include #endif #include From 65d2f7a827becf05b3b8fe83be206ae86772fb62 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 31 Oct 2024 22:48:09 +0100 Subject: [PATCH 589/771] coord: Make comparisons not ambiguous. --- libopenage/coord/coord.h.template | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/libopenage/coord/coord.h.template b/libopenage/coord/coord.h.template index 8aeafadc5f..278b486ae4 100644 --- a/libopenage/coord/coord.h.template +++ b/libopenage/coord/coord.h.template @@ -74,12 +74,8 @@ struct Coord${camelcase}Absolute { return static_cast(*this); } - constexpr bool operator ==(const Absolute &other) const { - return ${formatted_members("(this->{0} == other.{0})", join_with=" && ")}; - } - - constexpr bool operator !=(const Absolute &other) const { - return !(*this == other); + friend constexpr bool operator ==(const Absolute &lhs, const Absolute &rhs) { + return ${formatted_members("(lhs.{0} == rhs.{0})", join_with=" && ")}; } }; @@ -167,12 +163,8 @@ struct Coord${camelcase}Relative { return static_cast(*this); } - constexpr bool operator ==(const Relative &other) const { - return ${formatted_members("(this->{0} == other.{0})", join_with=" && ")}; - } - - constexpr bool operator !=(const Relative &other) const { - return !(*this == other); + friend constexpr bool operator ==(const Relative &lhs, const Relative &rhs) { + return ${formatted_members("(lhs.{0} == rhs.{0})", join_with=" && ")}; } }; From 4f5308435bbbd56256e4f9b243b009a2bd82eaeb Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Wed, 6 Nov 2024 18:08:20 +0000 Subject: [PATCH 590/771] includes DbgHelp.h after windows.h to prevent errors --- libopenage/util/compiler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/util/compiler.cpp b/libopenage/util/compiler.cpp index 082240a704..2fd850920d 100644 --- a/libopenage/util/compiler.cpp +++ b/libopenage/util/compiler.cpp @@ -7,8 +7,8 @@ #include #else #define WIN32_LEAN_AND_MEAN - #include #include + #include #endif #include "strings.h" From 73f06b74db74180cb40e47e6964ded7b5b991565 Mon Sep 17 00:00:00 2001 From: edvin Date: Wed, 25 Sep 2024 15:10:02 +0200 Subject: [PATCH 591/771] Renderer: Added camera boundaries to CameraManager and clamping to the camera movement. --- libopenage/renderer/camera/camera.cpp | 30 ++++++++++++++++--- libopenage/renderer/camera/camera.h | 9 ++++-- libopenage/renderer/demo/demo_5.cpp | 10 ++++--- libopenage/renderer/demo/demo_6.cpp | 10 ++++--- libopenage/renderer/demo/demo_6.h | 4 +++ libopenage/renderer/stages/camera/manager.cpp | 14 +++++---- libopenage/renderer/stages/camera/manager.h | 12 ++++++++ 7 files changed, 70 insertions(+), 19 deletions(-) diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index f70efbee64..c2aa74cbe4 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -102,15 +102,37 @@ void Camera::look_at_coord(coord::scene3 coord_pos) { this->look_at_scene(scene_pos); } -void Camera::move_to(Eigen::Vector3f scene_pos) { - // TODO: Check and set bounds for where the camera can go and check them here +void Camera::move_to(Eigen::Vector3f scene_pos, + std::optional> x_bounds, + std::optional> z_bounds) { + // Calculate look at coordinates using same method as in look_at_scene(..) + auto y_delta = this->scene_pos[1] - scene_pos[1]; + auto xz_distance = y_delta * std::numbers::sqrt3; + + float side_length = xz_distance / std::numbers::sqrt2; + scene_pos[0] += side_length; + scene_pos[2] += side_length; + + // Clamp the x coordinate if x_bounds are provided + if (x_bounds.has_value()) { + scene_pos[0] = std::clamp(scene_pos[0], x_bounds->first, x_bounds->second); + } + + // Clamp the z coordinate if z_bounds are provided + if (z_bounds.has_value()) { + scene_pos[2] = std::clamp(scene_pos[2], z_bounds->first, z_bounds->second); + } this->scene_pos = scene_pos; this->moved = true; } -void Camera::move_rel(Eigen::Vector3f direction, float delta) { - this->move_to(this->scene_pos + (direction * delta)); +void Camera::move_rel(Eigen::Vector3f direction, + std::pair x_bounds, + std::pair z_bounds, + float delta) { + + this->move_to(this->scene_pos + (direction * delta), x_bounds, z_bounds); } void Camera::set_zoom(float zoom) { diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index eccb5b40e6..629b04dbe3 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -15,6 +15,7 @@ #include "renderer/camera/definitions.h" #include "renderer/camera/frustum_2d.h" #include "renderer/camera/frustum_3d.h" +#include namespace openage::renderer { @@ -84,7 +85,9 @@ class Camera { * * @param scene_pos New 3D position of the camera in the scene. */ - void move_to(Eigen::Vector3f scene_pos); + void move_to(Eigen::Vector3f scene_pos, std::optional> x_bounds = std::nullopt, + std::optional> z_bounds = std::nullopt); + /** * Move the camera position in the direction of a given vector. @@ -94,7 +97,9 @@ class Camera { * value is multiplied with the directional vector before its applied to * the positional vector. */ - void move_rel(Eigen::Vector3f direction, float delta = 1.0f); + void move_rel(Eigen::Vector3f direction, std::pair x_bounds, + std::pair z_bounds, + float delta = 1.0f); /** * Set the zoom level of the camera. Values smaller than 1.0f let the diff --git a/libopenage/renderer/demo/demo_5.cpp b/libopenage/renderer/demo/demo_5.cpp index 85752db18c..465e61921a 100644 --- a/libopenage/renderer/demo/demo_5.cpp +++ b/libopenage/renderer/demo/demo_5.cpp @@ -17,6 +17,7 @@ #include "renderer/shader_program.h" #include "renderer/uniform_buffer.h" #include "renderer/uniform_input.h" +#include "renderer/stages/camera/manager.h" namespace openage::renderer::tests { @@ -38,6 +39,7 @@ void renderer_demo_5(const util::Path &path) { camera->resize(w, h); }); + camera::CameraManager camera_manager(camera); /* Display the subtextures using the meta information */ log::log(INFO << "Loading shaders..."); @@ -142,7 +144,7 @@ void renderer_demo_5(const util::Path &path) { switch (key) { case Qt::Key_W: { // forward - camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), 0.5f); + camera_manager.move_frame(camera::MoveDirection::FORWARD, 0.5f); cam_update = true; log::log(INFO << "Camera moved forward."); @@ -150,13 +152,13 @@ void renderer_demo_5(const util::Path &path) { case Qt::Key_A: { // left // half the speed because the relationship between forward/back and // left/right is 1:2 in our ortho projection. - camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), 0.25f); + camera_manager.move_frame(camera::MoveDirection::LEFT, 0.25f); cam_update = true; log::log(INFO << "Camera moved left."); } break; case Qt::Key_S: { // back - camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), 0.5f); + camera_manager.move_frame(camera::MoveDirection::BACKWARD, 0.5f); cam_update = true; log::log(INFO << "Camera moved back."); @@ -164,7 +166,7 @@ void renderer_demo_5(const util::Path &path) { case Qt::Key_D: { // right // half the speed because the relationship between forward/back and // left/right is 1:2 in our ortho projection. - camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), 0.25f); + camera_manager.move_frame(camera::MoveDirection::RIGHT, 0.25f); cam_update = true; log::log(INFO << "Camera moved right."); diff --git a/libopenage/renderer/demo/demo_6.cpp b/libopenage/renderer/demo/demo_6.cpp index 9844503e64..96f894d69d 100644 --- a/libopenage/renderer/demo/demo_6.cpp +++ b/libopenage/renderer/demo/demo_6.cpp @@ -23,6 +23,7 @@ #include "renderer/shader_program.h" #include "renderer/texture.h" #include "renderer/uniform_buffer.h" +#include "renderer/stages/camera/manager.h" #include "time/clock.h" #include "util/path.h" #include "util/vector.h" @@ -52,16 +53,16 @@ void renderer_demo_6(const util::Path &path) { // move_frame moves the camera in the specified direction in the next drawn frame switch (key) { case Qt::Key_W: { // forward - render_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), 0.2f); + render_mgr.camera_manager->move_frame(camera::MoveDirection::FORWARD, 0.2f); } break; case Qt::Key_A: { // left - render_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), 0.1f); + render_mgr.camera_manager->move_frame(camera::MoveDirection::LEFT, 0.1f); } break; case Qt::Key_S: { // back - render_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), 0.2f); + render_mgr.camera_manager->move_frame(camera::MoveDirection::BACKWARD, 0.2f); } break; case Qt::Key_D: { // right - render_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), 0.1f); + render_mgr.camera_manager->move_frame(camera::MoveDirection::RIGHT, 0.1f); } break; default: break; @@ -438,6 +439,7 @@ void RenderManagerDemo6::create_camera() { 1.0f / static_cast(viewport_size[1])}; camera_unifs->update("inv_viewport_size", viewport_size_vec); this->camera->get_uniform_buffer()->update_uniforms(camera_unifs); + this->camera_manager = std::make_shared(camera); } void RenderManagerDemo6::create_render_passes() { diff --git a/libopenage/renderer/demo/demo_6.h b/libopenage/renderer/demo/demo_6.h index f86728819b..279276122a 100644 --- a/libopenage/renderer/demo/demo_6.h +++ b/libopenage/renderer/demo/demo_6.h @@ -19,6 +19,7 @@ class Texture2d; namespace camera { class Camera; +class CameraManager; } namespace gui { @@ -72,6 +73,9 @@ class RenderManagerDemo6 { /// Camera std::shared_ptr camera; + /// Camera manager + std::shared_ptr camera_manager; + /// Render passes std::shared_ptr obj_2d_pass; std::shared_ptr obj_3d_pass; diff --git a/libopenage/renderer/stages/camera/manager.cpp b/libopenage/renderer/stages/camera/manager.cpp index 39eddd2b52..4d96d36af9 100644 --- a/libopenage/renderer/stages/camera/manager.cpp +++ b/libopenage/renderer/stages/camera/manager.cpp @@ -3,6 +3,7 @@ #include "manager.h" #include +#include #include "renderer/camera/camera.h" #include "renderer/uniform_buffer.h" @@ -21,6 +22,9 @@ CameraManager::CameraManager(const std::shared_ptr &ca camera->get_view_matrix(), "proj", camera->get_projection_matrix()); + + x_bounds = std::make_pair(XMIN, XMAX); + z_bounds = std::make_pair(ZMIN, ZMAX); } void CameraManager::update() { @@ -33,18 +37,18 @@ void CameraManager::move_frame(MoveDirection direction, float speed) { case MoveDirection::LEFT: // half the speed because the relationship between forward/back and // left/right is 1:2 in our ortho projection. - this->camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), speed / 2); + this->camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), this->x_bounds, this->z_bounds, speed / 2); break; case MoveDirection::RIGHT: // half the speed because the relationship between forward/back and // left/right is 1:2 in our ortho projection. - this->camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), speed / 2); + this->camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), this->x_bounds, this->z_bounds, speed / 2); break; case MoveDirection::FORWARD: - this->camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), speed); + this->camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), this->x_bounds, this->z_bounds, speed); break; case MoveDirection::BACKWARD: - this->camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), speed); + this->camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), this->x_bounds, this->z_bounds, speed); break; default: @@ -83,7 +87,7 @@ void CameraManager::update_motion() { move_dir += Eigen::Vector3f(1.0f, 0.0f, 1.0f); } - this->camera->move_rel(move_dir, this->move_motion_speed); + this->camera->move_rel(move_dir, this->x_bounds, this->z_bounds, this->move_motion_speed); } if (this->zoom_motion_direction != static_cast(ZoomDirection::NONE)) { diff --git a/libopenage/renderer/stages/camera/manager.h b/libopenage/renderer/stages/camera/manager.h index 13d876b25f..73bb06a506 100644 --- a/libopenage/renderer/stages/camera/manager.h +++ b/libopenage/renderer/stages/camera/manager.h @@ -3,6 +3,7 @@ #pragma once #include +#include namespace openage::renderer { @@ -143,6 +144,17 @@ class CameraManager { * Uniform buffer input for the camera. */ std::shared_ptr uniforms; + + /** + * x and z bounds for the camera. + */ + std::pair x_bounds, z_bounds; + + /** + * Constant values for the camera bounds. + * TODO: Make boundaries dynamic based on map size. + */ + const float XMIN = 12.25f, XMAX = 32.25f, ZMIN = -8.25f, ZMAX = 12.25f; }; } // namespace camera From 85918c944f40456f9365510515b45dd14774e848 Mon Sep 17 00:00:00 2001 From: edvin Date: Fri, 18 Oct 2024 14:27:37 +0200 Subject: [PATCH 592/771] Refactoring and documentation. --- libopenage/renderer/camera/camera.cpp | 105 ++++++++---------- libopenage/renderer/camera/camera.h | 38 ++++++- libopenage/renderer/camera/definitions.h | 6 + libopenage/renderer/demo/demo_5.cpp | 10 +- libopenage/renderer/demo/demo_6.cpp | 10 +- libopenage/renderer/stages/camera/manager.cpp | 17 ++- libopenage/renderer/stages/camera/manager.h | 11 +- 7 files changed, 104 insertions(+), 93 deletions(-) diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index c2aa74cbe4..2ea7adc4c9 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -61,38 +61,7 @@ Camera::Camera(const std::shared_ptr &renderer, } void Camera::look_at_scene(Eigen::Vector3f scene_pos) { - if (scene_pos[1] > this->scene_pos[1]) { - // TODO: camera can't look at a position that's - // higher than it's own position - } - - // TODO: Although the below method should be faster, calculating and adding the direction - // vector from scene_pos to new_pos may be easier to understand - // i.e. new_pos = scene_pos + b/sin(30) * direction_vec - - // due to the fixed angle, the centered scene position - // and the new camera position form a right triangle. - // - // c - + new camera pos - // - |b - // center +------+ - // a - // - // we can calculate the new camera position via the offset a - // using the angle and length of side b. - auto y_delta = this->scene_pos[1] - scene_pos[1]; // b (vertical distance) - auto xz_distance = y_delta * std::numbers::sqrt3; // a (horizontal distance); a = b * (cos(30°) / sin(30°)) - - // get x and z offsets - // the camera is pointed diagonally to the negative x and z axis - // a is the length of the diagonal from camera.xz to scene_pos.xz - // so the x and z offest are sides of a square with the same diagonal - auto side_length = xz_distance / std::numbers::sqrt2; - auto new_pos = Eigen::Vector3f( - scene_pos[0] + side_length, - this->scene_pos[1], // height unchanged - scene_pos[2] + side_length); - + auto new_pos = calc_look_at(scene_pos); this->move_to(new_pos); } @@ -102,37 +71,22 @@ void Camera::look_at_coord(coord::scene3 coord_pos) { this->look_at_scene(scene_pos); } -void Camera::move_to(Eigen::Vector3f scene_pos, - std::optional> x_bounds, - std::optional> z_bounds) { - // Calculate look at coordinates using same method as in look_at_scene(..) - auto y_delta = this->scene_pos[1] - scene_pos[1]; - auto xz_distance = y_delta * std::numbers::sqrt3; - - float side_length = xz_distance / std::numbers::sqrt2; - scene_pos[0] += side_length; - scene_pos[2] += side_length; - - // Clamp the x coordinate if x_bounds are provided - if (x_bounds.has_value()) { - scene_pos[0] = std::clamp(scene_pos[0], x_bounds->first, x_bounds->second); - } - - // Clamp the z coordinate if z_bounds are provided - if (z_bounds.has_value()) { - scene_pos[2] = std::clamp(scene_pos[2], z_bounds->first, z_bounds->second); - } - +void Camera::move_to(Eigen::Vector3f scene_pos) { this->scene_pos = scene_pos; this->moved = true; } -void Camera::move_rel(Eigen::Vector3f direction, - std::pair x_bounds, - std::pair z_bounds, - float delta) { +void Camera::move_rel(Eigen::Vector3f direction, float delta) { + this->move_to(this->scene_pos + (direction * delta)); +} + +void Camera::move_rel(Eigen::Vector3f direction, float delta, struct CameraBoundaries camera_boundaries) { + auto new_pos = calc_look_at(this->scene_pos + (direction * delta)); + + new_pos[0] = std::clamp(new_pos[0], camera_boundaries.x_min, camera_boundaries.x_max); + new_pos[2] = std::clamp(new_pos[2], camera_boundaries.z_min, camera_boundaries.z_max); - this->move_to(this->scene_pos + (direction * delta), x_bounds, z_bounds); + this->move_to(new_pos); } void Camera::set_zoom(float zoom) { @@ -314,8 +268,43 @@ void Camera::init_uniform_buffer(const std::shared_ptr &renderer) { this->uniform_buffer = renderer->add_uniform_buffer(ubo_info); } +Eigen::Vector3f Camera::calc_look_at(Eigen::Vector3f target) { + if (target[1] > this->scene_pos[1]) { + // TODO: camera can't look at a position that's + // higher than it's own position + } + + // TODO: Although the below method should be faster, calculating and adding the direction + // vector from scene_pos to new_pos may be easier to understand + // i.e. new_pos = scene_pos + b/sin(30) * direction_vec + + // due to the fixed angle, the centered scene position + // and the new camera position form a right triangle. + // + // c - + new camera pos + // - |b + // center +------+ + // a + // + // we can calculate the new camera position via the offset a + // using the angle and length of side b. + auto y_delta = this->scene_pos[1] - target[1]; // b (vertical distance) + auto xz_distance = y_delta * std::numbers::sqrt3; // a (horizontal distance); a = b * (cos(30°) / sin(30°)) + + // get x and z offsets + // the camera is pointed diagonally to the negative x and z axis + // a is the length of the diagonal from camera.xz to scene_pos.xz + // so the x and z offest are sides of a square with the same diagonal + auto side_length = xz_distance / std::numbers::sqrt2; + return Eigen::Vector3f( + target[0] + side_length, + this->scene_pos[1], // height unchanged + target[2] + side_length); +} + inline float Camera::get_real_zoom_factor() const { return 0.5f * this->default_zoom_ratio * this->zoom; } + } // namespace openage::renderer::camera diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index 629b04dbe3..1ba0e3bdfb 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -4,7 +4,9 @@ #include #include +#include #include +#include #include @@ -15,7 +17,6 @@ #include "renderer/camera/definitions.h" #include "renderer/camera/frustum_2d.h" #include "renderer/camera/frustum_3d.h" -#include namespace openage::renderer { @@ -24,6 +25,13 @@ class UniformBuffer; namespace camera { +/** + * Defines constant boundaries for the camera's view in the X and Z axes. + */ +struct CameraBoundaries { + const float x_min, x_max, z_min, z_max; +}; + /** * Camera for selecting what part of the ingame world is displayed. * @@ -85,8 +93,7 @@ class Camera { * * @param scene_pos New 3D position of the camera in the scene. */ - void move_to(Eigen::Vector3f scene_pos, std::optional> x_bounds = std::nullopt, - std::optional> z_bounds = std::nullopt); + void move_to(Eigen::Vector3f scene_pos); /** @@ -97,9 +104,20 @@ class Camera { * value is multiplied with the directional vector before its applied to * the positional vector. */ - void move_rel(Eigen::Vector3f direction, std::pair x_bounds, - std::pair z_bounds, - float delta = 1.0f); + void move_rel(Eigen::Vector3f direction, float delta = 1.0f); + + + /** + * Move the camera position in the direction of a given vector taking the + * camera boundaries into account. + * + * @param direction Direction vector. Added to the current position. + * @param delta Delta for controlling the amount by which the camera is moved. The + * value is multiplied with the directional vector before its applied to + * the positional vector. + * @param camera_boundaries X and Z boundaries for the camera in the scene. + */ + void move_rel(Eigen::Vector3f direction, float delta, struct CameraBoundaries camera_boundaries); /** * Set the zoom level of the camera. Values smaller than 1.0f let the @@ -205,6 +223,7 @@ class Camera { */ const Frustum3d get_frustum_3d() const; + private: /** * Create the uniform buffer for the camera. @@ -213,6 +232,13 @@ class Camera { */ void init_uniform_buffer(const std::shared_ptr &renderer); + /** + * Calculates the camera's position needed to center its view on the given target. + * + * @param target The target position in the 3D scene the camera should focus on. + */ + Eigen::Vector3f calc_look_at(Eigen::Vector3f target); + /** * Get the zoom factor applied to the camera projection. * diff --git a/libopenage/renderer/camera/definitions.h b/libopenage/renderer/camera/definitions.h index 9586239a2f..1a35c7fe5e 100644 --- a/libopenage/renderer/camera/definitions.h +++ b/libopenage/renderer/camera/definitions.h @@ -58,4 +58,10 @@ static constexpr float DEFAULT_MAX_ZOOM_OUT = 64.0f; */ static constexpr float DEFAULT_ZOOM_RATIO = 1.0f / 49; +/** + * Constant values for the camera bounds. + * TODO: Make boundaries dynamic based on map size. + */ +static const float X_MIN = 12.25f, X_MAX = 32.25f, Z_MIN = -8.25f, Z_MAX = 12.25f; + } // namespace openage::renderer::camera diff --git a/libopenage/renderer/demo/demo_5.cpp b/libopenage/renderer/demo/demo_5.cpp index 465e61921a..85752db18c 100644 --- a/libopenage/renderer/demo/demo_5.cpp +++ b/libopenage/renderer/demo/demo_5.cpp @@ -17,7 +17,6 @@ #include "renderer/shader_program.h" #include "renderer/uniform_buffer.h" #include "renderer/uniform_input.h" -#include "renderer/stages/camera/manager.h" namespace openage::renderer::tests { @@ -39,7 +38,6 @@ void renderer_demo_5(const util::Path &path) { camera->resize(w, h); }); - camera::CameraManager camera_manager(camera); /* Display the subtextures using the meta information */ log::log(INFO << "Loading shaders..."); @@ -144,7 +142,7 @@ void renderer_demo_5(const util::Path &path) { switch (key) { case Qt::Key_W: { // forward - camera_manager.move_frame(camera::MoveDirection::FORWARD, 0.5f); + camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), 0.5f); cam_update = true; log::log(INFO << "Camera moved forward."); @@ -152,13 +150,13 @@ void renderer_demo_5(const util::Path &path) { case Qt::Key_A: { // left // half the speed because the relationship between forward/back and // left/right is 1:2 in our ortho projection. - camera_manager.move_frame(camera::MoveDirection::LEFT, 0.25f); + camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), 0.25f); cam_update = true; log::log(INFO << "Camera moved left."); } break; case Qt::Key_S: { // back - camera_manager.move_frame(camera::MoveDirection::BACKWARD, 0.5f); + camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), 0.5f); cam_update = true; log::log(INFO << "Camera moved back."); @@ -166,7 +164,7 @@ void renderer_demo_5(const util::Path &path) { case Qt::Key_D: { // right // half the speed because the relationship between forward/back and // left/right is 1:2 in our ortho projection. - camera_manager.move_frame(camera::MoveDirection::RIGHT, 0.25f); + camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), 0.25f); cam_update = true; log::log(INFO << "Camera moved right."); diff --git a/libopenage/renderer/demo/demo_6.cpp b/libopenage/renderer/demo/demo_6.cpp index 96f894d69d..9844503e64 100644 --- a/libopenage/renderer/demo/demo_6.cpp +++ b/libopenage/renderer/demo/demo_6.cpp @@ -23,7 +23,6 @@ #include "renderer/shader_program.h" #include "renderer/texture.h" #include "renderer/uniform_buffer.h" -#include "renderer/stages/camera/manager.h" #include "time/clock.h" #include "util/path.h" #include "util/vector.h" @@ -53,16 +52,16 @@ void renderer_demo_6(const util::Path &path) { // move_frame moves the camera in the specified direction in the next drawn frame switch (key) { case Qt::Key_W: { // forward - render_mgr.camera_manager->move_frame(camera::MoveDirection::FORWARD, 0.2f); + render_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), 0.2f); } break; case Qt::Key_A: { // left - render_mgr.camera_manager->move_frame(camera::MoveDirection::LEFT, 0.1f); + render_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), 0.1f); } break; case Qt::Key_S: { // back - render_mgr.camera_manager->move_frame(camera::MoveDirection::BACKWARD, 0.2f); + render_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), 0.2f); } break; case Qt::Key_D: { // right - render_mgr.camera_manager->move_frame(camera::MoveDirection::RIGHT, 0.1f); + render_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), 0.1f); } break; default: break; @@ -439,7 +438,6 @@ void RenderManagerDemo6::create_camera() { 1.0f / static_cast(viewport_size[1])}; camera_unifs->update("inv_viewport_size", viewport_size_vec); this->camera->get_uniform_buffer()->update_uniforms(camera_unifs); - this->camera_manager = std::make_shared(camera); } void RenderManagerDemo6::create_render_passes() { diff --git a/libopenage/renderer/stages/camera/manager.cpp b/libopenage/renderer/stages/camera/manager.cpp index 4d96d36af9..380657b6db 100644 --- a/libopenage/renderer/stages/camera/manager.cpp +++ b/libopenage/renderer/stages/camera/manager.cpp @@ -5,7 +5,6 @@ #include #include -#include "renderer/camera/camera.h" #include "renderer/uniform_buffer.h" #include "renderer/uniform_input.h" @@ -16,15 +15,13 @@ CameraManager::CameraManager(const std::shared_ptr &ca move_motion_directions{static_cast(MoveDirection::NONE)}, zoom_motion_direction{static_cast(ZoomDirection::NONE)}, move_motion_speed{0.2f}, - zoom_motion_speed{0.05f} { + zoom_motion_speed{0.05f}, + camera_boundaries(CameraBoundaries(X_MIN, X_MAX, Z_MIN, Z_MAX)) { this->uniforms = this->camera->get_uniform_buffer()->new_uniform_input( "view", camera->get_view_matrix(), "proj", camera->get_projection_matrix()); - - x_bounds = std::make_pair(XMIN, XMAX); - z_bounds = std::make_pair(ZMIN, ZMAX); } void CameraManager::update() { @@ -37,18 +34,18 @@ void CameraManager::move_frame(MoveDirection direction, float speed) { case MoveDirection::LEFT: // half the speed because the relationship between forward/back and // left/right is 1:2 in our ortho projection. - this->camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), this->x_bounds, this->z_bounds, speed / 2); + this->camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), speed / 2, this->camera_boundaries); break; case MoveDirection::RIGHT: // half the speed because the relationship between forward/back and // left/right is 1:2 in our ortho projection. - this->camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), this->x_bounds, this->z_bounds, speed / 2); + this->camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), speed / 2, this->camera_boundaries); break; case MoveDirection::FORWARD: - this->camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), this->x_bounds, this->z_bounds, speed); + this->camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), speed, this->camera_boundaries); break; case MoveDirection::BACKWARD: - this->camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), this->x_bounds, this->z_bounds, speed); + this->camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), speed, this->camera_boundaries); break; default: @@ -87,7 +84,7 @@ void CameraManager::update_motion() { move_dir += Eigen::Vector3f(1.0f, 0.0f, 1.0f); } - this->camera->move_rel(move_dir, this->x_bounds, this->z_bounds, this->move_motion_speed); + this->camera->move_rel(move_dir, this->move_motion_speed, this->camera_boundaries); } if (this->zoom_motion_direction != static_cast(ZoomDirection::NONE)) { diff --git a/libopenage/renderer/stages/camera/manager.h b/libopenage/renderer/stages/camera/manager.h index 73bb06a506..22773a3b25 100644 --- a/libopenage/renderer/stages/camera/manager.h +++ b/libopenage/renderer/stages/camera/manager.h @@ -5,6 +5,7 @@ #include #include +#include "renderer/camera/camera.h" namespace openage::renderer { class UniformBufferInput; @@ -12,6 +13,7 @@ class UniformBufferInput; namespace camera { class Camera; +struct CameraBoundaries; enum class MoveDirection { NONE = 0x0000, @@ -146,15 +148,10 @@ class CameraManager { std::shared_ptr uniforms; /** - * x and z bounds for the camera. + * Camera boundaries for X and Z movement. Contains minimum and maximum values for each axes. */ - std::pair x_bounds, z_bounds; + struct CameraBoundaries camera_boundaries; - /** - * Constant values for the camera bounds. - * TODO: Make boundaries dynamic based on map size. - */ - const float XMIN = 12.25f, XMAX = 32.25f, ZMIN = -8.25f, ZMAX = 12.25f; }; } // namespace camera From b907a7dae80a74c7b19d68b4e25800a4daaf30ca Mon Sep 17 00:00:00 2001 From: edvin Date: Fri, 18 Oct 2024 14:55:44 +0200 Subject: [PATCH 593/771] Added constructor for CameraBoundaries. --- libopenage/renderer/camera/camera.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index 1ba0e3bdfb..c477c184a2 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -30,6 +30,9 @@ namespace camera { */ struct CameraBoundaries { const float x_min, x_max, z_min, z_max; + + CameraBoundaries(float x_min, float x_max, float z_min, float z_max) + : x_min(x_min), x_max(x_max), z_min(z_min), z_max(z_max) {} }; /** From 780949f52f599e98dbb70975acce39e9dd5e26c6 Mon Sep 17 00:00:00 2001 From: edvin Date: Sun, 20 Oct 2024 15:51:48 +0200 Subject: [PATCH 594/771] Struct refactoring --- libopenage/renderer/camera/camera.cpp | 2 +- libopenage/renderer/camera/camera.h | 7 ++----- libopenage/renderer/stages/camera/manager.cpp | 2 +- libopenage/renderer/stages/camera/manager.h | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index 2ea7adc4c9..ec5a926594 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -80,7 +80,7 @@ void Camera::move_rel(Eigen::Vector3f direction, float delta) { this->move_to(this->scene_pos + (direction * delta)); } -void Camera::move_rel(Eigen::Vector3f direction, float delta, struct CameraBoundaries camera_boundaries) { +void Camera::move_rel(Eigen::Vector3f direction, float delta, CameraBoundaries camera_boundaries) { auto new_pos = calc_look_at(this->scene_pos + (direction * delta)); new_pos[0] = std::clamp(new_pos[0], camera_boundaries.x_min, camera_boundaries.x_max); diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index c477c184a2..132ee4ed81 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -29,10 +29,7 @@ namespace camera { * Defines constant boundaries for the camera's view in the X and Z axes. */ struct CameraBoundaries { - const float x_min, x_max, z_min, z_max; - - CameraBoundaries(float x_min, float x_max, float z_min, float z_max) - : x_min(x_min), x_max(x_max), z_min(z_min), z_max(z_max) {} + float x_min, x_max, z_min, z_max; }; /** @@ -120,7 +117,7 @@ class Camera { * the positional vector. * @param camera_boundaries X and Z boundaries for the camera in the scene. */ - void move_rel(Eigen::Vector3f direction, float delta, struct CameraBoundaries camera_boundaries); + void move_rel(Eigen::Vector3f direction, float delta, CameraBoundaries camera_boundaries); /** * Set the zoom level of the camera. Values smaller than 1.0f let the diff --git a/libopenage/renderer/stages/camera/manager.cpp b/libopenage/renderer/stages/camera/manager.cpp index 380657b6db..0574dcc47e 100644 --- a/libopenage/renderer/stages/camera/manager.cpp +++ b/libopenage/renderer/stages/camera/manager.cpp @@ -16,7 +16,7 @@ CameraManager::CameraManager(const std::shared_ptr &ca zoom_motion_direction{static_cast(ZoomDirection::NONE)}, move_motion_speed{0.2f}, zoom_motion_speed{0.05f}, - camera_boundaries(CameraBoundaries(X_MIN, X_MAX, Z_MIN, Z_MAX)) { + camera_boundaries({X_MIN, X_MAX, Z_MIN, Z_MAX}) { this->uniforms = this->camera->get_uniform_buffer()->new_uniform_input( "view", camera->get_view_matrix(), diff --git a/libopenage/renderer/stages/camera/manager.h b/libopenage/renderer/stages/camera/manager.h index 22773a3b25..d5dcf07b46 100644 --- a/libopenage/renderer/stages/camera/manager.h +++ b/libopenage/renderer/stages/camera/manager.h @@ -150,7 +150,7 @@ class CameraManager { /** * Camera boundaries for X and Z movement. Contains minimum and maximum values for each axes. */ - struct CameraBoundaries camera_boundaries; + CameraBoundaries camera_boundaries; }; From 15565029113a156b9f2d3fdd29281c312f44fa98 Mon Sep 17 00:00:00 2001 From: edvin Date: Thu, 24 Oct 2024 08:33:01 +0200 Subject: [PATCH 595/771] Refactoring and cleanup --- libopenage/renderer/camera/boundaries.h | 25 +++++++++++++++++ libopenage/renderer/camera/camera.cpp | 18 +++++-------- libopenage/renderer/camera/camera.h | 27 ++++--------------- libopenage/renderer/camera/definitions.h | 12 ++++++++- libopenage/renderer/demo/demo_6.h | 4 --- libopenage/renderer/stages/camera/manager.cpp | 2 +- libopenage/renderer/stages/camera/manager.h | 2 -- 7 files changed, 48 insertions(+), 42 deletions(-) create mode 100644 libopenage/renderer/camera/boundaries.h diff --git a/libopenage/renderer/camera/boundaries.h b/libopenage/renderer/camera/boundaries.h new file mode 100644 index 0000000000..9368815bef --- /dev/null +++ b/libopenage/renderer/camera/boundaries.h @@ -0,0 +1,25 @@ +// Copyright 2022-2024 the openage authors. See copying.md for legal info. + +#pragma once + +namespace openage::renderer::camera { + +/** + * Defines boundaries for the camera's view. + */ +struct CameraBoundaries { + // The minimum boundary for the camera's X-coordinate. + float x_min; + // The maximum boundary for the camera's X-coordinate. + float x_max; + // The minimum boundary for the camera's Y-coordinate. + float y_min; + // The maximum boundary for the camera's Y-coordinate. + float y_max; + // The minimum boundary for the camera's Z-coordinate. + float z_min; + // The maximum boundary for the camera's Z-coordinate. + float z_max; +}; + +} // namespace openage::renderer::camera diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index ec5a926594..a0321f110f 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -71,22 +71,16 @@ void Camera::look_at_coord(coord::scene3 coord_pos) { this->look_at_scene(scene_pos); } -void Camera::move_to(Eigen::Vector3f scene_pos) { +void Camera::move_to(Eigen::Vector3f scene_pos, const CameraBoundaries &camera_boundaries) { + scene_pos[0] = std::clamp(scene_pos[0], camera_boundaries.x_min, camera_boundaries.x_max); + scene_pos[2] = std::clamp(scene_pos[2], camera_boundaries.z_min, camera_boundaries.z_max); + this->scene_pos = scene_pos; this->moved = true; } -void Camera::move_rel(Eigen::Vector3f direction, float delta) { - this->move_to(this->scene_pos + (direction * delta)); -} - -void Camera::move_rel(Eigen::Vector3f direction, float delta, CameraBoundaries camera_boundaries) { - auto new_pos = calc_look_at(this->scene_pos + (direction * delta)); - - new_pos[0] = std::clamp(new_pos[0], camera_boundaries.x_min, camera_boundaries.x_max); - new_pos[2] = std::clamp(new_pos[2], camera_boundaries.z_min, camera_boundaries.z_max); - - this->move_to(new_pos); +void Camera::move_rel(Eigen::Vector3f direction, float delta, const CameraBoundaries &camera_boundaries) { + this->move_to(this->scene_pos + (direction * delta), camera_boundaries); } void Camera::set_zoom(float zoom) { diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index 132ee4ed81..5e5759c06e 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -14,6 +14,7 @@ #include "coord/scene.h" #include "util/vector.h" +#include "renderer/camera/boundaries.h" #include "renderer/camera/definitions.h" #include "renderer/camera/frustum_2d.h" #include "renderer/camera/frustum_3d.h" @@ -25,13 +26,6 @@ class UniformBuffer; namespace camera { -/** - * Defines constant boundaries for the camera's view in the X and Z axes. - */ -struct CameraBoundaries { - float x_min, x_max, z_min, z_max; -}; - /** * Camera for selecting what part of the ingame world is displayed. * @@ -92,20 +86,9 @@ class Camera { * Move the camera position in the direction of a given vector. * * @param scene_pos New 3D position of the camera in the scene. + * @param camera_boundaries 3D boundaries for the camera. */ - void move_to(Eigen::Vector3f scene_pos); - - - /** - * Move the camera position in the direction of a given vector. - * - * @param direction Direction vector. Added to the current position. - * @param delta Delta for controlling the amount by which the camera is moved. The - * value is multiplied with the directional vector before its applied to - * the positional vector. - */ - void move_rel(Eigen::Vector3f direction, float delta = 1.0f); - + void move_to(Eigen::Vector3f scene_pos, const CameraBoundaries &camera_boundaries = DEFAULT_CAM_BOUNDARIES); /** * Move the camera position in the direction of a given vector taking the @@ -115,9 +98,9 @@ class Camera { * @param delta Delta for controlling the amount by which the camera is moved. The * value is multiplied with the directional vector before its applied to * the positional vector. - * @param camera_boundaries X and Z boundaries for the camera in the scene. + * @param camera_boundaries 3D boundaries for the camera. */ - void move_rel(Eigen::Vector3f direction, float delta, CameraBoundaries camera_boundaries); + void move_rel(Eigen::Vector3f direction, float delta = 1.0f, const CameraBoundaries &camera_boundaries = DEFAULT_CAM_BOUNDARIES); /** * Set the zoom level of the camera. Values smaller than 1.0f let the diff --git a/libopenage/renderer/camera/definitions.h b/libopenage/renderer/camera/definitions.h index 1a35c7fe5e..21f9e43ed7 100644 --- a/libopenage/renderer/camera/definitions.h +++ b/libopenage/renderer/camera/definitions.h @@ -3,7 +3,9 @@ #pragma once #include +#include +#include "renderer/camera/boundaries.h" namespace openage::renderer::camera { @@ -58,10 +60,18 @@ static constexpr float DEFAULT_MAX_ZOOM_OUT = 64.0f; */ static constexpr float DEFAULT_ZOOM_RATIO = 1.0f / 49; +static constexpr CameraBoundaries DEFAULT_CAM_BOUNDARIES{ + std::numeric_limits::min(), + std::numeric_limits::max(), + std::numeric_limits::min(), + std::numeric_limits::max(), + std::numeric_limits::min(), + std::numeric_limits::max()}; + /** * Constant values for the camera bounds. * TODO: Make boundaries dynamic based on map size. */ -static const float X_MIN = 12.25f, X_MAX = 32.25f, Z_MIN = -8.25f, Z_MAX = 12.25f; +static constexpr float X_MIN = 12.25f, X_MAX = 32.25f, Z_MIN = -8.25f, Z_MAX = 12.25f; } // namespace openage::renderer::camera diff --git a/libopenage/renderer/demo/demo_6.h b/libopenage/renderer/demo/demo_6.h index 279276122a..f86728819b 100644 --- a/libopenage/renderer/demo/demo_6.h +++ b/libopenage/renderer/demo/demo_6.h @@ -19,7 +19,6 @@ class Texture2d; namespace camera { class Camera; -class CameraManager; } namespace gui { @@ -73,9 +72,6 @@ class RenderManagerDemo6 { /// Camera std::shared_ptr camera; - /// Camera manager - std::shared_ptr camera_manager; - /// Render passes std::shared_ptr obj_2d_pass; std::shared_ptr obj_3d_pass; diff --git a/libopenage/renderer/stages/camera/manager.cpp b/libopenage/renderer/stages/camera/manager.cpp index 0574dcc47e..88a5dea1ac 100644 --- a/libopenage/renderer/stages/camera/manager.cpp +++ b/libopenage/renderer/stages/camera/manager.cpp @@ -16,7 +16,7 @@ CameraManager::CameraManager(const std::shared_ptr &ca zoom_motion_direction{static_cast(ZoomDirection::NONE)}, move_motion_speed{0.2f}, zoom_motion_speed{0.05f}, - camera_boundaries({X_MIN, X_MAX, Z_MIN, Z_MAX}) { + camera_boundaries{X_MIN, X_MAX, 0.0f, 0.0f, Z_MIN, Z_MAX} { this->uniforms = this->camera->get_uniform_buffer()->new_uniform_input( "view", camera->get_view_matrix(), diff --git a/libopenage/renderer/stages/camera/manager.h b/libopenage/renderer/stages/camera/manager.h index d5dcf07b46..689f917416 100644 --- a/libopenage/renderer/stages/camera/manager.h +++ b/libopenage/renderer/stages/camera/manager.h @@ -13,7 +13,6 @@ class UniformBufferInput; namespace camera { class Camera; -struct CameraBoundaries; enum class MoveDirection { NONE = 0x0000, @@ -151,7 +150,6 @@ class CameraManager { * Camera boundaries for X and Z movement. Contains minimum and maximum values for each axes. */ CameraBoundaries camera_boundaries; - }; } // namespace camera From 0c129fb24428d19d23eb3a509f63d782dae368a8 Mon Sep 17 00:00:00 2001 From: edvin Date: Thu, 24 Oct 2024 08:59:48 +0200 Subject: [PATCH 596/771] Changed default minimum boundary to be largest negative value instead of smallest positive value. --- libopenage/renderer/camera/definitions.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libopenage/renderer/camera/definitions.h b/libopenage/renderer/camera/definitions.h index 21f9e43ed7..5a4d57c01a 100644 --- a/libopenage/renderer/camera/definitions.h +++ b/libopenage/renderer/camera/definitions.h @@ -61,11 +61,11 @@ static constexpr float DEFAULT_MAX_ZOOM_OUT = 64.0f; static constexpr float DEFAULT_ZOOM_RATIO = 1.0f / 49; static constexpr CameraBoundaries DEFAULT_CAM_BOUNDARIES{ - std::numeric_limits::min(), + std::numeric_limits::lowest(), std::numeric_limits::max(), - std::numeric_limits::min(), + std::numeric_limits::lowest(), std::numeric_limits::max(), - std::numeric_limits::min(), + std::numeric_limits::lowest(), std::numeric_limits::max()}; /** From 59cee6a417ebfa2b8e8b5bb3afae0bd053261860 Mon Sep 17 00:00:00 2001 From: edvin Date: Thu, 31 Oct 2024 09:00:31 +0100 Subject: [PATCH 597/771] Fixes for Y boundary --- libopenage/renderer/camera/boundaries.h | 12 ++++++------ libopenage/renderer/camera/camera.cpp | 1 + libopenage/renderer/camera/definitions.h | 2 +- libopenage/renderer/stages/camera/manager.cpp | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/libopenage/renderer/camera/boundaries.h b/libopenage/renderer/camera/boundaries.h index 9368815bef..63a63a7a39 100644 --- a/libopenage/renderer/camera/boundaries.h +++ b/libopenage/renderer/camera/boundaries.h @@ -8,17 +8,17 @@ namespace openage::renderer::camera { * Defines boundaries for the camera's view. */ struct CameraBoundaries { - // The minimum boundary for the camera's X-coordinate. + /// The minimum boundary for the camera's X-coordinate. float x_min; - // The maximum boundary for the camera's X-coordinate. + /// The maximum boundary for the camera's X-coordinate. float x_max; - // The minimum boundary for the camera's Y-coordinate. + /// The minimum boundary for the camera's Y-coordinate. float y_min; - // The maximum boundary for the camera's Y-coordinate. + /// The maximum boundary for the camera's Y-coordinate. float y_max; - // The minimum boundary for the camera's Z-coordinate. + /// The minimum boundary for the camera's Z-coordinate. float z_min; - // The maximum boundary for the camera's Z-coordinate. + /// The maximum boundary for the camera's Z-coordinate. float z_max; }; diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index a0321f110f..14504998e0 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -73,6 +73,7 @@ void Camera::look_at_coord(coord::scene3 coord_pos) { void Camera::move_to(Eigen::Vector3f scene_pos, const CameraBoundaries &camera_boundaries) { scene_pos[0] = std::clamp(scene_pos[0], camera_boundaries.x_min, camera_boundaries.x_max); + scene_pos[1] = std::clamp(scene_pos[1], camera_boundaries.y_min, camera_boundaries.y_max); scene_pos[2] = std::clamp(scene_pos[2], camera_boundaries.z_min, camera_boundaries.z_max); this->scene_pos = scene_pos; diff --git a/libopenage/renderer/camera/definitions.h b/libopenage/renderer/camera/definitions.h index 5a4d57c01a..4c801269c3 100644 --- a/libopenage/renderer/camera/definitions.h +++ b/libopenage/renderer/camera/definitions.h @@ -72,6 +72,6 @@ static constexpr CameraBoundaries DEFAULT_CAM_BOUNDARIES{ * Constant values for the camera bounds. * TODO: Make boundaries dynamic based on map size. */ -static constexpr float X_MIN = 12.25f, X_MAX = 32.25f, Z_MIN = -8.25f, Z_MAX = 12.25f; +static constexpr float X_MIN = 12.25f, X_MAX = 32.25f, Y_MIN = 0.0f, Y_MAX = 20.0f, Z_MIN = -8.25f, Z_MAX = 12.25f; } // namespace openage::renderer::camera diff --git a/libopenage/renderer/stages/camera/manager.cpp b/libopenage/renderer/stages/camera/manager.cpp index 88a5dea1ac..207fdab319 100644 --- a/libopenage/renderer/stages/camera/manager.cpp +++ b/libopenage/renderer/stages/camera/manager.cpp @@ -16,7 +16,7 @@ CameraManager::CameraManager(const std::shared_ptr &ca zoom_motion_direction{static_cast(ZoomDirection::NONE)}, move_motion_speed{0.2f}, zoom_motion_speed{0.05f}, - camera_boundaries{X_MIN, X_MAX, 0.0f, 0.0f, Z_MIN, Z_MAX} { + camera_boundaries{X_MIN, X_MAX, Y_MIN, Y_MAX, Z_MIN, Z_MAX} { this->uniforms = this->camera->get_uniform_buffer()->new_uniform_input( "view", camera->get_view_matrix(), From af804e676281365efea44d82893953a9c043dc68 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 7 Nov 2024 22:36:53 +0100 Subject: [PATCH 598/771] renderer: Create boundaries.cpp file for compilation checks. --- libopenage/renderer/camera/CMakeLists.txt | 1 + libopenage/renderer/camera/boundaries.cpp | 9 +++++++++ libopenage/renderer/camera/boundaries.h | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 libopenage/renderer/camera/boundaries.cpp diff --git a/libopenage/renderer/camera/CMakeLists.txt b/libopenage/renderer/camera/CMakeLists.txt index aea6391e7a..71de5c7bd1 100644 --- a/libopenage/renderer/camera/CMakeLists.txt +++ b/libopenage/renderer/camera/CMakeLists.txt @@ -1,4 +1,5 @@ add_sources(libopenage + boundaries.cpp camera.cpp definitions.cpp frustum_2d.cpp diff --git a/libopenage/renderer/camera/boundaries.cpp b/libopenage/renderer/camera/boundaries.cpp new file mode 100644 index 0000000000..ca77344c7e --- /dev/null +++ b/libopenage/renderer/camera/boundaries.cpp @@ -0,0 +1,9 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "boundaries.h" + + +namespace openage::renderer::camera { + + +} // namespace openage::renderer::camera diff --git a/libopenage/renderer/camera/boundaries.h b/libopenage/renderer/camera/boundaries.h index 63a63a7a39..484a398455 100644 --- a/libopenage/renderer/camera/boundaries.h +++ b/libopenage/renderer/camera/boundaries.h @@ -1,4 +1,4 @@ -// Copyright 2022-2024 the openage authors. See copying.md for legal info. +// Copyright 2024-2024 the openage authors. See copying.md for legal info. #pragma once From 179e1adfb7261983190a15673808fd0c9345ed19 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 7 Nov 2024 22:54:10 +0100 Subject: [PATCH 599/771] renderer: Add method to change camera boundaries in cam manager. --- libopenage/presenter/presenter.cpp | 10 ++++++++++ libopenage/renderer/demo/demo_3.cpp | 12 ++++++++++++ libopenage/renderer/stages/camera/manager.cpp | 9 +++++++-- libopenage/renderer/stages/camera/manager.h | 11 ++++++++++- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index 33c798bcef..ffb215135f 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -121,7 +121,17 @@ void Presenter::init_graphics(bool debug) { this->camera->resize(w, h); }); + // Camera manager this->camera_manager = std::make_shared(this->camera); + // TODO: Make boundaries dynamic based on map size. + this->camera_manager->set_camera_boundaries( + renderer::camera::CameraBoundaries{ + renderer::camera::X_MIN, + renderer::camera::X_MAX, + renderer::camera::Y_MIN, + renderer::camera::Y_MAX, + renderer::camera::Z_MIN, + renderer::camera::Z_MAX}); // Skybox this->skybox_renderer = std::make_shared( diff --git a/libopenage/renderer/demo/demo_3.cpp b/libopenage/renderer/demo/demo_3.cpp index 9c294285f9..7de6372303 100644 --- a/libopenage/renderer/demo/demo_3.cpp +++ b/libopenage/renderer/demo/demo_3.cpp @@ -52,6 +52,18 @@ void renderer_demo_3(const util::Path &path) { // it is updated each frame before the render stages auto cam_manager = std::make_shared(camera); + // Set boundaries for camera movement in the scene + // this restricts camera movement to the area defined by the boundaries + // i.e. the map terrain in this case + cam_manager->set_camera_boundaries( + camera::CameraBoundaries{ + 12.25f, + 22.25f, + 0.0f, + 20.0f, + 2.25f, + 12.25f}); + // Render stages // every stage use a different subrenderer that manages renderables, // shaders, textures & more. diff --git a/libopenage/renderer/stages/camera/manager.cpp b/libopenage/renderer/stages/camera/manager.cpp index 207fdab319..d3cbd3f3b2 100644 --- a/libopenage/renderer/stages/camera/manager.cpp +++ b/libopenage/renderer/stages/camera/manager.cpp @@ -10,13 +10,14 @@ namespace openage::renderer::camera { -CameraManager::CameraManager(const std::shared_ptr &camera) : +CameraManager::CameraManager(const std::shared_ptr &camera, + const CameraBoundaries &camera_boundaries) : camera{camera}, move_motion_directions{static_cast(MoveDirection::NONE)}, zoom_motion_direction{static_cast(ZoomDirection::NONE)}, move_motion_speed{0.2f}, zoom_motion_speed{0.05f}, - camera_boundaries{X_MIN, X_MAX, Y_MIN, Y_MAX, Z_MIN, Z_MAX} { + camera_boundaries{camera_boundaries} { this->uniforms = this->camera->get_uniform_buffer()->new_uniform_input( "view", camera->get_view_matrix(), @@ -67,6 +68,10 @@ void CameraManager::zoom_frame(ZoomDirection direction, float speed) { } } +void CameraManager::set_camera_boundaries(const CameraBoundaries &camera_boundaries) { + this->camera_boundaries = camera_boundaries; +} + void CameraManager::update_motion() { if (this->move_motion_directions != static_cast(MoveDirection::NONE)) { Eigen::Vector3f move_dir{0.0f, 0.0f, 0.0f}; diff --git a/libopenage/renderer/stages/camera/manager.h b/libopenage/renderer/stages/camera/manager.h index 689f917416..36d605e959 100644 --- a/libopenage/renderer/stages/camera/manager.h +++ b/libopenage/renderer/stages/camera/manager.h @@ -49,8 +49,10 @@ class CameraManager { * Create a new camera manager. * * @param camera Camera to manage. + * @param camera_boundaries Boundaries for the camera movement in the scene. */ - CameraManager(const std::shared_ptr &camera); + CameraManager(const std::shared_ptr &camera, + const CameraBoundaries &camera_boundaries = DEFAULT_CAM_BOUNDARIES); ~CameraManager() = default; /** @@ -105,6 +107,13 @@ class CameraManager { */ void set_zoom_motion_speed(float speed); + /** + * Set boundaries for camera movement in the scene. + * + * @param camera_boundaries XYZ boundaries for the camera movement. + */ + void set_camera_boundaries(const CameraBoundaries &camera_boundaries); + private: /** * Update the camera parameters. From 6d6a2dd168590ea1f4e04efd15af9e449a431e39 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 7 Nov 2024 22:59:58 +0100 Subject: [PATCH 600/771] renderer: Rename current hardcoded camera boundaries. --- libopenage/presenter/presenter.cpp | 12 ++++++------ libopenage/renderer/camera/definitions.h | 9 +++++++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index ffb215135f..a37362be95 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -126,12 +126,12 @@ void Presenter::init_graphics(bool debug) { // TODO: Make boundaries dynamic based on map size. this->camera_manager->set_camera_boundaries( renderer::camera::CameraBoundaries{ - renderer::camera::X_MIN, - renderer::camera::X_MAX, - renderer::camera::Y_MIN, - renderer::camera::Y_MAX, - renderer::camera::Z_MIN, - renderer::camera::Z_MAX}); + renderer::camera::X_BOUND_MIN, + renderer::camera::X_BOUND_MAX, + renderer::camera::Y_BOUND_MIN, + renderer::camera::Y_BOUND_MAX, + renderer::camera::Z_BOUND_MIN, + renderer::camera::Z_BOUND_MAX}); // Skybox this->skybox_renderer = std::make_shared( diff --git a/libopenage/renderer/camera/definitions.h b/libopenage/renderer/camera/definitions.h index 4c801269c3..7c3bbdd5d3 100644 --- a/libopenage/renderer/camera/definitions.h +++ b/libopenage/renderer/camera/definitions.h @@ -69,9 +69,14 @@ static constexpr CameraBoundaries DEFAULT_CAM_BOUNDARIES{ std::numeric_limits::max()}; /** - * Constant values for the camera bounds. + * Constant values for the camera bounds (based on current fix terrain grid of 20x20). * TODO: Make boundaries dynamic based on map size. */ -static constexpr float X_MIN = 12.25f, X_MAX = 32.25f, Y_MIN = 0.0f, Y_MAX = 20.0f, Z_MIN = -8.25f, Z_MAX = 12.25f; +static constexpr float X_BOUND_MIN = 12.25f; +static constexpr float X_BOUND_MAX = 32.25f; +static constexpr float Y_BOUND_MIN = 0.0f; +static constexpr float Y_BOUND_MAX = 20.0f; +static constexpr float Z_BOUND_MIN = -8.25f; +static constexpr float Z_BOUND_MAX = 12.25f; } // namespace openage::renderer::camera From a3376ca234d4101ff104d2921a9e582834372a08 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 7 Nov 2024 23:07:26 +0100 Subject: [PATCH 601/771] Include missing email for #1691 --- copying.md | 1 + 1 file changed, 1 insertion(+) diff --git a/copying.md b/copying.md index 1002c8321a..ec1ddd2a46 100644 --- a/copying.md +++ b/copying.md @@ -155,6 +155,7 @@ _the openage authors_ are: | Michael Seibt | RoboSchmied | github à roboschmie dawt de | | Nikhil Ghosh | NikhilGhosh75 | nghosh606 à gmail dawt com | | Edvin Lindholm | EdvinLndh | edvinlndh à gmail dawt com | +| Jeremiah Morgan | jere8184 | jeremiahmorgan dawt bham à outlook dawt com | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. From c8d6c1ddc33224c4a55d80a4d762b5040487d373 Mon Sep 17 00:00:00 2001 From: Tobias Alam Date: Thu, 7 Nov 2024 15:36:33 -0500 Subject: [PATCH 602/771] file system api c++: added support for creating temp. files --- libopenage/util/path.cpp | 11 +++++++++++ libopenage/util/path.h | 3 +-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/libopenage/util/path.cpp b/libopenage/util/path.cpp index ff23253533..34ed9adbba 100644 --- a/libopenage/util/path.cpp +++ b/libopenage/util/path.cpp @@ -3,6 +3,7 @@ #include "path.h" #include +#include #include "../error/error.h" #include "compiler.h" @@ -11,6 +12,7 @@ #include "fslike/python.h" #include "misc.h" #include "strings.h" +#include "file.h" namespace openage::util { @@ -392,5 +394,14 @@ std::string dirname(const std::string &fullpath) { } } +static File get_temp_file() { + std::FILE* tmp_file = std::tmpfile(); + int temp_fd = fileno(tmp_file); + std::string tf_path = "/proc/self/fd/" + std::to_string(temp_fd); + mode_t mode = 0777; + File file_wrapper = File(tf_path, mode); + return file_wrapper; +} + } // namespace openage::util diff --git a/libopenage/util/path.h b/libopenage/util/path.h index d16e1734c6..f9161343d2 100644 --- a/libopenage/util/path.h +++ b/libopenage/util/path.h @@ -96,7 +96,7 @@ class OAAPI Path { File open_rw() const; File open_a() const; File open_ar() const; - + static File get_temp_file(); /** * Resolve the native path by flattening all underlying * filesystem objects (like unions). @@ -137,7 +137,6 @@ class OAAPI Path { Path operator[](const parts_t &subpaths) const; Path operator[](const part_t &subpath) const; Path operator/(const part_t &subpath) const; - Path with_name(const part_t &name) const; Path with_suffix(const part_t &suffix) const; From 1195687eaf5099d68e25bc6e92453917ceaf2ec3 Mon Sep 17 00:00:00 2001 From: Tobias Alam Date: Sat, 9 Nov 2024 14:22:45 -0500 Subject: [PATCH 603/771] placed static temp file creation method in file.cpp, created temp directory creation method in directory.cpp --- libopenage/util/file.cpp | 8 ++++++++ libopenage/util/file.h | 3 ++- libopenage/util/fslike/directory.cpp | 8 ++++++++ libopenage/util/fslike/directory.h | 6 +++--- libopenage/util/path.cpp | 11 ----------- libopenage/util/path.h | 3 ++- 6 files changed, 23 insertions(+), 16 deletions(-) diff --git a/libopenage/util/file.cpp b/libopenage/util/file.cpp index 5aa630d4cc..10fcfe6910 100644 --- a/libopenage/util/file.cpp +++ b/libopenage/util/file.cpp @@ -121,5 +121,13 @@ std::ostream &operator<<(std::ostream &stream, const File &file) { return stream; } +static File get_temp_file() { + std::FILE* tmp_file = std::tmpfile(); + int temp_fd = fileno(tmp_file); + std::string tf_path = "/proc/self/fd/" + std::to_string(temp_fd); + mode_t mode = 0777; + File file_wrapper = File(tf_path, mode); + return file_wrapper; +} } // namespace openage::util diff --git a/libopenage/util/file.h b/libopenage/util/file.h index 6724b5f795..f5339adf67 100644 --- a/libopenage/util/file.h +++ b/libopenage/util/file.h @@ -98,8 +98,9 @@ class OAAPI File { void flush(); ssize_t size(); std::vector get_lines(); - std::shared_ptr get_fileobj() const; + + static File get_temp_file(); protected: std::shared_ptr filelike; diff --git a/libopenage/util/fslike/directory.cpp b/libopenage/util/fslike/directory.cpp index 420757f3a6..5695b3fc13 100644 --- a/libopenage/util/fslike/directory.cpp +++ b/libopenage/util/fslike/directory.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -292,4 +293,11 @@ std::ostream &Directory::repr(std::ostream &stream) { return stream; } +static Directory get_temp_directory() { + std::string temp_dir_path = std::filesystem::temp_directory_path() / std::tmpnam(nullptr); + bool create = true; + Directory directory = Directory(temp_dir_path, create); + return directory; +} + } // namespace openage::util::fslike diff --git a/libopenage/util/fslike/directory.h b/libopenage/util/fslike/directory.h index 2872b9554b..9fd1e2a020 100644 --- a/libopenage/util/fslike/directory.h +++ b/libopenage/util/fslike/directory.h @@ -47,6 +47,8 @@ class Directory : public FSLike { uint64_t get_filesize(const Path::parts_t &parts) override; std::ostream &repr(std::ostream &) override; + + static Directory get_temp_directory(); protected: /** @@ -54,12 +56,10 @@ class Directory : public FSLike { * basically basepath + "/".join(parts) */ std::string resolve(const Path::parts_t &parts) const; - std::tuple do_stat(const Path::parts_t &parts) const; - + std::string basepath; }; - } // namespace fslike } // namespace util } // namespace openage diff --git a/libopenage/util/path.cpp b/libopenage/util/path.cpp index 34ed9adbba..ff23253533 100644 --- a/libopenage/util/path.cpp +++ b/libopenage/util/path.cpp @@ -3,7 +3,6 @@ #include "path.h" #include -#include #include "../error/error.h" #include "compiler.h" @@ -12,7 +11,6 @@ #include "fslike/python.h" #include "misc.h" #include "strings.h" -#include "file.h" namespace openage::util { @@ -394,14 +392,5 @@ std::string dirname(const std::string &fullpath) { } } -static File get_temp_file() { - std::FILE* tmp_file = std::tmpfile(); - int temp_fd = fileno(tmp_file); - std::string tf_path = "/proc/self/fd/" + std::to_string(temp_fd); - mode_t mode = 0777; - File file_wrapper = File(tf_path, mode); - return file_wrapper; -} - } // namespace openage::util diff --git a/libopenage/util/path.h b/libopenage/util/path.h index f9161343d2..d16e1734c6 100644 --- a/libopenage/util/path.h +++ b/libopenage/util/path.h @@ -96,7 +96,7 @@ class OAAPI Path { File open_rw() const; File open_a() const; File open_ar() const; - static File get_temp_file(); + /** * Resolve the native path by flattening all underlying * filesystem objects (like unions). @@ -137,6 +137,7 @@ class OAAPI Path { Path operator[](const parts_t &subpaths) const; Path operator[](const part_t &subpath) const; Path operator/(const part_t &subpath) const; + Path with_name(const part_t &name) const; Path with_suffix(const part_t &suffix) const; From 8c525dddfcd88679c3540cc7fbb8f34a7d9a0732 Mon Sep 17 00:00:00 2001 From: Tobias Alam Date: Wed, 13 Nov 2024 10:26:53 -0500 Subject: [PATCH 604/771] modified file.cpp to utilize get_temp_dir from directory.cpp and then place a file into it --- libopenage/util/file.cpp | 10 ++++++---- libopenage/util/fslike/directory.h | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/libopenage/util/file.cpp b/libopenage/util/file.cpp index 10fcfe6910..3d0d4032ac 100644 --- a/libopenage/util/file.cpp +++ b/libopenage/util/file.cpp @@ -16,6 +16,7 @@ #include "util/filelike/python.h" #include "util/path.h" #include "util/strings.h" +#include "util/fslike/directory.h" namespace openage::util { @@ -122,11 +123,12 @@ std::ostream &operator<<(std::ostream &stream, const File &file) { } static File get_temp_file() { - std::FILE* tmp_file = std::tmpfile(); - int temp_fd = fileno(tmp_file); - std::string tf_path = "/proc/self/fd/" + std::to_string(temp_fd); + fslike::Directory temp_dir = fslike::Directory::get_temp_directory(); + std::string file_name = std::tmpnam(nullptr); + std::ostringstream dir_path; + temp_dir.repr(dir_path); mode_t mode = 0777; - File file_wrapper = File(tf_path, mode); + File file_wrapper = File(dir_path.str() + file_name, mode); return file_wrapper; } diff --git a/libopenage/util/fslike/directory.h b/libopenage/util/fslike/directory.h index 9fd1e2a020..43c922b8b5 100644 --- a/libopenage/util/fslike/directory.h +++ b/libopenage/util/fslike/directory.h @@ -56,8 +56,9 @@ class Directory : public FSLike { * basically basepath + "/".join(parts) */ std::string resolve(const Path::parts_t &parts) const; + std::tuple do_stat(const Path::parts_t &parts) const; - + std::string basepath; }; } // namespace fslike From a144c8cda72a4e2fa90beb8cde5915e3f13c3cbe Mon Sep 17 00:00:00 2001 From: Tobias Alam Date: Mon, 18 Nov 2024 08:29:11 -0500 Subject: [PATCH 605/771] sanity check fixes --- copying.md | 1 + libopenage/util/file.h | 2 +- libopenage/util/fslike/directory.h | 2 +- libopenage/util/path.cpp | 2 +- libopenage/util/path.h | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/copying.md b/copying.md index ec1ddd2a46..61153fb9cf 100644 --- a/copying.md +++ b/copying.md @@ -156,6 +156,7 @@ _the openage authors_ are: | Nikhil Ghosh | NikhilGhosh75 | nghosh606 à gmail dawt com | | Edvin Lindholm | EdvinLndh | edvinlndh à gmail dawt com | | Jeremiah Morgan | jere8184 | jeremiahmorgan dawt bham à outlook dawt com | +| Tobias Alam | alamt22 | tobiasal à umich dawt edu | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. diff --git a/libopenage/util/file.h b/libopenage/util/file.h index f5339adf67..44d24b027a 100644 --- a/libopenage/util/file.h +++ b/libopenage/util/file.h @@ -99,7 +99,7 @@ class OAAPI File { ssize_t size(); std::vector get_lines(); std::shared_ptr get_fileobj() const; - + static File get_temp_file(); protected: diff --git a/libopenage/util/fslike/directory.h b/libopenage/util/fslike/directory.h index 43c922b8b5..9f72d49766 100644 --- a/libopenage/util/fslike/directory.h +++ b/libopenage/util/fslike/directory.h @@ -47,7 +47,7 @@ class Directory : public FSLike { uint64_t get_filesize(const Path::parts_t &parts) override; std::ostream &repr(std::ostream &) override; - + static Directory get_temp_directory(); protected: diff --git a/libopenage/util/path.cpp b/libopenage/util/path.cpp index ff23253533..e521a4ed9e 100644 --- a/libopenage/util/path.cpp +++ b/libopenage/util/path.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "path.h" diff --git a/libopenage/util/path.h b/libopenage/util/path.h index d16e1734c6..5d4e079812 100644 --- a/libopenage/util/path.h +++ b/libopenage/util/path.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once From 11b539d581d7f058c585be4937635b813a8ef21d Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Mon, 14 Oct 2024 16:41:05 +0100 Subject: [PATCH 606/771] Pathfinding: Precomputeportal nodes for A Star --- libopenage/pathfinding/demo/demo_1.cpp | 1 + libopenage/pathfinding/grid.cpp | 23 +++++++ libopenage/pathfinding/grid.h | 18 ++++++ libopenage/pathfinding/pathfinder.cpp | 85 +++++++++++++++++--------- libopenage/pathfinding/pathfinder.h | 63 +++++++++++++++---- 5 files changed, 148 insertions(+), 42 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index cd67e7b79f..1edeba52a1 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -92,6 +92,7 @@ void path_demo_1(const util::Path &path) { start, target, }; + grid->init_portal_nodes(); timer.start(); Path path_result = pathfinder->get_path(path_request); timer.stop(); diff --git a/libopenage/pathfinding/grid.cpp b/libopenage/pathfinding/grid.cpp index 62a2d3156f..45427b009d 100644 --- a/libopenage/pathfinding/grid.cpp +++ b/libopenage/pathfinding/grid.cpp @@ -98,4 +98,27 @@ void Grid::init_portals() { } } +const nodemap_t &Grid::get_portal_map() { + return portal_nodes; +} + +void Grid::init_portal_nodes() { + // create portal_nodes + for (auto §or : this->sectors) { + for (auto &portal : sector->get_portals()) { + if (!this->portal_nodes.contains(portal->get_id())) { + auto portal_node = std::make_shared(portal); + portal_node->node_sector_0 = sector->get_id(); + portal_node->node_sector_1 = portal_node->portal->get_exit_sector(sector->get_id()); + this->portal_nodes[portal->get_id()] = portal_node; + } + } + } + + // init portal_node exits + for (auto &[id, node] : this->portal_nodes) { + node->init_exits(this->portal_nodes); + } +} + } // namespace openage::path diff --git a/libopenage/pathfinding/grid.h b/libopenage/pathfinding/grid.h index 8f24743433..314d107a2e 100644 --- a/libopenage/pathfinding/grid.h +++ b/libopenage/pathfinding/grid.h @@ -7,6 +7,7 @@ #include #include +#include "pathfinding/pathfinder.h" #include "pathfinding/types.h" #include "util/vector.h" @@ -95,6 +96,16 @@ class Grid { */ void init_portals(); + /** + * returns map of portal ids to portal nodes + */ + const nodemap_t &get_portal_map(); + + /** + * Initialize the portal nodes of the grid with neigbouring nodes and distance costs. + */ + void init_portal_nodes(); + private: /** * ID of the grid. @@ -115,6 +126,13 @@ class Grid { * Sectors of the grid. */ std::vector> sectors; + + /** + * maps portal_ids to portal nodes, which store their neigbouring nodes and associated distance costs + * for pathfinding + */ + + nodemap_t portal_nodes; }; diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 288f7875df..e46919e62b 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -4,6 +4,7 @@ #include "coord/chunk.h" #include "coord/phys.h" +#include "error/error.h" #include "pathfinding/cost_field.h" #include "pathfinding/flow_field.h" #include "pathfinding/grid.h" @@ -201,6 +202,7 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req std::vector> result; auto grid = this->grids.at(request.grid_id); + auto &portal_map = grid->get_portal_map(); auto sector_size = grid->get_sector_size(); auto start_sector_x = request.start.ne / sector_size; @@ -210,8 +212,7 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req // path node storage, always provides cheapest next node. heap_t node_candidates; - // list of known portals and corresponding node. - nodemap_t visited_portals; + std::unordered_set visited_portals; // TODO: Compute cost to travel from one portal to another when creating portals // const int distance_cost = 1; @@ -223,7 +224,8 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req continue; } - auto portal_node = std::make_shared(portal, start_sector->get_id(), nullptr); + auto &portal_node = portal_map.at(portal->get_id()); + portal_node->entry_sector = start_sector->get_id(); auto sector_pos = grid->get_sector(portal->get_exit_sector(start_sector->get_id()))->get_position().to_tile(sector_size); auto portal_pos = portal->get_exit_center(start_sector->get_id()); @@ -235,7 +237,9 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req portal_node->future_cost = portal_node->current_cost + heuristic_cost; portal_node->heap_node = node_candidates.push(portal_node); - visited_portals[portal->get_id()] = portal_node; + portal_node->prev_portal = nullptr; + portal_node->was_best = false; + visited_portals.insert(portal->get_id()); } // track the closest we can get to the end position @@ -266,20 +270,21 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req } // get the exits of the current node - auto exits = current_node->get_exits(visited_portals, current_node->entry_sector); + const auto &exits = current_node->get_exits(current_node->entry_sector); // evaluate all neighbors of the current candidate for further progress - for (auto &exit : exits) { - if (exit->was_best) { + for (auto &[exit, distance_cost] : exits) { + exit->entry_sector = current_node->portal->get_exit_sector(current_node->entry_sector); + bool not_visited = !visited_portals.contains(exit->portal->get_id()); + + if (not_visited) { + exit->was_best = false; + } + else if (exit->was_best) { continue; } - // Get distance cost (from current node to exit node) - auto distance_cost = Pathfinder::distance_cost( - current_node->portal->get_exit_center(current_node->entry_sector), - exit->portal->get_entry_center(exit->entry_sector)); - bool not_visited = !visited_portals.contains(exit->portal->get_id()); auto tentative_cost = current_node->current_cost + distance_cost; if (not_visited or tentative_cost < exit->current_cost) { @@ -300,7 +305,7 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req if (not_visited) { exit->heap_node = node_candidates.push(exit); - visited_portals[exit->portal->get_id()] = exit; + visited_portals.insert(exit->portal->get_id()); } else { node_candidates.decrease(exit->heap_node); @@ -463,6 +468,16 @@ int Pathfinder::distance_cost(const coord::tile_delta &portal1_pos, } +PortalNode::PortalNode(const std::shared_ptr &portal) : + portal{portal}, + entry_sector{NULL}, + future_cost{std::numeric_limits::max()}, + current_cost{std::numeric_limits::max()}, + heuristic_cost{std::numeric_limits::max()}, + was_best{false}, + prev_portal{nullptr}, + heap_node{nullptr} {} + PortalNode::PortalNode(const std::shared_ptr &portal, sector_id_t entry_sector, const node_t &prev_portal) : @@ -511,27 +526,37 @@ std::vector PortalNode::generate_backtrace() { return waypoints; } -std::vector PortalNode::get_exits(const nodemap_t &nodes, - sector_id_t entry_sector) { - auto &exits = this->portal->get_exits(entry_sector); - std::vector exit_nodes; - exit_nodes.reserve(exits.size()); +void PortalNode::init_exits(const nodemap_t &node_map) { + auto exits = this->portal->get_exits(this->node_sector_0); + for (auto &exit : exits) { + int distance_cost = Pathfinder::distance_cost( + this->portal->get_exit_center(this->node_sector_0), + exit->get_entry_center(this->node_sector_1)); + + auto exit_node = node_map.at(exit->get_id()); + this->exits_1[exit_node] = distance_cost; + } - auto exit_sector = this->portal->get_exit_sector(entry_sector); + exits = this->portal->get_exits(this->node_sector_1); for (auto &exit : exits) { - auto exit_id = exit->get_id(); + int distance_cost = Pathfinder::distance_cost( + this->portal->get_exit_center(this->node_sector_1), + exit->get_entry_center(this->node_sector_0)); - auto exit_node = nodes.find(exit_id); - if (exit_node != nodes.end()) { - exit_nodes.push_back(exit_node->second); - } - else { - exit_nodes.push_back(std::make_shared(exit, - exit_sector, - this->shared_from_this())); - } + auto exit_node = node_map.at(exit->get_id()); + this->exits_0[exit_node] = distance_cost; + } +} + +const PortalNode::exits_t &PortalNode::get_exits(sector_id_t entry_sector) { + ENSURE(entry_sector == this->node_sector_0 || entry_sector == this->node_sector_1, "Invalid entry sector"); + + if (this->node_sector_0 == entry_sector) { + return exits_1; + } + else { + return exits_0; } - return exit_nodes; } diff --git a/libopenage/pathfinding/pathfinder.h b/libopenage/pathfinding/pathfinder.h index 1809b7524d..e2529987b7 100644 --- a/libopenage/pathfinding/pathfinder.h +++ b/libopenage/pathfinding/pathfinder.h @@ -2,9 +2,9 @@ #pragma once +#include #include #include -#include #include "coord/tile.h" #include "datastructure/pairing_heap.h" @@ -62,6 +62,17 @@ class Pathfinder { */ const Path get_path(const PathRequest &request); + + /** + * Calculate the distance cost between two portals. + * + * @param portal1_pos Center of the first portal (relative to sector origin). + * @param portal2_pos Center of the second portal (relative to sector origin). + * + * @return Distance cost between the portal centers. + */ + static int distance_cost(const coord::tile_delta &portal1_pos, const coord::tile_delta &portal2_pos); + private: using portal_star_t = std::pair>>; @@ -100,15 +111,6 @@ class Pathfinder { */ static int heuristic_cost(const coord::tile &portal_pos, const coord::tile &target_pos); - /** - * Calculate the distance cost between two portals. - * - * @param portal1_pos Center of the first portal (relative to sector origin). - * @param portal2_pos Center of the second portal (relative to sector origin). - * - * @return Distance cost between the portal centers. - */ - static int distance_cost(const coord::tile_delta &portal1_pos, const coord::tile_delta &portal2_pos); /** * Grids managed by this pathfinder. @@ -147,6 +149,7 @@ using nodemap_t = std::unordered_map; */ class PortalNode : public std::enable_shared_from_this { public: + PortalNode(const std::shared_ptr &portal); PortalNode(const std::shared_ptr &portal, sector_id_t entry_sector, const node_t &prev_portal); @@ -178,9 +181,25 @@ class PortalNode : public std::enable_shared_from_this { std::vector generate_backtrace(); /** - * Get all exits of a node. + * init PortalNode::exits. + */ + void init_exits(const nodemap_t &node_map); + + + /** + * maps node_t of a neigbhour portal to the distance cost to travel between the portals */ - std::vector get_exits(const nodemap_t &nodes, sector_id_t entry_sector); + using exits_t = std::map; + + + /** + * Get the exit portals reachable via the portal when entering from a specified sector. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Exit portals nodes reachable from the portal. + */ + const exits_t &get_exits(sector_id_t entry_sector); /** * The portal this node is associated to. @@ -226,6 +245,26 @@ class PortalNode : public std::enable_shared_from_this { * Priority queue node that contains this path node. */ heap_t::element_t heap_node; + + /** + * First sector connected by the portal. + */ + sector_id_t node_sector_0; + + /** + * Second sector connected by the portal. + */ + sector_id_t node_sector_1; + + /** + * Exits in sector 0 reachable from the portal. + */ + exits_t exits_0; + + /** + * Exits in sector 1 reachable from the portal. + */ + exits_t exits_1; }; From 3d6a3aceeb261d7a367edb44cc1d3630b1a15636 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 18 Nov 2024 23:13:28 +0100 Subject: [PATCH 607/771] util: Fix definition of static functions. --- libopenage/util/file.cpp | 7 +++---- libopenage/util/fslike/directory.cpp | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/libopenage/util/file.cpp b/libopenage/util/file.cpp index 3d0d4032ac..4a010c119a 100644 --- a/libopenage/util/file.cpp +++ b/libopenage/util/file.cpp @@ -14,9 +14,9 @@ #include "util/filelike/native.h" #include "util/filelike/python.h" +#include "util/fslike/directory.h" #include "util/path.h" #include "util/strings.h" -#include "util/fslike/directory.h" namespace openage::util { @@ -122,13 +122,12 @@ std::ostream &operator<<(std::ostream &stream, const File &file) { return stream; } -static File get_temp_file() { +File File::get_temp_file() { fslike::Directory temp_dir = fslike::Directory::get_temp_directory(); std::string file_name = std::tmpnam(nullptr); std::ostringstream dir_path; temp_dir.repr(dir_path); - mode_t mode = 0777; - File file_wrapper = File(dir_path.str() + file_name, mode); + File file_wrapper = File(dir_path.str() + file_name, 0777); return file_wrapper; } diff --git a/libopenage/util/fslike/directory.cpp b/libopenage/util/fslike/directory.cpp index 5695b3fc13..4143e376e3 100644 --- a/libopenage/util/fslike/directory.cpp +++ b/libopenage/util/fslike/directory.cpp @@ -11,8 +11,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -293,7 +293,7 @@ std::ostream &Directory::repr(std::ostream &stream) { return stream; } -static Directory get_temp_directory() { +Directory Directory::get_temp_directory() { std::string temp_dir_path = std::filesystem::temp_directory_path() / std::tmpnam(nullptr); bool create = true; Directory directory = Directory(temp_dir_path, create); From c0c082afee9cc49415f6b71527f4d0211d50f811 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 20 Nov 2024 05:41:23 +0100 Subject: [PATCH 608/771] util: Allow setting executable flags for temp file. --- libopenage/util/file.cpp | 12 ++++++++++-- libopenage/util/file.h | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/libopenage/util/file.cpp b/libopenage/util/file.cpp index 4a010c119a..d035ee34f2 100644 --- a/libopenage/util/file.cpp +++ b/libopenage/util/file.cpp @@ -122,12 +122,20 @@ std::ostream &operator<<(std::ostream &stream, const File &file) { return stream; } -File File::get_temp_file() { +File File::get_temp_file(bool executable) { fslike::Directory temp_dir = fslike::Directory::get_temp_directory(); std::string file_name = std::tmpnam(nullptr); std::ostringstream dir_path; temp_dir.repr(dir_path); - File file_wrapper = File(dir_path.str() + file_name, 0777); + + if (executable) { + // 0755 == rwxr-xr-x + File file_wrapper = File(dir_path.str() + file_name, 0755); + return file_wrapper; + } + + // 0644 == rw-r--r-- + File file_wrapper = File(dir_path.str() + file_name, 0644); return file_wrapper; } diff --git a/libopenage/util/file.h b/libopenage/util/file.h index 44d24b027a..e2c9a8a992 100644 --- a/libopenage/util/file.h +++ b/libopenage/util/file.h @@ -100,7 +100,7 @@ class OAAPI File { std::vector get_lines(); std::shared_ptr get_fileobj() const; - static File get_temp_file(); + static File get_temp_file(bool executable = false); protected: std::shared_ptr filelike; From cf2fd630b761220ada5afb25b72b3bda45ee4370 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Tue, 19 Nov 2024 18:18:02 +0000 Subject: [PATCH 609/771] fix type error in get_temp_directory --- libopenage/util/fslike/directory.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libopenage/util/fslike/directory.cpp b/libopenage/util/fslike/directory.cpp index 4143e376e3..847ffe4d80 100644 --- a/libopenage/util/fslike/directory.cpp +++ b/libopenage/util/fslike/directory.cpp @@ -294,7 +294,8 @@ std::ostream &Directory::repr(std::ostream &stream) { } Directory Directory::get_temp_directory() { - std::string temp_dir_path = std::filesystem::temp_directory_path() / std::tmpnam(nullptr); + std::filesystem::path path = std::filesystem::temp_directory_path() / std::tmpnam(nullptr); + std::string temp_dir_path = path.string(); bool create = true; Directory directory = Directory(temp_dir_path, create); return directory; From 35113bfb335366600c6f51df5c296e748299781b Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Nov 2024 18:43:03 +0100 Subject: [PATCH 610/771] convert: Fix setting allowed member type when array is empty. --- .../value_object/read/genie_structure.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/openage/convert/value_object/read/genie_structure.py b/openage/convert/value_object/read/genie_structure.py index bfc6324f1c..dcfd0f3baf 100644 --- a/openage/convert/value_object/read/genie_structure.py +++ b/openage/convert/value_object/read/genie_structure.py @@ -1,4 +1,4 @@ -# Copyright 2014-2023 the openage authors. See copying.md for legal info. +# Copyright 2014-2024 the openage authors. See copying.md for legal info. # TODO pylint: disable=C,R @@ -456,30 +456,40 @@ def _read_primitive( array_members = [] allowed_member_type = None + if storage_type is StorageType.ARRAY_INT: + allowed_member_type = StorageType.INT_MEMBER + + elif storage_type is StorageType.ARRAY_FLOAT: + allowed_member_type = StorageType.FLOAT_MEMBER + + elif storage_type is StorageType.ARRAY_BOOL: + allowed_member_type = StorageType.BOOLEAN_MEMBER + + elif storage_type is StorageType.ARRAY_ID: + allowed_member_type = StorageType.ID_MEMBER + + elif storage_type is StorageType.ARRAY_STRING: + allowed_member_type = StorageType.STRING_MEMBER + for elem in result: if storage_type is StorageType.ARRAY_INT: gen_member = IntMember(var_name, elem) - allowed_member_type = StorageType.INT_MEMBER array_members.append(gen_member) elif storage_type is StorageType.ARRAY_FLOAT: gen_member = FloatMember(var_name, elem) - allowed_member_type = StorageType.FLOAT_MEMBER array_members.append(gen_member) elif storage_type is StorageType.ARRAY_BOOL: gen_member = BooleanMember(var_name, elem) - allowed_member_type = StorageType.BOOLEAN_MEMBER array_members.append(gen_member) elif storage_type is StorageType.ARRAY_ID: gen_member = IDMember(var_name, elem) - allowed_member_type = StorageType.ID_MEMBER array_members.append(gen_member) elif storage_type is StorageType.ARRAY_STRING: gen_member = StringMember(var_name, elem) - allowed_member_type = StorageType.STRING_MEMBER array_members.append(gen_member) else: From 24e2f17ceeb4dd5cee8603559621db79a106d3a2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Nov 2024 20:55:56 +0100 Subject: [PATCH 611/771] convert: Allow multiple trading post targets for units. --- openage/convert/processor/conversion/aoc/processor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openage/convert/processor/conversion/aoc/processor.py b/openage/convert/processor/conversion/aoc/processor.py index 718ab8af01..d042e32065 100644 --- a/openage/convert/processor/conversion/aoc/processor.py +++ b/openage/convert/processor/conversion/aoc/processor.py @@ -1361,10 +1361,10 @@ def link_trade_posts(full_data_set: GenieObjectContainer) -> None: continue trade_post_id = command["unit_id"].value - break - # Notify buiding - full_data_set.building_lines[trade_post_id].add_trading_line(unit_line) + # Notify buiding + if trade_post_id in full_data_set.building_lines.keys(): + full_data_set.building_lines[trade_post_id].add_trading_line(unit_line) @staticmethod def link_repairables(full_data_set: GenieObjectContainer) -> None: From f8701971a5d2901e53433c920ac6194374294ef9 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Nov 2024 20:57:13 +0100 Subject: [PATCH 612/771] convert: Add new names for 'Battle for Greece' DLC. --- .../conversion/de2/internal_nyan_names.py | 71 ++++++++++++++++++- .../read/media/datfile/lookup_dicts.py | 9 +++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/openage/convert/value_object/conversion/de2/internal_nyan_names.py b/openage/convert/value_object/conversion/de2/internal_nyan_names.py index 30f9826ba3..e4f3c995bd 100644 --- a/openage/convert/value_object/conversion/de2/internal_nyan_names.py +++ b/openage/convert/value_object/conversion/de2/internal_nyan_names.py @@ -70,6 +70,37 @@ 1802: ("EliteCompositeBowman", "elite_composite_bowman"), 1805: ("EliteMonaspa", "elite_monaspa"), + + # BfG + 2101: ("BfGUnkown_2101", "bfg_unkown_2101"), + 2102: ("BfGUnkown_2102", "bfg_unkown_2102"), + 2104: ("BfGUnkown_2104", "bfg_unkown_2104"), + 2105: ("BfGUnkown_2105", "bfg_unkown_2105"), + 2107: ("BfGUnkown_2107", "bfg_unkown_2107"), + 2108: ("BfGUnkown_2108", "bfg_unkown_2108"), + 2110: ("BfGUnkown_2110", "bfg_unkown_2110"), + 2111: ("BfGUnkown_2111", "bfg_unkown_2111"), + 2123: ("BfGUnkown_2123", "bfg_unkown_2123"), + 2124: ("BfGUnkown_2124", "bfg_unkown_2124"), + 2125: ("BfGUnkown_2125", "bfg_unkown_2125"), + 2126: ("BfGUnkown_2126", "bfg_unkown_2126"), + 2127: ("BfGUnkown_2127", "bfg_unkown_2127"), + 2128: ("BfGUnkown_2128", "bfg_unkown_2128"), + 2129: ("BfGUnkown_2129", "bfg_unkown_2129"), + 2130: ("BfGUnkown_2130", "bfg_unkown_2130"), + 2131: ("BfGUnkown_2131", "bfg_unkown_2131"), + 2132: ("BfGUnkown_2132", "bfg_unkown_2132"), + 2133: ("BfGUnkown_2133", "bfg_unkown_2133"), + 2134: ("BfGUnkown_2134", "bfg_unkown_2134"), + 2135: ("BfGUnkown_2135", "bfg_unkown_2135"), + 2138: ("BfGUnkown_2138", "bfg_unkown_2138"), + 2139: ("BfGUnkown_2139", "bfg_unkown_2139"), + 2140: ("BfGUnkown_2140", "bfg_unkown_2140"), + 2148: ("BfGUnkown_2148", "bfg_unkown_2148"), + 2149: ("BfGUnkown_2149", "bfg_unkown_2149"), + 2150: ("BfGUnkown_2150", "bfg_unkown_2150"), + 2151: ("BfGUnkown_2151", "bfg_unkown_2151"), + 2162: ("BfGUnkown_2162", "bfg_unkown_2162"), } # key: head unit id; value: (nyan object name, filename prefix) @@ -88,6 +119,10 @@ # TMR 1808: ("MuleCart", "mule_cart"), + + # BfG + 2119: ("BfGUnkown_2119", "bfg_unkown_2119"), + 2172: ("BfGUnkown_2172", "bfg_unkown_2172"), } # key: (head) unit id; value: (nyan object name, filename prefix) @@ -171,6 +206,33 @@ 924: ("AsnuariCavalry", "asnuari_cavalry"), 929: ("FortifiedChurch", "fortified_church"), 967: ("EliteQizilbashWarrior", "elite_qizilbash_warrior"), + + # BfG + 1110: ("BfGUnkown_1110", "bfg_unkown_1110"), + 1111: ("BfGUnkown_1111", "bfg_unkown_1111"), + 1112: ("BfGUnkown_1112", "bfg_unkown_1112"), + 1113: ("BfGUnkown_1113", "bfg_unkown_1113"), + 1120: ("BfGUnkown_1120", "bfg_unkown_1120"), + 1121: ("BfGUnkown_1121", "bfg_unkown_1121"), + 1122: ("BfGUnkown_1122", "bfg_unkown_1122"), + 1123: ("BfGUnkown_1123", "bfg_unkown_1123"), + 1130: ("BfGUnkown_1130", "bfg_unkown_1130"), + 1131: ("BfGUnkown_1131", "bfg_unkown_1131"), + 1132: ("BfGUnkown_1132", "bfg_unkown_1132"), + 1133: ("BfGUnkown_1133", "bfg_unkown_1133"), + 1161: ("BfGUnkown_1161", "bfg_unkown_1161"), + 1162: ("BfGUnkown_1162", "bfg_unkown_1162"), + 1165: ("BfGUnkown_1165", "bfg_unkown_1165"), + 1167: ("BfGUnkown_1167", "bfg_unkown_1167"), + 1173: ("BfGUnkown_1173", "bfg_unkown_1173"), + 1198: ("BfGUnkown_1198", "bfg_unkown_1198"), + 1202: ("BfGUnkown_1202", "bfg_unkown_1202"), + 1203: ("BfGUnkown_1203", "bfg_unkown_1203"), + 1204: ("BfGUnkown_1204", "bfg_unkown_1204"), + 1223: ("BfGUnkown_1223", "bfg_unkown_1223"), + 1224: ("BfGUnkown_1224", "bfg_unkown_1224"), + 1225: ("BfGUnkown_1225", "bfg_unkown_1225"), + 1226: ("BfGUnkown_1226", "bfg_unkown_1226"), } # key: civ index; value: (nyan object name, filename prefix) @@ -201,6 +263,11 @@ # TMR 44: ("Armenians", "armenians"), 45: ("Georgians", "georgians"), + + # BfG + 46: ("BfGUnkown_46", "bfg_unkown_46"), + 47: ("BfGUnkown_47", "bfg_unkown_47"), + 48: ("BfGUnkown_48", "bfg_unkown_48"), } # key: civ index; value: (civ ids, nyan object name, filename prefix) @@ -208,7 +275,7 @@ GRAPHICS_SET_LOOKUPS = { 0: ((0, 1, 2, 13, 14, 36), "WesternEuropean", "western_european"), 4: ((7, 37), "Byzantine", "byzantine"), - 6: ((19, 24, 43, 44, 45), "Mediterranean", "mediterranean"), + 6: ((19, 24, 43, 44, 45, 46, 47, 48), "Mediterranean", "mediterranean"), 7: ((20, 40, 41, 42), "Indian", "indian"), 8: ((22, 23, 32, 35, 38, 39), "EasternEuropean", "eastern_european"), 11: ((33, 34), "CentralAsian", "central_asian"), @@ -238,4 +305,6 @@ 37: "SiegeBallista", 38: "DE2Skirmisher", 39: "DE2CamelRider", + 40: "BfGUnkown_40", + 60: "BfGUnkown_60", } diff --git a/openage/convert/value_object/read/media/datfile/lookup_dicts.py b/openage/convert/value_object/read/media/datfile/lookup_dicts.py index 7f52816dff..2fd46f118d 100644 --- a/openage/convert/value_object/read/media/datfile/lookup_dicts.py +++ b/openage/convert/value_object/read/media/datfile/lookup_dicts.py @@ -279,6 +279,8 @@ 248: "DE2_UNKNOWN_248", 249: "DE2_UNKNOWN_249", 250: "DE2_UNKNOWN_250", + 501: "DE2_UNKNOWN_501", + 506: "DE2_UNKNOWN_506", } EFFECT_APPLY_TYPE = { @@ -338,6 +340,10 @@ 102: "TECH_TOGGLE", # d == research_id 103: "TECH_TIME_MODIFY", # a == research_id, if c == 0: d==absval else d==relval + -54: "UNKNOWN", # 199: "UNKNOWN", + -55: "UNKNOWN", # 200: "UNKNOWN", + -56: "UNKNOWN", # 201: "UNKNOWN", + # attribute_id: # 0: hit points # 1: line of sight @@ -506,6 +512,8 @@ 37: "DE2_SIEGE_BALLISTA", 38: "DE2_SKIRMISHER", 39: "DE2_CAMEL_RIDER", + 40: "DE2_UNKNOWN_40", + 60: "DE2_UNKNOWN_60", } UNIT_CLASSES = { @@ -757,4 +765,5 @@ 0x0b: "NOCAVALRY", 0x0f: "ALL", 0x10: "SWGB_LIVESTOCK", + 0x40: "DE2_UNKNOWN_40", } From eda7c605c2f975ed2dbd359a7985ea964092b117 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 24 Nov 2024 20:57:34 +0100 Subject: [PATCH 613/771] convert: Add new attributes/resources for 'Battle for Greece' DLC. --- .../conversion/de2/tech_subprocessor.py | 9 ++ .../de2/upgrade_attribute_subprocessor.py | 86 +++++++++- .../de2/upgrade_resource_subprocessor.py | 150 ++++++++++++++++++ 3 files changed, 244 insertions(+), 1 deletion(-) diff --git a/openage/convert/processor/conversion/de2/tech_subprocessor.py b/openage/convert/processor/conversion/de2/tech_subprocessor.py index c9c3a71c17..7bce1d1163 100644 --- a/openage/convert/processor/conversion/de2/tech_subprocessor.py +++ b/openage/convert/processor/conversion/de2/tech_subprocessor.py @@ -63,11 +63,14 @@ class DE2TechSubprocessor: 57: AoCUpgradeAttributeSubprocessor.kidnap_storage_upgrade, 30: DE2UpgradeAttributeSubprocessor.herdable_capacity_upgrade, + 51: DE2UpgradeAttributeSubprocessor.bfg_unknown_51_upgrade, 59: DE2UpgradeAttributeSubprocessor.charge_attack_upgrade, 60: DE2UpgradeAttributeSubprocessor.charge_regen_upgrade, 61: DE2UpgradeAttributeSubprocessor.charge_event_upgrade, 62: DE2UpgradeAttributeSubprocessor.charge_type_upgrade, 63: AoCUpgradeAttributeSubprocessor.ignore_armor_upgrade, + 71: DE2UpgradeAttributeSubprocessor.bfg_unknown_71_upgrade, + 73: DE2UpgradeAttributeSubprocessor.bfg_unknown_73_upgrade, 100: AoCUpgradeAttributeSubprocessor.resource_cost_upgrade, 101: AoCUpgradeAttributeSubprocessor.creation_time_upgrade, 102: AoCUpgradeAttributeSubprocessor.min_projectiles_upgrade, @@ -99,6 +102,7 @@ class DE2TechSubprocessor: 46: AoCUpgradeResourceSubprocessor.tribute_inefficiency_upgrade, 47: AoCUpgradeResourceSubprocessor.gather_gold_efficiency_upgrade, 50: AoCUpgradeResourceSubprocessor.reveal_ally_upgrade, + 69: DE2UpgradeResourceSubprocessor.bfg_unknown_69_upgrade, 77: AoCUpgradeResourceSubprocessor.conversion_resistance_upgrade, 78: AoCUpgradeResourceSubprocessor.trade_penalty_upgrade, 79: AoCUpgradeResourceSubprocessor.gather_stone_efficiency_upgrade, @@ -137,6 +141,7 @@ class DE2TechSubprocessor: 214: DE2UpgradeResourceSubprocessor.free_kipchaks_upgrade, 216: DE2UpgradeResourceSubprocessor.sheep_food_amount_upgrade, 218: DE2UpgradeResourceSubprocessor.cuman_tc_upgrade, + 219: DE2UpgradeResourceSubprocessor.bfg_unknown_219_upgrade, 220: DE2UpgradeResourceSubprocessor.relic_food_production_upgrade, 234: DE2UpgradeResourceSubprocessor.first_crusade_upgrade, 236: DE2UpgradeResourceSubprocessor.burgundian_vineyards_upgrade, @@ -160,6 +165,10 @@ class DE2TechSubprocessor: 274: DE2UpgradeResourceSubprocessor.chieftains_upgrade, 280: DE2UpgradeResourceSubprocessor.conversion_range_upgrade, 282: DE2UpgradeResourceSubprocessor.unknown_recharge_rate_upgrade, + 502: DE2UpgradeResourceSubprocessor.bfg_unknown_502_upgrade, + 507: DE2UpgradeResourceSubprocessor.bfg_unknown_507_upgrade, + 521: DE2UpgradeResourceSubprocessor.bfg_unknown_521_upgrade, + 551: DE2UpgradeResourceSubprocessor.bfg_unknown_551_upgrade, } @classmethod diff --git a/openage/convert/processor/conversion/de2/upgrade_attribute_subprocessor.py b/openage/convert/processor/conversion/de2/upgrade_attribute_subprocessor.py index f60ee8eb64..60a5a4f6ac 100644 --- a/openage/convert/processor/conversion/de2/upgrade_attribute_subprocessor.py +++ b/openage/convert/processor/conversion/de2/upgrade_attribute_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-few-public-methods # @@ -24,6 +24,90 @@ class DE2UpgradeAttributeSubprocessor: Creates raw API objects for attribute upgrade effects in DE2. """ + @staticmethod + def bfg_unknown_51_upgrade( + converter_group: ConverterObjectGroup, + line: GenieGameEntityGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for a BfG unknown attribute effect (ID: 51). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param line: Unit/Building line that has the ability. + :type line: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: int, float + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + + @staticmethod + def bfg_unknown_71_upgrade( + converter_group: ConverterObjectGroup, + line: GenieGameEntityGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for a BfG unknown attribute effect (ID: 71). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param line: Unit/Building line that has the ability. + :type line: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: int, float + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + + @staticmethod + def bfg_unknown_73_upgrade( + converter_group: ConverterObjectGroup, + line: GenieGameEntityGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for a BfG unknown attribute effect (ID: 73). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param line: Unit/Building line that has the ability. + :type line: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: int, float + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + @staticmethod def charge_attack_upgrade( converter_group: ConverterObjectGroup, diff --git a/openage/convert/processor/conversion/de2/upgrade_resource_subprocessor.py b/openage/convert/processor/conversion/de2/upgrade_resource_subprocessor.py index 9abf477831..9ccd9a68f2 100644 --- a/openage/convert/processor/conversion/de2/upgrade_resource_subprocessor.py +++ b/openage/convert/processor/conversion/de2/upgrade_resource_subprocessor.py @@ -48,6 +48,156 @@ def bengali_conversion_resistance_upgrade( return patches + @staticmethod + def bfg_unknown_69_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for a BfG unknown resource effect (ID: 69). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: MemberOperator + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + + @staticmethod + def bfg_unknown_219_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for a BfG unknown resource effect (ID: 219). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: MemberOperator + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + + @staticmethod + def bfg_unknown_502_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for a BfG unknown resource effect (ID: 502). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: MemberOperator + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + + @staticmethod + def bfg_unknown_507_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for a BfG unknown resource effect (ID: 507). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: MemberOperator + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + + @staticmethod + def bfg_unknown_521_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for a BfG unknown resource effect (ID: 521). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: MemberOperator + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + + @staticmethod + def bfg_unknown_551_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for a BfG unknown resource effect (ID: 551). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: MemberOperator + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + @staticmethod def burgundian_vineyards_upgrade( converter_group: ConverterObjectGroup, From 6571e0d6402c737845409fbf343d578ca9ecf997 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Thu, 7 Nov 2024 23:11:12 +0000 Subject: [PATCH 614/771] data structure: implement pairing heap without use of shared_ptr --- libopenage/datastructure/pairing_heap.h | 167 ++++++++++++------------ libopenage/datastructure/tests.cpp | 5 +- libopenage/event/eventstore.cpp | 4 +- 3 files changed, 91 insertions(+), 85 deletions(-) diff --git a/libopenage/datastructure/pairing_heap.h b/libopenage/datastructure/pairing_heap.h index dd5438091a..8a571d1ea2 100644 --- a/libopenage/datastructure/pairing_heap.h +++ b/libopenage/datastructure/pairing_heap.h @@ -38,7 +38,7 @@ class PairingHeap; template > -class PairingHeapNode : public std::enable_shared_from_this> { +class PairingHeapNode { public: using this_type = PairingHeapNode; @@ -47,7 +47,6 @@ class PairingHeapNode : public std::enable_shared_from_this &node) { - node->add_child(this->shared_from_this()); + void become_child_of(this_type *const node) { + node->add_child(this); } /** * Add the given node as a child to this one. */ - void add_child(const std::shared_ptr &new_child) { + void add_child(this_type *const new_child) { // first child is the most recently attached one // it must not have siblings as they will get lost. @@ -85,7 +88,7 @@ class PairingHeapNode : public std::enable_shared_from_thisfirst_child = new_child; - new_child->parent = this->shared_from_this(); + new_child->parent = this; } /** @@ -93,23 +96,23 @@ class PairingHeapNode : public std::enable_shared_from_this link_with(const std::shared_ptr &node) { - std::shared_ptr new_root; - std::shared_ptr new_child; + this_type *link_with(this_type *const node) { + this_type *new_root; + this_type *new_child; if (this->cmp(this->data, node->data)) { - new_root = this->shared_from_this(); + new_root = this; new_child = node; } else { new_root = node; - new_child = this->shared_from_this(); + new_child = this; } // children of new root become siblings of new new_child // -> parent of new child = new root - // this whll be set by the add_child method + // this will be set by the add_child method new_child->prev_sibling = nullptr; new_child->next_sibling = nullptr; @@ -128,15 +131,15 @@ class PairingHeapNode : public std::enable_shared_from_this link_backwards() { + this_type *link_backwards() { if (this->next_sibling == nullptr) { // reached end, return this as current root, // the previous siblings will be linked to it. - return this->shared_from_this(); + return this; } // recurse to last sibling, - std::shared_ptr node = this->next_sibling->link_backwards(); + this_type *node = this->next_sibling->link_backwards(); // then link ourself to the new root. this->next_sibling = nullptr; @@ -153,9 +156,9 @@ class PairingHeapNode : public std::enable_shared_from_thisparent and this->parent->first_child == this->shared_from_this()) { - // we are the first child - // make the next sibling the first child + if (this->parent and this->parent->first_child == this) { + // we are child + // make the next sibling child this->parent->first_child = this->next_sibling; } // if we have a previous sibling @@ -176,10 +179,10 @@ class PairingHeapNode : public std::enable_shared_from_this first_child; - std::shared_ptr prev_sibling; - std::shared_ptr next_sibling; - std::shared_ptr parent; // for decrease-key and delete + this_type *first_child = nullptr; + this_type *prev_sibling = nullptr; + this_type *next_sibling = nullptr; + this_type *parent = nullptr; // for decrease-key and delete }; @@ -191,10 +194,8 @@ template > class PairingHeap final { public: - using node_t = heapnode_t; - using element_t = std::shared_ptr; - using this_type = PairingHeap; - using cmp_t = compare; + using element_t = heapnode_t *; + using this_type = PairingHeap; /** * create a empty heap. @@ -204,14 +205,16 @@ class PairingHeap final { root_node(nullptr) { } - ~PairingHeap() = default; + ~PairingHeap() { + this->clear(); + }; /** * adds the given item to the heap. * O(1) */ element_t push(const T &item) { - element_t new_node = std::make_shared(item); + element_t new_node = new heapnode_t(item); this->push_node(new_node); return new_node; } @@ -221,25 +224,18 @@ class PairingHeap final { * O(1) */ element_t push(T &&item) { - element_t new_node = std::make_shared(std::move(item)); + element_t new_node = new heapnode_t(std::move(item)); this->push_node(new_node); return new_node; } - /** - * returns and removes the smallest item on the heap. - */ - T pop() { - return std::move(this->pop_node()->data); - } - /** * returns the smallest item on the heap and deletes it. * also known as delete_min. * _________ * Ω(log log n), O(2^(2*√log log n')) */ - element_t pop_node() { + T pop() { if (this->root_node == nullptr) { throw Error{MSG(err) << "Can't pop an empty heap!"}; } @@ -316,36 +312,9 @@ class PairingHeap final { ret->first_child = nullptr; // and it's done! - return ret; - } - - /** - * Unlink a node from the heap. - * - * If the item is the current root, just pop(). - * else, cut the node from its parent, pop() that subtree - * and merge these trees. - * - * O(pop_node) - */ - void unlink_node(const element_t &node) { - if (node == this->root_node) { - this->pop_node(); - } - else { - node->loosen(); - - element_t real_root = this->root_node; - this->root_node = node; - this->pop_node(); - - element_t new_root = this->root_node; - this->root_node = real_root; - - if (new_root != nullptr) { - this->root_insert(new_root); - } - } + T data = std::move(ret->data); + delete ret; + return data; } /** @@ -391,14 +360,43 @@ class PairingHeap final { * * O(1) (but slower than decrease), and O(pop) when node is the root. */ - void update(const element_t &node) { + void update(element_t &node) { if (node != this->root_node) [[likely]] { - this->unlink_node(node); - this->push_node(node); + node = this->push(this->remove_node(node)); } else { // it's the root node, so we just pop and push it. - this->push_node(this->pop_node()); + node = this->push(this->pop()); + } + } + + /** + * remove a node from the heap. Return its data. + * + * If the item is the current root, just pop(). + * else, cut the node from its parent, pop() that subtree + * and merge these trees. + * + * O(pop_node) + */ + T remove_node(const element_t &node) { + if (node == this->root_node) { + return this->pop(); + } + else { + node->loosen(); + + element_t real_root = this->root_node; + this->root_node = node; + T data = this->pop(); + + element_t new_root = this->root_node; + this->root_node = real_root; + + if (new_root != nullptr) { + this->root_insert(new_root); + } + return data; } } @@ -406,6 +404,8 @@ class PairingHeap final { * erase all elements on the heap. */ void clear() { + auto delete_node = [](element_t node) { delete node; }; + this->iter_all(delete_node, true); this->root_node = nullptr; this->node_count = 0; #if OPENAGE_PAIRINGHEAP_DEBUG @@ -579,14 +579,17 @@ class PairingHeap final { } #endif - void iter_all(const std::function &func) const { - this->walk_tree(this->root_node, func); + void iter_all(const std::function &func, bool reverse = true) const { + this->walk_tree(this->root_node, func, reverse); } -protected: +private: void walk_tree(const element_t &root, - const std::function &func) const { - func(root); + const std::function &func, + bool reverse = false) const { + if (!reverse) { + func(root); + } if (root) { auto node = root->first_child; @@ -595,12 +598,16 @@ class PairingHeap final { break; } - this->walk_tree(node, func); + this->walk_tree(node, func, reverse); node = node->next_sibling; } + if (reverse) { + func(root); + } } } + /** * adds the given node to the heap. * use this if the node was not in the heap before. @@ -608,10 +615,9 @@ class PairingHeap final { */ void push_node(const element_t &node) { this->root_insert(node); - #if OPENAGE_PAIRINGHEAP_DEBUG - auto ins = this->nodes.insert(node); - if (not ins.second) { + auto [iter, result] = this->nodes.insert(node); + if (not result) { throw Error{ERR << "node already known"}; } #endif @@ -631,7 +637,6 @@ class PairingHeap final { } } -protected: compare cmp; size_t node_count; element_t root_node; diff --git a/libopenage/datastructure/tests.cpp b/libopenage/datastructure/tests.cpp index 95fa02a9da..c375f78c17 100644 --- a/libopenage/datastructure/tests.cpp +++ b/libopenage/datastructure/tests.cpp @@ -1,4 +1,4 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #include "tests.h" @@ -118,7 +118,8 @@ void pairing_heap_2() { heap.push(heap_elem{3}); // state: 1 2 3, now remove 2 - heap.unlink_node(node); + auto data = heap.remove_node(node); + TESTEQUALS(data.data, 2); // state: 1 3 TESTEQUALS(heap.pop().data, 1); diff --git a/libopenage/event/eventstore.cpp b/libopenage/event/eventstore.cpp index 7e88b4e638..f8a949f7a9 100644 --- a/libopenage/event/eventstore.cpp +++ b/libopenage/event/eventstore.cpp @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #include "eventstore.h" @@ -56,7 +56,7 @@ bool EventStore::erase(const std::shared_ptr &event) { bool erased = false; auto it = this->events.find(event); if (it != std::end(this->events)) { - this->heap.unlink_node(it->second); + this->heap.remove_node(it->second); this->events.erase(it); erased = true; } From 5a0c3eccccd5aac6eb855b4771bb83214980ee46 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Nov 2024 19:00:18 +0100 Subject: [PATCH 615/771] datastructure: Make iteration order a constexpr template parameter. --- libopenage/datastructure/pairing_heap.h | 40 +++++++++++++++++-------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/libopenage/datastructure/pairing_heap.h b/libopenage/datastructure/pairing_heap.h index 8a571d1ea2..dd348d8c55 100644 --- a/libopenage/datastructure/pairing_heap.h +++ b/libopenage/datastructure/pairing_heap.h @@ -405,7 +405,7 @@ class PairingHeap final { */ void clear() { auto delete_node = [](element_t node) { delete node; }; - this->iter_all(delete_node, true); + this->iter_all(delete_node); this->root_node = nullptr; this->node_count = 0; #if OPENAGE_PAIRINGHEAP_DEBUG @@ -579,30 +579,44 @@ class PairingHeap final { } #endif - void iter_all(const std::function &func, bool reverse = true) const { - this->walk_tree(this->root_node, func, reverse); + /** + * Apply the given function to all nodes in the tree. + * + * @tparam reverse If true, the function is applied to the nodes in reverse order. + * @param func Function to apply to each node. + */ + template + void iter_all(const std::function &func) const { + this->walk_tree(this->root_node, func); } private: - void walk_tree(const element_t &root, - const std::function &func, - bool reverse = false) const { - if (!reverse) { - func(root); + /** + * Apply the given function to all nodes in the tree. + * + * @tparam reverse If true, the function is applied to the nodes in reverse order. + * @param start Starting node. + * @param func Function to apply to each node. + */ + template + void walk_tree(const element_t &start, + const std::function &func) const { + if constexpr (not reverse) { + func(start); } - if (root) { - auto node = root->first_child; + if (start) { + auto node = start->first_child; while (true) { if (not node) { break; } - this->walk_tree(node, func, reverse); + this->walk_tree(node, func); node = node->next_sibling; } - if (reverse) { - func(root); + if constexpr (reverse) { + func(start); } } } From a18192f51e4c41a9299056d3efd04fdb6f9bb569 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Nov 2024 21:45:13 +0100 Subject: [PATCH 616/771] gamestate: Fix portal node initialization. --- libopenage/gamestate/map.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libopenage/gamestate/map.cpp b/libopenage/gamestate/map.cpp index 1de5b0fe27..8300ce273d 100644 --- a/libopenage/gamestate/map.cpp +++ b/libopenage/gamestate/map.cpp @@ -58,6 +58,7 @@ Map::Map(const std::shared_ptr &state, for (const auto &path_type : this->grid_lookup) { auto grid = this->pathfinder->get_grid(path_type.second); grid->init_portals(); + grid->init_portal_nodes(); } } From f372a2bb350f31abb2abccbde40903c9609cabb6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Nov 2024 22:08:07 +0100 Subject: [PATCH 617/771] convert Change type of 'effect_apply_type' to 'uint8_t'. --- .../value_object/read/media/datfile/lookup_dicts.py | 9 +++++---- openage/convert/value_object/read/media/datfile/tech.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/openage/convert/value_object/read/media/datfile/lookup_dicts.py b/openage/convert/value_object/read/media/datfile/lookup_dicts.py index 2fd46f118d..dfd2206612 100644 --- a/openage/convert/value_object/read/media/datfile/lookup_dicts.py +++ b/openage/convert/value_object/read/media/datfile/lookup_dicts.py @@ -285,7 +285,7 @@ EFFECT_APPLY_TYPE = { # unused assignage: a = -1, b = -1, c = -1, d = 0 - -1: "DISABLED", + 255: "DISABLED", # if a != -1: a == unit_id, else b == unit_class_id; c = # attribute_id, d = new_value 0: "ATTRIBUTE_ABSSET", @@ -340,9 +340,10 @@ 102: "TECH_TOGGLE", # d == research_id 103: "TECH_TIME_MODIFY", # a == research_id, if c == 0: d==absval else d==relval - -54: "UNKNOWN", # 199: "UNKNOWN", - -55: "UNKNOWN", # 200: "UNKNOWN", - -56: "UNKNOWN", # 201: "UNKNOWN", + # unknown; used in DE2 BfG + 199: "UNKNOWN", + 200: "UNKNOWN", + 201: "UNKNOWN", # attribute_id: # 0: hit points diff --git a/openage/convert/value_object/read/media/datfile/tech.py b/openage/convert/value_object/read/media/datfile/tech.py index a6720d3c25..54c350dadd 100644 --- a/openage/convert/value_object/read/media/datfile/tech.py +++ b/openage/convert/value_object/read/media/datfile/tech.py @@ -1,4 +1,4 @@ -# Copyright 2013-2023 the openage authors. See copying.md for legal info. +# Copyright 2013-2024 the openage authors. See copying.md for legal info. # TODO pylint: disable=C,R from __future__ import annotations @@ -31,7 +31,7 @@ def get_data_format_members( """ data_format = [ (READ_GEN, "type_id", StorageType.ID_MEMBER, EnumLookupMember( - raw_type="int8_t", + raw_type="uint8_t", type_name="effect_apply_type", lookup_dict=EFFECT_APPLY_TYPE )), From 4c3e811b62cfe9a2300065fabf0d4f2355440a2f Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Nov 2024 23:55:19 +0100 Subject: [PATCH 618/771] doc: Changlog for release 0.6.0. --- doc/changelogs/engine/v0.6.0.md | 76 +++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 doc/changelogs/engine/v0.6.0.md diff --git a/doc/changelogs/engine/v0.6.0.md b/doc/changelogs/engine/v0.6.0.md new file mode 100644 index 0000000000..18e981abc0 --- /dev/null +++ b/doc/changelogs/engine/v0.6.0.md @@ -0,0 +1,76 @@ +# [0.6.0] - 2024-11-26 +All notable changes for version [0.6.0] are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [0.4.0]. + +## Added + +- Flow field pathfinder +- Activity system for controlling the behavior of game entities +- Drag selection of game entities in the UI +- `clang-format` comment formatting +- Log messages for creation of uniform buffers +- nix flake +- GDB pretty printers for various internal data types + - Time types (`time::time_t`) + - Fixed point values + - Coordinate types + - openage arrays (`util::Vector`) + - Curve keyframes + - Flow field types +- Support for multiple meshes per terrain +- Frustum culling +- Example screenshots for the renderer demos +- Check for outdated modpacks on startup +- Camera boundaries to prevent camera movement outside of the map terrain +- Creation of temporary files/directories +- [Windows] Default paths for DLL searching on startup +- [Windows] DLL manager class in converter to support loading DLLs in multi-threaded conversion + +## Changed + +- Use multithreading for media export in the converter +- Rework curve container `Queue` to be more user-friendly + - `front(t)`/`pop_front(t)` now return the most recently added element before time `t` + - track lifetime of elements by storing insertion time (`alive` time) and erasure time (`dead` time) + - queue elements are now sorted by insertion time +- Curves now use `std::vector` for their keyframe storage to increase performance +- Window settings are passed as `struct` instead of arguments +- Sprite scaling is now handled in shader +- Replace `constexpr` with `consteval` where appropriate +- Optimize renderer + - Vectorize shader uniform (buffer) input storage + - Replace shared pointer usage with references +- Use raw pointers instead of shared pointers in pairing heap implementation + + +## Deprecated + +- Old pathfnder code (`libopenage/pathfinding`) + +## Removed + +- Exception propagation to Python with `_PyTraceback_Add` + +## Fixed + +- Bullet point formatting in event system documentation +- Uniform alignment in uniform buffers +- Input contexts are now handled in the correct order (top to bottom) +- Support for GCC 14 +- Support for Clang 19 +- DE2 conversion + - *The Mountain Royals* DLC + - *Battle for Greece* DLC +- Thread-safety of render entity +- Documentation for the single file converter +- Numerous documentation typos and mistakes +- [Windows] Order of inclusion for `DbgHelp.h` +- [Windows] Build instructions +- [macOS] Build instructions + + +## Full commit log + +https://github.com/SFTtech/openage/compare/v0.5.3...v0.6.0 From 1baaa9b90dbe8feeac9eddca32152e5bffb6b5b8 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 25 Nov 2024 23:55:51 +0100 Subject: [PATCH 619/771] Bump version to 0.6.0. --- openage_version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openage_version b/openage_version index be14282b7f..a918a2aa18 100644 --- a/openage_version +++ b/openage_version @@ -1 +1 @@ -0.5.3 +0.6.0 From 1d07cb51104bb5177e04693379d8f6b58d5bd503 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Sat, 30 Nov 2024 12:12:12 +0000 Subject: [PATCH 620/771] pathfinding_improvement --- libopenage/pathfinding/pathfinder.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index e46919e62b..6a6b81632d 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -231,8 +231,8 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req auto portal_pos = portal->get_exit_center(start_sector->get_id()); auto portal_abs_pos = sector_pos + portal_pos; auto heuristic_cost = Pathfinder::heuristic_cost(portal_abs_pos, request.target); - - portal_node->current_cost = 0; + std::cout << portal->get_id() << ": " << heuristic_cost << std::endl; + portal_node->current_cost = Pathfinder::heuristic_cost(portal_abs_pos, request.start); portal_node->heuristic_cost = heuristic_cost; portal_node->future_cost = portal_node->current_cost + heuristic_cost; From 7f7d5e222b7db4730cc86e0c74d503d300609e7b Mon Sep 17 00:00:00 2001 From: jere8184 Date: Sat, 30 Nov 2024 12:13:36 +0000 Subject: [PATCH 621/771] Update pathfinder.cpp --- libopenage/pathfinding/pathfinder.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 6a6b81632d..2fef36053a 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -231,7 +231,6 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req auto portal_pos = portal->get_exit_center(start_sector->get_id()); auto portal_abs_pos = sector_pos + portal_pos; auto heuristic_cost = Pathfinder::heuristic_cost(portal_abs_pos, request.target); - std::cout << portal->get_id() << ": " << heuristic_cost << std::endl; portal_node->current_cost = Pathfinder::heuristic_cost(portal_abs_pos, request.start); portal_node->heuristic_cost = heuristic_cost; portal_node->future_cost = portal_node->current_cost + heuristic_cost; From 5ad3d4c2ce0305390caff15f212cf6213ccb75df Mon Sep 17 00:00:00 2001 From: ZhuohaoHe Date: Wed, 27 Nov 2024 13:49:27 -0500 Subject: [PATCH 622/771] renderer: implement `is_complete()` to check if all uniforms have been set --- copying.md | 1 + libopenage/renderer/demo/demo_0.cpp | 7 ++++++- libopenage/renderer/demo/demo_1.cpp | 6 ++++++ libopenage/renderer/demo/demo_2.cpp | 5 +++++ libopenage/renderer/demo/demo_4.cpp | 5 +++++ libopenage/renderer/demo/demo_5.cpp | 5 +++++ libopenage/renderer/demo/demo_6.cpp | 12 ++++++++++++ libopenage/renderer/opengl/uniform_input.cpp | 9 +++++++++ libopenage/renderer/opengl/uniform_input.h | 5 +++++ libopenage/renderer/uniform_input.h | 2 ++ 10 files changed, 56 insertions(+), 1 deletion(-) diff --git a/copying.md b/copying.md index 61153fb9cf..3d97be2f8b 100644 --- a/copying.md +++ b/copying.md @@ -157,6 +157,7 @@ _the openage authors_ are: | Edvin Lindholm | EdvinLndh | edvinlndh à gmail dawt com | | Jeremiah Morgan | jere8184 | jeremiahmorgan dawt bham à outlook dawt com | | Tobias Alam | alamt22 | tobiasal à umich dawt edu | +| Alex Zhuohao He | ZzzhHe | zhuohao dawt he à outlook dawt com | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. diff --git a/libopenage/renderer/demo/demo_0.cpp b/libopenage/renderer/demo/demo_0.cpp index 38fd467d0b..16ce725aef 100644 --- a/libopenage/renderer/demo/demo_0.cpp +++ b/libopenage/renderer/demo/demo_0.cpp @@ -43,8 +43,13 @@ void renderer_demo_0(const util::Path &path) { auto display_shader = renderer->add_shader({display_vshader_src, display_fshader_src}); auto quad = renderer->add_mesh_geometry(resources::MeshData::make_quad()); + auto display_unif = display_shader->create_empty_input(); + /* Check if all uniform values for uniform inputs have been set */ + if (!display_unif->is_complete()) { + log::log(WARN << "Some Uniform values have not been set."); + } Renderable display_stuff{ - display_shader->create_empty_input(), + display_unif, quad, false, false, diff --git a/libopenage/renderer/demo/demo_1.cpp b/libopenage/renderer/demo/demo_1.cpp index 23dfdfa67b..65da534b2a 100644 --- a/libopenage/renderer/demo/demo_1.cpp +++ b/libopenage/renderer/demo/demo_1.cpp @@ -172,6 +172,12 @@ void renderer_demo_1(const util::Path &path) { auto pass2 = renderer->add_render_pass({display_obj}, renderer->get_display_target()); + /* Check if all uniform values for uniform inputs have been set */ + if (!obj1_unifs->is_complete() || !obj2_unifs->is_complete() || !obj3_unifs->is_complete() + || !proj_unif->is_complete() || !color_texture_unif->is_complete()) { + log::log(WARN << "Some Uniform values have not been set."); + } + /* Data retrieved from the object index texture. */ resources::Texture2dData id_texture_data = id_texture->into_data(); bool texture_data_valid = false; diff --git a/libopenage/renderer/demo/demo_2.cpp b/libopenage/renderer/demo/demo_2.cpp index e615d238ba..c5392f8590 100644 --- a/libopenage/renderer/demo/demo_2.cpp +++ b/libopenage/renderer/demo/demo_2.cpp @@ -224,6 +224,11 @@ void renderer_demo_2(const util::Path &path) { auto pass2 = renderer->add_render_pass({display_obj}, renderer->get_display_target()); + /* Check if all uniform values for uniform inputs have been set */ + if (!obj1_unifs->is_complete() || !proj_unif->is_complete() || !color_texture_unif->is_complete()) { + log::log(WARN << "Some Uniform values have not been set."); + } + /* Data retrieved from the object index texture. */ resources::Texture2dData id_texture_data = id_texture->into_data(); bool texture_data_valid = false; diff --git a/libopenage/renderer/demo/demo_4.cpp b/libopenage/renderer/demo/demo_4.cpp index fb39057e5d..515de4933b 100644 --- a/libopenage/renderer/demo/demo_4.cpp +++ b/libopenage/renderer/demo/demo_4.cpp @@ -165,6 +165,11 @@ void renderer_demo_4(const util::Path &path) { auto pass2 = renderer->add_render_pass({display_obj}, renderer->get_display_target()); + /* Check if all uniform values for uniform inputs have been set */ + if (!obj1_unifs->is_complete() || !proj_unif->is_complete() || !color_texture_unif->is_complete()) { + log::log(WARN << "Some Uniform values have not been set."); + } + window.add_resize_callback([&](size_t w, size_t h, double /*scale*/) { /* Calculate a projection matrix for the new screen size. */ float aspectRatio = float(w) / float(h); diff --git a/libopenage/renderer/demo/demo_5.cpp b/libopenage/renderer/demo/demo_5.cpp index 85752db18c..1361d274fa 100644 --- a/libopenage/renderer/demo/demo_5.cpp +++ b/libopenage/renderer/demo/demo_5.cpp @@ -134,6 +134,11 @@ void renderer_demo_5(const util::Path &path) { "tex", gltex); + /* Check if all uniform values for uniform inputs have been set */ + if (!transform_unifs->is_complete()) { + log::log(WARN << "Some Uniform values have not been set."); + } + // Move around the scene with WASD window.add_key_callback([&](const QKeyEvent &ev) { bool cam_update = false; diff --git a/libopenage/renderer/demo/demo_6.cpp b/libopenage/renderer/demo/demo_6.cpp index 9844503e64..1c80e50150 100644 --- a/libopenage/renderer/demo/demo_6.cpp +++ b/libopenage/renderer/demo/demo_6.cpp @@ -179,6 +179,10 @@ const std::vector RenderManagerDemo6::create_2d_obj() { this->obj_2d_texture, "tile_params", tile_params); + /* Check if all uniform values for uniform inputs have been set */ + if (!animation_2d_unifs->is_complete()) { + log::log(WARN << "Some Uniform values have not been set."); + } auto quad = this->renderer->add_mesh_geometry(resources::MeshData::make_quad()); Renderable animation_2d_obj{ animation_2d_unifs, @@ -198,6 +202,11 @@ const renderer::Renderable RenderManagerDemo6::create_3d_obj() { auto terrain_unifs = this->obj_3d_shader->new_uniform_input( "tex", this->obj_3d_texture); + /* Check if all uniform values for uniform inputs have been set */ + if (!terrain_unifs->is_complete()) { + log::log(WARN << "Some Uniform values have not been set."); + } + std::vector terrain_pos{}; terrain_pos.push_back({-25, -25, 0}); terrain_pos.push_back({25, -25, 0}); @@ -260,6 +269,9 @@ const std::vector RenderManagerDemo6::create_frame_obj() { frame_size, "incol", Eigen::Vector4f{0.0f, 0.0f, 1.0f, 1.0f}); + if (!frame_unifs->is_complete()) { + log::log(WARN << "Some Uniform values have not been set."); + } Renderable frame_obj{ frame_unifs, frame_geometry, diff --git a/libopenage/renderer/opengl/uniform_input.cpp b/libopenage/renderer/opengl/uniform_input.cpp index 38c63f2d66..8c021959b7 100644 --- a/libopenage/renderer/opengl/uniform_input.cpp +++ b/libopenage/renderer/opengl/uniform_input.cpp @@ -50,4 +50,13 @@ GlUniformBufferInput::GlUniformBufferInput(const std::shared_ptr this->update_data.resize(offset); } +bool GlUniformInput::is_complete() const { + for (const auto& uniform : this->update_offs) { + if (!uniform.used) { + return false; + } + } + return true; +} + } // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/uniform_input.h b/libopenage/renderer/opengl/uniform_input.h index 961e12a2a5..d635836b05 100644 --- a/libopenage/renderer/opengl/uniform_input.h +++ b/libopenage/renderer/opengl/uniform_input.h @@ -35,6 +35,11 @@ class GlUniformInput final : public UniformInput { public: GlUniformInput(const std::shared_ptr &prog); + /** + * Check if all uniforms have been set. + */ + bool is_complete() const override; + /** * Store the IDs of the uniforms from the shader set by this uniform input. */ diff --git a/libopenage/renderer/uniform_input.h b/libopenage/renderer/uniform_input.h index 6f2536459b..39bb4ea983 100644 --- a/libopenage/renderer/uniform_input.h +++ b/libopenage/renderer/uniform_input.h @@ -66,6 +66,8 @@ class UniformInput : public DataInput { public: virtual ~UniformInput() = default; + virtual bool is_complete() const = 0; + void update() override; void update(const char *unif, int32_t val) override; void update(const char *unif, uint32_t val) override; From a107605d80c9c931fd92f048a597ee24cfef6ee8 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Fri, 29 Nov 2024 20:57:54 -0500 Subject: [PATCH 623/771] add util function in demo to check all uniforms --- libopenage/renderer/demo/demo_0.cpp | 12 ++++++------ libopenage/renderer/demo/demo_1.cpp | 7 +++---- libopenage/renderer/demo/demo_2.cpp | 6 +++--- libopenage/renderer/demo/demo_4.cpp | 6 +++--- libopenage/renderer/demo/demo_5.cpp | 6 +++--- libopenage/renderer/demo/demo_6.cpp | 16 +++++----------- libopenage/renderer/demo/util.cpp | 14 ++++++++++++++ libopenage/renderer/demo/util.h | 10 ++++++++++ libopenage/renderer/opengl/uniform_input.cpp | 12 ++++++------ 9 files changed, 53 insertions(+), 36 deletions(-) diff --git a/libopenage/renderer/demo/demo_0.cpp b/libopenage/renderer/demo/demo_0.cpp index 16ce725aef..d1d679c0e4 100644 --- a/libopenage/renderer/demo/demo_0.cpp +++ b/libopenage/renderer/demo/demo_0.cpp @@ -9,6 +9,7 @@ #include "renderer/resources/mesh_data.h" #include "renderer/resources/shader_source.h" #include "renderer/shader_program.h" +#include "renderer/demo/util.h" namespace openage::renderer::tests { @@ -43,18 +44,17 @@ void renderer_demo_0(const util::Path &path) { auto display_shader = renderer->add_shader({display_vshader_src, display_fshader_src}); auto quad = renderer->add_mesh_geometry(resources::MeshData::make_quad()); - auto display_unif = display_shader->create_empty_input(); - /* Check if all uniform values for uniform inputs have been set */ - if (!display_unif->is_complete()) { - log::log(WARN << "Some Uniform values have not been set."); - } Renderable display_stuff{ - display_unif, + display_shader->create_empty_input(), quad, false, false, }; + if (!check_uniform_completeness({display_stuff})) { + log::log(WARN << "Uniforms not complete."); + } + auto pass = renderer->add_render_pass({display_stuff}, renderer->get_display_target()); while (not window.should_close()) { diff --git a/libopenage/renderer/demo/demo_1.cpp b/libopenage/renderer/demo/demo_1.cpp index 65da534b2a..2fb3ce6201 100644 --- a/libopenage/renderer/demo/demo_1.cpp +++ b/libopenage/renderer/demo/demo_1.cpp @@ -15,6 +15,7 @@ #include "renderer/shader_program.h" #include "renderer/texture.h" #include "util/math_constants.h" +#include "renderer/demo/util.h" namespace openage::renderer::tests { @@ -172,10 +173,8 @@ void renderer_demo_1(const util::Path &path) { auto pass2 = renderer->add_render_pass({display_obj}, renderer->get_display_target()); - /* Check if all uniform values for uniform inputs have been set */ - if (!obj1_unifs->is_complete() || !obj2_unifs->is_complete() || !obj3_unifs->is_complete() - || !proj_unif->is_complete() || !color_texture_unif->is_complete()) { - log::log(WARN << "Some Uniform values have not been set."); + if (!check_uniform_completeness({obj1, obj2, obj3, proj_update, display_obj})) { + log::log(WARN << "Uniforms not complete."); } /* Data retrieved from the object index texture. */ diff --git a/libopenage/renderer/demo/demo_2.cpp b/libopenage/renderer/demo/demo_2.cpp index c5392f8590..863ea8c979 100644 --- a/libopenage/renderer/demo/demo_2.cpp +++ b/libopenage/renderer/demo/demo_2.cpp @@ -18,6 +18,7 @@ #include "renderer/resources/texture_data.h" #include "renderer/shader_program.h" #include "renderer/texture.h" +#include "renderer/demo/util.h" namespace openage::renderer::tests { @@ -224,9 +225,8 @@ void renderer_demo_2(const util::Path &path) { auto pass2 = renderer->add_render_pass({display_obj}, renderer->get_display_target()); - /* Check if all uniform values for uniform inputs have been set */ - if (!obj1_unifs->is_complete() || !proj_unif->is_complete() || !color_texture_unif->is_complete()) { - log::log(WARN << "Some Uniform values have not been set."); + if (!check_uniform_completeness({proj_update, obj1, display_obj})) { + log::log(WARN << "Uniforms not complete."); } /* Data retrieved from the object index texture. */ diff --git a/libopenage/renderer/demo/demo_4.cpp b/libopenage/renderer/demo/demo_4.cpp index 515de4933b..35c795c427 100644 --- a/libopenage/renderer/demo/demo_4.cpp +++ b/libopenage/renderer/demo/demo_4.cpp @@ -16,6 +16,7 @@ #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_data.h" #include "renderer/shader_program.h" +#include "renderer/demo/util.h" #include "time/clock.h" @@ -165,9 +166,8 @@ void renderer_demo_4(const util::Path &path) { auto pass2 = renderer->add_render_pass({display_obj}, renderer->get_display_target()); - /* Check if all uniform values for uniform inputs have been set */ - if (!obj1_unifs->is_complete() || !proj_unif->is_complete() || !color_texture_unif->is_complete()) { - log::log(WARN << "Some Uniform values have not been set."); + if (!check_uniform_completeness({proj_update, obj1, display_obj})) { + log::log(WARN << "Uniforms not complete."); } window.add_resize_callback([&](size_t w, size_t h, double /*scale*/) { diff --git a/libopenage/renderer/demo/demo_5.cpp b/libopenage/renderer/demo/demo_5.cpp index 1361d274fa..744c1b9fc7 100644 --- a/libopenage/renderer/demo/demo_5.cpp +++ b/libopenage/renderer/demo/demo_5.cpp @@ -17,6 +17,7 @@ #include "renderer/shader_program.h" #include "renderer/uniform_buffer.h" #include "renderer/uniform_input.h" +#include "renderer/demo/util.h" namespace openage::renderer::tests { @@ -134,9 +135,8 @@ void renderer_demo_5(const util::Path &path) { "tex", gltex); - /* Check if all uniform values for uniform inputs have been set */ - if (!transform_unifs->is_complete()) { - log::log(WARN << "Some Uniform values have not been set."); + if (!check_uniform_completeness({terrain_obj})) { + log::log(WARN << "Uniforms not complete."); } // Move around the scene with WASD diff --git a/libopenage/renderer/demo/demo_6.cpp b/libopenage/renderer/demo/demo_6.cpp index 1c80e50150..016d9d41a3 100644 --- a/libopenage/renderer/demo/demo_6.cpp +++ b/libopenage/renderer/demo/demo_6.cpp @@ -23,6 +23,7 @@ #include "renderer/shader_program.h" #include "renderer/texture.h" #include "renderer/uniform_buffer.h" +#include "renderer/demo/util.h" #include "time/clock.h" #include "util/path.h" #include "util/vector.h" @@ -179,10 +180,6 @@ const std::vector RenderManagerDemo6::create_2d_obj() { this->obj_2d_texture, "tile_params", tile_params); - /* Check if all uniform values for uniform inputs have been set */ - if (!animation_2d_unifs->is_complete()) { - log::log(WARN << "Some Uniform values have not been set."); - } auto quad = this->renderer->add_mesh_geometry(resources::MeshData::make_quad()); Renderable animation_2d_obj{ animation_2d_unifs, @@ -202,10 +199,6 @@ const renderer::Renderable RenderManagerDemo6::create_3d_obj() { auto terrain_unifs = this->obj_3d_shader->new_uniform_input( "tex", this->obj_3d_texture); - /* Check if all uniform values for uniform inputs have been set */ - if (!terrain_unifs->is_complete()) { - log::log(WARN << "Some Uniform values have not been set."); - } std::vector terrain_pos{}; terrain_pos.push_back({-25, -25, 0}); @@ -269,9 +262,6 @@ const std::vector RenderManagerDemo6::create_frame_obj() { frame_size, "incol", Eigen::Vector4f{0.0f, 0.0f, 1.0f, 1.0f}); - if (!frame_unifs->is_complete()) { - log::log(WARN << "Some Uniform values have not been set."); - } Renderable frame_obj{ frame_unifs, frame_geometry, @@ -499,6 +489,10 @@ void RenderManagerDemo6::create_render_passes() { this->display_pass = renderer->add_render_pass( {display_obj_3d, display_obj_2d, display_obj_frame}, renderer->get_display_target()); + + if (!check_uniform_completeness({display_obj_3d, display_obj_2d, display_obj_frame})) { + log::log(WARN << "Uniforms not complete."); + } } } // namespace openage::renderer::tests diff --git a/libopenage/renderer/demo/util.cpp b/libopenage/renderer/demo/util.cpp index aafffebf8d..b160e9251c 100644 --- a/libopenage/renderer/demo/util.cpp +++ b/libopenage/renderer/demo/util.cpp @@ -1,8 +1,22 @@ // Copyright 2015-2023 the openage authors. See copying.md for legal info. #include "util.h" +#include "renderer/uniform_input.h" namespace openage::renderer::tests { +bool check_uniform_completeness(const std::vector &renderables) { + bool all_complete = true; + + // Iterate over each renderable object + for (const auto &renderable : renderables) { + if (renderable.uniform && !renderable.uniform->is_complete()) { + all_complete = false; + } + } + + return all_complete; +} + } // namespace openage::renderer::tests diff --git a/libopenage/renderer/demo/util.h b/libopenage/renderer/demo/util.h index 9899867e2b..cfb19f665a 100644 --- a/libopenage/renderer/demo/util.h +++ b/libopenage/renderer/demo/util.h @@ -2,6 +2,8 @@ #pragma once +#include +#include "renderer/renderable.h" namespace openage::renderer::tests { @@ -11,4 +13,12 @@ namespace openage::renderer::tests { opengl::GlContext::check_error(); \ printf("after %s\n", txt); +/** + * Check if all uniform values for the given renderables have been set. + * + * @param renderables The list of renderable objects to check. + * @return true if all uniforms have been set, false otherwise. + */ +bool check_uniform_completeness(const std::vector &renderables); + } // namespace openage::renderer::tests diff --git a/libopenage/renderer/opengl/uniform_input.cpp b/libopenage/renderer/opengl/uniform_input.cpp index 8c021959b7..4a8a4b0496 100644 --- a/libopenage/renderer/opengl/uniform_input.cpp +++ b/libopenage/renderer/opengl/uniform_input.cpp @@ -51,12 +51,12 @@ GlUniformBufferInput::GlUniformBufferInput(const std::shared_ptr } bool GlUniformInput::is_complete() const { - for (const auto& uniform : this->update_offs) { - if (!uniform.used) { - return false; - } - } - return true; + for (const auto& uniform : this->update_offs) { + if (!uniform.used) { + return false; + } + } + return true; } } // namespace openage::renderer::opengl From 1ea8aefd79d41e5adfa0225880720bd76ce89afd Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Fri, 29 Nov 2024 21:52:27 -0500 Subject: [PATCH 624/771] Minor code cleanup and formatting fixes --- libopenage/renderer/demo/util.cpp | 2 +- libopenage/renderer/demo/util.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/renderer/demo/util.cpp b/libopenage/renderer/demo/util.cpp index b160e9251c..d94a0d6209 100644 --- a/libopenage/renderer/demo/util.cpp +++ b/libopenage/renderer/demo/util.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "util.h" #include "renderer/uniform_input.h" diff --git a/libopenage/renderer/demo/util.h b/libopenage/renderer/demo/util.h index cfb19f665a..a584d94af9 100644 --- a/libopenage/renderer/demo/util.h +++ b/libopenage/renderer/demo/util.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 From 006cdf27cd37129933a2c9f7f25479682f878754 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Sat, 30 Nov 2024 11:07:26 -0500 Subject: [PATCH 625/771] format code using clang-format and simplify the check loop --- libopenage/renderer/demo/demo_0.cpp | 4 ++-- libopenage/renderer/demo/demo_1.cpp | 4 ++-- libopenage/renderer/demo/demo_2.cpp | 4 ++-- libopenage/renderer/demo/demo_4.cpp | 4 ++-- libopenage/renderer/demo/demo_5.cpp | 4 ++-- libopenage/renderer/demo/demo_6.cpp | 4 ++-- libopenage/renderer/demo/util.cpp | 9 ++++----- libopenage/renderer/demo/util.h | 1 + libopenage/renderer/opengl/uniform_input.cpp | 2 +- libopenage/renderer/opengl/uniform_input.h | 8 ++++---- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/libopenage/renderer/demo/demo_0.cpp b/libopenage/renderer/demo/demo_0.cpp index d1d679c0e4..d027c2d1e0 100644 --- a/libopenage/renderer/demo/demo_0.cpp +++ b/libopenage/renderer/demo/demo_0.cpp @@ -2,6 +2,7 @@ #include "demo_0.h" +#include "renderer/demo/util.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" #include "renderer/render_pass.h" @@ -9,7 +10,6 @@ #include "renderer/resources/mesh_data.h" #include "renderer/resources/shader_source.h" #include "renderer/shader_program.h" -#include "renderer/demo/util.h" namespace openage::renderer::tests { @@ -51,7 +51,7 @@ void renderer_demo_0(const util::Path &path) { false, }; - if (!check_uniform_completeness({display_stuff})) { + if (not check_uniform_completeness({display_stuff})) { log::log(WARN << "Uniforms not complete."); } diff --git a/libopenage/renderer/demo/demo_1.cpp b/libopenage/renderer/demo/demo_1.cpp index 2fb3ce6201..baa471b7b2 100644 --- a/libopenage/renderer/demo/demo_1.cpp +++ b/libopenage/renderer/demo/demo_1.cpp @@ -6,6 +6,7 @@ #include #include +#include "renderer/demo/util.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" #include "renderer/render_pass.h" @@ -15,7 +16,6 @@ #include "renderer/shader_program.h" #include "renderer/texture.h" #include "util/math_constants.h" -#include "renderer/demo/util.h" namespace openage::renderer::tests { @@ -173,7 +173,7 @@ void renderer_demo_1(const util::Path &path) { auto pass2 = renderer->add_render_pass({display_obj}, renderer->get_display_target()); - if (!check_uniform_completeness({obj1, obj2, obj3, proj_update, display_obj})) { + if (not check_uniform_completeness({obj1, obj2, obj3, proj_update, display_obj})) { log::log(WARN << "Uniforms not complete."); } diff --git a/libopenage/renderer/demo/demo_2.cpp b/libopenage/renderer/demo/demo_2.cpp index 863ea8c979..1c92469bb5 100644 --- a/libopenage/renderer/demo/demo_2.cpp +++ b/libopenage/renderer/demo/demo_2.cpp @@ -6,6 +6,7 @@ #include #include +#include "renderer/demo/util.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" #include "renderer/render_pass.h" @@ -18,7 +19,6 @@ #include "renderer/resources/texture_data.h" #include "renderer/shader_program.h" #include "renderer/texture.h" -#include "renderer/demo/util.h" namespace openage::renderer::tests { @@ -225,7 +225,7 @@ void renderer_demo_2(const util::Path &path) { auto pass2 = renderer->add_render_pass({display_obj}, renderer->get_display_target()); - if (!check_uniform_completeness({proj_update, obj1, display_obj})) { + if (not check_uniform_completeness({proj_update, obj1, display_obj})) { log::log(WARN << "Uniforms not complete."); } diff --git a/libopenage/renderer/demo/demo_4.cpp b/libopenage/renderer/demo/demo_4.cpp index 35c795c427..7f4704a38a 100644 --- a/libopenage/renderer/demo/demo_4.cpp +++ b/libopenage/renderer/demo/demo_4.cpp @@ -5,6 +5,7 @@ #include #include +#include "renderer/demo/util.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" #include "renderer/render_pass.h" @@ -16,7 +17,6 @@ #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_data.h" #include "renderer/shader_program.h" -#include "renderer/demo/util.h" #include "time/clock.h" @@ -166,7 +166,7 @@ void renderer_demo_4(const util::Path &path) { auto pass2 = renderer->add_render_pass({display_obj}, renderer->get_display_target()); - if (!check_uniform_completeness({proj_update, obj1, display_obj})) { + if (not check_uniform_completeness({proj_update, obj1, display_obj})) { log::log(WARN << "Uniforms not complete."); } diff --git a/libopenage/renderer/demo/demo_5.cpp b/libopenage/renderer/demo/demo_5.cpp index 744c1b9fc7..34e8f4db22 100644 --- a/libopenage/renderer/demo/demo_5.cpp +++ b/libopenage/renderer/demo/demo_5.cpp @@ -7,6 +7,7 @@ #include #include "renderer/camera/camera.h" +#include "renderer/demo/util.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" #include "renderer/render_pass.h" @@ -17,7 +18,6 @@ #include "renderer/shader_program.h" #include "renderer/uniform_buffer.h" #include "renderer/uniform_input.h" -#include "renderer/demo/util.h" namespace openage::renderer::tests { @@ -135,7 +135,7 @@ void renderer_demo_5(const util::Path &path) { "tex", gltex); - if (!check_uniform_completeness({terrain_obj})) { + if (not check_uniform_completeness({terrain_obj})) { log::log(WARN << "Uniforms not complete."); } diff --git a/libopenage/renderer/demo/demo_6.cpp b/libopenage/renderer/demo/demo_6.cpp index 016d9d41a3..e8beaafc6c 100644 --- a/libopenage/renderer/demo/demo_6.cpp +++ b/libopenage/renderer/demo/demo_6.cpp @@ -9,6 +9,7 @@ #include "renderer/camera/camera.h" #include "renderer/camera/frustum_2d.h" #include "renderer/camera/frustum_3d.h" +#include "renderer/demo/util.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" #include "renderer/render_pass.h" @@ -23,7 +24,6 @@ #include "renderer/shader_program.h" #include "renderer/texture.h" #include "renderer/uniform_buffer.h" -#include "renderer/demo/util.h" #include "time/clock.h" #include "util/path.h" #include "util/vector.h" @@ -490,7 +490,7 @@ void RenderManagerDemo6::create_render_passes() { {display_obj_3d, display_obj_2d, display_obj_frame}, renderer->get_display_target()); - if (!check_uniform_completeness({display_obj_3d, display_obj_2d, display_obj_frame})) { + if (not check_uniform_completeness({display_obj_3d, display_obj_2d, display_obj_frame})) { log::log(WARN << "Uniforms not complete."); } } diff --git a/libopenage/renderer/demo/util.cpp b/libopenage/renderer/demo/util.cpp index d94a0d6209..04f7ad6b7d 100644 --- a/libopenage/renderer/demo/util.cpp +++ b/libopenage/renderer/demo/util.cpp @@ -1,22 +1,21 @@ // Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "util.h" + #include "renderer/uniform_input.h" namespace openage::renderer::tests { bool check_uniform_completeness(const std::vector &renderables) { - bool all_complete = true; - // Iterate over each renderable object for (const auto &renderable : renderables) { - if (renderable.uniform && !renderable.uniform->is_complete()) { - all_complete = false; + if (renderable.uniform && not renderable.uniform->is_complete()) { + return false; } } - return all_complete; + return true; } } // namespace openage::renderer::tests diff --git a/libopenage/renderer/demo/util.h b/libopenage/renderer/demo/util.h index a584d94af9..8d8130a306 100644 --- a/libopenage/renderer/demo/util.h +++ b/libopenage/renderer/demo/util.h @@ -3,6 +3,7 @@ #pragma once #include + #include "renderer/renderable.h" namespace openage::renderer::tests { diff --git a/libopenage/renderer/opengl/uniform_input.cpp b/libopenage/renderer/opengl/uniform_input.cpp index 4a8a4b0496..2be81e287f 100644 --- a/libopenage/renderer/opengl/uniform_input.cpp +++ b/libopenage/renderer/opengl/uniform_input.cpp @@ -51,7 +51,7 @@ GlUniformBufferInput::GlUniformBufferInput(const std::shared_ptr } bool GlUniformInput::is_complete() const { - for (const auto& uniform : this->update_offs) { + for (const auto &uniform : this->update_offs) { if (!uniform.used) { return false; } diff --git a/libopenage/renderer/opengl/uniform_input.h b/libopenage/renderer/opengl/uniform_input.h index d635836b05..c46c235414 100644 --- a/libopenage/renderer/opengl/uniform_input.h +++ b/libopenage/renderer/opengl/uniform_input.h @@ -35,10 +35,10 @@ class GlUniformInput final : public UniformInput { public: GlUniformInput(const std::shared_ptr &prog); - /** - * Check if all uniforms have been set. - */ - bool is_complete() const override; + /** + * Check if all uniforms have been set. + */ + bool is_complete() const override; /** * Store the IDs of the uniforms from the shader set by this uniform input. From c15fe6fd9af25d4188558808f5c86e5d2fb875c3 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Sat, 30 Nov 2024 14:26:10 -0500 Subject: [PATCH 626/771] update `!` to `not` --- libopenage/renderer/demo/demo_1.cpp | 2 +- libopenage/renderer/demo/demo_2.cpp | 2 +- libopenage/renderer/opengl/uniform_input.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libopenage/renderer/demo/demo_1.cpp b/libopenage/renderer/demo/demo_1.cpp index baa471b7b2..c28b7dc296 100644 --- a/libopenage/renderer/demo/demo_1.cpp +++ b/libopenage/renderer/demo/demo_1.cpp @@ -193,7 +193,7 @@ void renderer_demo_1(const util::Path &path) { ssize_t y = qpos.y(); log::log(INFO << "Clicked at location (" << x << ", " << y << ")"); - if (!texture_data_valid) { + if (not texture_data_valid) { id_texture_data = id_texture->into_data(); texture_data_valid = true; } diff --git a/libopenage/renderer/demo/demo_2.cpp b/libopenage/renderer/demo/demo_2.cpp index 1c92469bb5..c2a223d51b 100644 --- a/libopenage/renderer/demo/demo_2.cpp +++ b/libopenage/renderer/demo/demo_2.cpp @@ -248,7 +248,7 @@ void renderer_demo_2(const util::Path &path) { ssize_t y = qpos.y(); log::log(INFO << "Clicked at location (" << x << ", " << y << ")"); - if (!texture_data_valid) { + if (not texture_data_valid) { id_texture_data = id_texture->into_data(); texture_data_valid = true; } diff --git a/libopenage/renderer/opengl/uniform_input.cpp b/libopenage/renderer/opengl/uniform_input.cpp index 2be81e287f..e6caed4ca8 100644 --- a/libopenage/renderer/opengl/uniform_input.cpp +++ b/libopenage/renderer/opengl/uniform_input.cpp @@ -52,7 +52,7 @@ GlUniformBufferInput::GlUniformBufferInput(const std::shared_ptr bool GlUniformInput::is_complete() const { for (const auto &uniform : this->update_offs) { - if (!uniform.used) { + if (not uniform.used) { return false; } } From 89719a9e882b9189375bebcaa02c21ba2b183ec7 Mon Sep 17 00:00:00 2001 From: Christoph Heine Date: Tue, 3 Dec 2024 04:38:23 +0100 Subject: [PATCH 627/771] Change '&&' to 'and'. --- libopenage/renderer/demo/util.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/renderer/demo/util.cpp b/libopenage/renderer/demo/util.cpp index 04f7ad6b7d..0f39691cdb 100644 --- a/libopenage/renderer/demo/util.cpp +++ b/libopenage/renderer/demo/util.cpp @@ -10,7 +10,7 @@ namespace openage::renderer::tests { bool check_uniform_completeness(const std::vector &renderables) { // Iterate over each renderable object for (const auto &renderable : renderables) { - if (renderable.uniform && not renderable.uniform->is_complete()) { + if (renderable.uniform and not renderable.uniform->is_complete()) { return false; } } From 8f396007b88a3b3f883fd9d877af883e580b9ad1 Mon Sep 17 00:00:00 2001 From: jere8184 Date: Mon, 2 Dec 2024 23:55:27 +0000 Subject: [PATCH 628/771] Update curves.md --- doc/code/curves.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/code/curves.md b/doc/code/curves.md index 376c4c938c..ff4bbb248d 100644 --- a/doc/code/curves.md +++ b/doc/code/curves.md @@ -42,7 +42,7 @@ directly invalidating the state, making curves more reliable in async scenarios resolving dependencies for keyframes in the past can still be challenging). The usage of curves has a few downsides though. They are less space efficient due to the -keyframe storage, interpolation are more costly more costly than incremental changes, and +keyframe storage, interpolation are more costly than incremental changes, and their integration is more complex than the usage of simpler data structures. However, in situations where operations are predictable, long-lasting, and easy to calculate - which is the case for most RTS games - the positives may outweigh the downsides. From b0346085291c180dcfb4fa3123703ba85acb3827 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Tue, 3 Dec 2024 11:45:49 -0500 Subject: [PATCH 629/771] update .py and .pyx by adding new window arguments --- openage/game/main.py | 21 +++++++++++++++++++++ openage/game/main_cpp.pyx | 8 +++++++- openage/main/main.py | 21 +++++++++++++++++++++ openage/main/main_cpp.pyx | 8 +++++++- 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/openage/game/main.py b/openage/game/main.py index 0fdf007259..44b74ee058 100644 --- a/openage/game/main.py +++ b/openage/game/main.py @@ -36,6 +36,19 @@ def init_subparser(cli: ArgumentParser) -> None: help="Check if the assets are up to date" ) + cli.add_argument( + "--window-size", nargs=2, type=int, default=[1024, 768], + metavar=('WIDTH', 'HEIGHT'), + help="initial window size in pixels, e.g., --window-size 1024 768") + + cli.add_argument( + "--vsync", action='store_true', + help="enable vertical synchronization") + + cli.add_argument( + "--window-mode", choices=["fullscreen", "borderless", "windowed"], default="windowed", + help="set the window mode: fullscreen, borderless, or windowed (default)") + def main(args, error): """ @@ -98,5 +111,13 @@ def main(args, error): # encode modpacks as bytes for the C++ interface args.modpacks = [modpack.encode('utf-8') for modpack in args.modpacks] + # Pass window parameters to engine + args.window_args = { + "width": args.window_size[0], + "height": args.window_size[1], + "vsync": args.vsync, + "window_mode": args.window_mode, + } + # start the game, continue in main_cpp.pyx! return run_game(args, root) diff --git a/openage/game/main_cpp.pyx b/openage/game/main_cpp.pyx index e7697b0ddb..d37545f87c 100644 --- a/openage/game/main_cpp.pyx +++ b/openage/game/main_cpp.pyx @@ -1,4 +1,4 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. from cpython.ref cimport PyObject from libcpp.string cimport string @@ -37,6 +37,12 @@ def run_game(args, root_path): else: args_cpp.mods = vector[string]() + # window + args_cpp.window_args.width = args.window_args["width"] + args_cpp.window_args.height = args.window_args["height"] + args_cpp.window_args.vsync = args.window_args["vsync"] + args_cpp.window_args.mode = args.window_args["window_mode"].encode('utf-8') + # run the game! with nogil: result = run_game_cpp(args_cpp) diff --git a/openage/main/main.py b/openage/main/main.py index fbeb527ee4..5fa628c3e5 100644 --- a/openage/main/main.py +++ b/openage/main/main.py @@ -28,6 +28,19 @@ def init_subparser(cli: ArgumentParser): "--modpacks", nargs="+", type=str, help="list of modpacks to load") + cli.add_argument( + "--window-size", nargs=2, type=int, default=[1024, 768], + metavar=('WIDTH', 'HEIGHT'), + help="initial window size in pixels, e.g., --window-size 1024 768") + + cli.add_argument( + "--vsync", action='store_true', + help="enable vertical synchronization") + + cli.add_argument( + "--window-mode", choices=["fullscreen", "borderless", "windowed"], default="windowed", + help="set the window mode: fullscreen, borderless, or windowed (default)") + def main(args, error): """ @@ -106,5 +119,13 @@ def main(args, error): else: args.modpacks = [query_modpack(list(available_modpacks.keys())).encode("utf-8")] + # Pass window parameters to engine + args.window_args = { + "width": args.window_size[0], + "height": args.window_size[1], + "vsync": args.vsync, + "window_mode": args.window_mode, + } + # start the game, continue in main_cpp.pyx! return run_game(args, root) diff --git a/openage/main/main_cpp.pyx b/openage/main/main_cpp.pyx index e7697b0ddb..d37545f87c 100644 --- a/openage/main/main_cpp.pyx +++ b/openage/main/main_cpp.pyx @@ -1,4 +1,4 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. from cpython.ref cimport PyObject from libcpp.string cimport string @@ -37,6 +37,12 @@ def run_game(args, root_path): else: args_cpp.mods = vector[string]() + # window + args_cpp.window_args.width = args.window_args["width"] + args_cpp.window_args.height = args.window_args["height"] + args_cpp.window_args.vsync = args.window_args["vsync"] + args_cpp.window_args.mode = args.window_args["window_mode"].encode('utf-8') + # run the game! with nogil: result = run_game_cpp(args_cpp) From a4335bef262429aa43fb75e0af0d9daee2bb47a0 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Wed, 4 Dec 2024 16:24:20 -0500 Subject: [PATCH 630/771] update main, engine, window and presenter to get window arguments passed from py --- libopenage/engine/engine.cpp | 7 ++++--- libopenage/engine/engine.h | 6 +++++- libopenage/main.cpp | 22 ++++++++++++++++++++-- libopenage/main.h | 22 +++++++++++++++++++++- libopenage/presenter/presenter.cpp | 13 ++++--------- libopenage/presenter/presenter.h | 8 ++++++-- libopenage/renderer/opengl/window.cpp | 15 +++++++++++++++ libopenage/renderer/window.h | 11 +++++++++++ 8 files changed, 86 insertions(+), 18 deletions(-) diff --git a/libopenage/engine/engine.cpp b/libopenage/engine/engine.cpp index fa2c251b4d..dc787ebcf1 100644 --- a/libopenage/engine/engine.cpp +++ b/libopenage/engine/engine.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 "engine.h" @@ -16,7 +16,8 @@ namespace openage::engine { Engine::Engine(mode mode, const util::Path &root_dir, const std::vector &mods, - bool debug_graphics) : + bool debug_graphics, + const renderer::window_settings &window_settings) : running{true}, run_mode{mode}, root_dir{root_dir}, @@ -56,7 +57,7 @@ Engine::Engine(mode mode, // if presenter is used, run it in a separate thread if (this->run_mode == mode::FULL) { this->threads.emplace_back([&, debug_graphics]() { - this->presenter->run(debug_graphics); + this->presenter->run(debug_graphics, window_settings); // Make sure that the presenter gets destructed in the same thread // otherwise OpenGL complains about missing contexts diff --git a/libopenage/engine/engine.h b/libopenage/engine/engine.h index 1fe4298061..d324fe4fda 100644 --- a/libopenage/engine/engine.h +++ b/libopenage/engine/engine.h @@ -9,6 +9,8 @@ #include "util/path.h" +#include + // TODO: Remove custom jthread definition when clang/libc++ finally supports it #if __llvm__ #if !__cpp_lib_jthread @@ -72,11 +74,13 @@ class Engine { * @param root_dir openage root directory. * @param mods The mods to load. * @param debug_graphics If true, enable OpenGL debug logging. + * @param window_settings window display setting */ Engine(mode mode, const util::Path &root_dir, const std::vector &mods, - bool debug_graphics = false); + bool debug_graphics = false, + const renderer::window_settings &window_settings = {}); // engine should not be copied or moved Engine(const Engine &) = delete; diff --git a/libopenage/main.cpp b/libopenage/main.cpp index e7794be40c..f8e6b45f3f 100644 --- a/libopenage/main.cpp +++ b/libopenage/main.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "main.h" @@ -31,7 +31,25 @@ int run_game(const main_arguments &args) { run_mode = openage::engine::Engine::mode::HEADLESS; } - openage::engine::Engine engine{run_mode, args.root_path, args.mods, args.gl_debug}; + // convert window arguments to window settings + renderer::window_settings win_settings = {}; + win_settings.width = args.window_args.width; + win_settings.height = args.window_args.height; + win_settings.vsync = args.window_args.vsync; + + renderer::window_mode wmode; + if (args.window_args.mode == "fullscreen") { + wmode = renderer::window_mode::FULLSCREEN; + } + else if (args.window_args.mode == "borderless") { + wmode = renderer::window_mode::BORDERLESS; + } + else { + wmode = renderer::window_mode::WINDOWED; + } + win_settings.mode = wmode; + + openage::engine::Engine engine{run_mode, args.root_path, args.mods, args.gl_debug, win_settings}; engine.loop(); diff --git a/libopenage/main.h b/libopenage/main.h index 0e60c386cc..e99b374642 100644 --- a/libopenage/main.h +++ b/libopenage/main.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #pragma once @@ -16,6 +16,24 @@ namespace openage { +/** + * Window parameters struct. + * + * pxd: + * + * cppclass window_arguments: + * int width + * int height + * bool vsync + * string mode + */ +struct window_arguments { + int width; + int height; + bool vsync; + std::string mode; +}; + /** * Used for passing arguments to run_game. * @@ -26,12 +44,14 @@ namespace openage { * bool gl_debug * bool headless * vector[string] mods + * window_arguments window_args */ struct main_arguments { util::Path root_path; bool gl_debug; bool headless; std::vector mods; + window_arguments window_args; }; diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index a37362be95..6d77a72710 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -32,7 +32,6 @@ #include "renderer/stages/skybox/render_stage.h" #include "renderer/stages/terrain/render_stage.h" #include "renderer/stages/world/render_stage.h" -#include "renderer/window.h" #include "time/time_loop.h" #include "util/path.h" @@ -48,10 +47,10 @@ Presenter::Presenter(const util::Path &root_dir, time_loop{time_loop} {} -void Presenter::run(bool debug_graphics) { +void Presenter::run(bool debug_graphics, const renderer::window_settings &window_settings) { log::log(INFO << "Presenter: Launching subsystems..."); - this->init_graphics(debug_graphics); + this->init_graphics(debug_graphics, window_settings); this->init_input(); @@ -93,18 +92,14 @@ std::shared_ptr Presenter::init_window_system() { return std::make_shared(); } -void Presenter::init_graphics(bool debug) { +void Presenter::init_graphics(bool debug, const renderer::window_settings &window_settings) { log::log(INFO << "Presenter: Initializing graphics subsystems..."); // Start up rendering framework this->gui_app = this->init_window_system(); // Window and renderer - renderer::window_settings settings; - settings.width = 1024; - settings.height = 768; - settings.debug = debug; - this->window = renderer::Window::create("openage presenter test", settings); + this->window = renderer::Window::create("openage presenter test", window_settings); this->renderer = this->window->make_renderer(); // Asset mangement diff --git a/libopenage/presenter/presenter.h b/libopenage/presenter/presenter.h index b43d4fc60f..6f45800a32 100644 --- a/libopenage/presenter/presenter.h +++ b/libopenage/presenter/presenter.h @@ -7,6 +7,9 @@ #include "util/path.h" +#include + + namespace qtgui { class GuiApplication; } @@ -88,8 +91,9 @@ class Presenter { * Start the presenter and initialize subsystems. * * @param debug_graphics If true, enable OpenGL debug logging. + * @param window_settings window display setting */ - void run(bool debug_graphics = false); + void run(bool debug_graphics = false, const renderer::window_settings &window_settings = {}); /** * Set the game simulation controlled by this presenter. @@ -120,7 +124,7 @@ class Presenter { * - main renderer * - component renderers (Terrain, Game Entities, GUI) */ - void init_graphics(bool debug = false); + void init_graphics(bool debug = false, const renderer::window_settings &window_settings = {}); /** * Initialize the GUI. diff --git a/libopenage/renderer/opengl/window.cpp b/libopenage/renderer/opengl/window.cpp index 5f75717540..d832c49337 100644 --- a/libopenage/renderer/opengl/window.cpp +++ b/libopenage/renderer/opengl/window.cpp @@ -57,6 +57,21 @@ GlWindow::GlWindow(const std::string &title, this->window->setFormat(format); this->window->create(); + // set display mode + switch (settings.mode) { + case window_mode::FULLSCREEN: + this->window->showFullScreen(); + break; + case window_mode::BORDERLESS: + this->window->setFlags(this->window->flags() | Qt::FramelessWindowHint); + this->window->show(); + break; + case window_mode::WINDOWED: + default: + this->window->showNormal(); + break; + } + this->context = std::make_shared(this->window, settings.debug); if (not this->context->get_raw_context()->isValid()) { throw Error{MSG(err) << "Failed to create Qt OpenGL context."}; diff --git a/libopenage/renderer/window.h b/libopenage/renderer/window.h index 71960e2d17..6f34aa58e3 100644 --- a/libopenage/renderer/window.h +++ b/libopenage/renderer/window.h @@ -21,6 +21,15 @@ namespace openage::renderer { class WindowEventHandler; +/** + * Modes for window display. + */ +enum class window_mode { + FULLSCREEN, + BORDERLESS, + WINDOWED +}; + /** * Settings for creating a window. */ @@ -35,6 +44,8 @@ struct window_settings { bool vsync = true; // If true, enable debug logging for the selected backend. bool debug = false; + // Display mode for the window. + window_mode mode = window_mode::FULLSCREEN; }; From 87da14ecafb6f043c62e230b00900251b763e725 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Fri, 6 Dec 2024 10:20:13 -0500 Subject: [PATCH 631/771] Address review feedback: fix formatting, remove redundant debug arg, and change display mode setting method. --- libopenage/engine/engine.cpp | 5 ++--- libopenage/engine/engine.h | 6 ++---- libopenage/main.cpp | 8 ++++++-- libopenage/presenter/presenter.cpp | 6 +++--- libopenage/presenter/presenter.h | 10 ++++------ libopenage/renderer/opengl/window.cpp | 14 ++++++++------ libopenage/renderer/window.h | 2 +- openage/game/main.py | 6 +++--- openage/main/main.py | 6 +++--- 9 files changed, 32 insertions(+), 31 deletions(-) diff --git a/libopenage/engine/engine.cpp b/libopenage/engine/engine.cpp index dc787ebcf1..02c0dd87d3 100644 --- a/libopenage/engine/engine.cpp +++ b/libopenage/engine/engine.cpp @@ -16,7 +16,6 @@ namespace openage::engine { Engine::Engine(mode mode, const util::Path &root_dir, const std::vector &mods, - bool debug_graphics, const renderer::window_settings &window_settings) : running{true}, run_mode{mode}, @@ -56,8 +55,8 @@ Engine::Engine(mode mode, // if presenter is used, run it in a separate thread if (this->run_mode == mode::FULL) { - this->threads.emplace_back([&, debug_graphics]() { - this->presenter->run(debug_graphics, window_settings); + this->threads.emplace_back([&]() { + this->presenter->run(window_settings); // Make sure that the presenter gets destructed in the same thread // otherwise OpenGL complains about missing contexts diff --git a/libopenage/engine/engine.h b/libopenage/engine/engine.h index d324fe4fda..0231054628 100644 --- a/libopenage/engine/engine.h +++ b/libopenage/engine/engine.h @@ -7,9 +7,9 @@ #include #include +#include "renderer/window.h" #include "util/path.h" -#include // TODO: Remove custom jthread definition when clang/libc++ finally supports it #if __llvm__ @@ -73,13 +73,11 @@ class Engine { * @param mode The run mode to use. * @param root_dir openage root directory. * @param mods The mods to load. - * @param debug_graphics If true, enable OpenGL debug logging. - * @param window_settings window display setting + * @param window_settings The settings to customize the display window (e.g. size, display mode, vsync). */ Engine(mode mode, const util::Path &root_dir, const std::vector &mods, - bool debug_graphics = false, const renderer::window_settings &window_settings = {}); // engine should not be copied or moved diff --git a/libopenage/main.cpp b/libopenage/main.cpp index f8e6b45f3f..2147cbe4e0 100644 --- a/libopenage/main.cpp +++ b/libopenage/main.cpp @@ -44,12 +44,16 @@ int run_game(const main_arguments &args) { else if (args.window_args.mode == "borderless") { wmode = renderer::window_mode::BORDERLESS; } - else { + else if (args.window_args.mode == "windowed") { wmode = renderer::window_mode::WINDOWED; } + else { + throw Error(MSG(err) << "Invalid window mode: " << args.window_args.mode); + } win_settings.mode = wmode; + win_settings.debug = args.gl_debug; - openage::engine::Engine engine{run_mode, args.root_path, args.mods, args.gl_debug, win_settings}; + openage::engine::Engine engine{run_mode, args.root_path, args.mods, win_settings}; engine.loop(); diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index 6d77a72710..6db1e23136 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -47,10 +47,10 @@ Presenter::Presenter(const util::Path &root_dir, time_loop{time_loop} {} -void Presenter::run(bool debug_graphics, const renderer::window_settings &window_settings) { +void Presenter::run(const renderer::window_settings &window_settings) { log::log(INFO << "Presenter: Launching subsystems..."); - this->init_graphics(debug_graphics, window_settings); + this->init_graphics(window_settings); this->init_input(); @@ -92,7 +92,7 @@ std::shared_ptr Presenter::init_window_system() { return std::make_shared(); } -void Presenter::init_graphics(bool debug, const renderer::window_settings &window_settings) { +void Presenter::init_graphics(const renderer::window_settings &window_settings) { log::log(INFO << "Presenter: Initializing graphics subsystems..."); // Start up rendering framework diff --git a/libopenage/presenter/presenter.h b/libopenage/presenter/presenter.h index 6f45800a32..a4b5dec68e 100644 --- a/libopenage/presenter/presenter.h +++ b/libopenage/presenter/presenter.h @@ -5,10 +5,9 @@ #include #include +#include "renderer/window.h" #include "util/path.h" -#include - namespace qtgui { class GuiApplication; @@ -90,10 +89,9 @@ class Presenter { /** * Start the presenter and initialize subsystems. * - * @param debug_graphics If true, enable OpenGL debug logging. - * @param window_settings window display setting + * @param window_settings The settings to customize the display window (e.g. size, display mode, vsync). */ - void run(bool debug_graphics = false, const renderer::window_settings &window_settings = {}); + void run(const renderer::window_settings &window_settings = {}); /** * Set the game simulation controlled by this presenter. @@ -124,7 +122,7 @@ class Presenter { * - main renderer * - component renderers (Terrain, Game Entities, GUI) */ - void init_graphics(bool debug = false, const renderer::window_settings &window_settings = {}); + void init_graphics(const renderer::window_settings &window_settings = {}); /** * Initialize the GUI. diff --git a/libopenage/renderer/opengl/window.cpp b/libopenage/renderer/opengl/window.cpp index d832c49337..46772e14d9 100644 --- a/libopenage/renderer/opengl/window.cpp +++ b/libopenage/renderer/opengl/window.cpp @@ -58,18 +58,20 @@ GlWindow::GlWindow(const std::string &title, this->window->create(); // set display mode + // Reset to a known state + this->window->setWindowState(Qt::WindowNoState); switch (settings.mode) { - case window_mode::FULLSCREEN: - this->window->showFullScreen(); + case window_mode::WINDOWED: + this->window->setFlags(this->window->flags() & ~Qt::FramelessWindowHint); break; case window_mode::BORDERLESS: this->window->setFlags(this->window->flags() | Qt::FramelessWindowHint); - this->window->show(); break; - case window_mode::WINDOWED: - default: - this->window->showNormal(); + case window_mode::FULLSCREEN: + this->window->setWindowState(Qt::WindowFullScreen); break; + default: + throw Error{MSG(err) << "Invalid window mode."}; } this->context = std::make_shared(this->window, settings.debug); diff --git a/libopenage/renderer/window.h b/libopenage/renderer/window.h index 6f34aa58e3..e3465bef4d 100644 --- a/libopenage/renderer/window.h +++ b/libopenage/renderer/window.h @@ -45,7 +45,7 @@ struct window_settings { // If true, enable debug logging for the selected backend. bool debug = false; // Display mode for the window. - window_mode mode = window_mode::FULLSCREEN; + window_mode mode = window_mode::WINDOWED; }; diff --git a/openage/game/main.py b/openage/game/main.py index 44b74ee058..1ad1378426 100644 --- a/openage/game/main.py +++ b/openage/game/main.py @@ -39,15 +39,15 @@ def init_subparser(cli: ArgumentParser) -> None: cli.add_argument( "--window-size", nargs=2, type=int, default=[1024, 768], metavar=('WIDTH', 'HEIGHT'), - help="initial window size in pixels, e.g., --window-size 1024 768") + help="Initial window size in pixels") cli.add_argument( "--vsync", action='store_true', - help="enable vertical synchronization") + help="Enable vertical synchronization") cli.add_argument( "--window-mode", choices=["fullscreen", "borderless", "windowed"], default="windowed", - help="set the window mode: fullscreen, borderless, or windowed (default)") + help="Set the window mode") def main(args, error): diff --git a/openage/main/main.py b/openage/main/main.py index 5fa628c3e5..05266ec744 100644 --- a/openage/main/main.py +++ b/openage/main/main.py @@ -31,15 +31,15 @@ def init_subparser(cli: ArgumentParser): cli.add_argument( "--window-size", nargs=2, type=int, default=[1024, 768], metavar=('WIDTH', 'HEIGHT'), - help="initial window size in pixels, e.g., --window-size 1024 768") + help="Initial window size in pixels") cli.add_argument( "--vsync", action='store_true', - help="enable vertical synchronization") + help="Enable vertical synchronization") cli.add_argument( "--window-mode", choices=["fullscreen", "borderless", "windowed"], default="windowed", - help="set the window mode: fullscreen, borderless, or windowed (default)") + help="Set the window mode") def main(args, error): From 8094dc6ab19affc544200804f65feb0a9e25c0ae Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Sat, 7 Dec 2024 11:39:32 -0500 Subject: [PATCH 632/771] remove redundant window.setFlags() and pass window_settings to presenter by value --- libopenage/presenter/presenter.cpp | 2 +- libopenage/presenter/presenter.h | 2 +- libopenage/renderer/opengl/window.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index 6db1e23136..9775b62d9b 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -47,7 +47,7 @@ Presenter::Presenter(const util::Path &root_dir, time_loop{time_loop} {} -void Presenter::run(const renderer::window_settings &window_settings) { +void Presenter::run(const renderer::window_settings window_settings) { log::log(INFO << "Presenter: Launching subsystems..."); this->init_graphics(window_settings); diff --git a/libopenage/presenter/presenter.h b/libopenage/presenter/presenter.h index a4b5dec68e..2059daeed0 100644 --- a/libopenage/presenter/presenter.h +++ b/libopenage/presenter/presenter.h @@ -91,7 +91,7 @@ class Presenter { * * @param window_settings The settings to customize the display window (e.g. size, display mode, vsync). */ - void run(const renderer::window_settings &window_settings = {}); + void run(const renderer::window_settings window_settings = {}); /** * Set the game simulation controlled by this presenter. diff --git a/libopenage/renderer/opengl/window.cpp b/libopenage/renderer/opengl/window.cpp index 46772e14d9..548575dd2d 100644 --- a/libopenage/renderer/opengl/window.cpp +++ b/libopenage/renderer/opengl/window.cpp @@ -62,7 +62,7 @@ GlWindow::GlWindow(const std::string &title, this->window->setWindowState(Qt::WindowNoState); switch (settings.mode) { case window_mode::WINDOWED: - this->window->setFlags(this->window->flags() & ~Qt::FramelessWindowHint); + // nothing to do because it's the default break; case window_mode::BORDERLESS: this->window->setFlags(this->window->flags() | Qt::FramelessWindowHint); From e04eafa25fa1f1f48351f459fbca4ea7d2d60693 Mon Sep 17 00:00:00 2001 From: fabiobarkoski Date: Wed, 7 Aug 2024 00:22:52 -0300 Subject: [PATCH 633/771] convert: Speed up converter texture packing cythonized more the texture packing, also changed the list passed to factor(), before was a list of frame objects, now a list of only width, height and index of the frames. --- .../processor/export/texture_merge.pyx | 22 ++--- .../convert/service/export/png/binpack.pxd | 34 +++++--- .../convert/service/export/png/binpack.pyx | 87 +++++++++---------- 3 files changed, 77 insertions(+), 66 deletions(-) diff --git a/openage/convert/processor/export/texture_merge.pyx b/openage/convert/processor/export/texture_merge.pyx index f726d0af02..51ce4830ed 100644 --- a/openage/convert/processor/export/texture_merge.pyx +++ b/openage/convert/processor/export/texture_merge.pyx @@ -1,4 +1,4 @@ -# Copyright 2014-2023 the openage authors. See copying.md for legal info. +# Copyright 2014-2024 the openage authors. See copying.md for legal info. # # cython: infer_types=True # pylint: disable=too-many-locals @@ -9,15 +9,16 @@ a terrain texture. import numpy from enum import Enum +cimport cython +cimport numpy + from ....log import spam +from ...service.export.png.binpack cimport block from ...entity_object.export.texture import TextureImage -from ...service.export.png.binpack cimport Packer, DeterministicPacker, RowPacker, ColumnPacker, BinaryTreePacker, BestPacker +from ...service.export.png.binpack cimport DeterministicPacker, RowPacker, ColumnPacker, BinaryTreePacker, BestPacker from ...value_object.read.media.hardcoded.texture import (MAX_TEXTURE_DIMENSION, MARGIN, TERRAIN_ASPECT_RATIO) -cimport cython -cimport numpy - class PackerType(Enum): """ Packer types @@ -57,6 +58,7 @@ cdef void cmerge_frames(texture, packer_type=PackerType.BINPACK, cache=None) exc :type cache: list """ cdef list frames = texture.frames + cdef list blocks = [block(idx, frame.width, frame.height) for idx, frame in enumerate(frames)] if len(frames) == 0: raise ValueError("cannot create texture with empty input frame list") @@ -64,7 +66,7 @@ cdef void cmerge_frames(texture, packer_type=PackerType.BINPACK, cache=None) exc cdef BestPacker packer if cache: - packer = BestPacker([DeterministicPacker(margin=MARGIN,hints=cache)]) + packer = BestPacker([DeterministicPacker(margin=MARGIN, hints=cache)]) else: if packer_type == PackerType.ROW: @@ -81,7 +83,7 @@ cdef void cmerge_frames(texture, packer_type=PackerType.BINPACK, cache=None) exc RowPacker(margin=MARGIN), ColumnPacker(margin=MARGIN)]) - packer.pack(frames) + packer.pack(blocks) cdef int width = packer.width() cdef int height = packer.height() @@ -106,11 +108,11 @@ cdef void cmerge_frames(texture, packer_type=PackerType.BINPACK, cache=None) exc cdef int sub_h cdef list drawn_frames_meta = [] - for sub_frame in frames: + for index, sub_frame in enumerate(frames): sub_w = sub_frame.width sub_h = sub_frame.height - pos_x, pos_y = packer.pos(sub_frame) + pos_x, pos_y = packer.pos(index) spam("drawing frame %03d on atlas at %d x %d...", len(drawn_frames_meta), pos_x, pos_y) @@ -143,4 +145,4 @@ cdef void cmerge_frames(texture, packer_type=PackerType.BINPACK, cache=None) exc if isinstance(packer, BestPacker): # Only generate these values if no custom packer was used # TODO: It might make sense to do it anyway for debugging purposes - texture.best_packer_hints = packer.get_mapping_hints(frames) + texture.best_packer_hints = packer.get_mapping_hints(blocks) diff --git a/openage/convert/service/export/png/binpack.pxd b/openage/convert/service/export/png/binpack.pxd index e13725a367..5560441143 100644 --- a/openage/convert/service/export/png/binpack.pxd +++ b/openage/convert/service/export/png/binpack.pxd @@ -1,15 +1,19 @@ -# Copyright 2021-2021 the openage authors. See copying.md for legal info. +# Copyright 2021-2024 the openage authors. See copying.md for legal info. -from libcpp.memory cimport shared_ptr +from libcpp.unordered_map cimport unordered_map + +ctypedef (unsigned int, unsigned int, (unsigned int, unsigned int)) mapping_value cdef class Packer: cdef unsigned int margin - cdef dict mapping + cdef unordered_map[int, mapping_value] mapping cdef void pack(self, list blocks) - cdef (unsigned int, unsigned int) pos(self, block) + cdef (unsigned int, unsigned int) pos(self, int index) cdef unsigned int width(self) cdef unsigned int height(self) + cdef list get_mapping_hints(self, list blocks) + cdef (unsigned int) get_packer_settings(self) cdef class DeterministicPacker(Packer): pass @@ -20,9 +24,11 @@ cdef class BestPacker: cdef void pack(self, list blocks) cdef Packer best_packer(self) - cdef (unsigned int, unsigned int) pos(self, block) + cdef (unsigned int, unsigned int) pos(self, int index) cdef unsigned int width(self) cdef unsigned int height(self) + cdef list get_mapping_hints(self, list blocks) + cdef (unsigned int) get_packer_settings(self) cdef class RowPacker(Packer): pass @@ -34,12 +40,13 @@ cdef class BinaryTreePacker(Packer): cdef unsigned int aspect_ratio cdef packer_node *root - cdef void fit(self, block) - cdef packer_node *find_node(self, packer_node *root, unsigned int width, unsigned int height) - cdef packer_node *split_node(self, packer_node *node, unsigned int width, unsigned int height) - cdef packer_node *grow_node(self, unsigned int width, unsigned int height) - cdef packer_node *grow_right(self, unsigned int width, unsigned int height) - cdef packer_node *grow_down(self, unsigned int width, unsigned int height) + cdef void fit(self, block block) + cdef (unsigned int) get_packer_settings(self) + cdef packer_node *find_node(self, packer_node *root, unsigned int width, unsigned int height) noexcept + cdef packer_node *split_node(self, packer_node *node, unsigned int width, unsigned int height) noexcept + cdef packer_node *grow_node(self, unsigned int width, unsigned int height) noexcept + cdef packer_node *grow_right(self, unsigned int width, unsigned int height) noexcept + cdef packer_node *grow_down(self, unsigned int width, unsigned int height) noexcept cdef struct packer_node: unsigned int x @@ -49,3 +56,8 @@ cdef struct packer_node: bint used packer_node *down packer_node *right + +cdef struct block: + unsigned int index + unsigned int width + unsigned int height diff --git a/openage/convert/service/export/png/binpack.pyx b/openage/convert/service/export/png/binpack.pyx index 30e39cd95f..291be2a526 100644 --- a/openage/convert/service/export/png/binpack.pyx +++ b/openage/convert/service/export/png/binpack.pyx @@ -1,4 +1,4 @@ -# Copyright 2016-2023 the openage authors. See copying.md for legal info. +# Copyright 2016-2024 the openage authors. See copying.md for legal info. # # cython: infer_types=True,profile=False # TODO pylint: disable=C,R @@ -7,14 +7,10 @@ Routines for 2D binpacking """ -from enum import Enum - cimport cython -from libc.stdint cimport uintptr_t -from libc.stdlib cimport malloc - from libc.math cimport sqrt - +from libc.stdlib cimport malloc +from libcpp.unordered_map cimport unordered_map @cython.boundscheck(False) @cython.wraparound(False) @@ -24,6 +20,7 @@ cdef inline (unsigned int, unsigned int) factor(unsigned int n): Return two (preferable close) factors of n. """ cdef unsigned int a = sqrt(n) + cdef int num for num in range(a, 0, -1): if n % num == 0: return num, n // num @@ -33,9 +30,8 @@ cdef class Packer: """ Packs blocks. """ - def __init__(self, margin): + def __init__(self, int margin): self.margin = margin - self.mapping = {} cdef void pack(self, list blocks): """ @@ -45,31 +41,33 @@ cdef class Packer: """ raise NotImplementedError - cdef (unsigned int, unsigned int) pos(self, block): - return self.mapping[block] + cdef (unsigned int, unsigned int) pos(self, int index): + node = self.mapping[index] + return node[0], node[1] cdef unsigned int width(self): """ Gets the total width of the packing. """ - return max(self.pos(block)[0] + block.width for block in self.mapping) + return max(self.pos(idx)[0] + block[2][0] for idx, block in self.mapping) cdef unsigned int height(self): """ Gets the total height of the packing. """ - return max(self.pos(block)[1] + block.height for block in self.mapping) + return max(self.pos(idx)[1] + block[2][1] for idx, block in self.mapping) - def get_packer_settings(self): + cdef (unsigned int) get_packer_settings(self): """ Get the init parameters set for the packer. """ return (self.margin,) - def get_mapping_hints(self, blocks): - hints = [] + cdef list get_mapping_hints(self, list blocks): + cdef list hints = [] + cdef block block for block in blocks: - hints.append(self.pos(block)) + hints.append(self.pos(block.index)) return hints @@ -79,13 +77,13 @@ cdef class DeterministicPacker(Packer): Packs blocks based on predetermined settings. """ - def __init__(self, margin, hints): + def __init__(self, int margin, list hints): super().__init__(margin) self.hints = hints cdef void pack(self, list blocks): for idx, block in enumerate(blocks): - self.mapping[block] = self.hints[idx] + self.mapping[block.index] = self.hints[idx] cdef class BestPacker: @@ -97,18 +95,17 @@ cdef class BestPacker: self.current_best = None cdef void pack(self, list blocks): - cdef Packer p + cdef Packer packer for packer in self.packers: - p = packer - p.pack(blocks) + packer.pack(blocks) self.current_best = self.best_packer() cdef Packer best_packer(self): return min(self.packers, key=lambda Packer p: p.width() * p.height()) - cdef (unsigned int, unsigned int) pos(self, block): - return self.current_best.pos(block) + cdef (unsigned int, unsigned int) pos(self, int index): + return self.current_best.pos(index) cdef unsigned int width(self): return self.current_best.width() @@ -116,10 +113,10 @@ cdef class BestPacker: cdef unsigned int height(self): return self.current_best.height() - def get_packer_settings(self): + cdef (unsigned int) get_packer_settings(self): return self.current_best.get_packer_settings() - def get_mapping_hints(self, blocks): + cdef list get_mapping_hints(self, list blocks): return self.current_best.get_mapping_hints(blocks) @@ -129,8 +126,6 @@ cdef class RowPacker(Packer): """ cdef void pack(self, list blocks): - self.mapping = {} - cdef unsigned int num_rows cdef list rows @@ -148,7 +143,7 @@ cdef class RowPacker(Packer): x = 0 for block in row: - self.mapping[block] = (x, y) + self.mapping[block.index] = (x, y, (block.width, block.height)) x += block.width + self.margin y += max(block.height for block in row) + self.margin @@ -160,8 +155,6 @@ cdef class ColumnPacker(Packer): """ cdef void pack(self, list blocks): - self.mapping = {} - num_columns, _ = factor(len(blocks)) columns = [[] for _ in range(num_columns)] @@ -176,13 +169,13 @@ cdef class ColumnPacker(Packer): y = 0 for block in column: - self.mapping[block] = (x, y) + self.mapping[block.index] = (x, y, (block.width, block.height)) y += block.height + self.margin x += max(block.width for block in column) + self.margin -cdef inline (unsigned int, unsigned int, unsigned int, unsigned int) maxside_heuristic(block): +cdef inline (unsigned int, unsigned int, unsigned int, unsigned int) maxside_heuristic(block block): """ Heuristic: Order blocks by maximum side. """ @@ -202,27 +195,26 @@ cdef class BinaryTreePacker(Packer): textures. """ - def __init__(self, margin, aspect_ratio=1): + def __init__(self, int margin, int aspect_ratio=1): # ASF: what about heuristic=max_heuristic? super().__init__(margin) self.aspect_ratio = aspect_ratio self.root = NULL cdef void pack(self, list blocks): - self.mapping = {} self.root = NULL for block in sorted(blocks, key=maxside_heuristic, reverse=True): self.fit(block) - cdef (unsigned int, unsigned int) pos(self, block): - node = self.mapping[block] + cdef (unsigned int, unsigned int) pos(self, int index): + node = self.mapping[index] return node[0], node[1] - def get_packer_settings(self): + cdef (unsigned int) get_packer_settings(self): return (self.margin,) - cdef void fit(self, block): + cdef void fit(self, block block): cdef packer_node *node if self.root == NULL: self.root = malloc(sizeof(packer_node)) @@ -246,9 +238,9 @@ cdef class BinaryTreePacker(Packer): node = self.grow_node(block.width + self.margin, block.height + self.margin) - self.mapping[block] = (node.x, node.y) + self.mapping[block.index] = (node.x, node.y, (block.width, block.height)) - cdef packer_node *find_node(self, packer_node *root, unsigned int width, unsigned int height): + cdef packer_node *find_node(self, packer_node *root, unsigned int width, unsigned int height) noexcept: if root.used: return (self.find_node(root.right, width, height) or self.find_node(root.down, width, height)) @@ -256,7 +248,7 @@ cdef class BinaryTreePacker(Packer): elif width <= root.width and height <= root.height: return root - cdef packer_node *split_node(self, packer_node *node, unsigned int width, unsigned int height): + cdef packer_node *split_node(self, packer_node *node, unsigned int width, unsigned int height) noexcept: node.used = True node.down = malloc(sizeof(packer_node)) @@ -280,7 +272,7 @@ cdef class BinaryTreePacker(Packer): return node @cython.cdivision(True) - cdef packer_node *grow_node(self, unsigned int width, unsigned int height): + cdef packer_node *grow_node(self, unsigned int width, unsigned int height) noexcept: cdef bint can_grow_down = width <= self.root.width cdef bint can_grow_right = height <= self.root.height # assert can_grow_down or can_grow_right, "Bad block ordering heuristic" @@ -302,7 +294,7 @@ cdef class BinaryTreePacker(Packer): else: return self.grow_down(width, height) - cdef packer_node *grow_right(self, unsigned int width, unsigned int height): + cdef packer_node *grow_right(self, unsigned int width, unsigned int height) noexcept: old_root = self.root self.root = malloc(sizeof(packer_node)) @@ -327,7 +319,7 @@ cdef class BinaryTreePacker(Packer): if node != NULL: return self.split_node(node, width, height) - cdef packer_node *grow_down(self, unsigned int width, unsigned int height): + cdef packer_node *grow_down(self, unsigned int width, unsigned int height) noexcept: old_root = self.root self.root = malloc(sizeof(packer_node)) @@ -361,3 +353,8 @@ cdef struct packer_node: bint used packer_node *down packer_node *right + +cdef struct block: + unsigned int index + unsigned int width + unsigned int height From 2b39bf79882f545b615804bf4345c5bf8671771a Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 8 Dec 2024 03:10:50 +0100 Subject: [PATCH 634/771] etc: Update Ubuntu docker file. --- .../{Dockerfile.ubuntu.2204 => Dockerfile.ubuntu.2404} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename packaging/docker/devenv/{Dockerfile.ubuntu.2204 => Dockerfile.ubuntu.2404} (95%) diff --git a/packaging/docker/devenv/Dockerfile.ubuntu.2204 b/packaging/docker/devenv/Dockerfile.ubuntu.2404 similarity index 95% rename from packaging/docker/devenv/Dockerfile.ubuntu.2204 rename to packaging/docker/devenv/Dockerfile.ubuntu.2404 index 4976e0c693..d3d492f6af 100644 --- a/packaging/docker/devenv/Dockerfile.ubuntu.2204 +++ b/packaging/docker/devenv/Dockerfile.ubuntu.2404 @@ -1,4 +1,4 @@ -FROM ubuntu:23.04 +FROM ubuntu:24.04 RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y sudo \ && sudo apt-get update \ @@ -8,8 +8,8 @@ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y sudo \ cmake \ cython3 \ flex \ - gcc-11 \ - g++-11 \ + gcc \ + g++ \ git \ libeigen3-dev \ libepoxy-dev \ From 6d370952b4d805dc1ead0b9844f48055571a084f Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 8 Dec 2024 03:12:08 +0100 Subject: [PATCH 635/771] etc: Update GitHub actions for Ubuntu workflow. --- .github/workflows/{ubuntu-22.04.yml => ubuntu-24.04.yml} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename .github/workflows/{ubuntu-22.04.yml => ubuntu-24.04.yml} (94%) diff --git a/.github/workflows/ubuntu-22.04.yml b/.github/workflows/ubuntu-24.04.yml similarity index 94% rename from .github/workflows/ubuntu-22.04.yml rename to .github/workflows/ubuntu-24.04.yml index 311524f799..6fa9145279 100644 --- a/.github/workflows/ubuntu-22.04.yml +++ b/.github/workflows/ubuntu-24.04.yml @@ -1,14 +1,14 @@ -name: Ubuntu 22.04 CI +name: Ubuntu 24.04 CI on: [push, workflow_dispatch] jobs: build-devenv: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Build the Docker image - run: sudo DOCKER_BUILDKIT=1 docker build ./packaging/docker/devenv --file ./packaging/docker/devenv/Dockerfile.ubuntu.2204 --tag openage-devenv:latest + run: sudo DOCKER_BUILDKIT=1 docker build ./packaging/docker/devenv --file ./packaging/docker/devenv/Dockerfile.ubuntu.2404 --tag openage-devenv:latest shell: bash - name: Save the Docker image run: | @@ -24,7 +24,7 @@ jobs: retention-days: 30 build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: build-devenv steps: - uses: actions/checkout@v4 From 0bd54ce39a7a24a263a82b14f7dff04a9f770362 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 8 Dec 2024 03:19:20 +0100 Subject: [PATCH 636/771] doc: Update reference to Ubuntu 24.04 in README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 266cdfc5fa..4cd98b9070 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ If you're interested, we wrote detailed explanations on our blog: [Part 1](https | Operating System | Build status | | :-----------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | | Debian Sid | [Todo: Kevin #11] | -| Ubuntu 22.04 LTS | [![Ubuntu 22.04 build status](https://github.com/SFTTech/openage/actions/workflows/ubuntu-22.04.yml/badge.svg?branch=master)](https://github.com/SFTtech/openage/actions/workflows/ubuntu-22.04.yml) | +| Ubuntu 24.04 LTS | [![Ubuntu 24.04 build status](https://github.com/SFTTech/openage/actions/workflows/ubuntu-24.04.yml/badge.svg?branch=master)](https://github.com/SFTtech/openage/actions/workflows/ubuntu-24.04.yml) | | macOS | [![macOS build status](https://github.com/SFTtech/openage/workflows/macOS-CI/badge.svg)](https://github.com/SFTtech/openage/actions?query=workflow%3AmacOS-CI) | | Windows Server 2019 | [![Windows Server 2019 build status](https://github.com/SFTtech/openage/actions/workflows/windows-server-2019.yml/badge.svg?branch=master)](https://github.com/SFTtech/openage/actions/workflows/windows-server-2019.yml) | | Windows Server 2022 | [![Windows Server 2022 build status](https://github.com/SFTtech/openage/actions/workflows/windows-server-2022.yml/badge.svg?branch=master)](https://github.com/SFTtech/openage/actions/workflows/windows-server-2022.yml) | From b306c76cd8f9a7844cc0363e148be27e7f7ad205 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Tue, 3 Dec 2024 00:54:52 +0000 Subject: [PATCH 637/771] curve: common element wrapper --- libopenage/curve/element_wrapper.h | 54 ++++++++++++++++++++++++++ libopenage/curve/map.h | 22 +++-------- libopenage/curve/map_filter_iterator.h | 6 +-- libopenage/curve/queue.h | 33 ++-------------- 4 files changed, 66 insertions(+), 49 deletions(-) create mode 100644 libopenage/curve/element_wrapper.h diff --git a/libopenage/curve/element_wrapper.h b/libopenage/curve/element_wrapper.h new file mode 100644 index 0000000000..79756f0e5c --- /dev/null +++ b/libopenage/curve/element_wrapper.h @@ -0,0 +1,54 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include "time/time.h" + +namespace openage::curve { + +/** + * wrapper class for elements of a curve, store the insertion time and erase time of the element + * aswell the data of the element. + */ +template +struct element_wrapper { + // Insertion time of the element + time::time_t _alive; + // Erase time of the element + time::time_t _dead; + // Element value + T value; + + /** + * construct new element, set its start time and value + * its end time will be set to time::TIME_MAX + */ + element_wrapper(const time::time_t &time, const T &value) : + _alive{time}, + _dead{time::TIME_MAX}, + value{value} {} + + // construct new element, set its start time, end time and value + element_wrapper(const T &value, const time::time_t &alive, const time::time_t &dead) : + _alive{alive}, + _dead{dead}, + value{value} {} + + + // start time of this element + const time::time_t &alive() const { + return _alive; + } + + // end time of this element + const time::time_t &dead() const { + return _dead; + } + + // set the end time of this element + void set_dead(const time::time_t &time) { + _dead = time; + } +}; + +} // namespace openage::curve diff --git a/libopenage/curve/map.h b/libopenage/curve/map.h index 9712c89470..d996095d7c 100644 --- a/libopenage/curve/map.h +++ b/libopenage/curve/map.h @@ -1,4 +1,4 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -8,6 +8,7 @@ #include #include "curve/map_filter_iterator.h" +#include "curve/element_wrapper.h" #include "time/time.h" #include "util/fixed_point.h" @@ -20,26 +21,15 @@ namespace openage::curve { */ template class UnorderedMap { - /** Internal container to access all data and metadata */ - struct map_element { - map_element(const val_t &v, const time::time_t &a, const time::time_t &d) : - value(v), - alive(a), - dead(d) {} - - val_t value; - time::time_t alive; - time::time_t dead; - }; /** * Data holder. Maps keys to map elements. * Map elements themselves store when they are valid. */ - std::unordered_map container; + std::unordered_map> container; public: - using const_iterator = typename std::unordered_map::const_iterator; + using const_iterator = typename std::unordered_map>::const_iterator; std::optional> operator()(const time::time_t &, const key_t &) const; @@ -95,7 +85,7 @@ std::optional>> UnorderedMap::at(const time::time_t &time, const key_t &key) const { auto e = this->container.find(key); - if (e != this->container.end() and e->second.alive <= time and e->second.dead > time) { + if (e != this->container.end() and e->second.alive() <= time and e->second.dead() > time) { return MapFilterIterator>( e, this, @@ -160,7 +150,7 @@ UnorderedMap::insert(const time::time_t &alive, const time::time_t &dead, const key_t &key, const val_t &value) { - map_element e(value, alive, dead); + element_wrapper e(value, alive, dead); auto it = this->container.insert(std::make_pair(key, e)); return MapFilterIterator>( it.first, diff --git a/libopenage/curve/map_filter_iterator.h b/libopenage/curve/map_filter_iterator.h index 5e71c0789f..e60f762c26 100644 --- a/libopenage/curve/map_filter_iterator.h +++ b/libopenage/curve/map_filter_iterator.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 @@ -35,8 +35,8 @@ class MapFilterIterator : public CurveIterator { using CurveIterator::operator=; virtual bool valid() const override { - return (this->get_base()->second.alive >= this->from - and this->get_base()->second.dead < this->to); + return (this->get_base()->second.alive() >= this->from + and this->get_base()->second.dead() < this->to); } /** diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index 9604a76308..210670ee7b 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -13,6 +13,7 @@ #include "curve/iterator.h" #include "curve/queue_filter_iterator.h" +#include "curve/element_wrapper.h" #include "event/evententity.h" #include "time/time.h" #include "util/fixed_point.h" @@ -32,39 +33,11 @@ namespace curve { */ template class Queue : public event::EventEntity { - struct queue_wrapper { - // Insertion time of the element - time::time_t _alive; - // Erase time of the element - // TODO: this has to be mutable because erase() will complain otherwise - mutable time::time_t _dead; - // Element value - T value; - - queue_wrapper(const time::time_t &time, const T &value) : - _alive{time}, - _dead{time::TIME_MAX}, - value{value} {} - - const time::time_t &alive() const { - return _alive; - } - - const time::time_t &dead() const { - return _dead; - } - - // TODO: this has to be const because erase() will complain otherwise - void set_dead(const time::time_t &time) const { - _dead = time; - } - }; - public: /** * The underlaying container type. */ - using container_t = typename std::vector; + using container_t = typename std::vector>; /** * The index type to access elements in the container @@ -412,7 +385,7 @@ QueueFilterIterator> Queue::insert(const time::time_t &time, // Get the iterator to the insertion point iterator insertion_point = std::next(this->container.begin(), at); - insertion_point = this->container.insert(insertion_point, queue_wrapper{time, e}); + insertion_point = this->container.insert(insertion_point, element_wrapper{time, e}); // TODO: Inserting before any dead elements shoud reset their death time // since by definition, they cannot be popped before the new element From 775b3f63a5798f32be690262143208e4c1fd3dc3 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Wed, 11 Dec 2024 18:39:47 +0000 Subject: [PATCH 638/771] change portalNode::entry_sector to std::optional --- libopenage/pathfinding/pathfinder.cpp | 11 ++++++----- libopenage/pathfinding/pathfinder.h | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 2fef36053a..c99afd5b9d 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -269,11 +269,12 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req } // get the exits of the current node - const auto &exits = current_node->get_exits(current_node->entry_sector); + ENSURE(current_node->entry_sector != std::nullopt, "Entry sector not set for portal node."); + const auto &exits = current_node->get_exits(current_node->entry_sector.value()); // evaluate all neighbors of the current candidate for further progress for (auto &[exit, distance_cost] : exits) { - exit->entry_sector = current_node->portal->get_exit_sector(current_node->entry_sector); + exit->entry_sector = current_node->portal->get_exit_sector(current_node->entry_sector.value()); bool not_visited = !visited_portals.contains(exit->portal->get_id()); if (not_visited) { @@ -289,9 +290,9 @@ const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &req if (not_visited or tentative_cost < exit->current_cost) { if (not_visited) { // Get heuristic cost (from exit node to target cell) - auto exit_sector = grid->get_sector(exit->portal->get_exit_sector(exit->entry_sector)); + auto exit_sector = grid->get_sector(exit->portal->get_exit_sector(exit->entry_sector.value())); auto exit_sector_pos = exit_sector->get_position().to_tile(sector_size); - auto exit_portal_pos = exit->portal->get_exit_center(exit->entry_sector); + auto exit_portal_pos = exit->portal->get_exit_center(exit->entry_sector.value()); exit->heuristic_cost = Pathfinder::heuristic_cost( exit_sector_pos + exit_portal_pos, request.target); @@ -469,7 +470,7 @@ int Pathfinder::distance_cost(const coord::tile_delta &portal1_pos, PortalNode::PortalNode(const std::shared_ptr &portal) : portal{portal}, - entry_sector{NULL}, + entry_sector{std::nullopt}, future_cost{std::numeric_limits::max()}, current_cost{std::numeric_limits::max()}, heuristic_cost{std::numeric_limits::max()}, diff --git a/libopenage/pathfinding/pathfinder.h b/libopenage/pathfinding/pathfinder.h index e2529987b7..8d569d78d8 100644 --- a/libopenage/pathfinding/pathfinder.h +++ b/libopenage/pathfinding/pathfinder.h @@ -4,6 +4,7 @@ #include #include +#include #include #include "coord/tile.h" @@ -209,7 +210,7 @@ class PortalNode : public std::enable_shared_from_this { /** * Sector where the portal is entered. */ - sector_id_t entry_sector; + std::optional entry_sector; /** * Future cost estimation value for this node. From 54b2191c6c97346943b54bab08d48f78b2a0f259 Mon Sep 17 00:00:00 2001 From: jere8184 Date: Mon, 16 Dec 2024 20:18:25 +0000 Subject: [PATCH 639/771] Update windows-server CI to run tests --- .github/workflows/windows-server-2019.yml | 1 + .github/workflows/windows-server-2022.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/windows-server-2019.yml b/.github/workflows/windows-server-2019.yml index df9d96284b..7cd34e49b5 100644 --- a/.github/workflows/windows-server-2019.yml +++ b/.github/workflows/windows-server-2019.yml @@ -76,6 +76,7 @@ jobs: $DLL_PATH = Join-Path package dll | Resolve-Path cd package python -m openage --add-dll-search-path $DLL_PATH --version + ./run.exe test -a shell: pwsh - name: Publish build artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/windows-server-2022.yml b/.github/workflows/windows-server-2022.yml index bbc73b78c2..327c23584a 100644 --- a/.github/workflows/windows-server-2022.yml +++ b/.github/workflows/windows-server-2022.yml @@ -76,6 +76,7 @@ jobs: $DLL_PATH = Join-Path package dll | Resolve-Path cd package python -m openage --add-dll-search-path $DLL_PATH --version + ./run.exe test -a shell: pwsh - name: Publish build artifacts uses: actions/upload-artifact@v4 From 727fd328ce6ae94e9c9a9da268cb052d5a711faf Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 25 Dec 2024 14:41:39 +0100 Subject: [PATCH 640/771] curve: Add empty .cpp file for cmake. --- libopenage/curve/CMakeLists.txt | 1 + libopenage/curve/element_wrapper.cpp | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 libopenage/curve/element_wrapper.cpp diff --git a/libopenage/curve/CMakeLists.txt b/libopenage/curve/CMakeLists.txt index 1aa1efb55b..623a22a25a 100644 --- a/libopenage/curve/CMakeLists.txt +++ b/libopenage/curve/CMakeLists.txt @@ -3,6 +3,7 @@ add_sources(libopenage continuous.cpp discrete.cpp discrete_mod.cpp + element_wrapper.cpp interpolated.cpp iterator.cpp keyframe.cpp diff --git a/libopenage/curve/element_wrapper.cpp b/libopenage/curve/element_wrapper.cpp new file mode 100644 index 0000000000..5d2eaa08af --- /dev/null +++ b/libopenage/curve/element_wrapper.cpp @@ -0,0 +1,9 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "element_wrapper.h" + +namespace openage::curve { + +// This file is intended to be empty + +} // namespace openage::curve From 71cdc50ce8cfbcafbfdf898d3189cb5dffc79f0a Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 25 Dec 2024 14:43:43 +0100 Subject: [PATCH 641/771] curve: Fix comments of element wrapper. --- libopenage/curve/element_wrapper.h | 46 ++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/libopenage/curve/element_wrapper.h b/libopenage/curve/element_wrapper.h index 79756f0e5c..a0cfcd2109 100644 --- a/libopenage/curve/element_wrapper.h +++ b/libopenage/curve/element_wrapper.h @@ -7,45 +7,67 @@ namespace openage::curve { /** - * wrapper class for elements of a curve, store the insertion time and erase time of the element - * aswell the data of the element. + * Wrapper for elements in a curve container. + * + * Stores the lifetime of the element (insertion time and erasure time) alongside the value. */ template struct element_wrapper { - // Insertion time of the element + /// Time of insertion of the element into the container time::time_t _alive; - // Erase time of the element + /// Time of erasure of the element from the container time::time_t _dead; - // Element value + /// Element value T value; /** - * construct new element, set its start time and value - * its end time will be set to time::TIME_MAX + * Create a new element with insertion time \p time and a given value. + * + * Erasure time is set to time::TIME_MAX, i.e. the element is alive indefinitely. + * + * @param time Insertion time of the element. + * @param value Element value. */ element_wrapper(const time::time_t &time, const T &value) : _alive{time}, _dead{time::TIME_MAX}, value{value} {} - // construct new element, set its start time, end time and value + /** + * Create a new element with insertion time \p alive and erasure time \p dead and a given value. + * + * @param alive Insertion time of the element. + * @param dead Erasure time of the element. + * @param value Element value. + */ element_wrapper(const T &value, const time::time_t &alive, const time::time_t &dead) : _alive{alive}, _dead{dead}, value{value} {} - - // start time of this element + /** + * Get the insertion time of this element. + * + * @return Time when the element was inserted into the container. + */ const time::time_t &alive() const { return _alive; } - // end time of this element + /** + * Get the erasure time of this element. + * + * @return Time when the element was erased from the container. + */ const time::time_t &dead() const { return _dead; } - // set the end time of this element + /** + * Set the erasure time of this element. + * + * @param time Time when the element was erased from the container. + */ void set_dead(const time::time_t &time) { _dead = time; } From 41596023a1efc3e37ea682cb1ab8cbb4a3ce2843 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 25 Dec 2024 14:46:16 +0100 Subject: [PATCH 642/771] curve: Order arguments of both constructors the same way. --- libopenage/curve/element_wrapper.h | 2 +- libopenage/curve/map.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/curve/element_wrapper.h b/libopenage/curve/element_wrapper.h index a0cfcd2109..498ee550cc 100644 --- a/libopenage/curve/element_wrapper.h +++ b/libopenage/curve/element_wrapper.h @@ -40,7 +40,7 @@ struct element_wrapper { * @param dead Erasure time of the element. * @param value Element value. */ - element_wrapper(const T &value, const time::time_t &alive, const time::time_t &dead) : + element_wrapper(const time::time_t &alive, const time::time_t &dead, const T &value) : _alive{alive}, _dead{dead}, value{value} {} diff --git a/libopenage/curve/map.h b/libopenage/curve/map.h index d996095d7c..5a5e0a4d9b 100644 --- a/libopenage/curve/map.h +++ b/libopenage/curve/map.h @@ -150,7 +150,7 @@ UnorderedMap::insert(const time::time_t &alive, const time::time_t &dead, const key_t &key, const val_t &value) { - element_wrapper e(value, alive, dead); + element_wrapper e{alive, dead, value}; auto it = this->container.insert(std::make_pair(key, e)); return MapFilterIterator>( it.first, From d6bf5b910ce0425d9cf9d9f1a991ebef1c937abc Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 25 Dec 2024 14:56:03 +0100 Subject: [PATCH 643/771] curve: Make element wrapper member access private. Prevents unexpected manipulation. --- libopenage/curve/element_wrapper.h | 39 ++++++++++++++++++------ libopenage/curve/map_filter_iterator.h | 2 +- libopenage/curve/queue.h | 4 +-- libopenage/curve/queue_filter_iterator.h | 2 +- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/libopenage/curve/element_wrapper.h b/libopenage/curve/element_wrapper.h index 498ee550cc..764b61d5cd 100644 --- a/libopenage/curve/element_wrapper.h +++ b/libopenage/curve/element_wrapper.h @@ -12,14 +12,8 @@ namespace openage::curve { * Stores the lifetime of the element (insertion time and erasure time) alongside the value. */ template -struct element_wrapper { - /// Time of insertion of the element into the container - time::time_t _alive; - /// Time of erasure of the element from the container - time::time_t _dead; - /// Element value - T value; - +class element_wrapper { +public: /** * Create a new element with insertion time \p time and a given value. * @@ -31,7 +25,7 @@ struct element_wrapper { element_wrapper(const time::time_t &time, const T &value) : _alive{time}, _dead{time::TIME_MAX}, - value{value} {} + _value{value} {} /** * Create a new element with insertion time \p alive and erasure time \p dead and a given value. @@ -43,7 +37,7 @@ struct element_wrapper { element_wrapper(const time::time_t &alive, const time::time_t &dead, const T &value) : _alive{alive}, _dead{dead}, - value{value} {} + _value{value} {} /** * Get the insertion time of this element. @@ -71,6 +65,31 @@ struct element_wrapper { void set_dead(const time::time_t &time) { _dead = time; } + + /** + * Get the value of this element. + * + * @return Value of the element. + */ + const T &value() const { + return _value; + } + +private: + /** + * Time of insertion of the element into the container + */ + time::time_t _alive; + + /** + * Time of erasure of the element from the container + */ + time::time_t _dead; + + /** + * Element value + */ + T _value; }; } // namespace openage::curve diff --git a/libopenage/curve/map_filter_iterator.h b/libopenage/curve/map_filter_iterator.h index e60f762c26..3aec2a899e 100644 --- a/libopenage/curve/map_filter_iterator.h +++ b/libopenage/curve/map_filter_iterator.h @@ -44,7 +44,7 @@ class MapFilterIterator : public CurveIterator { * Nicer way of accessing it beside operator *. */ val_t const &value() const override { - return this->get_base()->second.value; + return this->get_base()->second.value(); } /** diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index 210670ee7b..9314dd3a0e 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -277,7 +277,7 @@ const T &Queue::front(const time::time_t &time) const { << ", container size: " << this->container.size() << ")"); - return this->container.at(at).value; + return this->container.at(at).value(); } @@ -303,7 +303,7 @@ const T &Queue::pop_front(const time::time_t &time) { this->changes(time); - return this->container.at(at).value; + return this->container.at(at).value(); } diff --git a/libopenage/curve/queue_filter_iterator.h b/libopenage/curve/queue_filter_iterator.h index 248abb44ad..cf6bc5aa2c 100644 --- a/libopenage/curve/queue_filter_iterator.h +++ b/libopenage/curve/queue_filter_iterator.h @@ -40,7 +40,7 @@ class QueueFilterIterator : public CurveIteratorget_base(); - return a.value; + return a.value(); } }; From 0a03e2d33c22b0e07950bec8e25f45ece35839ab Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Sun, 29 Dec 2024 20:40:48 +0000 Subject: [PATCH 644/771] suppress add-dll-search-path in main subparser --- openage/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openage/__main__.py b/openage/__main__.py index 970643bb51..60c1c1bb4f 100644 --- a/openage/__main__.py +++ b/openage/__main__.py @@ -127,7 +127,7 @@ def main(argv=None): "codegen", parents=[global_cli])) - args = cli.parse_args(argv) + args, remaining_args = cli.parse_known_args(argv) dll_manager = None if sys.platform == 'win32': @@ -139,7 +139,7 @@ def main(argv=None): if not args.subcommand: # the user didn't specify a subcommand. default to 'main'. - args = main_cli.parse_args(argv) + args = main_cli.parse_args(remaining_args) args.dll_manager = dll_manager From 26c79cb50b9cd2d34737e70e20e69a19b2ea67e1 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Sun, 29 Dec 2024 18:17:51 +0000 Subject: [PATCH 645/771] specify dll path --- .github/workflows/windows-server-2019.yml | 2 +- .github/workflows/windows-server-2022.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/windows-server-2019.yml b/.github/workflows/windows-server-2019.yml index 7cd34e49b5..bf3ff7856b 100644 --- a/.github/workflows/windows-server-2019.yml +++ b/.github/workflows/windows-server-2019.yml @@ -76,7 +76,7 @@ jobs: $DLL_PATH = Join-Path package dll | Resolve-Path cd package python -m openage --add-dll-search-path $DLL_PATH --version - ./run.exe test -a + python -m openage --add-dll-search-path $DLL_PATH test -a shell: pwsh - name: Publish build artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/windows-server-2022.yml b/.github/workflows/windows-server-2022.yml index 327c23584a..a721db5a52 100644 --- a/.github/workflows/windows-server-2022.yml +++ b/.github/workflows/windows-server-2022.yml @@ -76,7 +76,7 @@ jobs: $DLL_PATH = Join-Path package dll | Resolve-Path cd package python -m openage --add-dll-search-path $DLL_PATH --version - ./run.exe test -a + python -m openage --add-dll-search-path $DLL_PATH test -a shell: pwsh - name: Publish build artifacts uses: actions/upload-artifact@v4 From 21fbaa91849ddb5dcac177ade4c64c38c8f67de7 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Thu, 2 Jan 2025 15:05:53 -0500 Subject: [PATCH 646/771] bind empty VAO to bufferless quad rendering --- libopenage/renderer/opengl/geometry.cpp | 3 ++- libopenage/renderer/opengl/renderer.cpp | 13 ++++++++++--- libopenage/renderer/opengl/renderer.h | 11 ++++++++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/libopenage/renderer/opengl/geometry.cpp b/libopenage/renderer/opengl/geometry.cpp index 0e2c930a2a..e0898edbe8 100644 --- a/libopenage/renderer/opengl/geometry.cpp +++ b/libopenage/renderer/opengl/geometry.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 "geometry.h" @@ -54,6 +54,7 @@ void GlGeometry::update_verts_offset(std::vector const &verts, size_t o void GlGeometry::draw() const { switch (this->get_type()) { case geometry_t::bufferless_quad: + // any VAO must be bound before this call glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); break; diff --git a/libopenage/renderer/opengl/renderer.cpp b/libopenage/renderer/opengl/renderer.cpp index 33b734790f..6d8d909a5b 100644 --- a/libopenage/renderer/opengl/renderer.cpp +++ b/libopenage/renderer/opengl/renderer.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 "renderer.h" @@ -15,6 +15,7 @@ #include "renderer/opengl/texture.h" #include "renderer/opengl/uniform_buffer.h" #include "renderer/opengl/uniform_input.h" +#include "renderer/opengl/vertex_array.h" #include "renderer/opengl/window.h" #include "renderer/resources/buffer_info.h" @@ -26,7 +27,8 @@ GlRenderer::GlRenderer(const std::shared_ptr &ctx, gl_context{ctx}, display{std::make_shared(ctx, viewport_size[0], - viewport_size[1])} { + viewport_size[1])}, + shared_quad_vao{std::make_shared(ctx)} { // color used to clear the color buffers glClearColor(0.0f, 0.0f, 0.0f, 0.0f); @@ -100,7 +102,7 @@ std::shared_ptr GlRenderer::add_uniform_buffer(resources::Uniform resources::UniformBufferInfo::get_size(input, info.get_layout()), resources::UniformBufferInfo::get_stride_size(input.type, info.get_layout()), input.count, - input.name}); + input.name}); offset += size; } @@ -169,6 +171,11 @@ void GlRenderer::render(const std::shared_ptr &pass) { auto gl_target = std::dynamic_pointer_cast(pass->get_target()); gl_target->bind_write(); + // ensure that an (empty) VAO is bound before rendering geometry + // a bound VAO is required to render bufferless quad geometries by OpenGL + // see https://www.khronos.org/opengl/wiki/Vertex_Rendering#Causes_of_rendering_failure + shared_quad_vao->bind(); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // TODO: Option for face culling diff --git a/libopenage/renderer/opengl/renderer.h b/libopenage/renderer/opengl/renderer.h index 458af24358..751af7cacc 100644 --- a/libopenage/renderer/opengl/renderer.h +++ b/libopenage/renderer/opengl/renderer.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 @@ -17,6 +17,7 @@ namespace opengl { class GlContext; class GlRenderPass; class GlRenderTarget; +class GlVertexArray; class GlWindow; /// The OpenGL specialization of the rendering interface. @@ -67,6 +68,14 @@ class GlRenderer final : public Renderer { /// The main screen surface as a render target. std::shared_ptr display; + + /// An empty vertex array object (VAO). + /// + /// This VAO has to be bound at the start of a render pass to ensure + /// that bufferless quad geometry can be drawn without errors. Drawing a + /// bufferless quad requires any VAO to be bound + /// see https://www.khronos.org/opengl/wiki/Vertex_Rendering#Causes_of_rendering_failure + std::shared_ptr shared_quad_vao; }; } // namespace opengl From b424f672e3ec2c5104011b3a222a9f1925b57367 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Thu, 9 Jan 2025 01:14:15 +0000 Subject: [PATCH 647/771] remove msvc class or struct prefix in util::demangle --- libopenage/util/compiler.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libopenage/util/compiler.cpp b/libopenage/util/compiler.cpp index 2fd850920d..71100d3f92 100644 --- a/libopenage/util/compiler.cpp +++ b/libopenage/util/compiler.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 "compiler.h" @@ -24,10 +24,10 @@ namespace util { std::string demangle(const char *symbol) { #ifdef _WIN32 - // TODO: demangle names for MSVC; Possibly using UnDecorateSymbolName - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms681400(v=vs.85).aspx - // Could it be that MSVC's typeid(T).name() already returns a demangled name? It seems that .raw_name() returns the mangled name - return symbol; + // MSVC's typeid(T).name() already returns a demangled name + // unlike clang and gcc the MSVC demangled name is prefixed with "class " or "stuct " + // we remove the prefix to match the format of clang and gcc + return strchr(symbol, ' ') + 1; #else int status; char *buf = abi::__cxa_demangle(symbol, nullptr, nullptr, &status); From 251985936eee1dbacc78a45928efb1fd8a0c6fc6 Mon Sep 17 00:00:00 2001 From: Manuel <17874544+MKoesters@users.noreply.github.com> Date: Mon, 13 Jan 2025 22:59:30 +0100 Subject: [PATCH 648/771] Remove additional quotes in build instructions Remove additional " in configure command --- doc/build_instructions/macos.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/build_instructions/macos.md b/doc/build_instructions/macos.md index c9946f2d39..e38dee9932 100644 --- a/doc/build_instructions/macos.md +++ b/doc/build_instructions/macos.md @@ -38,7 +38,7 @@ We advise against using the clang version that comes with macOS (Apple Clang) as ``` # on Intel macOS, llvm is by default in /usr/local/Cellar/llvm/bin/ # on ARM macOS, llvm is by default in /opt/homebrew/Cellar/llvm/bin/ -./configure --compiler="$(brew --prefix llvm)/bin/clang"" --download-nyan +./configure --compiler="$(brew --prefix llvm)/bin/clang" --download-nyan ``` Afterwards, trigger the build using `make`: From 64dd525de649e4c36a30e7ec05bad74e62475164 Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Tue, 3 Dec 2024 21:47:26 -0600 Subject: [PATCH 649/771] Add dirty flag and integrate --- libopenage/pathfinding/CMakeLists.txt | 1 + libopenage/pathfinding/cost_field.cpp | 22 +++++++-- libopenage/pathfinding/cost_field.h | 28 +++++++++-- libopenage/pathfinding/demo/demo_0.cpp | 20 ++++---- libopenage/pathfinding/demo/demo_1.cpp | 16 ++++++- libopenage/pathfinding/field_cache.cpp | 32 +++++++++++++ libopenage/pathfinding/field_cache.h | 66 ++++++++++++++++++++++++++ libopenage/pathfinding/integrator.cpp | 64 +++++++++++++++++-------- libopenage/pathfinding/integrator.h | 34 ++++--------- libopenage/pathfinding/path.h | 5 ++ libopenage/pathfinding/pathfinder.cpp | 3 +- libopenage/pathfinding/sector.cpp | 4 ++ libopenage/pathfinding/sector.h | 8 ++++ libopenage/pathfinding/tests.cpp | 4 +- 14 files changed, 242 insertions(+), 65 deletions(-) create mode 100644 libopenage/pathfinding/field_cache.cpp create mode 100644 libopenage/pathfinding/field_cache.h diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index 7f85264d01..33c2844ace 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -1,6 +1,7 @@ add_sources(libopenage cost_field.cpp definitions.cpp + field_cache.cpp flow_field.cpp grid.cpp integration_field.cpp diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index 3299084548..4ad1318874 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -13,7 +13,8 @@ namespace openage::path { CostField::CostField(size_t size) : size{size}, - cells(this->size * this->size, COST_MIN) { + cells(this->size * this->size, COST_MIN), + changed{time::time_t::min_value()} { log::log(DBG << "Created cost field with size " << this->size << "x" << this->size); } @@ -33,29 +34,42 @@ cost_t CostField::get_cost(size_t idx) const { return this->cells.at(idx); } -void CostField::set_cost(const coord::tile_delta &pos, cost_t cost) { +void CostField::set_cost(const coord::tile_delta &pos, cost_t cost, time::time_t &changed) { this->cells[pos.ne + pos.se * this->size] = cost; + this->changed = changed; } -void CostField::set_cost(size_t x, size_t y, cost_t cost) { +void CostField::set_cost(size_t x, size_t y, cost_t cost, time::time_t &changed) { this->cells[x + y * this->size] = cost; + this->changed = changed; } void CostField::set_cost(size_t idx, cost_t cost) { this->cells[idx] = cost; + this->changed = time::TIME_ZERO; +} + +void CostField::set_cost(size_t idx, cost_t cost,time::time_t &changed) { + this->cells[idx] = cost; + this->changed = changed; } const std::vector &CostField::get_costs() const { return this->cells; } -void CostField::set_costs(std::vector &&cells) { +void CostField::set_costs(std::vector &&cells, time::time_t &changed) { ENSURE(cells.size() == this->cells.size(), "cells vector has wrong size: " << cells.size() << "; expected: " << this->cells.size()); this->cells = std::move(cells); + this->changed = changed; +} + +bool CostField::is_dirty(time::time_t &time) { + return time <= this->changed; } } // namespace openage::path diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index a1fc87b046..99dd16306c 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -6,6 +6,7 @@ #include #include "pathfinding/types.h" +#include "time/time.h" namespace openage { @@ -64,8 +65,9 @@ class CostField { * * @param pos Coordinates of the cell (relative to field origin). * @param cost Cost to set. + * @param changed Time cost is set. */ - void set_cost(const coord::tile_delta &pos, cost_t cost); + void set_cost(const coord::tile_delta &pos, cost_t cost, time::time_t &changed); /** * Set the cost at a specified position. @@ -73,8 +75,9 @@ class CostField { * @param x X-coordinate of the cell. * @param y Y-coordinate of the cell. * @param cost Cost to set. + * @param changed Time cost is set. */ - void set_cost(size_t x, size_t y, cost_t cost); + void set_cost(size_t x, size_t y, cost_t cost, time::time_t &changed); /** * Set the cost at a specified position. @@ -84,6 +87,15 @@ class CostField { */ void set_cost(size_t idx, cost_t cost); + /** + * Set the cost at a specified position. + * + * @param idx Index of the cell. + * @param cost Cost to set. + * @param changed Time cost is set. + */ + void set_cost(size_t idx, cost_t cost, time::time_t &changed); + /** * Get the cost field values. * @@ -95,8 +107,16 @@ class CostField { * Set the cost field values. * * @param cells Cost field values. + * @param changed Time cost is set. */ - void set_costs(std::vector &&cells); + void set_costs(std::vector &&cells, time::time_t &changed); + + /** + * Check if the cost field is dirty at the specified time. + * + * @param time Specified time to check. + */ + bool is_dirty(time::time_t &time); private: /** @@ -104,6 +124,8 @@ class CostField { */ size_t size; + time::time_t changed; + /** * Cost field values. */ diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 483261ebaf..fbe022e8ef 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -33,15 +33,17 @@ void path_demo_0(const util::Path &path) { // Cost field with some obstacles auto cost_field = std::make_shared(field_length); - cost_field->set_cost(coord::tile_delta{0, 0}, COST_IMPASSABLE); - cost_field->set_cost(coord::tile_delta{1, 0}, 254); - cost_field->set_cost(coord::tile_delta{4, 3}, 128); - cost_field->set_cost(coord::tile_delta{5, 3}, 128); - cost_field->set_cost(coord::tile_delta{6, 3}, 128); - cost_field->set_cost(coord::tile_delta{4, 4}, 128); - cost_field->set_cost(coord::tile_delta{5, 4}, 128); - cost_field->set_cost(coord::tile_delta{6, 4}, 128); - cost_field->set_cost(coord::tile_delta{1, 7}, COST_IMPASSABLE); + + time::time_t time = time::TIME_ZERO; + cost_field->set_cost(coord::tile_delta{0, 0}, COST_IMPASSABLE, time); + cost_field->set_cost(coord::tile_delta{1, 0}, 254, time); + cost_field->set_cost(coord::tile_delta{4, 3}, 128, time); + cost_field->set_cost(coord::tile_delta{5, 3}, 128, time); + cost_field->set_cost(coord::tile_delta{6, 3}, 128, time); + cost_field->set_cost(coord::tile_delta{4, 4}, 128, time); + cost_field->set_cost(coord::tile_delta{5, 4}, 128, time); + cost_field->set_cost(coord::tile_delta{6, 4}, 128, time); + cost_field->set_cost(coord::tile_delta{1, 7}, COST_IMPASSABLE, time); log::log(INFO << "Created cost field"); // Create an integration field from the cost field diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 1edeba52a1..ecb0e4cb63 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -11,6 +11,8 @@ #include "pathfinding/portal.h" #include "pathfinding/sector.h" #include "util/timer.h" +#include "time/time_loop.h" +#include "time/clock.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" @@ -25,13 +27,17 @@ namespace openage::path::tests { void path_demo_1(const util::Path &path) { + auto time_loop = std::make_shared(); + time_loop->run(); + auto clock = time_loop->get_clock(); auto grid = std::make_shared(0, util::Vector2s{4, 3}, 10); + time::time_t time = clock->get_time(); // Initialize the cost field for each sector. for (auto §or : grid->get_sectors()) { auto cost_field = sector->get_cost_field(); std::vector sector_cost = sectors_cost.at(sector->get_id()); - cost_field->set_costs(std::move(sector_cost)); + cost_field->set_costs(std::move(sector_cost), time); } // Initialize portals between sectors. @@ -87,16 +93,20 @@ void path_demo_1(const util::Path &path) { coord::tile start{2, 26}; coord::tile target{36, 2}; + time::time_t request_time = clock->get_time(); + PathRequest path_request{ grid->get_id(), start, target, + request_time }; + + log::log(INFO << "Pathfinding request at " << request_time); grid->init_portal_nodes(); timer.start(); Path path_result = pathfinder->get_path(path_request); timer.stop(); - log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " us"); // Create a renderer to display the grid and path @@ -127,6 +137,7 @@ void path_demo_1(const util::Path &path) { grid->get_id(), start, target, + clock->get_time() }; timer.reset(); @@ -147,6 +158,7 @@ void path_demo_1(const util::Path &path) { grid->get_id(), start, target, + clock->get_time() }; timer.reset(); diff --git a/libopenage/pathfinding/field_cache.cpp b/libopenage/pathfinding/field_cache.cpp new file mode 100644 index 0000000000..9d144e1cc4 --- /dev/null +++ b/libopenage/pathfinding/field_cache.cpp @@ -0,0 +1,32 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "field_cache.h" + +namespace openage::path { + +void FieldCache::add(std::pair cache_key, + std::shared_ptr &integration_field, + std::shared_ptr &flow_field) { + this->cache[cache_key] = std::make_pair(integration_field, flow_field); +} + +void FieldCache::evict(std::pair cache_key) { + this->cache.erase(cache_key); +} + +bool FieldCache::is_cached(std::pair cache_key) { + auto cached = this->cache.find(cache_key); + return cached != this->cache.end(); +} + +std::shared_ptr FieldCache::get_integration_field(std::pair cache_key) { + auto cached = this->cache.find(cache_key); + return cached->second.first; +} + +std::shared_ptr FieldCache::get_flow_field(std::pair cache_key) { + auto cached = this->cache.find(cache_key); + return cached->second.second; +} + +} // namespace openage::path \ No newline at end of file diff --git a/libopenage/pathfinding/field_cache.h b/libopenage/pathfinding/field_cache.h new file mode 100644 index 0000000000..7b98c4d957 --- /dev/null +++ b/libopenage/pathfinding/field_cache.h @@ -0,0 +1,66 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "pathfinding/types.h" +#include "util/hash.h" + +namespace openage { +namespace coord { +struct tile_delta; +} // namespace coord + +namespace path { +class IntegrationField; +class FlowField; + +class FieldCache +{ +public: + FieldCache() = default; + ~FieldCache() = default; + + void add(std::pair cache_key, + std::shared_ptr &integration_field, + std::shared_ptr &flow_field); + + void evict(std::pair cache_key); + + bool is_cached(std::pair cache_key); + + std::shared_ptr get_integration_field(std::pair cache_key); + + std::shared_ptr get_flow_field(std::pair cache_key); + + using get_return_t = std::pair, std::shared_ptr>; + +private: + /** + * Hash function for the field cache. + */ + struct pair_hash { + template + std::size_t operator()(const std::pair &pair) const { + return util::hash_combine(std::hash{}(pair.first), std::hash{}(pair.second)); + } + }; + + /** + * Cache for already computed fields. + * + * Key is the portal ID and the sector ID from which the field was entered. Fields that are cached are + * cleared of dynamic flags, i.e. wavefront or LOS flags. These have to be recalculated + * when the field is reused. + */ + + std::unordered_map, + get_return_t, + pair_hash> + cache; +}; + +} // namespace path +} // namespace openage \ No newline at end of file diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 265c5285cc..d91ac9ecb7 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -8,6 +8,7 @@ #include "pathfinding/flow_field.h" #include "pathfinding/integration_field.h" #include "pathfinding/portal.h" +#include "pathfinding/field_cache.h" namespace openage::path { @@ -36,24 +37,32 @@ std::shared_ptr Integrator::integrate(const std::shared_ptr &portal, const coord::tile_delta &target, - bool with_los) { + bool with_los, + bool evict_cache) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); - auto cached = this->field_cache.find(cache_key); - if (cached != this->field_cache.end()) { + if (evict_cache) { + this->field_cache->evict(cache_key); + } + if (this->field_cache->is_cached(cache_key)) { log::log(DBG << "Using cached integration field for portal " << portal->get_id() << " from sector " << other_sector_id); + + + //retrieve cached integration field + auto cached_integration_field = this->field_cache->get_integration_field(cache_key); + if (with_los) { log::log(SPAM << "Performing LOS pass on cached field"); // Make a copy of the cached field to avoid modifying the cached field - auto integration_field = std::make_shared(*cached->second.first); + auto integration_field = std::make_shared(*cached_integration_field); // Only integrate LOS; leave the rest of the field as is integration_field->integrate_los(cost_field, other, other_sector_id, portal, target); return integration_field; } - return cached->second.first; + return cached_integration_field; } log::log(DBG << "Integrating cost field for portal " << portal->get_id() @@ -97,17 +106,24 @@ std::shared_ptr Integrator::build(const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal, - bool with_los) { + bool with_los, + bool evict_cache) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); - auto cached = this->field_cache.find(cache_key); - if (cached != this->field_cache.end()) { + if (evict_cache) { + this->field_cache->evict(cache_key); + } + if (this->field_cache->is_cached(cache_key)) { log::log(DBG << "Using cached flow field for portal " << portal->get_id() << " from sector " << other_sector_id); + + //retrieve cached flow field + auto cached_flow_field = this->field_cache->get_flow_field(cache_key); + if (with_los) { log::log(SPAM << "Transferring LOS flags to cached flow field"); // Make a copy of the cached flow field - auto flow_field = std::make_shared(*cached->second.second); + auto flow_field = std::make_shared(*cached_flow_field); // Transfer the LOS flags to the flow field flow_field->transfer_dynamic_flags(integration_field); @@ -115,7 +131,7 @@ std::shared_ptr Integrator::build(const std::shared_ptrsecond.second; + return cached_flow_field; } log::log(DBG << "Building flow field for portal " << portal->get_id() @@ -140,17 +156,27 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ sector_id_t other_sector_id, const std::shared_ptr &portal, const coord::tile_delta &target, - bool with_los) { + bool with_los, + bool evict_cache) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); - auto cached = this->field_cache.find(cache_key); - if (cached != this->field_cache.end()) { + if (evict_cache) { + this->field_cache->evict(cache_key); + } + if (this->field_cache->is_cached(cache_key)) { log::log(DBG << "Using cached integration and flow fields for portal " << portal->get_id() << " from sector " << other_sector_id); + + + //retrieve cached fields + auto cached_integration_field = this->field_cache->get_integration_field(cache_key); + auto cached_flow_field = this->field_cache->get_flow_field(cache_key); + + if (with_los) { log::log(SPAM << "Performing LOS pass on cached field"); // Make a copy of the cached integration field - auto integration_field = std::make_shared(*cached->second.first); + auto integration_field = std::make_shared(*cached_integration_field); // Only integrate LOS; leave the rest of the field as is integration_field->integrate_los(cost_field, other, other_sector_id, portal, target); @@ -158,7 +184,7 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ log::log(SPAM << "Transferring LOS flags to cached flow field"); // Make a copy of the cached flow field - auto flow_field = std::make_shared(*cached->second.second); + auto flow_field = std::make_shared(*cached_flow_field); // Transfer the LOS flags to the flow field flow_field->transfer_dynamic_flags(integration_field); @@ -166,11 +192,11 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ return std::make_pair(integration_field, flow_field); } - return cached->second; + return std::make_pair(cached_integration_field, cached_flow_field); } - auto integration_field = this->integrate(cost_field, other, other_sector_id, portal, target, with_los); - auto flow_field = this->build(integration_field, other, other_sector_id, portal); + auto integration_field = this->integrate(cost_field, other, other_sector_id, portal, target, with_los, evict_cache); + auto flow_field = this->build(integration_field, other, other_sector_id, portal, evict_cache); log::log(DBG << "Caching integration and flow fields for portal ID: " << portal->get_id() << ", sector ID: " << other_sector_id); @@ -182,7 +208,7 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ std::shared_ptr cached_flow_field = std::make_shared(*flow_field); cached_flow_field->reset_dynamic_flags(); - this->field_cache[cache_key] = std::make_pair(cached_integration_field, cached_flow_field); + this->field_cache->add(cache_key, cached_integration_field, cached_flow_field); return std::make_pair(integration_field, flow_field); } diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index c27d3c1eb1..690c525ad7 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -2,9 +2,7 @@ #pragma once -#include #include -#include #include "pathfinding/types.h" #include "util/hash.h" @@ -20,6 +18,7 @@ class CostField; class FlowField; class IntegrationField; class Portal; +class FieldCache; /** * Integrator for the flow field pathfinding algorithm. @@ -65,7 +64,8 @@ class Integrator { sector_id_t other_sector_id, const std::shared_ptr &portal, const coord::tile_delta &target, - bool with_los = true); + bool with_los = true, + bool evict_cache = false); /** * Build the flow field from an integration field. @@ -91,7 +91,8 @@ class Integrator { const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal, - bool with_los = true); + bool with_los = true, + bool evict_cache = false); using get_return_t = std::pair, std::shared_ptr>; @@ -123,30 +124,11 @@ class Integrator { sector_id_t other_sector_id, const std::shared_ptr &portal, const coord::tile_delta &target, - bool with_los = true); + bool with_los = true, + bool evict_cache = false); private: - /** - * Hash function for the field cache. - */ - struct pair_hash { - template - std::size_t operator()(const std::pair &pair) const { - return util::hash_combine(std::hash{}(pair.first), std::hash{}(pair.second)); - } - }; - - /** - * Cache for already computed fields. - * - * Key is the portal ID and the sector ID from which the field was entered. Fields that are cached are - * cleared of dynamic flags, i.e. wavefront or LOS flags. These have to be recalculated - * when the field is reused. - */ - std::unordered_map, - get_return_t, - pair_hash> - field_cache; + std::shared_ptr field_cache; }; } // namespace path diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/path.h index d1c61ac325..cfeb7bc503 100644 --- a/libopenage/pathfinding/path.h +++ b/libopenage/pathfinding/path.h @@ -6,6 +6,7 @@ #include "coord/tile.h" #include "pathfinding/types.h" +#include "time/time.h" namespace openage::path { @@ -20,6 +21,8 @@ struct PathRequest { coord::tile start; /// Target position of the path. coord::tile target; + /// Time request was made. + time::time_t time; }; /** @@ -34,6 +37,8 @@ struct Path { /// First waypoint is the start position of the path request. /// Last waypoint is the target position of the path request. std::vector waypoints; + /// Time path was created. + time::time_t time; }; } // namespace openage::path diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index c99afd5b9d..8aec42a514 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -155,7 +155,8 @@ const Path Pathfinder::get_path(const PathRequest &request) { prev_sector_id, portal, target_delta, - with_los); + with_los, + next_sector->is_dirty(request.time)); flow_fields.push_back(std::make_pair(next_sector_id, sector_fields.second)); prev_integration_field = sector_fields.first; diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index f50dc209bf..08843fe016 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -249,4 +249,8 @@ void Sector::connect_exits() { } } +bool Sector::is_dirty(time::time_t time) { + return this->cost_field->is_dirty(time); +} + } // namespace openage::path diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h index a4ba94fa52..cc2950f25e 100644 --- a/libopenage/pathfinding/sector.h +++ b/libopenage/pathfinding/sector.h @@ -9,6 +9,7 @@ #include "coord/chunk.h" #include "pathfinding/portal.h" #include "pathfinding/types.h" +#include "time/time.h" namespace openage::path { @@ -103,6 +104,13 @@ class Sector { */ void connect_exits(); + /** + * Check if the cost field is dirty at the specified time. + * + * @param time Specified time to check. + */ + bool is_dirty(time::time_t time); + private: /** * ID of the sector. diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index 5f92496ff8..6ec26b392d 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -10,6 +10,7 @@ #include "pathfinding/integration_field.h" #include "pathfinding/integrator.h" #include "pathfinding/types.h" +#include "time/time.h" namespace openage { @@ -23,7 +24,8 @@ void flow_field() { // | 1 | 1 | 1 | // | 1 | X | 1 | // | 1 | 1 | 1 | - cost_field->set_costs({1, 1, 1, 1, 255, 1, 1, 1, 1}); + time::time_t time = time::TIME_ZERO; + cost_field->set_costs({1, 1, 1, 1, 255, 1, 1, 1, 1}, time); // Test the different field types { From 2110ff21c568314987df89cd5df5d70772f38465 Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Thu, 5 Dec 2024 19:50:20 -0600 Subject: [PATCH 650/771] Update libopenage/pathfinding/cost_field.cpp Co-authored-by: Christoph Heine <6852422+heinezen@users.noreply.github.com> --- libopenage/pathfinding/cost_field.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index 4ad1318874..d4ae7f8464 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -14,7 +14,7 @@ namespace openage::path { CostField::CostField(size_t size) : size{size}, cells(this->size * this->size, COST_MIN), - changed{time::time_t::min_value()} { + changed{time::TIME_MIN} { log::log(DBG << "Created cost field with size " << this->size << "x" << this->size); } From 5dcc8a82cdf8b354fb3126ae1237cd47f262308d Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Thu, 5 Dec 2024 20:26:51 -0600 Subject: [PATCH 651/771] Add const to time_t and improve docstrings --- libopenage/pathfinding/cost_field.cpp | 10 +++++----- libopenage/pathfinding/cost_field.h | 25 ++++++++++++++----------- libopenage/pathfinding/demo/demo_0.cpp | 2 +- libopenage/pathfinding/demo/demo_1.cpp | 6 +++--- libopenage/pathfinding/field_cache.cpp | 2 +- libopenage/pathfinding/field_cache.h | 2 +- libopenage/pathfinding/path.h | 4 ++-- libopenage/pathfinding/sector.cpp | 2 +- libopenage/pathfinding/sector.h | 2 +- libopenage/pathfinding/tests.cpp | 2 +- 10 files changed, 30 insertions(+), 27 deletions(-) diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index d4ae7f8464..c211d169c1 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -34,12 +34,12 @@ cost_t CostField::get_cost(size_t idx) const { return this->cells.at(idx); } -void CostField::set_cost(const coord::tile_delta &pos, cost_t cost, time::time_t &changed) { +void CostField::set_cost(const coord::tile_delta &pos, cost_t cost, const time::time_t &changed) { this->cells[pos.ne + pos.se * this->size] = cost; this->changed = changed; } -void CostField::set_cost(size_t x, size_t y, cost_t cost, time::time_t &changed) { +void CostField::set_cost(size_t x, size_t y, cost_t cost, const time::time_t &changed) { this->cells[x + y * this->size] = cost; this->changed = changed; } @@ -49,7 +49,7 @@ void CostField::set_cost(size_t idx, cost_t cost) { this->changed = time::TIME_ZERO; } -void CostField::set_cost(size_t idx, cost_t cost,time::time_t &changed) { +void CostField::set_cost(size_t idx, cost_t cost, const time::time_t &changed) { this->cells[idx] = cost; this->changed = changed; } @@ -58,7 +58,7 @@ const std::vector &CostField::get_costs() const { return this->cells; } -void CostField::set_costs(std::vector &&cells, time::time_t &changed) { +void CostField::set_costs(std::vector &&cells, const time::time_t &changed) { ENSURE(cells.size() == this->cells.size(), "cells vector has wrong size: " << cells.size() << "; expected: " @@ -68,7 +68,7 @@ void CostField::set_costs(std::vector &&cells, time::time_t &changed) { this->changed = changed; } -bool CostField::is_dirty(time::time_t &time) { +bool CostField::is_dirty(const time::time_t &time) { return time <= this->changed; } diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index 99dd16306c..e62496acf1 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -65,9 +65,9 @@ class CostField { * * @param pos Coordinates of the cell (relative to field origin). * @param cost Cost to set. - * @param changed Time cost is set. + * @param changed Time at which the cost value is changed. */ - void set_cost(const coord::tile_delta &pos, cost_t cost, time::time_t &changed); + void set_cost(const coord::tile_delta &pos, cost_t cost, const time::time_t &changed); /** * Set the cost at a specified position. @@ -75,9 +75,9 @@ class CostField { * @param x X-coordinate of the cell. * @param y Y-coordinate of the cell. * @param cost Cost to set. - * @param changed Time cost is set. + * @param changed Time at which the cost value is changed. */ - void set_cost(size_t x, size_t y, cost_t cost, time::time_t &changed); + void set_cost(size_t x, size_t y, cost_t cost, const time::time_t &changed); /** * Set the cost at a specified position. @@ -92,9 +92,9 @@ class CostField { * * @param idx Index of the cell. * @param cost Cost to set. - * @param changed Time cost is set. + * @param changed Time at which the cost value is changed. */ - void set_cost(size_t idx, cost_t cost, time::time_t &changed); + void set_cost(size_t idx, cost_t cost, const time::time_t &changed); /** * Get the cost field values. @@ -107,16 +107,16 @@ class CostField { * Set the cost field values. * * @param cells Cost field values. - * @param changed Time cost is set. + * @param changed Time at which the cost value is changed. */ - void set_costs(std::vector &&cells, time::time_t &changed); + void set_costs(std::vector &&cells, const time::time_t &changed); /** * Check if the cost field is dirty at the specified time. * - * @param time Specified time to check. + * @param time Cost field is dirty if the cost field is accessed before the latest change to the cost field. */ - bool is_dirty(time::time_t &time); + bool is_dirty(const time::time_t &time); private: /** @@ -124,7 +124,10 @@ class CostField { */ size_t size; - time::time_t changed; + /** + * Time the cost field was last changed. + */ + const time::time_t changed; /** * Cost field values. diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index fbe022e8ef..2dfb825fa0 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -34,7 +34,7 @@ void path_demo_0(const util::Path &path) { // Cost field with some obstacles auto cost_field = std::make_shared(field_length); - time::time_t time = time::TIME_ZERO; + const time::time_t time = time::TIME_ZERO; cost_field->set_cost(coord::tile_delta{0, 0}, COST_IMPASSABLE, time); cost_field->set_cost(coord::tile_delta{1, 0}, 254, time); cost_field->set_cost(coord::tile_delta{4, 3}, 128, time); diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index ecb0e4cb63..a31b9b786b 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -32,7 +32,7 @@ void path_demo_1(const util::Path &path) { auto clock = time_loop->get_clock(); auto grid = std::make_shared(0, util::Vector2s{4, 3}, 10); - time::time_t time = clock->get_time(); + const time::time_t time = clock->get_time(); // Initialize the cost field for each sector. for (auto §or : grid->get_sectors()) { auto cost_field = sector->get_cost_field(); @@ -93,7 +93,7 @@ void path_demo_1(const util::Path &path) { coord::tile start{2, 26}; coord::tile target{36, 2}; - time::time_t request_time = clock->get_time(); + const time::time_t request_time = clock->get_time(); PathRequest path_request{ grid->get_id(), @@ -102,11 +102,11 @@ void path_demo_1(const util::Path &path) { request_time }; - log::log(INFO << "Pathfinding request at " << request_time); grid->init_portal_nodes(); timer.start(); Path path_result = pathfinder->get_path(path_request); timer.stop(); + log::log(INFO << "Pathfinding request at " << request_time); log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " us"); // Create a renderer to display the grid and path diff --git a/libopenage/pathfinding/field_cache.cpp b/libopenage/pathfinding/field_cache.cpp index 9d144e1cc4..a798e9fbd7 100644 --- a/libopenage/pathfinding/field_cache.cpp +++ b/libopenage/pathfinding/field_cache.cpp @@ -29,4 +29,4 @@ std::shared_ptr FieldCache::get_flow_field(std::pairsecond.second; } -} // namespace openage::path \ No newline at end of file +} // namespace openage::path diff --git a/libopenage/pathfinding/field_cache.h b/libopenage/pathfinding/field_cache.h index 7b98c4d957..a1f43a563e 100644 --- a/libopenage/pathfinding/field_cache.h +++ b/libopenage/pathfinding/field_cache.h @@ -63,4 +63,4 @@ class FieldCache }; } // namespace path -} // namespace openage \ No newline at end of file +} // namespace openage diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/path.h index cfeb7bc503..ca6391bf67 100644 --- a/libopenage/pathfinding/path.h +++ b/libopenage/pathfinding/path.h @@ -22,7 +22,7 @@ struct PathRequest { /// Target position of the path. coord::tile target; /// Time request was made. - time::time_t time; + const time::time_t time; }; /** @@ -38,7 +38,7 @@ struct Path { /// Last waypoint is the target position of the path request. std::vector waypoints; /// Time path was created. - time::time_t time; + const time::time_t time; }; } // namespace openage::path diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index 08843fe016..4556fd42c3 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -249,7 +249,7 @@ void Sector::connect_exits() { } } -bool Sector::is_dirty(time::time_t time) { +bool Sector::is_dirty(const time::time_t time) { return this->cost_field->is_dirty(time); } diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h index cc2950f25e..e0f751b70d 100644 --- a/libopenage/pathfinding/sector.h +++ b/libopenage/pathfinding/sector.h @@ -109,7 +109,7 @@ class Sector { * * @param time Specified time to check. */ - bool is_dirty(time::time_t time); + bool is_dirty(const time::time_t time); private: /** diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index 6ec26b392d..2ac0960c34 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -24,7 +24,7 @@ void flow_field() { // | 1 | 1 | 1 | // | 1 | X | 1 | // | 1 | 1 | 1 | - time::time_t time = time::TIME_ZERO; + const time::time_t time = time::TIME_ZERO; cost_field->set_costs({1, 1, 1, 1, 255, 1, 1, 1, 1}, time); // Test the different field types From 04211fac4990272578ca70e8b5d5725a9e56edeb Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Thu, 5 Dec 2024 20:28:14 -0600 Subject: [PATCH 652/771] change sector is_dirty to a reference --- libopenage/pathfinding/sector.cpp | 2 +- libopenage/pathfinding/sector.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index 4556fd42c3..2da73690c0 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -249,7 +249,7 @@ void Sector::connect_exits() { } } -bool Sector::is_dirty(const time::time_t time) { +bool Sector::is_dirty(const time::time_t &time) { return this->cost_field->is_dirty(time); } diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h index e0f751b70d..a8aad24c01 100644 --- a/libopenage/pathfinding/sector.h +++ b/libopenage/pathfinding/sector.h @@ -109,7 +109,7 @@ class Sector { * * @param time Specified time to check. */ - bool is_dirty(const time::time_t time); + bool is_dirty(const time::time_t &time); private: /** From 7f75f5309c452ac8ad7c836f0d5862aec7fb35e8 Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Thu, 5 Dec 2024 22:56:07 -0600 Subject: [PATCH 653/771] Change fieldCache to unique_ptr and add docs --- libopenage/pathfinding/field_cache.h | 1 - libopenage/pathfinding/integrator.h | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/field_cache.h b/libopenage/pathfinding/field_cache.h index a1f43a563e..87dbdf60f9 100644 --- a/libopenage/pathfinding/field_cache.h +++ b/libopenage/pathfinding/field_cache.h @@ -55,7 +55,6 @@ class FieldCache * cleared of dynamic flags, i.e. wavefront or LOS flags. These have to be recalculated * when the field is reused. */ - std::unordered_map, get_return_t, pair_hash> diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index 690c525ad7..7d09c00a73 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -128,7 +128,11 @@ class Integrator { bool evict_cache = false); private: - std::shared_ptr field_cache; + + /** + * Cache for already computed fields. + */ + std::unique_ptr field_cache; }; } // namespace path From 219431d9b29b9af2661fa5675bce6a3ed2e99989 Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Thu, 5 Dec 2024 22:57:25 -0600 Subject: [PATCH 654/771] Grammar Co-authored-by: Christoph Heine <6852422+heinezen@users.noreply.github.com> --- libopenage/pathfinding/path.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/path.h index ca6391bf67..8be7f802f7 100644 --- a/libopenage/pathfinding/path.h +++ b/libopenage/pathfinding/path.h @@ -37,7 +37,7 @@ struct Path { /// First waypoint is the start position of the path request. /// Last waypoint is the target position of the path request. std::vector waypoints; - /// Time path was created. + /// Time the path was created. const time::time_t time; }; From 305b41264d48904f4dead1bae19f7c0cbc311f9e Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Thu, 5 Dec 2024 22:57:41 -0600 Subject: [PATCH 655/771] Grammar Co-authored-by: Christoph Heine <6852422+heinezen@users.noreply.github.com> --- libopenage/pathfinding/path.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/path.h index 8be7f802f7..2d1f1cd593 100644 --- a/libopenage/pathfinding/path.h +++ b/libopenage/pathfinding/path.h @@ -21,7 +21,7 @@ struct PathRequest { coord::tile start; /// Target position of the path. coord::tile target; - /// Time request was made. + /// Time the request was made. const time::time_t time; }; From 94101cbcd6edc1d73fd0284e848dd7fdb88eecae Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Thu, 5 Dec 2024 23:03:37 -0600 Subject: [PATCH 656/771] Use contains() Co-authored-by: Christoph Heine <6852422+heinezen@users.noreply.github.com> --- libopenage/pathfinding/field_cache.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libopenage/pathfinding/field_cache.cpp b/libopenage/pathfinding/field_cache.cpp index a798e9fbd7..81b21c8264 100644 --- a/libopenage/pathfinding/field_cache.cpp +++ b/libopenage/pathfinding/field_cache.cpp @@ -15,8 +15,7 @@ void FieldCache::evict(std::pair cache_k } bool FieldCache::is_cached(std::pair cache_key) { - auto cached = this->cache.find(cache_key); - return cached != this->cache.end(); + return this->cache.contains(cache_key); } std::shared_ptr FieldCache::get_integration_field(std::pair cache_key) { From 3cfdb11893650e3e7465476d32d555df86469aba Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Sat, 7 Dec 2024 12:04:36 -0600 Subject: [PATCH 657/771] several changes --- libopenage/gamestate/map.cpp | 2 +- libopenage/pathfinding/cost_field.cpp | 31 ++++++++--------- libopenage/pathfinding/cost_field.h | 21 +++++------- libopenage/pathfinding/field_cache.cpp | 27 ++++++++------- libopenage/pathfinding/field_cache.h | 46 +++++++++++++++++--------- libopenage/pathfinding/integrator.cpp | 22 ++++++------ libopenage/pathfinding/types.h | 15 +++++++++ 7 files changed, 92 insertions(+), 72 deletions(-) diff --git a/libopenage/gamestate/map.cpp b/libopenage/gamestate/map.cpp index 8300ce273d..88578fabdb 100644 --- a/libopenage/gamestate/map.cpp +++ b/libopenage/gamestate/map.cpp @@ -49,7 +49,7 @@ Map::Map(const std::shared_ptr &state, auto sector = grid->get_sector(chunk_idx); auto cost_field = sector->get_cost_field(); - cost_field->set_cost(tile_idx, path_cost.second); + cost_field->set_cost(tile_idx, path_cost.second, time::TIME_ZERO); } } } diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index c211d169c1..d737f550c0 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -14,7 +14,7 @@ namespace openage::path { CostField::CostField(size_t size) : size{size}, cells(this->size * this->size, COST_MIN), - changed{time::TIME_MIN} { + valid_until{time::TIME_MIN} { log::log(DBG << "Created cost field with size " << this->size << "x" << this->size); } @@ -34,42 +34,39 @@ cost_t CostField::get_cost(size_t idx) const { return this->cells.at(idx); } -void CostField::set_cost(const coord::tile_delta &pos, cost_t cost, const time::time_t &changed) { - this->cells[pos.ne + pos.se * this->size] = cost; - this->changed = changed; +void CostField::set_cost(const coord::tile_delta &pos, cost_t cost, const time::time_t &valid_until) { + this->set_cost(pos.ne + pos.se * this->size, cost, valid_until); } -void CostField::set_cost(size_t x, size_t y, cost_t cost, const time::time_t &changed) { - this->cells[x + y * this->size] = cost; - this->changed = changed; +void CostField::set_cost(size_t x, size_t y, cost_t cost, const time::time_t &valid_until) { + this->set_cost(x + y * this->size, cost, valid_until); } -void CostField::set_cost(size_t idx, cost_t cost) { +inline void CostField::set_cost(size_t idx, cost_t cost, const time::time_t &valid_until) { this->cells[idx] = cost; - this->changed = time::TIME_ZERO; -} - -void CostField::set_cost(size_t idx, cost_t cost, const time::time_t &changed) { - this->cells[idx] = cost; - this->changed = changed; + this->valid_until = valid_until; } const std::vector &CostField::get_costs() const { return this->cells; } -void CostField::set_costs(std::vector &&cells, const time::time_t &changed) { +void CostField::set_costs(std::vector &&cells, const time::time_t &valid_until) { ENSURE(cells.size() == this->cells.size(), "cells vector has wrong size: " << cells.size() << "; expected: " << this->cells.size()); this->cells = std::move(cells); - this->changed = changed; + this->valid_until = valid_until; } bool CostField::is_dirty(const time::time_t &time) { - return time <= this->changed; + return time >= this->valid_until; +} + +void CostField::clean() { + this->valid_until = time::TIME_MAX; } } // namespace openage::path diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index e62496acf1..f70adf9251 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -79,14 +79,6 @@ class CostField { */ void set_cost(size_t x, size_t y, cost_t cost, const time::time_t &changed); - /** - * Set the cost at a specified position. - * - * @param idx Index of the cell. - * @param cost Cost to set. - */ - void set_cost(size_t idx, cost_t cost); - /** * Set the cost at a specified position. * @@ -94,7 +86,7 @@ class CostField { * @param cost Cost to set. * @param changed Time at which the cost value is changed. */ - void set_cost(size_t idx, cost_t cost, const time::time_t &changed); + inline void set_cost(size_t idx, cost_t cost, const time::time_t &changed); /** * Get the cost field values. @@ -113,11 +105,16 @@ class CostField { /** * Check if the cost field is dirty at the specified time. - * - * @param time Cost field is dirty if the cost field is accessed before the latest change to the cost field. + * + * @param time Cost field is dirty if the cost field is accessed after the time given in valid_until. */ bool is_dirty(const time::time_t &time); + /** + * Cleans the dirty flag by setting it to time_MAX. + */ + void clean(); + private: /** * Side length of the field. @@ -127,7 +124,7 @@ class CostField { /** * Time the cost field was last changed. */ - const time::time_t changed; + time::time_t &valid_until; /** * Cost field values. diff --git a/libopenage/pathfinding/field_cache.cpp b/libopenage/pathfinding/field_cache.cpp index 81b21c8264..95276b0818 100644 --- a/libopenage/pathfinding/field_cache.cpp +++ b/libopenage/pathfinding/field_cache.cpp @@ -4,28 +4,27 @@ namespace openage::path { -void FieldCache::add(std::pair cache_key, - std::shared_ptr &integration_field, - std::shared_ptr &flow_field) { - this->cache[cache_key] = std::make_pair(integration_field, flow_field); +void FieldCache::add(cache_key_t cache_key, + field_cache_t cache_entry) { + this->cache[cache_key] = cache_entry; } -void FieldCache::evict(std::pair cache_key) { - this->cache.erase(cache_key); +void FieldCache::evict(cache_key_t cache_key) { + this->cache.erase(cache_key); } -bool FieldCache::is_cached(std::pair cache_key) { - return this->cache.contains(cache_key); +bool FieldCache::is_cached(cache_key_t cache_key) { + return this->cache.contains(cache_key); } -std::shared_ptr FieldCache::get_integration_field(std::pair cache_key) { - auto cached = this->cache.find(cache_key); - return cached->second.first; +std::shared_ptr FieldCache::get_integration_field(cache_key_t cache_key) { + auto cached = this->cache.find(cache_key); + return cached->second.first; } -std::shared_ptr FieldCache::get_flow_field(std::pair cache_key) { - auto cached = this->cache.find(cache_key); - return cached->second.second; +std::shared_ptr FieldCache::get_flow_field(cache_key_t cache_key) { + auto cached = this->cache.find(cache_key); + return cached->second.second; } } // namespace openage::path diff --git a/libopenage/pathfinding/field_cache.h b/libopenage/pathfinding/field_cache.h index 87dbdf60f9..23374e5123 100644 --- a/libopenage/pathfinding/field_cache.h +++ b/libopenage/pathfinding/field_cache.h @@ -17,25 +17,39 @@ namespace path { class IntegrationField; class FlowField; -class FieldCache -{ +/** + * Cache to store already calculated flow and integration fields for the pathfinding algorithm. + */ +class FieldCache { public: - FieldCache() = default; - ~FieldCache() = default; + FieldCache() = default; + ~FieldCache() = default; - void add(std::pair cache_key, - std::shared_ptr &integration_field, - std::shared_ptr &flow_field); + /** + * Adds a new field cache entry to the cache with a given portal and sector cache key. + */ + void add(cache_key_t cache_key, + field_cache_t cache_entry); - void evict(std::pair cache_key); + /** + * Evicts a given field cache entry from the cache at the given cache key. + */ + void evict(cache_key_t cache_key); - bool is_cached(std::pair cache_key); + /** + * Checks if there is a cached entry at a specific cache key. + */ + bool is_cached(cache_key_t cache_key); - std::shared_ptr get_integration_field(std::pair cache_key); - - std::shared_ptr get_flow_field(std::pair cache_key); - - using get_return_t = std::pair, std::shared_ptr>; + /** + * Gets the integration field from a given cache entry. + */ + std::shared_ptr get_integration_field(cache_key_t cache_key); + + /** + * Gets the flow field from a given cache entry. + */ + std::shared_ptr get_flow_field(cache_key_t cache_key); private: /** @@ -55,8 +69,8 @@ class FieldCache * cleared of dynamic flags, i.e. wavefront or LOS flags. These have to be recalculated * when the field is reused. */ - std::unordered_map, - get_return_t, + std::unordered_map cache; }; diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index d91ac9ecb7..e164f3a094 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -5,10 +5,10 @@ #include "log/log.h" #include "pathfinding/cost_field.h" +#include "pathfinding/field_cache.h" #include "pathfinding/flow_field.h" #include "pathfinding/integration_field.h" #include "pathfinding/portal.h" -#include "pathfinding/field_cache.h" namespace openage::path { @@ -38,17 +38,16 @@ std::shared_ptr Integrator::integrate(const std::shared_ptr &portal, const coord::tile_delta &target, bool with_los, - bool evict_cache) { + bool evict_cache) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); if (evict_cache) { this->field_cache->evict(cache_key); } - if (this->field_cache->is_cached(cache_key)) { + else if (this->field_cache->is_cached(cache_key)) { log::log(DBG << "Using cached integration field for portal " << portal->get_id() << " from sector " << other_sector_id); - - //retrieve cached integration field + // retrieve cached integration field auto cached_integration_field = this->field_cache->get_integration_field(cache_key); if (with_los) { @@ -107,16 +106,16 @@ std::shared_ptr Integrator::build(const std::shared_ptr &portal, bool with_los, - bool evict_cache) { + bool evict_cache) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); if (evict_cache) { this->field_cache->evict(cache_key); } - if (this->field_cache->is_cached(cache_key)) { + else if (this->field_cache->is_cached(cache_key)) { log::log(DBG << "Using cached flow field for portal " << portal->get_id() << " from sector " << other_sector_id); - //retrieve cached flow field + // retrieve cached flow field auto cached_flow_field = this->field_cache->get_flow_field(cache_key); if (with_los) { @@ -157,17 +156,16 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ const std::shared_ptr &portal, const coord::tile_delta &target, bool with_los, - bool evict_cache) { + bool evict_cache) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); if (evict_cache) { this->field_cache->evict(cache_key); } - if (this->field_cache->is_cached(cache_key)) { + else if (this->field_cache->is_cached(cache_key)) { log::log(DBG << "Using cached integration and flow fields for portal " << portal->get_id() << " from sector " << other_sector_id); - - //retrieve cached fields + // retrieve cached fields auto cached_integration_field = this->field_cache->get_integration_field(cache_key); auto cached_flow_field = this->field_cache->get_flow_field(cache_key); diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index 1b48a49654..3229010a91 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -4,6 +4,8 @@ #include #include +#include +#include namespace openage::path { @@ -109,4 +111,17 @@ using sector_id_t = size_t; */ using portal_id_t = size_t; +class FlowField; +class IntegrationField; + +/** + * Cache key for accessing the field cache using a portal id and a sector id. + */ +using cache_key_t = std::pair; + +/** + * Returnable field cache entry pair containing an integration field and a flow field. + */ +using field_cache_t = std::pair, std::shared_ptr>; + } // namespace openage::path From 5c4150505e2743780c8f409ea78a94ade4a6b411 Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:34:01 -0600 Subject: [PATCH 658/771] next round of changes --- libopenage/pathfinding/cost_field.cpp | 5 ----- libopenage/pathfinding/cost_field.h | 12 ++++++++---- libopenage/pathfinding/integrator.cpp | 4 +++- libopenage/pathfinding/integrator.h | 2 +- libopenage/pathfinding/path.h | 2 -- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index d737f550c0..3ba37a6242 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -42,11 +42,6 @@ void CostField::set_cost(size_t x, size_t y, cost_t cost, const time::time_t &va this->set_cost(x + y * this->size, cost, valid_until); } -inline void CostField::set_cost(size_t idx, cost_t cost, const time::time_t &valid_until) { - this->cells[idx] = cost; - this->valid_until = valid_until; -} - const std::vector &CostField::get_costs() const { return this->cells; } diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index f70adf9251..f214e30f96 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -67,7 +67,7 @@ class CostField { * @param cost Cost to set. * @param changed Time at which the cost value is changed. */ - void set_cost(const coord::tile_delta &pos, cost_t cost, const time::time_t &changed); + void set_cost(const coord::tile_delta &pos, cost_t cost, const time::time_t &valid_until); /** * Set the cost at a specified position. @@ -77,7 +77,7 @@ class CostField { * @param cost Cost to set. * @param changed Time at which the cost value is changed. */ - void set_cost(size_t x, size_t y, cost_t cost, const time::time_t &changed); + void set_cost(size_t x, size_t y, cost_t cost, const time::time_t &valid_until); /** * Set the cost at a specified position. @@ -86,7 +86,11 @@ class CostField { * @param cost Cost to set. * @param changed Time at which the cost value is changed. */ - inline void set_cost(size_t idx, cost_t cost, const time::time_t &changed); + + inline void set_cost(size_t idx, cost_t cost, const time::time_t &until) { + cells[idx] = cost; + valid_until = until; + } /** * Get the cost field values. @@ -124,7 +128,7 @@ class CostField { /** * Time the cost field was last changed. */ - time::time_t &valid_until; + time::time_t valid_until; /** * Cost field values. diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index e164f3a094..a71845cffb 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -206,7 +206,9 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ std::shared_ptr cached_flow_field = std::make_shared(*flow_field); cached_flow_field->reset_dynamic_flags(); - this->field_cache->add(cache_key, cached_integration_field, cached_flow_field); + field_cache_t field_cache = field_cache_t(cached_integration_field, cached_flow_field); + + this->field_cache->add(cache_key, field_cache); return std::make_pair(integration_field, flow_field); } diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index 7d09c00a73..daa0e226fd 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -5,6 +5,7 @@ #include #include "pathfinding/types.h" +#include "pathfinding/field_cache.h" #include "util/hash.h" @@ -18,7 +19,6 @@ class CostField; class FlowField; class IntegrationField; class Portal; -class FieldCache; /** * Integrator for the flow field pathfinding algorithm. diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/path.h index 2d1f1cd593..df1b34a599 100644 --- a/libopenage/pathfinding/path.h +++ b/libopenage/pathfinding/path.h @@ -37,8 +37,6 @@ struct Path { /// First waypoint is the start position of the path request. /// Last waypoint is the target position of the path request. std::vector waypoints; - /// Time the path was created. - const time::time_t time; }; } // namespace openage::path From ced50928e08b09c0a80023a8fcbf827129743df4 Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Tue, 24 Dec 2024 20:48:29 -0600 Subject: [PATCH 659/771] Update libopenage/pathfinding/cost_field.h Co-authored-by: Christoph Heine <6852422+heinezen@users.noreply.github.com> --- libopenage/pathfinding/cost_field.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index f214e30f96..fe31c8945e 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -65,7 +65,7 @@ class CostField { * * @param pos Coordinates of the cell (relative to field origin). * @param cost Cost to set. - * @param changed Time at which the cost value is changed. + * @param valid_until Time at which the cost value is changed. */ void set_cost(const coord::tile_delta &pos, cost_t cost, const time::time_t &valid_until); From 300b12360e6de07746ccbcedc55ce08e84f05a96 Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Tue, 24 Dec 2024 21:39:59 -0600 Subject: [PATCH 660/771] - more fixes --- libopenage/pathfinding/cost_field.cpp | 2 +- libopenage/pathfinding/cost_field.h | 8 ++++---- libopenage/pathfinding/demo/demo_1.cpp | 13 ++++++------- libopenage/pathfinding/integrator.cpp | 25 +++++++++++-------------- libopenage/pathfinding/integrator.h | 14 ++++++++------ libopenage/pathfinding/pathfinder.cpp | 4 ++-- 6 files changed, 32 insertions(+), 34 deletions(-) diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index 3ba37a6242..7e9f206a59 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -60,7 +60,7 @@ bool CostField::is_dirty(const time::time_t &time) { return time >= this->valid_until; } -void CostField::clean() { +void CostField::clear_dirty() { this->valid_until = time::TIME_MAX; } diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index fe31c8945e..6e731a0f92 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -87,9 +87,9 @@ class CostField { * @param changed Time at which the cost value is changed. */ - inline void set_cost(size_t idx, cost_t cost, const time::time_t &until) { - cells[idx] = cost; - valid_until = until; + inline void set_cost(size_t idx, cost_t cost, const time::time_t &valid_until) { + this->cells[idx] = cost; + this->valid_until = valid_until; } /** @@ -117,7 +117,7 @@ class CostField { /** * Cleans the dirty flag by setting it to time_MAX. */ - void clean(); + void clear_dirty(); private: /** diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index a31b9b786b..2af57b481f 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -27,12 +27,11 @@ namespace openage::path::tests { void path_demo_1(const util::Path &path) { - auto time_loop = std::make_shared(); - time_loop->run(); - auto clock = time_loop->get_clock(); auto grid = std::make_shared(0, util::Vector2s{4, 3}, 10); - const time::time_t time = clock->get_time(); + time::Clock clock = time::Clock(); + clock.start(); + const time::time_t time = clock.get_time(); // Initialize the cost field for each sector. for (auto §or : grid->get_sectors()) { auto cost_field = sector->get_cost_field(); @@ -93,7 +92,7 @@ void path_demo_1(const util::Path &path) { coord::tile start{2, 26}; coord::tile target{36, 2}; - const time::time_t request_time = clock->get_time(); + const time::time_t request_time = clock.get_time(); PathRequest path_request{ grid->get_id(), @@ -137,7 +136,7 @@ void path_demo_1(const util::Path &path) { grid->get_id(), start, target, - clock->get_time() + clock.get_time() }; timer.reset(); @@ -158,7 +157,7 @@ void path_demo_1(const util::Path &path) { grid->get_id(), start, target, - clock->get_time() + clock.get_time() }; timer.reset(); diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index a71845cffb..5bac928fe9 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -9,6 +9,7 @@ #include "pathfinding/flow_field.h" #include "pathfinding/integration_field.h" #include "pathfinding/portal.h" +#include "time/time.h" namespace openage::path { @@ -37,10 +38,10 @@ std::shared_ptr Integrator::integrate(const std::shared_ptr &portal, const coord::tile_delta &target, - bool with_los, - bool evict_cache) { + const time::time_t &time, + bool with_los) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); - if (evict_cache) { + if (cost_field->is_dirty(time)) { this->field_cache->evict(cache_key); } else if (this->field_cache->is_cached(cache_key)) { @@ -105,13 +106,9 @@ std::shared_ptr Integrator::build(const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal, - bool with_los, - bool evict_cache) { + bool with_los) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); - if (evict_cache) { - this->field_cache->evict(cache_key); - } - else if (this->field_cache->is_cached(cache_key)) { + if (this->field_cache->is_cached(cache_key)) { log::log(DBG << "Using cached flow field for portal " << portal->get_id() << " from sector " << other_sector_id); @@ -155,10 +152,10 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ sector_id_t other_sector_id, const std::shared_ptr &portal, const coord::tile_delta &target, - bool with_los, - bool evict_cache) { + const time::time_t &time, + bool with_los) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); - if (evict_cache) { + if (cost_field->is_dirty(time)) { this->field_cache->evict(cache_key); } else if (this->field_cache->is_cached(cache_key)) { @@ -193,8 +190,8 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ return std::make_pair(cached_integration_field, cached_flow_field); } - auto integration_field = this->integrate(cost_field, other, other_sector_id, portal, target, with_los, evict_cache); - auto flow_field = this->build(integration_field, other, other_sector_id, portal, evict_cache); + auto integration_field = this->integrate(cost_field, other, other_sector_id, portal, target, time, with_los); + auto flow_field = this->build(integration_field, other, other_sector_id, portal); log::log(DBG << "Caching integration and flow fields for portal ID: " << portal->get_id() << ", sector ID: " << other_sector_id); diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index daa0e226fd..e8181a1ac3 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -7,6 +7,7 @@ #include "pathfinding/types.h" #include "pathfinding/field_cache.h" #include "util/hash.h" +#include "time/time.h" namespace openage { @@ -55,6 +56,7 @@ class Integrator { * @param other_sector_id Sector ID of the other side of the portal. * @param portal Portal. * @param target Coordinates of the target cell, relative to the integration field origin. + * @param time The time to check is the cached cost field is dirty. * @param with_los If true an LOS pass is performed before cost integration. * * @return Integration field. @@ -64,8 +66,8 @@ class Integrator { sector_id_t other_sector_id, const std::shared_ptr &portal, const coord::tile_delta &target, - bool with_los = true, - bool evict_cache = false); + const time::time_t &time, + bool with_los = true); /** * Build the flow field from an integration field. @@ -91,8 +93,7 @@ class Integrator { const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal, - bool with_los = true, - bool evict_cache = false); + bool with_los = true); using get_return_t = std::pair, std::shared_ptr>; @@ -115,6 +116,7 @@ class Integrator { * @param other_sector_id Sector ID of the other side of the portal. * @param portal Portal. * @param target Coordinates of the target cell, relative to the integration field origin. + * @param time The time to check is the cached cost field is dirty. * @param with_los If true an LOS pass is performed before cost integration. * * @return Integration field and flow field. @@ -124,8 +126,8 @@ class Integrator { sector_id_t other_sector_id, const std::shared_ptr &portal, const coord::tile_delta &target, - bool with_los = true, - bool evict_cache = false); + const time::time_t &time, + bool with_los = true); private: diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 8aec42a514..5a9c785f0c 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -155,8 +155,8 @@ const Path Pathfinder::get_path(const PathRequest &request) { prev_sector_id, portal, target_delta, - with_los, - next_sector->is_dirty(request.time)); + request.time, + with_los); flow_fields.push_back(std::make_pair(next_sector_id, sector_fields.second)); prev_integration_field = sector_fields.first; From 3cef612e94db0b90dab792544fb507bc2d37fc5d Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Thu, 2 Jan 2025 17:20:58 -0600 Subject: [PATCH 661/771] Update cost_field.h --- libopenage/pathfinding/cost_field.h | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index 6e731a0f92..a25bdaadf8 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -65,7 +65,7 @@ class CostField { * * @param pos Coordinates of the cell (relative to field origin). * @param cost Cost to set. - * @param valid_until Time at which the cost value is changed. + * @param valid_until Time at which the cost value expires. */ void set_cost(const coord::tile_delta &pos, cost_t cost, const time::time_t &valid_until); @@ -75,7 +75,7 @@ class CostField { * @param x X-coordinate of the cell. * @param y Y-coordinate of the cell. * @param cost Cost to set. - * @param changed Time at which the cost value is changed. + * @param valid_until Time at which the cost value expires. */ void set_cost(size_t x, size_t y, cost_t cost, const time::time_t &valid_until); @@ -84,9 +84,8 @@ class CostField { * * @param idx Index of the cell. * @param cost Cost to set. - * @param changed Time at which the cost value is changed. + * @param valid_until Time at which the cost value expires. */ - inline void set_cost(size_t idx, cost_t cost, const time::time_t &valid_until) { this->cells[idx] = cost; this->valid_until = valid_until; @@ -103,7 +102,7 @@ class CostField { * Set the cost field values. * * @param cells Cost field values. - * @param changed Time at which the cost value is changed. + * @param valid_until Time at which the cost value expires. */ void set_costs(std::vector &&cells, const time::time_t &changed); @@ -126,7 +125,7 @@ class CostField { size_t size; /** - * Time the cost field was last changed. + * Time the cost field expires. */ time::time_t valid_until; From 4ed876bb4aecc01d0b3bc9236896734351195a76 Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Thu, 2 Jan 2025 17:22:25 -0600 Subject: [PATCH 662/771] Update libopenage/pathfinding/cost_field.h Co-authored-by: Christoph Heine <6852422+heinezen@users.noreply.github.com> --- libopenage/pathfinding/cost_field.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index a25bdaadf8..f32cc2f2b7 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -114,7 +114,7 @@ class CostField { bool is_dirty(const time::time_t &time); /** - * Cleans the dirty flag by setting it to time_MAX. + * Clear the dirty flag. */ void clear_dirty(); From e0fc329bf29b68abca84a0a660cd1ef1bf21de7a Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Thu, 2 Jan 2025 17:22:42 -0600 Subject: [PATCH 663/771] Update libopenage/pathfinding/cost_field.h Co-authored-by: Christoph Heine <6852422+heinezen@users.noreply.github.com> --- libopenage/pathfinding/cost_field.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index f32cc2f2b7..967d4880f6 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -109,7 +109,7 @@ class CostField { /** * Check if the cost field is dirty at the specified time. * - * @param time Cost field is dirty if the cost field is accessed after the time given in valid_until. + * @param time Time of access to the cost field. */ bool is_dirty(const time::time_t &time); From ba7aec4551469df4d0e150354ae779d1291d945d Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Thu, 2 Jan 2025 17:23:31 -0600 Subject: [PATCH 664/771] Update libopenage/pathfinding/integrator.h Co-authored-by: Christoph Heine <6852422+heinezen@users.noreply.github.com> --- libopenage/pathfinding/integrator.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index e8181a1ac3..ee0d605758 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -56,7 +56,7 @@ class Integrator { * @param other_sector_id Sector ID of the other side of the portal. * @param portal Portal. * @param target Coordinates of the target cell, relative to the integration field origin. - * @param time The time to check is the cached cost field is dirty. + * @param time Time of the path request. * @param with_los If true an LOS pass is performed before cost integration. * * @return Integration field. From f0d923c5f61ce69d9d969629e8294780259b43e0 Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Thu, 2 Jan 2025 17:23:45 -0600 Subject: [PATCH 665/771] Update libopenage/pathfinding/integrator.h Co-authored-by: Christoph Heine <6852422+heinezen@users.noreply.github.com> --- libopenage/pathfinding/integrator.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index ee0d605758..29a5cf4717 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -116,7 +116,7 @@ class Integrator { * @param other_sector_id Sector ID of the other side of the portal. * @param portal Portal. * @param target Coordinates of the target cell, relative to the integration field origin. - * @param time The time to check is the cached cost field is dirty. + * @param time Time of the path request. * @param with_los If true an LOS pass is performed before cost integration. * * @return Integration field and flow field. From d839bc532fe833db075fe6e843dfc6c914fd1810 Mon Sep 17 00:00:00 2001 From: dmwever <56411717+dmwever@users.noreply.github.com> Date: Thu, 2 Jan 2025 19:22:05 -0600 Subject: [PATCH 666/771] Update Demo --- libopenage/pathfinding/cost_field.h | 4 +++- libopenage/pathfinding/demo/demo_1.cpp | 17 ++++++----------- libopenage/pathfinding/integrator.h | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index 967d4880f6..fbdbbb6d94 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -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. #pragma once @@ -110,6 +110,8 @@ class CostField { * Check if the cost field is dirty at the specified time. * * @param time Time of access to the cost field. + * + * @return Whether the cost field is dirty. */ bool is_dirty(const time::time_t &time); diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 2af57b481f..6f594b1a78 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -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. #include "demo_1.h" @@ -29,14 +29,12 @@ namespace openage::path::tests { void path_demo_1(const util::Path &path) { auto grid = std::make_shared(0, util::Vector2s{4, 3}, 10); - time::Clock clock = time::Clock(); - clock.start(); - const time::time_t time = clock.get_time(); // Initialize the cost field for each sector. for (auto §or : grid->get_sectors()) { auto cost_field = sector->get_cost_field(); std::vector sector_cost = sectors_cost.at(sector->get_id()); - cost_field->set_costs(std::move(sector_cost), time); + cost_field->set_costs(std::move(sector_cost), 0); + cost_field->clear_dirty(); } // Initialize portals between sectors. @@ -92,20 +90,17 @@ void path_demo_1(const util::Path &path) { coord::tile start{2, 26}; coord::tile target{36, 2}; - const time::time_t request_time = clock.get_time(); - PathRequest path_request{ grid->get_id(), start, target, - request_time + 0 }; grid->init_portal_nodes(); timer.start(); Path path_result = pathfinder->get_path(path_request); timer.stop(); - log::log(INFO << "Pathfinding request at " << request_time); log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " us"); // Create a renderer to display the grid and path @@ -136,7 +131,7 @@ void path_demo_1(const util::Path &path) { grid->get_id(), start, target, - clock.get_time() + 0 }; timer.reset(); @@ -157,7 +152,7 @@ void path_demo_1(const util::Path &path) { grid->get_id(), start, target, - clock.get_time() + 0 }; timer.reset(); diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index 29a5cf4717..eebe55c266 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -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. #pragma once From 5249d778a2ab8b397773b02d3b71b965bc31ddcf Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 13 Jan 2025 03:54:45 +0100 Subject: [PATCH 667/771] gamestate: Add request time to pathfinding request. --- libopenage/gamestate/system/move.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libopenage/gamestate/system/move.cpp b/libopenage/gamestate/system/move.cpp index 3da45ceab3..fac4f16412 100644 --- a/libopenage/gamestate/system/move.cpp +++ b/libopenage/gamestate/system/move.cpp @@ -37,7 +37,8 @@ namespace openage::gamestate::system { std::vector find_path(const std::shared_ptr &pathfinder, path::grid_id_t grid_id, const coord::phys3 &start, - const coord::phys3 &end) { + const coord::phys3 &end, + const time::time_t &start_time) { auto start_tile = start.to_tile(); auto end_tile = end.to_tile(); @@ -46,6 +47,7 @@ std::vector find_path(const std::shared_ptr &pat grid_id, start_tile, end_tile, + start_time, }; auto tile_path = pathfinder->get_path(request); @@ -122,7 +124,7 @@ const time::time_t Move::move_default(const std::shared_ptrget_map(); auto pathfinder = map->get_pathfinder(); auto grid_id = map->get_grid_id(move_path_grid->get_name()); - auto waypoints = find_path(pathfinder, grid_id, current_pos, destination); + auto waypoints = find_path(pathfinder, grid_id, current_pos, destination, start_time); // use waypoints for movement double total_time = 0; From 1fec0c9b7a1922fd7c10ba96db2d84cc5e277f5b Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 13 Jan 2025 03:55:06 +0100 Subject: [PATCH 668/771] path: Fix warning for cost field member ordering. --- libopenage/pathfinding/cost_field.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index 7e9f206a59..0afac1a07a 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -13,8 +13,8 @@ namespace openage::path { CostField::CostField(size_t size) : size{size}, - cells(this->size * this->size, COST_MIN), - valid_until{time::TIME_MIN} { + valid_until{time::TIME_MIN}, + cells(this->size * this->size, COST_MIN) { log::log(DBG << "Created cost field with size " << this->size << "x" << this->size); } From 87776b6fa9a85c8970df79c3e462d4c07ef42330 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 13 Jan 2025 03:57:57 +0100 Subject: [PATCH 669/771] path: Fix missing initialization of integrator's field cache. --- libopenage/pathfinding/integrator.cpp | 5 +++++ libopenage/pathfinding/integrator.h | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 5bac928fe9..db573f480d 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -14,6 +14,11 @@ namespace openage::path { +Integrator::Integrator() : + field_cache{std::make_unique()} { +} + + std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field, const coord::tile_delta &target, bool with_los) { diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index eebe55c266..490d5eaacb 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -26,7 +26,11 @@ class Portal; */ class Integrator { public: - Integrator() = default; + /** + * Create a new integrator. + */ + Integrator(); + ~Integrator() = default; /** @@ -130,7 +134,6 @@ class Integrator { bool with_los = true); private: - /** * Cache for already computed fields. */ From c5a31302f0a1f3763ce7ac12122a8a4b08f651d7 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 13 Jan 2025 03:59:44 +0100 Subject: [PATCH 670/771] Fix sanity check complaints. --- libopenage/gamestate/system/move.cpp | 2 +- libopenage/pathfinding/cost_field.cpp | 2 +- libopenage/pathfinding/cost_field.h | 2 +- libopenage/pathfinding/demo/demo_0.cpp | 2 +- libopenage/pathfinding/integrator.cpp | 2 +- libopenage/pathfinding/sector.h | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libopenage/gamestate/system/move.cpp b/libopenage/gamestate/system/move.cpp index fac4f16412..5a44baf113 100644 --- a/libopenage/gamestate/system/move.cpp +++ b/libopenage/gamestate/system/move.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 "move.h" diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index 0afac1a07a..0cb217093a 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -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. #include "cost_field.h" diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index fbdbbb6d94..0c5f18dabb 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -110,7 +110,7 @@ class CostField { * Check if the cost field is dirty at the specified time. * * @param time Time of access to the cost field. - * + * * @return Whether the cost field is dirty. */ bool is_dirty(const time::time_t &time); diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 2dfb825fa0..70d4f8be1f 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -33,7 +33,7 @@ void path_demo_0(const util::Path &path) { // Cost field with some obstacles auto cost_field = std::make_shared(field_length); - + const time::time_t time = time::TIME_ZERO; cost_field->set_cost(coord::tile_delta{0, 0}, COST_IMPASSABLE, time); cost_field->set_cost(coord::tile_delta{1, 0}, 254, time); diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index db573f480d..6befb014b3 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -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. #include "integrator.h" diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h index a8aad24c01..0a8172f996 100644 --- a/libopenage/pathfinding/sector.h +++ b/libopenage/pathfinding/sector.h @@ -106,7 +106,7 @@ class Sector { /** * Check if the cost field is dirty at the specified time. - * + * * @param time Specified time to check. */ bool is_dirty(const time::time_t &time); From 8dba1e118a018ad4d26a6822c1e8fecc770140b8 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 13 Jan 2025 04:07:39 +0100 Subject: [PATCH 671/771] path: Make is_dirty check const. --- libopenage/pathfinding/cost_field.cpp | 2 +- libopenage/pathfinding/cost_field.h | 2 +- libopenage/pathfinding/demo/demo_0.cpp | 2 +- libopenage/pathfinding/sector.cpp | 2 +- libopenage/pathfinding/sector.h | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index 0cb217093a..12889663eb 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -56,7 +56,7 @@ void CostField::set_costs(std::vector &&cells, const time::time_t &valid this->valid_until = valid_until; } -bool CostField::is_dirty(const time::time_t &time) { +bool CostField::is_dirty(const time::time_t &time) const { return time >= this->valid_until; } diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index 0c5f18dabb..c03b494a84 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -113,7 +113,7 @@ class CostField { * * @return Whether the cost field is dirty. */ - bool is_dirty(const time::time_t &time); + bool is_dirty(const time::time_t &time) const; /** * Clear the dirty flag. diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 70d4f8be1f..31f66bb51f 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -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. #include "demo_0.h" diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index 2da73690c0..d41bcaefef 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -249,7 +249,7 @@ void Sector::connect_exits() { } } -bool Sector::is_dirty(const time::time_t &time) { +bool Sector::is_dirty(const time::time_t &time) const { return this->cost_field->is_dirty(time); } diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h index 0a8172f996..4749ac62b1 100644 --- a/libopenage/pathfinding/sector.h +++ b/libopenage/pathfinding/sector.h @@ -109,7 +109,7 @@ class Sector { * * @param time Specified time to check. */ - bool is_dirty(const time::time_t &time); + bool is_dirty(const time::time_t &time) const; private: /** From 9b20825500a2654e4d4796842a92bd94e08b0e51 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 13 Jan 2025 04:32:08 +0100 Subject: [PATCH 672/771] path: Make demo 1 comments more verbose. --- libopenage/pathfinding/demo/demo_1.cpp | 43 +++++++++++++++++++------- libopenage/pathfinding/demo/demo_1.h | 4 +-- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 6f594b1a78..ed1b1ebf84 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -11,8 +11,6 @@ #include "pathfinding/portal.h" #include "pathfinding/sector.h" #include "util/timer.h" -#include "time/time_loop.h" -#include "time/clock.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" @@ -32,21 +30,30 @@ void path_demo_1(const util::Path &path) { // Initialize the cost field for each sector. for (auto §or : grid->get_sectors()) { auto cost_field = sector->get_cost_field(); - std::vector sector_cost = sectors_cost.at(sector->get_id()); - cost_field->set_costs(std::move(sector_cost), 0); - cost_field->clear_dirty(); + + // Read the data from the preconfigured table + std::vector sector_cost = SECTORS_COST.at(sector->get_id()); + + // Set the cost field for the sector + cost_field->set_costs(std::move(sector_cost), time::TIME_ZERO); } - // Initialize portals between sectors. + // Initialize portals between sectors util::Vector2s grid_size = grid->get_size(); portal_id_t portal_id = 0; for (size_t y = 0; y < grid_size[1]; y++) { for (size_t x = 0; x < grid_size[0]; x++) { + // For each sector on the grid, we will seatch for portals to the east and south + // sectors and connect them + auto sector = grid->get_sector(x, y); if (x < grid_size[0] - 1) { + // Check the sector to the east auto neighbor = grid->get_sector(x + 1, y); auto portals = sector->find_portals(neighbor, PortalDirection::EAST_WEST, portal_id); + + // Add the portals to the sector and the neighbor for (auto &portal : portals) { sector->add_portal(portal); neighbor->add_portal(portal); @@ -54,8 +61,11 @@ void path_demo_1(const util::Path &path) { portal_id += portals.size(); } if (y < grid_size[1] - 1) { + // Check the sector to the south auto neighbor = grid->get_sector(x, y + 1); auto portals = sector->find_portals(neighbor, PortalDirection::NORTH_SOUTH, portal_id); + + // Add the portals to the sector and the neighbor for (auto &portal : portals) { sector->add_portal(portal); neighbor->add_portal(portal); @@ -65,7 +75,8 @@ void path_demo_1(const util::Path &path) { } } - // Connect portals inside sectors. + // Connect portals that can mutually reach each other + // within the same sector for (auto sector : grid->get_sectors()) { sector->connect_exits(); @@ -84,23 +95,28 @@ void path_demo_1(const util::Path &path) { auto pathfinder = std::make_shared(); pathfinder->add_grid(grid); + // Add a timer to measure the pathfinding speed util::Timer timer; - // Create a path request and get the path + // Create a path request from one end of the grid to the other coord::tile start{2, 26}; coord::tile target{36, 2}; - PathRequest path_request{ grid->get_id(), start, target, - 0 + time::TIME_ZERO, }; + // Initialize the portal nodes of the grid + // This is used for the A* pathfinding search at the beginning grid->init_portal_nodes(); + timer.start(); + // Let the pathfinder search for a path Path path_result = pathfinder->get_path(path_request); timer.stop(); + log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " us"); // Create a renderer to display the grid and path @@ -113,8 +129,11 @@ void path_demo_1(const util::Path &path) { auto render_manager = std::make_shared(qtapp, window, path, grid); log::log(INFO << "Created render manager for pathfinding demo"); + // Callbacks for mouse button events + // Used to set the start and target cells for pathfinding window->add_mouse_button_callback([&](const QMouseEvent &ev) { if (ev.type() == QEvent::MouseButtonRelease) { + // From the mouse position, calculate the position/cell on the grid auto cell_count_x = grid->get_size()[0] * grid->get_sector_size(); auto cell_count_y = grid->get_size()[1] * grid->get_sector_size(); auto window_size = window->get_size(); @@ -131,7 +150,7 @@ void path_demo_1(const util::Path &path) { grid->get_id(), start, target, - 0 + time::TIME_ZERO, }; timer.reset(); @@ -152,7 +171,7 @@ void path_demo_1(const util::Path &path) { grid->get_id(), start, target, - 0 + time::TIME_ZERO, }; timer.reset(); diff --git a/libopenage/pathfinding/demo/demo_1.h b/libopenage/pathfinding/demo/demo_1.h index db70efe084..b2d3037eb8 100644 --- a/libopenage/pathfinding/demo/demo_1.h +++ b/libopenage/pathfinding/demo/demo_1.h @@ -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. #pragma once @@ -182,7 +182,7 @@ class RenderManager1 { // Cost for the sectors in the grid // taken from Figure 23.1 in "Crowd Pathfinding and Steering Using Flow Field Tiles" -const std::vector> sectors_cost = { +const std::vector> SECTORS_COST = { { // clang-format off 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, From b43113adcfd2f8f409bc1fb091bcf362dba45f17 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 13 Jan 2025 04:38:19 +0100 Subject: [PATCH 673/771] path: Activate cache usage in demo 1. --- libopenage/pathfinding/demo/demo_1.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index ed1b1ebf84..61df3e8995 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -35,7 +35,7 @@ void path_demo_1(const util::Path &path) { std::vector sector_cost = SECTORS_COST.at(sector->get_id()); // Set the cost field for the sector - cost_field->set_costs(std::move(sector_cost), time::TIME_ZERO); + cost_field->set_costs(std::move(sector_cost), time::TIME_MAX); } // Initialize portals between sectors From 62889e643fd9db2e6efe6985b979e047015d9a08 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 13 Jan 2025 04:42:22 +0100 Subject: [PATCH 674/771] patg: Remove 'is_dirty' method from sector code. It should only be checked from the cost field. --- libopenage/pathfinding/sector.cpp | 4 ---- libopenage/pathfinding/sector.h | 8 -------- 2 files changed, 12 deletions(-) diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index d41bcaefef..f50dc209bf 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -249,8 +249,4 @@ void Sector::connect_exits() { } } -bool Sector::is_dirty(const time::time_t &time) const { - return this->cost_field->is_dirty(time); -} - } // namespace openage::path diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h index 4749ac62b1..a4ba94fa52 100644 --- a/libopenage/pathfinding/sector.h +++ b/libopenage/pathfinding/sector.h @@ -9,7 +9,6 @@ #include "coord/chunk.h" #include "pathfinding/portal.h" #include "pathfinding/types.h" -#include "time/time.h" namespace openage::path { @@ -104,13 +103,6 @@ class Sector { */ void connect_exits(); - /** - * Check if the cost field is dirty at the specified time. - * - * @param time Specified time to check. - */ - bool is_dirty(const time::time_t &time) const; - private: /** * ID of the sector. From ec72342fc42d0aeaf741dd488d022a0c3d0a1de2 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 13 Jan 2025 05:02:42 +0100 Subject: [PATCH 675/771] path: Cleanup field cache code. - Make read-only methods const - Add get(..) method for getting entire field entry to avoid two lookups - Simplify lookup of entries - Add more docstrings --- libopenage/pathfinding/field_cache.cpp | 24 +++++------ libopenage/pathfinding/field_cache.h | 55 ++++++++++++++++++++------ 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/libopenage/pathfinding/field_cache.cpp b/libopenage/pathfinding/field_cache.cpp index 95276b0818..417c8a2bbd 100644 --- a/libopenage/pathfinding/field_cache.cpp +++ b/libopenage/pathfinding/field_cache.cpp @@ -4,27 +4,29 @@ namespace openage::path { -void FieldCache::add(cache_key_t cache_key, - field_cache_t cache_entry) { +void FieldCache::add(const cache_key_t cache_key, + const field_cache_t cache_entry) { this->cache[cache_key] = cache_entry; } -void FieldCache::evict(cache_key_t cache_key) { - this->cache.erase(cache_key); +bool FieldCache::evict(const cache_key_t cache_key) { + return this->cache.erase(cache_key) != 0; } -bool FieldCache::is_cached(cache_key_t cache_key) { +bool FieldCache::is_cached(const cache_key_t cache_key) const { return this->cache.contains(cache_key); } -std::shared_ptr FieldCache::get_integration_field(cache_key_t cache_key) { - auto cached = this->cache.find(cache_key); - return cached->second.first; +std::shared_ptr FieldCache::get_integration_field(const cache_key_t cache_key) const { + return this->cache.at(cache_key).first; } -std::shared_ptr FieldCache::get_flow_field(cache_key_t cache_key) { - auto cached = this->cache.find(cache_key); - return cached->second.second; +std::shared_ptr FieldCache::get_flow_field(const cache_key_t cache_key) const { + return this->cache.at(cache_key).second; +} + +field_cache_t FieldCache::get(const cache_key_t cache_key) const { + return this->cache.at(cache_key); } } // namespace openage::path diff --git a/libopenage/pathfinding/field_cache.h b/libopenage/pathfinding/field_cache.h index 23374e5123..b5c39b44de 100644 --- a/libopenage/pathfinding/field_cache.h +++ b/libopenage/pathfinding/field_cache.h @@ -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. #pragma once @@ -8,6 +8,7 @@ #include "pathfinding/types.h" #include "util/hash.h" + namespace openage { namespace coord { struct tile_delta; @@ -26,30 +27,60 @@ class FieldCache { ~FieldCache() = default; /** - * Adds a new field cache entry to the cache with a given portal and sector cache key. + * Adds a new field entry to the cache. + * + * @param cache_key Cache key for the field entry. + * @param cache_entry Field entry (integration field, flow field). + */ + void add(const cache_key_t cache_key, + const field_cache_t cache_entry); + + /** + * Evict field entry from the cache. + * + * @param cache_key Cache key for the field entry to evict. + * + * @return true if the cache key was found and evicted, false otherwise. */ - void add(cache_key_t cache_key, - field_cache_t cache_entry); + bool evict(const cache_key_t cache_key); /** - * Evicts a given field cache entry from the cache at the given cache key. + * Check if there is a cached entry for a specific cache key. + * + * @param cache_key Cache key to check. + * + * @return true if a field entry is found for the cache key, false otherwise. */ - void evict(cache_key_t cache_key); + bool is_cached(const cache_key_t cache_key) const; /** - * Checks if there is a cached entry at a specific cache key. + * Get a cached integration field. + * + * @param cache_key Cache key for the field entry. + * + * @return Integration field. */ - bool is_cached(cache_key_t cache_key); + std::shared_ptr get_integration_field(const cache_key_t cache_key) const; /** - * Gets the integration field from a given cache entry. + * Get a cached flow field. + * + * @param cache_key Cache key for the field entry. + * + * @return Flow field. */ - std::shared_ptr get_integration_field(cache_key_t cache_key); + std::shared_ptr get_flow_field(const cache_key_t cache_key) const; /** - * Gets the flow field from a given cache entry. + * Get a cached field entry. + * + * Contains both integration field and flow field. + * + * @param cache_key Cache key for the field entry. + * + * @return Field entry (integration field, flow field). */ - std::shared_ptr get_flow_field(cache_key_t cache_key); + field_cache_t get(const cache_key_t cache_key) const; private: /** From 863ba6c1ad8188bd180e9634c910703daa3b6f5e Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 13 Jan 2025 05:06:45 +0100 Subject: [PATCH 676/771] path: Avoid duplicate lookups for field cache in integrator. --- libopenage/pathfinding/integrator.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 6befb014b3..f4dcf324b1 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -18,7 +18,6 @@ Integrator::Integrator() : field_cache{std::make_unique()} { } - std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field, const coord::tile_delta &target, bool with_los) { @@ -47,6 +46,8 @@ std::shared_ptr Integrator::integrate(const std::shared_ptrget_id(), other_sector_id); if (cost_field->is_dirty(time)) { + log::log(DBG << "Evicting cached integration and flow fields for portal " << portal->get_id() + << " from sector " << other_sector_id); this->field_cache->evict(cache_key); } else if (this->field_cache->is_cached(cache_key)) { @@ -161,6 +162,8 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ bool with_los) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); if (cost_field->is_dirty(time)) { + log::log(DBG << "Evicting cached integration and flow fields for portal " << portal->get_id() + << " from sector " << other_sector_id); this->field_cache->evict(cache_key); } else if (this->field_cache->is_cached(cache_key)) { @@ -168,9 +171,9 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ << " from sector " << other_sector_id); // retrieve cached fields - auto cached_integration_field = this->field_cache->get_integration_field(cache_key); - auto cached_flow_field = this->field_cache->get_flow_field(cache_key); - + auto cached_fields = this->field_cache->get(cache_key); + auto cached_integration_field = cached_fields.first; + auto cached_flow_field = cached_fields.second; if (with_los) { log::log(SPAM << "Performing LOS pass on cached field"); From eb8f73df790d56661908dddb67b9f1e2246e6ebd Mon Sep 17 00:00:00 2001 From: dmwever Date: Mon, 13 Jan 2025 09:11:19 -0600 Subject: [PATCH 677/771] Update copying.md --- copying.md | 1 + 1 file changed, 1 insertion(+) diff --git a/copying.md b/copying.md index 3d97be2f8b..d1434fe4a7 100644 --- a/copying.md +++ b/copying.md @@ -158,6 +158,7 @@ _the openage authors_ are: | Jeremiah Morgan | jere8184 | jeremiahmorgan dawt bham à outlook dawt com | | Tobias Alam | alamt22 | tobiasal à umich dawt edu | | Alex Zhuohao He | ZzzhHe | zhuohao dawt he à outlook dawt com | +| David Wever | dmwever | dmwever à crimson dawt ua dawt edu | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. From dfa93447192857fd5423e3bd231551dd9535d938 Mon Sep 17 00:00:00 2001 From: dmwever Date: Thu, 16 Jan 2025 12:54:35 -0600 Subject: [PATCH 678/771] - Sanity Fixes --- libopenage/pathfinding/field_cache.cpp | 2 +- libopenage/pathfinding/sector.cpp | 2 +- libopenage/pathfinding/sector.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libopenage/pathfinding/field_cache.cpp b/libopenage/pathfinding/field_cache.cpp index 417c8a2bbd..86f985e3ba 100644 --- a/libopenage/pathfinding/field_cache.cpp +++ b/libopenage/pathfinding/field_cache.cpp @@ -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. #include "field_cache.h" diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index f50dc209bf..5897bbef84 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -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. #include "sector.h" diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h index a4ba94fa52..d684a399b6 100644 --- a/libopenage/pathfinding/sector.h +++ b/libopenage/pathfinding/sector.h @@ -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. #pragma once From 378f69c07377d811aba32307c384580fd5bba4be Mon Sep 17 00:00:00 2001 From: dmwever Date: Thu, 16 Jan 2025 13:02:59 -0600 Subject: [PATCH 679/771] Add old email for sanity checks --- .mailmap | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.mailmap b/.mailmap index fbf10ad63f..c9a2ee2afd 100644 --- a/.mailmap +++ b/.mailmap @@ -20,4 +20,5 @@ Tobias Feldballe Tobias Feldballe Jonas Borchelt Derek Frogget <114030121+derekfrogget@users.noreply.github.com> -Nikhil Ghosh \ No newline at end of file +Nikhil Ghosh +David Wever <56411717+dmwever@users.noreply.github.com> \ No newline at end of file From 06454693c182fc5f24b3b119782823daf50db133 Mon Sep 17 00:00:00 2001 From: dmwever Date: Thu, 16 Jan 2025 13:11:09 -0600 Subject: [PATCH 680/771] Update .mailmap --- .mailmap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mailmap b/.mailmap index c9a2ee2afd..8a1a8727b1 100644 --- a/.mailmap +++ b/.mailmap @@ -21,4 +21,4 @@ Tobias Feldballe Jonas Borchelt Derek Frogget <114030121+derekfrogget@users.noreply.github.com> Nikhil Ghosh -David Wever <56411717+dmwever@users.noreply.github.com> \ No newline at end of file +David Wever <56411717+dmwever@users.noreply.github.com> \ No newline at end of file From 3fca4ad4dca7859ff4bacff4b4d1140688688d2d Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Sat, 18 Jan 2025 07:51:40 -0500 Subject: [PATCH 681/771] Add Nix build result folder to gitignore The nix build step creates an output folder called /result. I've added this to the .gitignore file so that git doesn't try to add generated files/binaries to the source tree. --- .gitignore | 3 +++ copying.md | 1 + 2 files changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 1910b8d6ac..06263991e6 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,9 @@ __pycache__ /.ccls-cache /.cache +# Nix build output +/result + # root dir run script /run /run.cpp diff --git a/copying.md b/copying.md index d1434fe4a7..53ced52632 100644 --- a/copying.md +++ b/copying.md @@ -159,6 +159,7 @@ _the openage authors_ are: | Tobias Alam | alamt22 | tobiasal à umich dawt edu | | Alex Zhuohao He | ZzzhHe | zhuohao dawt he à outlook dawt com | | David Wever | dmwever | dmwever à crimson dawt ua dawt edu | +| Michael Lynch | mtlynch | git à mtlynch dawt io | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. From 9ba478ede2945c64a3e3cc8436d1695d4463cf34 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Fri, 13 Dec 2024 16:17:45 +0000 Subject: [PATCH 682/771] impliment curve::array --- libopenage/curve/CMakeLists.txt | 1 + libopenage/curve/array.cpp | 10 + libopenage/curve/array.h | 244 +++++++++++++++++++++++++ libopenage/curve/keyframe.h | 9 + libopenage/curve/keyframe_container.h | 2 - libopenage/curve/tests/curve_types.cpp | 87 ++++++++- 6 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 libopenage/curve/array.cpp create mode 100644 libopenage/curve/array.h diff --git a/libopenage/curve/CMakeLists.txt b/libopenage/curve/CMakeLists.txt index 623a22a25a..174498e23b 100644 --- a/libopenage/curve/CMakeLists.txt +++ b/libopenage/curve/CMakeLists.txt @@ -1,4 +1,5 @@ add_sources(libopenage + array.cpp base_curve.cpp continuous.cpp discrete.cpp diff --git a/libopenage/curve/array.cpp b/libopenage/curve/array.cpp new file mode 100644 index 0000000000..97e07033ce --- /dev/null +++ b/libopenage/curve/array.cpp @@ -0,0 +1,10 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + + +#include "array.h" + +namespace openage::curve { + +// This file is intended to be empty + +} // openage::curve diff --git a/libopenage/curve/array.h b/libopenage/curve/array.h new file mode 100644 index 0000000000..7076082459 --- /dev/null +++ b/libopenage/curve/array.h @@ -0,0 +1,244 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include "curve/keyframe_container.h" +#include "event/evententity.h" + + +// remember to update docs +namespace openage { +namespace curve { + +template +class Array : event::EventEntity { +public: + Array(const std::shared_ptr &loop, + size_t id, + const std::string &idstr = "", + const EventEntity::single_change_notifier ¬ifier = nullptr) : + EventEntity(loop, notifier), _id{id}, _idstr{idstr}, loop{loop}{} + + Array(const Array &) = delete; + + + /** + * Get the last element with elem->time <= time from + * the keyfram container at a given index + */ + T get(const time::time_t &t, const size_t index) const; + + + /** + * Get an array of the last elements with elem->time <= time from + * all keyfram containers contained within this array curve + */ + std::array get_all(const time::time_t &t) const; + + + consteval size_t size() const; + + /** + * Get the last element and associated time which has elem->time <= time from + * the keyfram container at a given index + */ + std::pair frame(const time::time_t &t, const size_t index) const; + + + /** + * Get the first element and associated time which has elem->time >= time from + * the keyfram container at a given index + */ + std::pair next_frame(const time::time_t &t, const size_t index) const; + + + /** + * Insert a new keyframe value at time t at index + */ + void set_insert(const time::time_t &t, const size_t index, T value); + + + /** + * Insert a new keyframe value at time t; delete all keyframes after time t at index + */ + void set_last(const time::time_t &t, const size_t index, T value); + + + /** + * Insert a new keyframe value at time t at index i; remove all other keyframes with time t at index i + */ + void set_replace(const time::time_t &t, const size_t index, T value); + + + /** + * Copy keyframes from another container to this container. + * + * Replaces all keyframes beginning at t >= start with keyframes from \p other. + * + * @param other Curve that keyframes are copied from. + * @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. + */ + void sync(const Array &other, const time::time_t &start); + + + /** + * Get the identifier of this curve. + * + * @return Identifier. + */ + size_t id() const override { + return this->_id; + } + + /** + * Get the human-readable identifier of this curve. + * + * @return Human-readable identifier. + */ + std::string idstr() const override { + if (this->_idstr.size() == 0) { + return std::to_string(this->id()); + } + return this->_idstr; + } + + + KeyframeContainer& operator[] (size_t index) + { + return this->container[index]; + } + + KeyframeContainer operator[] (size_t index) const + { + return this->container[index]; + } + + class Iterator { + public: + Iterator(Array *curve, const time::time_t &time = time::TIME_MAX, size_t offset = 0) : + curve(curve), time(time), offset(offset) {}; + + const T operator*() { + return curve->frame(this->time, this->offset).second; + } + + void operator++() { + this->offset++; + } + + bool operator!=(const Array::Iterator &rhs) const { + return this->offset != rhs.offset; + } + + + private: + size_t offset; + Array *curve; + time::time_t time; + }; + + + Iterator begin(const time::time_t &time = time::TIME_MAX); + + Iterator end(const time::time_t &time = time::TIME_MAX); + + +private: + std::array, Size> container; + + //hint for KeyframeContainer operations + mutable std::array last_element = {}; + + /** + * Identifier for the container + */ + const size_t _id; + + /** + * Human-readable identifier for the container + */ + const std::string _idstr; + + /** + * The eventloop this curve was registered to + */ + const std::shared_ptr loop; +}; + + +template +std::pair Array::frame(const time::time_t &t, const size_t index) const { + size_t frmae_index = container[index].last(t, this->last_element[index]); + this->last_element[index] = frmae_index; + return container[index].get(frmae_index).make_pair(); +} + +template +std::pair Array::next_frame(const time::time_t &t, const size_t index) const { + size_t frmae_index = container[index].last(t, this->last_element[index]); + this->last_element[index] = frmae_index; + return container[index].get(frmae_index + 1).make_pair(); +} + +template +T Array::get(const time::time_t &t, const size_t index) const { + return this->frame(t, index).second; +} + +template +std::array Array::get_all(const time::time_t &t) const { + return [&](std::index_sequence) { + return std::array{this->get(t, I)...}; + }(std::make_index_sequence{}); +} + +template +consteval size_t Array::size() const { + return Size; +} + + +template +void Array::set_insert(const time::time_t &t, const size_t index, T value) { + this->last_element[index] = this->container[index].insert_after(Keyframe(t, value), this->last_element[index]); +} + + +template +void Array::set_last(const time::time_t &t, const size_t index, T value) { + + size_t frame_index = this->container[index].insert_after(Keyframe(t, value), this->last_element[index]); + this->last_element[index] = frame_index; + this->container[index].erase_after(frame_index); +} + + +template +void Array::set_replace(const time::time_t &t, const size_t index, T value) { + this->container[index].insert_overwrite(Keyframe(t, value), this->last_element[index]); +} + +template +void Array::sync(const Array &other, const time::time_t &start) { + for (int i = 0; i < Size; i++) { + this->container[i].sync(other.container[i], start); + } +} + +template +typename Array::Iterator Array::begin(const time::time_t &time) { + return Array::Iterator(this, time); +} + + +template +typename Array::Iterator Array::end(const time::time_t &time) { + return Array::Iterator(this, time, this->container.size()); +} + +} // namespace curve +} // namespace openage diff --git a/libopenage/curve/keyframe.h b/libopenage/curve/keyframe.h index e868783cbb..a34b0c6262 100644 --- a/libopenage/curve/keyframe.h +++ b/libopenage/curve/keyframe.h @@ -54,6 +54,15 @@ class Keyframe { return this->value; } + /** + * Get the value and timestamp of the keyframe in form of std::pair + * @return keyframe pair + */ + std::pair make_pair() const + { + return {time(), val()}; + } + public: /** * Value of the keyframe. diff --git a/libopenage/curve/keyframe_container.h b/libopenage/curve/keyframe_container.h index d9bb9a0bbd..8434942058 100644 --- a/libopenage/curve/keyframe_container.h +++ b/libopenage/curve/keyframe_container.h @@ -279,8 +279,6 @@ class KeyframeContainer { * Replaces all keyframes beginning at t >= start with keyframes from \p other. * * @param other Curve that keyframes are copied from. - * @param converter Function that converts the value type of \p other to the - * value type of \p this. * @param start Start time at which keyframes are replaced (default = -INF). * Using the default value replaces ALL keyframes of \p this with * the keyframes of \p other. diff --git a/libopenage/curve/tests/curve_types.cpp b/libopenage/curve/tests/curve_types.cpp index 421d3fb4d7..6c754e5df9 100644 --- a/libopenage/curve/tests/curve_types.cpp +++ b/libopenage/curve/tests/curve_types.cpp @@ -5,6 +5,7 @@ #include #include +#include "curve/array.h" #include "curve/continuous.h" #include "curve/discrete.h" #include "curve/discrete_mod.h" @@ -232,7 +233,7 @@ void curve_types() { TESTEQUALS(c.get(8), 4); } - //Check the discrete type + // Check the discrete type { auto f = std::make_shared(); Discrete c(f, 0); @@ -257,7 +258,7 @@ void curve_types() { TESTEQUALS(complex.get(10), "Test 10"); } - //Check the discrete mod type + // Check the discrete mod type { auto f = std::make_shared(); DiscreteMod c(f, 0); @@ -290,7 +291,7 @@ void curve_types() { TESTEQUALS(c.get_mod(15, 0), 0); } - //check set_last + // check set_last { auto f = std::make_shared(); Discrete c(f, 0); @@ -386,6 +387,86 @@ void curve_types() { TESTEQUALS(c.get(1), 0); TESTEQUALS(c.get(5), 0); } + + { // array + auto f = std::make_shared(); + + Array a(f,0); + a.set_insert(time::time_t(1), 0, 0); + a.set_insert(time::time_t(1), 1, 1); + a.set_insert(time::time_t(1), 2, 2); + a.set_insert(time::time_t(1), 3, 3); + //a = [[0:0, 1:0],[0:0, 1:1],[0:0, 1:2],[0:0, 1:3]] + + auto res = a.get_all(time::time_t(1)); + TESTEQUALS(res[0], 0); + TESTEQUALS(res[1], 1); + TESTEQUALS(res[2], 2); + TESTEQUALS(res[3], 3); + + Array other(f,0); + other[0].last(999); + other[1].last(999); + other[2].last(999); + other[3].last(999); + other.set_insert(time::time_t(1), 0, 4); + other.set_insert(time::time_t(1), 1, 5); + other.set_insert(time::time_t(1), 2, 6); + other.set_insert(time::time_t(1), 3, 7); + //other = [[0:999, 1:4],[0:999, 1:5],[0:999, 1:6],[0:999, 1:7]] + + + a.sync(other, time::time_t(1)); + //a = [[0:0, 1:4],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]] + + res = a.get_all(time::time_t(0)); + TESTEQUALS(res[0], 0); + TESTEQUALS(res[1], 0); + TESTEQUALS(res[2], 0); + TESTEQUALS(res[3], 0); + res = a.get_all(time::time_t(1)); + TESTEQUALS(res[0], 4); + TESTEQUALS(res[1], 5); + TESTEQUALS(res[2], 6); + TESTEQUALS(res[3], 7); + + // Additional tests + a.set_insert(time::time_t(2), 0, 15); + a.set_insert(time::time_t(2), 0, 20); + a.set_replace(time::time_t(2), 0, 25); + TESTEQUALS(a.get(time::time_t(2), 0), 25); + // a = [[0:0, 1:4, 2:25],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]] + + a.set_insert(time::time_t(3), 0, 30); + a.set_insert(time::time_t(4), 0, 40); + a.set_last(time::time_t(3), 0, 35); + TESTEQUALS(a.get(time::time_t(3), 0), 35); + // a = [[0:0, 1:4, 2:25, 3:35],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]] + + auto frame = a.frame(time::time_t(1), 2); + TESTEQUALS(frame.second, 6); + TESTEQUALS(frame.first, time::time_t(1)); + + a.set_insert(time::time_t(5), 3, 40); + auto next_frame = a.next_frame(time::time_t(1), 3); + TESTEQUALS(next_frame.second, 40); + TESTEQUALS(next_frame.first, time::time_t(5)); + + // Test operator[] + TESTEQUALS(a[0].get(a[0].last(time::time_t(2))).val(), 25); + TESTEQUALS(a[1].get(a[1].last(time::time_t(2))).val(), 5); + + // Test begin and end + auto it = a.begin(time::time_t(1)); + TESTEQUALS(*it, 4); + ++it; + TESTEQUALS(*it, 5); + ++it; + TESTEQUALS(*it, 6); + ++it; + TESTEQUALS(*it, 7); + ++it; + } } } // namespace openage::curve::tests From 8647a7bc98ef652c5b1e96ddfc3ea3c2cef74f1d Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Thu, 26 Dec 2024 15:44:48 +0000 Subject: [PATCH 683/771] address comments --- libopenage/curve/array.h | 94 +++++++++++++++----------- libopenage/curve/base_curve.h | 4 +- libopenage/curve/tests/container.cpp | 84 ++++++++++++++++++++++- libopenage/curve/tests/curve_types.cpp | 81 ---------------------- 4 files changed, 139 insertions(+), 124 deletions(-) diff --git a/libopenage/curve/array.h b/libopenage/curve/array.h index 7076082459..34109365db 100644 --- a/libopenage/curve/array.h +++ b/libopenage/curve/array.h @@ -16,10 +16,10 @@ template class Array : event::EventEntity { public: Array(const std::shared_ptr &loop, - size_t id, - const std::string &idstr = "", - const EventEntity::single_change_notifier ¬ifier = nullptr) : - EventEntity(loop, notifier), _id{id}, _idstr{idstr}, loop{loop}{} + size_t id, + const std::string &idstr = "", + const EventEntity::single_change_notifier ¬ifier = nullptr) : + EventEntity(loop, notifier), _id{id}, _idstr{idstr}, loop{loop} {} Array(const Array &) = delete; @@ -28,16 +28,16 @@ class Array : event::EventEntity { * Get the last element with elem->time <= time from * the keyfram container at a given index */ - T get(const time::time_t &t, const size_t index) const; + T at(const time::time_t &t, const size_t index) const; /** * Get an array of the last elements with elem->time <= time from * all keyfram containers contained within this array curve */ - std::array get_all(const time::time_t &t) const; - + std::array get(const time::time_t &t) const; + // Get the amount of KeyframeContainers in array curve consteval size_t size() const; /** @@ -107,92 +107,107 @@ class Array : event::EventEntity { } - KeyframeContainer& operator[] (size_t index) - { - return this->container[index]; - } - - KeyframeContainer operator[] (size_t index) const - { - return this->container[index]; + // get a copy to the KeyframeContainer at index + KeyframeContainer operator[](size_t index) const { + return this->container.at(index); } + // Array::Iterator is used to iterate over KeyframeContainers contained in a curve at a given time. class Iterator { public: Iterator(Array *curve, const time::time_t &time = time::TIME_MAX, size_t offset = 0) : curve(curve), time(time), offset(offset) {}; + // returns a copy of the keyframe at the current offset and time const T operator*() { - return curve->frame(this->time, this->offset).second; + return this->curve->frame(this->time, this->offset).second; } + // increments the Iterator to point at the next KeyframeContainer void operator++() { this->offset++; } + // Compare two Iterators by their offset bool operator!=(const Array::Iterator &rhs) const { return this->offset != rhs.offset; } private: + // used to index the Curve::Array pointed to by this iterator size_t offset; + + // curve::Array that this iterator is iterating over Array *curve; + + // time at which this iterator is iterating over time::time_t time; }; - Iterator begin(const time::time_t &time = time::TIME_MAX); + // iterator pointing to a keyframe of the first KeyframeContainer in the curve at a given time + Iterator begin(const time::time_t &time = time::TIME_MIN); - Iterator end(const time::time_t &time = time::TIME_MAX); + // iterator pointing after the last KeyframeContainer in the curve at a given time + Iterator end(const time::time_t &time = time::TIME_MIN); private: std::array, Size> container; - //hint for KeyframeContainer operations + /** + * hints for KeyframeContainer operations, mutable as hints + * are updated by const read functions. + * This is used to speed up the search for next keyframe to be accessed + */ mutable std::array last_element = {}; /** - * Identifier for the container - */ + * Identifier for the container + */ const size_t _id; /** - * Human-readable identifier for the container - */ + * Human-readable identifier for the container + */ const std::string _idstr; /** - * The eventloop this curve was registered to - */ + * The eventloop this curve was registered to + */ const std::shared_ptr loop; }; template std::pair Array::frame(const time::time_t &t, const size_t index) const { - size_t frmae_index = container[index].last(t, this->last_element[index]); - this->last_element[index] = frmae_index; - return container[index].get(frmae_index).make_pair(); + size_t &hint = this->last_element[index]; + size_t frame_index = this->container.at(index).last(t, hint); + hint = frame_index; + return this->container.at(index).get(frame_index).make_pair(); } template std::pair Array::next_frame(const time::time_t &t, const size_t index) const { - size_t frmae_index = container[index].last(t, this->last_element[index]); - this->last_element[index] = frmae_index; - return container[index].get(frmae_index + 1).make_pair(); + size_t &hint = this->last_element[index]; + size_t frame_index = this->container.at(index).last(t, hint); + hint = frame_index; + return this->container.at(index).get(frame_index + 1).make_pair(); } template -T Array::get(const time::time_t &t, const size_t index) const { - return this->frame(t, index).second; +T Array::at(const time::time_t &t, const size_t index) const { + size_t &hint = this->last_element[index]; + size_t frame_index = this->container.at(index).last(t, hint); + hint = frame_index; + return this->container.at(index).get(frame_index).val(); } template -std::array Array::get_all(const time::time_t &t) const { +std::array Array::get(const time::time_t &t) const { return [&](std::index_sequence) { - return std::array{this->get(t, I)...}; + return std::array{this->at(t, I)...}; }(std::make_index_sequence{}); } @@ -204,22 +219,21 @@ consteval size_t Array::size() const { template void Array::set_insert(const time::time_t &t, const size_t index, T value) { - this->last_element[index] = this->container[index].insert_after(Keyframe(t, value), this->last_element[index]); + this->last_element[index] = this->container.at(index).insert_after(Keyframe{t, value}, this->last_element[index]); } template void Array::set_last(const time::time_t &t, const size_t index, T value) { - - size_t frame_index = this->container[index].insert_after(Keyframe(t, value), this->last_element[index]); + size_t frame_index = this->container.at(index).insert_after(Keyframe{t, value}, this->last_element[index]); this->last_element[index] = frame_index; - this->container[index].erase_after(frame_index); + this->container.at(index).erase_after(frame_index); } template void Array::set_replace(const time::time_t &t, const size_t index, T value) { - this->container[index].insert_overwrite(Keyframe(t, value), this->last_element[index]); + this->container.at(index).insert_overwrite(Keyframe{t, value}, this->last_element[index]); } template diff --git a/libopenage/curve/base_curve.h b/libopenage/curve/base_curve.h index dc58885b98..3d2ff20343 100644 --- a/libopenage/curve/base_curve.h +++ b/libopenage/curve/base_curve.h @@ -245,7 +245,7 @@ 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); - return std::make_pair(elem.time(), elem.val()); + return elem.make_pair(); } @@ -254,7 +254,7 @@ std::pair BaseCurve::next_frame(const time::time_t &ti auto e = this->container.last(time, this->container.size()); e++; auto elem = this->container.get(e); - return std::make_pair(elem.time(), elem.val()); + return elem.make_pair(); } template diff --git a/libopenage/curve/tests/container.cpp b/libopenage/curve/tests/container.cpp index 020e2aad99..51a5e4bee2 100644 --- a/libopenage/curve/tests/container.cpp +++ b/libopenage/curve/tests/container.cpp @@ -9,6 +9,7 @@ #include #include +#include "curve/array.h" #include "curve/iterator.h" #include "curve/map.h" #include "curve/map_filter_iterator.h" @@ -56,7 +57,7 @@ void test_map() { // Basic tests test lookup in the middle of the range. { - auto t = map.at(2, 0); //At timestamp 2 element 0 + auto t = map.at(2, 0); // At timestamp 2 element 0 TESTEQUALS(t.has_value(), true); TESTEQUALS(t.value().value(), 0); t = map.at(20, 5); @@ -242,11 +243,92 @@ void test_queue() { TESTEQUALS(q.empty(100001), false); } +void test_array() { + auto f = std::make_shared(); + + Array a(f, 0); + a.set_insert(1, 0, 0); + a.set_insert(1, 1, 1); + a.set_insert(1, 2, 2); + a.set_insert(1, 3, 3); + // a = [[0:0, 1:0],[0:0, 1:1],[0:0, 1:2],[0:0, 1:3]] + + auto res = a.get(1); + TESTEQUALS(res.at(0), 0); + TESTEQUALS(res.at(1), 1); + TESTEQUALS(res.at(2), 2); + TESTEQUALS(res.at(3), 3); + + Array other(f, 0); + other.set_last(0, 0, 999); + other.set_last(0, 1, 999); + other.set_last(0, 2, 999); + other.set_last(0, 3, 999); + + other.set_insert(1, 0, 4); + other.set_insert(1, 1, 5); + other.set_insert(1, 2, 6); + other.set_insert(1, 3, 7); + // other = [[0:999, 1:4],[0:999, 1:5],[0:999, 1:6],[0:999, 1:7]] + + + a.sync(other, 1); + // a = [[0:0, 1:4],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]] + + res = a.get(0); + TESTEQUALS(res.at(0), 0); + TESTEQUALS(res.at(1), 0); + TESTEQUALS(res.at(2), 0); + TESTEQUALS(res.at(3), 0); + res = a.get(1); + TESTEQUALS(res.at(0), 4); + TESTEQUALS(res.at(1), 5); + TESTEQUALS(res.at(2), 6); + TESTEQUALS(res.at(3), 7); + + // Additional tests + a.set_insert(2, 0, 15); + a.set_insert(2, 0, 20); + a.set_replace(2, 0, 25); + TESTEQUALS(a.at(2, 0), 25); + // a = [[0:0, 1:4, 2:25],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]] + + a.set_insert(3, 0, 30); + a.set_insert(4, 0, 40); + a.set_last(3, 0, 35); + TESTEQUALS(a.at(4, 0), 35); + // a = [[0:0, 1:4, 2:25, 3:35],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]] + + auto frame = a.frame(1, 2); + TESTEQUALS(frame.second, 6); + TESTEQUALS(frame.first, 1); + + a.set_insert(5, 3, 40); + auto next_frame = a.next_frame(1, 3); + TESTEQUALS(next_frame.second, 40); + TESTEQUALS(next_frame.first, 5); + + // Test operator[] + TESTEQUALS(a[0].get(a[0].last(2)).val(), 25); + TESTEQUALS(a[1].get(a[1].last(2)).val(), 5); + + // Test begin and end + auto it = a.begin(1); + TESTEQUALS(*it, 4); + ++it; + TESTEQUALS(*it, 5); + ++it; + TESTEQUALS(*it, 6); + ++it; + TESTEQUALS(*it, 7); +} + void container() { test_map(); test_list(); test_queue(); + test_array(); } diff --git a/libopenage/curve/tests/curve_types.cpp b/libopenage/curve/tests/curve_types.cpp index 6c754e5df9..935aa7141d 100644 --- a/libopenage/curve/tests/curve_types.cpp +++ b/libopenage/curve/tests/curve_types.cpp @@ -5,7 +5,6 @@ #include #include -#include "curve/array.h" #include "curve/continuous.h" #include "curve/discrete.h" #include "curve/discrete_mod.h" @@ -387,86 +386,6 @@ void curve_types() { TESTEQUALS(c.get(1), 0); TESTEQUALS(c.get(5), 0); } - - { // array - auto f = std::make_shared(); - - Array a(f,0); - a.set_insert(time::time_t(1), 0, 0); - a.set_insert(time::time_t(1), 1, 1); - a.set_insert(time::time_t(1), 2, 2); - a.set_insert(time::time_t(1), 3, 3); - //a = [[0:0, 1:0],[0:0, 1:1],[0:0, 1:2],[0:0, 1:3]] - - auto res = a.get_all(time::time_t(1)); - TESTEQUALS(res[0], 0); - TESTEQUALS(res[1], 1); - TESTEQUALS(res[2], 2); - TESTEQUALS(res[3], 3); - - Array other(f,0); - other[0].last(999); - other[1].last(999); - other[2].last(999); - other[3].last(999); - other.set_insert(time::time_t(1), 0, 4); - other.set_insert(time::time_t(1), 1, 5); - other.set_insert(time::time_t(1), 2, 6); - other.set_insert(time::time_t(1), 3, 7); - //other = [[0:999, 1:4],[0:999, 1:5],[0:999, 1:6],[0:999, 1:7]] - - - a.sync(other, time::time_t(1)); - //a = [[0:0, 1:4],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]] - - res = a.get_all(time::time_t(0)); - TESTEQUALS(res[0], 0); - TESTEQUALS(res[1], 0); - TESTEQUALS(res[2], 0); - TESTEQUALS(res[3], 0); - res = a.get_all(time::time_t(1)); - TESTEQUALS(res[0], 4); - TESTEQUALS(res[1], 5); - TESTEQUALS(res[2], 6); - TESTEQUALS(res[3], 7); - - // Additional tests - a.set_insert(time::time_t(2), 0, 15); - a.set_insert(time::time_t(2), 0, 20); - a.set_replace(time::time_t(2), 0, 25); - TESTEQUALS(a.get(time::time_t(2), 0), 25); - // a = [[0:0, 1:4, 2:25],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]] - - a.set_insert(time::time_t(3), 0, 30); - a.set_insert(time::time_t(4), 0, 40); - a.set_last(time::time_t(3), 0, 35); - TESTEQUALS(a.get(time::time_t(3), 0), 35); - // a = [[0:0, 1:4, 2:25, 3:35],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]] - - auto frame = a.frame(time::time_t(1), 2); - TESTEQUALS(frame.second, 6); - TESTEQUALS(frame.first, time::time_t(1)); - - a.set_insert(time::time_t(5), 3, 40); - auto next_frame = a.next_frame(time::time_t(1), 3); - TESTEQUALS(next_frame.second, 40); - TESTEQUALS(next_frame.first, time::time_t(5)); - - // Test operator[] - TESTEQUALS(a[0].get(a[0].last(time::time_t(2))).val(), 25); - TESTEQUALS(a[1].get(a[1].last(time::time_t(2))).val(), 5); - - // Test begin and end - auto it = a.begin(time::time_t(1)); - TESTEQUALS(*it, 4); - ++it; - TESTEQUALS(*it, 5); - ++it; - TESTEQUALS(*it, 6); - ++it; - TESTEQUALS(*it, 7); - ++it; - } } } // namespace openage::curve::tests From 5f5c0f4b4d9011f9f4d8bc0ff8c937a5aa3d4be1 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Fri, 27 Dec 2024 11:12:18 +0000 Subject: [PATCH 684/771] libopenage/curve/base_curve.h --- libopenage/curve/base_curve.h | 1 + 1 file changed, 1 insertion(+) diff --git a/libopenage/curve/base_curve.h b/libopenage/curve/base_curve.h index 3d2ff20343..e5e265b5a0 100644 --- a/libopenage/curve/base_curve.h +++ b/libopenage/curve/base_curve.h @@ -47,6 +47,7 @@ class BaseCurve : public event::EventEntity { // registration. If you need to copy a curve, use the sync() method. // TODO: if copying is enabled again, these members have to be reassigned: _id, _idstr, last_element BaseCurve(const BaseCurve &) = delete; + BaseCurve &operator=(const BaseCurve &) = delete; BaseCurve(BaseCurve &&) = default; From a5600f44d2f5450c636b3352a978c7f4b7b1f11f Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Wed, 8 Jan 2025 23:55:27 +0000 Subject: [PATCH 685/771] address new comments --- libopenage/curve/array.h | 74 ++++++++++++++++++++-------- libopenage/curve/tests/container.cpp | 5 +- 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/libopenage/curve/array.h b/libopenage/curve/array.h index 34109365db..fb5be284cd 100644 --- a/libopenage/curve/array.h +++ b/libopenage/curve/array.h @@ -1,25 +1,34 @@ -// Copyright 2024-2024 the openage authors. See copying.md for legal info. +// Copyright 2024-2025 the openage authors. See copying.md for legal info. #pragma once #include +#include "curve/iterator.h" #include "curve/keyframe_container.h" #include "event/evententity.h" -// remember to update docs +// ASDF: remember to update docs namespace openage { namespace curve { template class Array : event::EventEntity { public: + /** + * The underlaying container type. + */ + using container_t = std::array, Size>; + Array(const std::shared_ptr &loop, size_t id, + const T &default_val = T(), const std::string &idstr = "", const EventEntity::single_change_notifier ¬ifier = nullptr) : - EventEntity(loop, notifier), _id{id}, _idstr{idstr}, loop{loop} {} + EventEntity(loop, notifier), + _id{id}, _idstr{idstr}, loop{loop}, container{KeyframeContainer(default_val)} { + } Array(const Array &) = delete; @@ -37,7 +46,9 @@ class Array : event::EventEntity { */ std::array get(const time::time_t &t) const; - // Get the amount of KeyframeContainers in array curve + /** + * Get the amount of KeyframeContainers in array curve + */ consteval size_t size() const; /** @@ -107,59 +118,80 @@ class Array : event::EventEntity { } - // get a copy to the KeyframeContainer at index - KeyframeContainer operator[](size_t index) const { - return this->container.at(index); - } - - // Array::Iterator is used to iterate over KeyframeContainers contained in a curve at a given time. + /** + * Array::Iterator is used to iterate over KeyframeContainers contained in a curve at a given time. + */ class Iterator { public: Iterator(Array *curve, const time::time_t &time = time::TIME_MAX, size_t offset = 0) : curve(curve), time(time), offset(offset) {}; - // returns a copy of the keyframe at the current offset and time + /** + * returns a copy of the keyframe at the current offset and time + */ const T operator*() { return this->curve->frame(this->time, this->offset).second; } - // increments the Iterator to point at the next KeyframeContainer + /** + * increments the Iterator to point at the next KeyframeContainer + */ void operator++() { this->offset++; } - // Compare two Iterators by their offset + /** + * Compare two Iterators by their offset + */ bool operator!=(const Array::Iterator &rhs) const { return this->offset != rhs.offset; } private: - // used to index the Curve::Array pointed to by this iterator + /** + * used to index the Curve::Array pointed to by this iterator + */ size_t offset; - // curve::Array that this iterator is iterating over + /** + * the curve object that this iterator, iterates over + */ Array *curve; - // time at which this iterator is iterating over + /** + * time at which this iterator is iterating over + */ time::time_t time; }; - // iterator pointing to a keyframe of the first KeyframeContainer in the curve at a given time + /** + * iterator pointing to a keyframe of the first KeyframeContainer in the curve at a given time + */ Iterator begin(const time::time_t &time = time::TIME_MIN); - // iterator pointing after the last KeyframeContainer in the curve at a given time + /** + * iterator pointing after the last KeyframeContainer in the curve at a given time + */ Iterator end(const time::time_t &time = time::TIME_MIN); private: + /** + * get a copy to the KeyframeContainer at index + */ + KeyframeContainer operator[](size_t index) const { + return this->container.at(index); + } + + std::array, Size> container; /** - * hints for KeyframeContainer operations, mutable as hints - * are updated by const read functions. - * This is used to speed up the search for next keyframe to be accessed + * hints for KeyframeContainer operations + * hints is used to speed up the search for next keyframe to be accessed + * mutable as hints are updated by const read-only functions. */ mutable std::array last_element = {}; diff --git a/libopenage/curve/tests/container.cpp b/libopenage/curve/tests/container.cpp index 51a5e4bee2..8aed3c8d05 100644 --- a/libopenage/curve/tests/container.cpp +++ b/libopenage/curve/tests/container.cpp @@ -247,6 +247,7 @@ void test_array() { auto f = std::make_shared(); Array a(f, 0); + const std::array &default_val = std::array(); a.set_insert(1, 0, 0); a.set_insert(1, 1, 1); a.set_insert(1, 2, 2); @@ -308,10 +309,6 @@ void test_array() { TESTEQUALS(next_frame.second, 40); TESTEQUALS(next_frame.first, 5); - // Test operator[] - TESTEQUALS(a[0].get(a[0].last(2)).val(), 25); - TESTEQUALS(a[1].get(a[1].last(2)).val(), 5); - // Test begin and end auto it = a.begin(1); TESTEQUALS(*it, 4); From 2f3573236af59c0a5ada1226860bb31c6e84d5ae Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Thu, 9 Jan 2025 16:04:55 +0000 Subject: [PATCH 686/771] update curves.md --- doc/code/curves.md | 35 ++++++++++++++++++++++++++++++++++- libopenage/curve/array.h | 1 - 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/doc/code/curves.md b/doc/code/curves.md index ff4bbb248d..372a4b0153 100644 --- a/doc/code/curves.md +++ b/doc/code/curves.md @@ -198,7 +198,7 @@ e.g. angles between 0 and 360 degrees. ### Container Container curves are intended for storing changes to collections and containers. -The currently supported containers are `Queue` and `UnorderedMap`. +The currently supported containers are `Queue`, `UnorderedMap` and `Array`. The most important distinction between regular C++ containers and curve containers is that curve containers track the *lifespan* of each element, i.e. their insertion time, @@ -253,3 +253,36 @@ Unordered map curve containers store key-value pairs while additionally keeping track of element insertion time. Requests for a key `k` at time `t` will return the value of `k` at that time. The unordered map can also be iterated over for a specific time `t` which allows access to all key-value pairs that were in the map at time `t`. + + +#### Array +Array curve containers are the equivalent to the `std::array` C++ containers. Unlike other curve containers, +each element of the underlying std::array is a `keyframecontainer` and when a value is added to the `Array curve` +at a given index they are added to the respective `keyframecontainer` at that index as a keyframe. + +**Read** + +Read operations retrieve values for a specific point in time. + +| Method | Description | +| ----------------- | --------------------------------------- ---------------------------------| +| `get(t, i)` | Get value of index `i` at time <= `t` | +| `get(t)` | Get array of keyframes at time <= `t` | +| `size()` | Get size of the array | +| `frame(t, i)` | Get the time and value of the keyframe at index `i` with time <= `t` | +| `next_frame(t, i)`| Get the time and value of the first keyframe at index `i` with time > `t`| + +**Modify** + +Modify operations insert values for a specific point in time. + +| Method | Description | +| ------------------------- | ------------------------------------------------------------------------------------------| +| `set_insert(t, i, value)` | Insert a new keyframe(`t`, `value`) at index `i` | +| `set_last(t, i, value)` | Insert a new keyframe(`t`, `value`) at index `i`; delete all keyframes after time `t` | +| `set_replace(t, i, value)`| Insert a new keyframe(`t`, `value`) at index `i`; remove all other keyframes with time `t`| + +**Copy** +| Method | Description | +| ---------------- | ------------------------------------------------------------------------------------------------ | +| `sync(Curve, t)` | Replace all keyframes from self after time `t` with keyframes from source `Curve` after time `t` | diff --git a/libopenage/curve/array.h b/libopenage/curve/array.h index fb5be284cd..8b8ca00e77 100644 --- a/libopenage/curve/array.h +++ b/libopenage/curve/array.h @@ -9,7 +9,6 @@ #include "event/evententity.h" -// ASDF: remember to update docs namespace openage { namespace curve { From bb9b9673c74c467b6a5cd8a66bdd53b04c961a0c Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 16 Jan 2025 03:48:16 +0100 Subject: [PATCH 687/771] curve: Add more docstrings to array container. --- libopenage/curve/array.h | 176 ++++++++++++++++++++++++++------------- 1 file changed, 117 insertions(+), 59 deletions(-) diff --git a/libopenage/curve/array.h b/libopenage/curve/array.h index 8b8ca00e77..4e39ff28ab 100644 --- a/libopenage/curve/array.h +++ b/libopenage/curve/array.h @@ -20,68 +20,111 @@ class Array : event::EventEntity { */ using container_t = std::array, Size>; + /** + * Create a new array curve container. + * + * @param loop Event loop this curve is registered on for notifications. + * @param id Unique identifier for this curve. + * @param idstr Human-readable identifier for this curve. + * @param notifier Function to call when this curve changes. + * @param default_val Default value for all elements in the array. + */ Array(const std::shared_ptr &loop, size_t id, - const T &default_val = T(), const std::string &idstr = "", - const EventEntity::single_change_notifier ¬ifier = nullptr) : + const EventEntity::single_change_notifier ¬ifier = nullptr, + const T &default_val = T()) : EventEntity(loop, notifier), - _id{id}, _idstr{idstr}, loop{loop}, container{KeyframeContainer(default_val)} { - } - + containers{KeyframeContainer(default_val)}, + _id{id}, + _idstr{idstr}, + loop{loop}, + last_element{} {} + + // prevent copying because it invalidates the usage of unique ids and event + // registration. If you need to copy a curve, use the sync() method. Array(const Array &) = delete; + Array &operator=(const Array &) = delete; + Array(Array &&) = default; /** - * Get the last element with elem->time <= time from - * the keyfram container at a given index + * Get the last element with elem->time <= time. + * + * @param t Time of access. + * @param index Index of the array element. + * + * @return Value of the last element with time <= t. */ T at(const time::time_t &t, const size_t index) const; - /** - * Get an array of the last elements with elem->time <= time from - * all keyfram containers contained within this array curve + * Get all elements at time t. + * + * @param t Time of access. + * + * @return Array of values at time t. */ std::array get(const time::time_t &t) const; /** - * Get the amount of KeyframeContainers in array curve + * Get the size of the array. + * + * @return Array size. */ consteval size_t size() const; /** - * Get the last element and associated time which has elem->time <= time from - * the keyfram container at a given index + * Get the last keyframe value and time with elem->time <= time. + * + * @param t Time of access. + * @param index Index of the array element. + * + * @return Time-value pair of the last keyframe with time <= t. */ std::pair frame(const time::time_t &t, const size_t index) const; - /** - * Get the first element and associated time which has elem->time >= time from - * the keyfram container at a given index + * Get the first keyframe value and time with elem->time > time. + * + * If there is no keyframe with time > t, the behavior is undefined. + * + * @param t Time of access. + * @param index Index of the array element. + * + * @return Time-value pair of the first keyframe with time > t. */ std::pair next_frame(const time::time_t &t, const size_t index) const; - /** - * Insert a new keyframe value at time t at index + * Insert a new keyframe value at time t. + * + * If there is already a keyframe at time t, the new keyframe is inserted after the existing one. + * + * @param t Time of insertion. + * @param index Index of the array element. + * @param value Keyframe value. */ void set_insert(const time::time_t &t, const size_t index, T value); - /** - * Insert a new keyframe value at time t; delete all keyframes after time t at index + * Insert a new keyframe value at time t. Erase all other keyframes with elem->time > t. + * + * @param t Time of insertion. + * @param index Index of the array element. + * @param value Keyframe value. */ void set_last(const time::time_t &t, const size_t index, T value); - /** - * Insert a new keyframe value at time t at index i; remove all other keyframes with time t at index i + * Replace all keyframes at elem->time == t with a new keyframe value. + * + * @param t Time of insertion. + * @param index Index of the array element. + * @param value Keyframe value. */ void set_replace(const time::time_t &t, const size_t index, T value); - /** * Copy keyframes from another container to this container. * @@ -94,7 +137,6 @@ class Array : event::EventEntity { */ void sync(const Array &other, const time::time_t &start); - /** * Get the identifier of this curve. * @@ -116,7 +158,6 @@ class Array : event::EventEntity { return this->_idstr; } - /** * Array::Iterator is used to iterate over KeyframeContainers contained in a curve at a given time. */ @@ -146,7 +187,6 @@ class Array : event::EventEntity { return this->offset != rhs.offset; } - private: /** * used to index the Curve::Array pointed to by this iterator @@ -164,7 +204,6 @@ class Array : event::EventEntity { time::time_t time; }; - /** * iterator pointing to a keyframe of the first KeyframeContainer in the curve at a given time */ @@ -175,24 +214,20 @@ class Array : event::EventEntity { */ Iterator end(const time::time_t &time = time::TIME_MIN); - private: /** * get a copy to the KeyframeContainer at index */ KeyframeContainer operator[](size_t index) const { - return this->container.at(index); + return this->containers.at(index); } - - std::array, Size> container; - /** - * hints for KeyframeContainer operations - * hints is used to speed up the search for next keyframe to be accessed - * mutable as hints are updated by const read-only functions. + * Containers for each array element. + * + * Each element is managed by a KeyframeContainer. */ - mutable std::array last_element = {}; + std::array, Size> containers; /** * Identifier for the container @@ -208,31 +243,43 @@ class Array : event::EventEntity { * The eventloop this curve was registered to */ const std::shared_ptr loop; + + /** + * Cache hints for containers. Stores the index of the last keyframe accessed in each container. + * + * hints is used to speed up the search for keyframes. + * + * mutable as hints are updated by const read-only functions. + */ + mutable std::array::elem_ptr, Size> last_element = {}; }; template -std::pair Array::frame(const time::time_t &t, const size_t index) const { +std::pair Array::frame(const time::time_t &t, + const size_t index) const { size_t &hint = this->last_element[index]; - size_t frame_index = this->container.at(index).last(t, hint); + size_t frame_index = this->containers.at(index).last(t, hint); hint = frame_index; - return this->container.at(index).get(frame_index).make_pair(); + return this->containers.at(index).get(frame_index).make_pair(); } template -std::pair Array::next_frame(const time::time_t &t, const size_t index) const { +std::pair Array::next_frame(const time::time_t &t, + const size_t index) const { size_t &hint = this->last_element[index]; - size_t frame_index = this->container.at(index).last(t, hint); + size_t frame_index = this->containers.at(index).last(t, hint); hint = frame_index; - return this->container.at(index).get(frame_index + 1).make_pair(); + return this->containers.at(index).get(frame_index + 1).make_pair(); } template -T Array::at(const time::time_t &t, const size_t index) const { +T Array::at(const time::time_t &t, + const size_t index) const { size_t &hint = this->last_element[index]; - size_t frame_index = this->container.at(index).last(t, hint); + size_t frame_index = this->containers.at(index).last(t, hint); hint = frame_index; - return this->container.at(index).get(frame_index).val(); + return this->containers.at(index).get(frame_index).val(); } template @@ -247,31 +294,43 @@ consteval size_t Array::size() const { return Size; } - template -void Array::set_insert(const time::time_t &t, const size_t index, T value) { - this->last_element[index] = this->container.at(index).insert_after(Keyframe{t, value}, this->last_element[index]); -} +void Array::set_insert(const time::time_t &t, + const size_t index, + T value) { + this->last_element[index] = this->containers.at(index).insert_after(Keyframe{t, value}, this->last_element[index]); + // ASDF: Change notification +} template -void Array::set_last(const time::time_t &t, const size_t index, T value) { - size_t frame_index = this->container.at(index).insert_after(Keyframe{t, value}, this->last_element[index]); +void Array::set_last(const time::time_t &t, + const size_t index, + T value) { + size_t frame_index = this->containers.at(index).insert_after(Keyframe{t, value}, this->last_element[index]); this->last_element[index] = frame_index; - this->container.at(index).erase_after(frame_index); -} + this->containers.at(index).erase_after(frame_index); + // ASDF: Change notification +} template -void Array::set_replace(const time::time_t &t, const size_t index, T value) { - this->container.at(index).insert_overwrite(Keyframe{t, value}, this->last_element[index]); +void Array::set_replace(const time::time_t &t, + const size_t index, + T value) { + this->containers.at(index).insert_overwrite(Keyframe{t, value}, this->last_element[index]); + + // ASDF: Change notification } template -void Array::sync(const Array &other, const time::time_t &start) { +void Array::sync(const Array &other, + const time::time_t &start) { for (int i = 0; i < Size; i++) { - this->container[i].sync(other.container[i], start); + this->containers[i].sync(other.containers[i], start); } + + // ASDF: Change notification } template @@ -279,10 +338,9 @@ typename Array::Iterator Array::begin(const time::time_t &time return Array::Iterator(this, time); } - template typename Array::Iterator Array::end(const time::time_t &time) { - return Array::Iterator(this, time, this->container.size()); + return Array::Iterator(this, time, this->containers.size()); } } // namespace curve From e36e94fc47242be79cf821042c91c529df6531e1 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 16 Jan 2025 03:49:13 +0100 Subject: [PATCH 688/771] curve: Add event notification function call to modify operations. --- libopenage/curve/array.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libopenage/curve/array.h b/libopenage/curve/array.h index 4e39ff28ab..2bcf52522d 100644 --- a/libopenage/curve/array.h +++ b/libopenage/curve/array.h @@ -300,7 +300,7 @@ void Array::set_insert(const time::time_t &t, T value) { this->last_element[index] = this->containers.at(index).insert_after(Keyframe{t, value}, this->last_element[index]); - // ASDF: Change notification + this->changes(t); } template @@ -311,7 +311,7 @@ void Array::set_last(const time::time_t &t, this->last_element[index] = frame_index; this->containers.at(index).erase_after(frame_index); - // ASDF: Change notification + this->changes(t); } template @@ -320,7 +320,7 @@ void Array::set_replace(const time::time_t &t, T value) { this->containers.at(index).insert_overwrite(Keyframe{t, value}, this->last_element[index]); - // ASDF: Change notification + this->changes(t); } template @@ -330,7 +330,7 @@ void Array::sync(const Array &other, this->containers[i].sync(other.containers[i], start); } - // ASDF: Change notification + this->changes(start); } template From 4b12a991fdbc1db7d39d18922e5408ce266b71bc Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 16 Jan 2025 04:28:09 +0100 Subject: [PATCH 689/771] curve: Allow array default values to be different for each element. --- libopenage/curve/array.h | 49 ++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/libopenage/curve/array.h b/libopenage/curve/array.h index 2bcf52522d..857638f5e0 100644 --- a/libopenage/curve/array.h +++ b/libopenage/curve/array.h @@ -12,13 +12,22 @@ namespace openage { namespace curve { +template +constexpr std::array, Size> init_default_vals(const std::array &default_vals) { + std::array, Size> containers; + for (size_t i = 0; i < Size; i++) { + containers[i] = KeyframeContainer(default_vals[i]); + } + return containers; +} + template class Array : event::EventEntity { public: - /** - * The underlaying container type. - */ + /// Underlying container type. using container_t = std::array, Size>; + /// Index type to access elements in the container. + using index_t = typename container_t::size_type; /** * Create a new array curve container. @@ -27,15 +36,15 @@ class Array : event::EventEntity { * @param id Unique identifier for this curve. * @param idstr Human-readable identifier for this curve. * @param notifier Function to call when this curve changes. - * @param default_val Default value for all elements in the array. + * @param default_vals Default values for the array elements. */ Array(const std::shared_ptr &loop, size_t id, const std::string &idstr = "", const EventEntity::single_change_notifier ¬ifier = nullptr, - const T &default_val = T()) : + const std::array &default_vals = {}) : EventEntity(loop, notifier), - containers{KeyframeContainer(default_val)}, + containers{init_default_vals(default_vals)}, _id{id}, _idstr{idstr}, loop{loop}, @@ -56,7 +65,7 @@ class Array : event::EventEntity { * * @return Value of the last element with time <= t. */ - T at(const time::time_t &t, const size_t index) const; + T at(const time::time_t &t, const index_t index) const; /** * Get all elements at time t. @@ -82,7 +91,7 @@ class Array : event::EventEntity { * * @return Time-value pair of the last keyframe with time <= t. */ - std::pair frame(const time::time_t &t, const size_t index) const; + std::pair frame(const time::time_t &t, const index_t index) const; /** * Get the first keyframe value and time with elem->time > time. @@ -94,7 +103,7 @@ class Array : event::EventEntity { * * @return Time-value pair of the first keyframe with time > t. */ - std::pair next_frame(const time::time_t &t, const size_t index) const; + std::pair next_frame(const time::time_t &t, const index_t index) const; /** * Insert a new keyframe value at time t. @@ -105,7 +114,7 @@ class Array : event::EventEntity { * @param index Index of the array element. * @param value Keyframe value. */ - void set_insert(const time::time_t &t, const size_t index, T value); + void set_insert(const time::time_t &t, const index_t index, T value); /** * Insert a new keyframe value at time t. Erase all other keyframes with elem->time > t. @@ -114,7 +123,7 @@ class Array : event::EventEntity { * @param index Index of the array element. * @param value Keyframe value. */ - void set_last(const time::time_t &t, const size_t index, T value); + void set_last(const time::time_t &t, const index_t index, T value); /** * Replace all keyframes at elem->time == t with a new keyframe value. @@ -123,7 +132,7 @@ class Array : event::EventEntity { * @param index Index of the array element. * @param value Keyframe value. */ - void set_replace(const time::time_t &t, const size_t index, T value); + void set_replace(const time::time_t &t, const index_t index, T value); /** * Copy keyframes from another container to this container. @@ -218,7 +227,7 @@ class Array : event::EventEntity { /** * get a copy to the KeyframeContainer at index */ - KeyframeContainer operator[](size_t index) const { + KeyframeContainer operator[](index_t index) const { return this->containers.at(index); } @@ -227,7 +236,7 @@ class Array : event::EventEntity { * * Each element is managed by a KeyframeContainer. */ - std::array, Size> containers; + container_t containers; /** * Identifier for the container @@ -257,7 +266,7 @@ class Array : event::EventEntity { template std::pair Array::frame(const time::time_t &t, - const size_t index) const { + const index_t index) const { size_t &hint = this->last_element[index]; size_t frame_index = this->containers.at(index).last(t, hint); hint = frame_index; @@ -266,7 +275,7 @@ std::pair Array::frame(const time::time_t &t, template std::pair Array::next_frame(const time::time_t &t, - const size_t index) const { + const index_t index) const { size_t &hint = this->last_element[index]; size_t frame_index = this->containers.at(index).last(t, hint); hint = frame_index; @@ -275,7 +284,7 @@ std::pair Array::next_frame(const time::time_t &t, template T Array::at(const time::time_t &t, - const size_t index) const { + const index_t index) const { size_t &hint = this->last_element[index]; size_t frame_index = this->containers.at(index).last(t, hint); hint = frame_index; @@ -296,7 +305,7 @@ consteval size_t Array::size() const { template void Array::set_insert(const time::time_t &t, - const size_t index, + const index_t index, T value) { this->last_element[index] = this->containers.at(index).insert_after(Keyframe{t, value}, this->last_element[index]); @@ -305,7 +314,7 @@ void Array::set_insert(const time::time_t &t, template void Array::set_last(const time::time_t &t, - const size_t index, + const index_t index, T value) { size_t frame_index = this->containers.at(index).insert_after(Keyframe{t, value}, this->last_element[index]); this->last_element[index] = frame_index; @@ -316,7 +325,7 @@ void Array::set_last(const time::time_t &t, template void Array::set_replace(const time::time_t &t, - const size_t index, + const index_t index, T value) { this->containers.at(index).insert_overwrite(Keyframe{t, value}, this->last_element[index]); From e8c124294386b836c36e81727996bb871fc50f59 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 16 Jan 2025 04:31:39 +0100 Subject: [PATCH 690/771] curve: Fix gcc compiler warnings. --- libopenage/curve/array.h | 12 ++++++------ libopenage/curve/tests/container.cpp | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libopenage/curve/array.h b/libopenage/curve/array.h index 857638f5e0..ea1cb71724 100644 --- a/libopenage/curve/array.h +++ b/libopenage/curve/array.h @@ -197,11 +197,6 @@ class Array : event::EventEntity { } private: - /** - * used to index the Curve::Array pointed to by this iterator - */ - size_t offset; - /** * the curve object that this iterator, iterates over */ @@ -211,6 +206,11 @@ class Array : event::EventEntity { * time at which this iterator is iterating over */ time::time_t time; + + /** + * used to index the Curve::Array pointed to by this iterator + */ + size_t offset; }; /** @@ -335,7 +335,7 @@ void Array::set_replace(const time::time_t &t, template void Array::sync(const Array &other, const time::time_t &start) { - for (int i = 0; i < Size; i++) { + for (index_t i = 0; i < Size; i++) { this->containers[i].sync(other.containers[i], start); } diff --git a/libopenage/curve/tests/container.cpp b/libopenage/curve/tests/container.cpp index 8aed3c8d05..48a3b19202 100644 --- a/libopenage/curve/tests/container.cpp +++ b/libopenage/curve/tests/container.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 @@ -247,7 +247,7 @@ void test_array() { auto f = std::make_shared(); Array a(f, 0); - const std::array &default_val = std::array(); + // const std::array &default_val = std::array(); a.set_insert(1, 0, 0); a.set_insert(1, 1, 1); a.set_insert(1, 2, 2); From 599f7bd8645b3ec1db64b7f06e467c7c3543b467 Mon Sep 17 00:00:00 2001 From: heinezen Date: Thu, 16 Jan 2025 04:52:33 +0100 Subject: [PATCH 691/771] curve: Fix elem_ptr caching for array. --- libopenage/curve/array.h | 77 ++++++++++++++++++++++++++--------- libopenage/curve/base_curve.h | 6 +-- libopenage/curve/discrete.h | 6 +-- libopenage/curve/keyframe.h | 12 +++--- 4 files changed, 70 insertions(+), 31 deletions(-) diff --git a/libopenage/curve/array.h b/libopenage/curve/array.h index ea1cb71724..9f9bacd09c 100644 --- a/libopenage/curve/array.h +++ b/libopenage/curve/array.h @@ -48,7 +48,7 @@ class Array : event::EventEntity { _id{id}, _idstr{idstr}, loop{loop}, - last_element{} {} + last_elements{} {} // prevent copying because it invalidates the usage of unique ids and event // registration. If you need to copy a curve, use the sync() method. @@ -260,35 +260,50 @@ class Array : event::EventEntity { * * mutable as hints are updated by const read-only functions. */ - mutable std::array::elem_ptr, Size> last_element = {}; + mutable std::array::elem_ptr, Size> last_elements = {}; }; template std::pair Array::frame(const time::time_t &t, const index_t index) const { - size_t &hint = this->last_element[index]; - size_t frame_index = this->containers.at(index).last(t, hint); - hint = frame_index; - return this->containers.at(index).get(frame_index).make_pair(); + // find elem_ptr in container to get the last keyframe + auto hint = this->last_elements[index]; + auto frame_index = this->containers.at(index).last(t, hint); + + // update the hint + this->last_elements[index] = frame_index; + + return this->containers.at(index).get(frame_index).as_pair(); } template std::pair Array::next_frame(const time::time_t &t, const index_t index) const { - size_t &hint = this->last_element[index]; - size_t frame_index = this->containers.at(index).last(t, hint); - hint = frame_index; - return this->containers.at(index).get(frame_index + 1).make_pair(); + // find elem_ptr in container to get the last keyframe with time <= t + auto hint = this->last_elements[index]; + auto frame_index = this->containers.at(index).last(t, hint); + + // increment the index to get the next keyframe + frame_index++; + + // update the hint + this->last_elements[index] = frame_index; + + return this->containers.at(index).get(frame_index).as_pair(); } template T Array::at(const time::time_t &t, const index_t index) const { - size_t &hint = this->last_element[index]; - size_t frame_index = this->containers.at(index).last(t, hint); - hint = frame_index; - return this->containers.at(index).get(frame_index).val(); + // find elem_ptr in container to get the last keyframe with time <= t + auto hint = this->last_elements[index]; + auto e = this->containers.at(index).last(t, hint); + + // update the hint + this->last_elements[index] = e; + + return this->containers.at(index).get(e).val(); } template @@ -307,7 +322,12 @@ template void Array::set_insert(const time::time_t &t, const index_t index, T value) { - this->last_element[index] = this->containers.at(index).insert_after(Keyframe{t, value}, this->last_element[index]); + // find elem_ptr in container to get the last keyframe with time <= t + auto hint = this->last_elements[index]; + auto e = this->containers.at(index).insert_after(Keyframe{t, value}, hint); + + // update the hint + this->last_elements[index] = e; this->changes(t); } @@ -316,9 +336,23 @@ template void Array::set_last(const time::time_t &t, const index_t index, T value) { - size_t frame_index = this->containers.at(index).insert_after(Keyframe{t, value}, this->last_element[index]); - this->last_element[index] = frame_index; - this->containers.at(index).erase_after(frame_index); + // find elem_ptr in container to get the last keyframe with time <= t + auto hint = this->last_elements[index]; + auto e = this->containers.at(index).last(t, hint); + + // erase max one same-time value + if (this->containers.at(index).get(e).time() == t) { + e--; + } + + // erase all keyframes with time > t + this->containers.at(index).erase_after(e); + + // insert the new keyframe at the end + this->containers.at(index).insert_before(Keyframe{t, value}, e); + + // update the hint + this->last_elements[index] = hint; this->changes(t); } @@ -327,7 +361,12 @@ template void Array::set_replace(const time::time_t &t, const index_t index, T value) { - this->containers.at(index).insert_overwrite(Keyframe{t, value}, this->last_element[index]); + // find elem_ptr in container to get the last keyframe with time <= t + auto hint = this->last_elements[index]; + auto e = this->containers.at(index).insert_overwrite(Keyframe{t, value}, hint); + + // update the hint + this->last_elements[index] = e; this->changes(t); } diff --git a/libopenage/curve/base_curve.h b/libopenage/curve/base_curve.h index e5e265b5a0..de5c14201d 100644 --- a/libopenage/curve/base_curve.h +++ b/libopenage/curve/base_curve.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 @@ -246,7 +246,7 @@ 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); - return elem.make_pair(); + return elem.as_pair(); } @@ -255,7 +255,7 @@ std::pair BaseCurve::next_frame(const time::time_t &ti auto e = this->container.last(time, this->container.size()); e++; auto elem = this->container.get(e); - return elem.make_pair(); + return elem.as_pair(); } template diff --git a/libopenage/curve/discrete.h b/libopenage/curve/discrete.h index 44fe132de6..b9f9b6b00c 100644 --- a/libopenage/curve/discrete.h +++ b/libopenage/curve/discrete.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 @@ -80,7 +80,7 @@ std::pair Discrete::get_time(const time::time_t &time) const this->last_element = e; auto elem = this->container.get(e); - return std::make_pair(elem.time, elem.value); + return elem.as_pair(); } @@ -97,7 +97,7 @@ std::optional> Discrete::get_previous(const time:: e--; auto elem = this->container.get(e); - return std::make_pair(elem.time(), elem.val()); + return elem.as_pair(); } } // namespace openage::curve diff --git a/libopenage/curve/keyframe.h b/libopenage/curve/keyframe.h index a34b0c6262..cd2ebf6ced 100644 --- a/libopenage/curve/keyframe.h +++ b/libopenage/curve/keyframe.h @@ -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. #pragma once @@ -55,12 +55,12 @@ class Keyframe { } /** - * Get the value and timestamp of the keyframe in form of std::pair - * @return keyframe pair + * Get a time-value pair of this keyframe. + * + * @return Keyframe time-value pair. */ - std::pair make_pair() const - { - return {time(), val()}; + std::pair as_pair() const { + return {this->timestamp, this->value}; } public: From 156d009daa69df970e8aeaa36d5560a7b411a6aa Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 19 Jan 2025 15:49:28 +0100 Subject: [PATCH 692/771] curve: Add more comments to tests and check additional methods. --- libopenage/curve/tests/container.cpp | 56 ++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/libopenage/curve/tests/container.cpp b/libopenage/curve/tests/container.cpp index 48a3b19202..92b885320d 100644 --- a/libopenage/curve/tests/container.cpp +++ b/libopenage/curve/tests/container.cpp @@ -244,35 +244,58 @@ void test_queue() { } void test_array() { - auto f = std::make_shared(); + auto loop = std::make_shared(); - Array a(f, 0); - // const std::array &default_val = std::array(); + Array a(loop, 0); a.set_insert(1, 0, 0); a.set_insert(1, 1, 1); a.set_insert(1, 2, 2); a.set_insert(1, 3, 3); // a = [[0:0, 1:0],[0:0, 1:1],[0:0, 1:2],[0:0, 1:3]] + // test size + TESTEQUALS(a.size(), 4); + + // extracting array at time t == 1 auto res = a.get(1); + auto expected = std::array{0, 1, 2, 3}; + TESTEQUALS(res.at(0), expected.at(0)); + TESTEQUALS(res.at(1), expected.at(1)); + TESTEQUALS(res.at(2), expected.at(2)); + TESTEQUALS(res.at(3), expected.at(3)); + TESTEQUALS(res.size(), expected.size()); + + // extracting array at time t == 0 + // array should have default values (== 0) for all keyframes + res = a.get(0); TESTEQUALS(res.at(0), 0); - TESTEQUALS(res.at(1), 1); - TESTEQUALS(res.at(2), 2); - TESTEQUALS(res.at(3), 3); + TESTEQUALS(res.at(1), 0); + TESTEQUALS(res.at(2), 0); + TESTEQUALS(res.at(3), 0); - Array other(f, 0); + Array other(loop, 0); other.set_last(0, 0, 999); other.set_last(0, 1, 999); other.set_last(0, 2, 999); other.set_last(0, 3, 999); + // inserting keyframes at time t == 1 other.set_insert(1, 0, 4); other.set_insert(1, 1, 5); other.set_insert(1, 2, 6); other.set_insert(1, 3, 7); // other = [[0:999, 1:4],[0:999, 1:5],[0:999, 1:6],[0:999, 1:7]] + TESTEQUALS(other.at(0, 0), 999); + TESTEQUALS(other.at(0, 1), 999); + TESTEQUALS(other.at(0, 2), 999); + TESTEQUALS(other.at(0, 3), 999); + TESTEQUALS(other.at(1, 0), 4); + TESTEQUALS(other.at(1, 1), 5); + TESTEQUALS(other.at(1, 2), 6); + TESTEQUALS(other.at(1, 3), 7); + // sync keyframes from other to a a.sync(other, 1); // a = [[0:0, 1:4],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]] @@ -287,27 +310,30 @@ void test_array() { TESTEQUALS(res.at(2), 6); TESTEQUALS(res.at(3), 7); - // Additional tests + // replace keyframes at time t == 2 a.set_insert(2, 0, 15); a.set_insert(2, 0, 20); a.set_replace(2, 0, 25); TESTEQUALS(a.at(2, 0), 25); // a = [[0:0, 1:4, 2:25],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]] - a.set_insert(3, 0, 30); - a.set_insert(4, 0, 40); - a.set_last(3, 0, 35); + // set last keyframe at time t == 3 + a.set_insert(3, 0, 30); // a = [[0:0, 1:4, 2:25, 3:30], ... + a.set_insert(4, 0, 40); // a = [[0:0, 1:4, 2:25, 3:30, 4:40], ... + a.set_last(3, 0, 35); // a = [[0:0, 1:4, 2:25, 3:35],... TESTEQUALS(a.at(4, 0), 35); // a = [[0:0, 1:4, 2:25, 3:35],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]] + // test frame and next_frame auto frame = a.frame(1, 2); - TESTEQUALS(frame.second, 6); - TESTEQUALS(frame.first, 1); + TESTEQUALS(frame.first, 1); // time + TESTEQUALS(frame.second, 6); // value a.set_insert(5, 3, 40); + // a = [[0:0, 1:4, 2:25, 3:35],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7, 5:40]] auto next_frame = a.next_frame(1, 3); - TESTEQUALS(next_frame.second, 40); - TESTEQUALS(next_frame.first, 5); + TESTEQUALS(next_frame.first, 5); // time + TESTEQUALS(next_frame.second, 40); // value // Test begin and end auto it = a.begin(1); From ad747a6bc0fb71689cc540a4e0fef978e2a053cb Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 19 Jan 2025 16:03:41 +0100 Subject: [PATCH 693/771] doc: Fix formatting of curve docs and reformulate array docs a bit. --- doc/code/curves.md | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/doc/code/curves.md b/doc/code/curves.md index 372a4b0153..e914f8bcfe 100644 --- a/doc/code/curves.md +++ b/doc/code/curves.md @@ -17,6 +17,7 @@ Curves are an integral part of openage's event-based game simulation. 2. [Container](#container) 1. [Queue](#queue) 2. [Unordered Map](#unordered-map) + 3. [Array](#array) ## Motivation @@ -256,33 +257,38 @@ allows access to all key-value pairs that were in the map at time `t`. #### Array -Array curve containers are the equivalent to the `std::array` C++ containers. Unlike other curve containers, -each element of the underlying std::array is a `keyframecontainer` and when a value is added to the `Array curve` -at a given index they are added to the respective `keyframecontainer` at that index as a keyframe. + +Array curve containers store a fixed number of `n` elements where `n` is determined at compile-time. +They are the curve equivalent to the `std::array` C++ containers. In comparison to `std::array` each +element in the array curve container is tracked individually over time. Hence, each index is associated +with its own `KeyframeContainer` whose keyframes can be updated independent from other indices. +When a value is added to the `Array` curve at a given index, a new keyframe is added to the respective +`KeyframeContainer` stored at that index. **Read** Read operations retrieve values for a specific point in time. -| Method | Description | -| ----------------- | --------------------------------------- ---------------------------------| -| `get(t, i)` | Get value of index `i` at time <= `t` | -| `get(t)` | Get array of keyframes at time <= `t` | -| `size()` | Get size of the array | -| `frame(t, i)` | Get the time and value of the keyframe at index `i` with time <= `t` | -| `next_frame(t, i)`| Get the time and value of the first keyframe at index `i` with time > `t`| +| Method | Description | +| ------------------ | ------------------------------------------------------------------------ | +| `get(t, i)` | Get value of element at index `i` at time <= `t` | +| `get(t)` | Get array of values at time <= `t` | +| `size()` | Get the number of elements in the array | +| `frame(t, i)` | Get the previous keyframe (time and value) at index `i` before or at `t` | +| `next_frame(t, i)` | Get the next keyframe (time and value) at index `i` after `t` | **Modify** Modify operations insert values for a specific point in time. -| Method | Description | -| ------------------------- | ------------------------------------------------------------------------------------------| -| `set_insert(t, i, value)` | Insert a new keyframe(`t`, `value`) at index `i` | -| `set_last(t, i, value)` | Insert a new keyframe(`t`, `value`) at index `i`; delete all keyframes after time `t` | -| `set_replace(t, i, value)`| Insert a new keyframe(`t`, `value`) at index `i`; remove all other keyframes with time `t`| +| Method | Description | +| -------------------------- | ------------------------------------------------------------------------------------------ | +| `set_insert(t, i, value)` | Insert a new keyframe(`t`, `value`) at index `i` | +| `set_last(t, i, value)` | Insert a new keyframe(`t`, `value`) at index `i`; delete all keyframes after time `t` | +| `set_replace(t, i, value)` | Insert a new keyframe(`t`, `value`) at index `i`; remove all other keyframes with time `t` | **Copy** + | Method | Description | -| ---------------- | ------------------------------------------------------------------------------------------------ | +| ---------------- | ------------------------------------------------------------------------------------------------ | | `sync(Curve, t)` | Replace all keyframes from self after time `t` with keyframes from source `Curve` after time `t` | From d571822ef9f341be22b7b3c93e5c599a7dbbb8ae Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 19 Jan 2025 16:10:58 +0100 Subject: [PATCH 694/771] curve: Refactor and move curve containers to subfolder. --- libopenage/curve/CMakeLists.txt | 8 +------- libopenage/curve/container/CMakeLists.txt | 9 +++++++++ libopenage/curve/{ => container}/array.cpp | 2 +- libopenage/curve/{ => container}/array.h | 2 +- libopenage/curve/{ => container}/element_wrapper.cpp | 2 +- libopenage/curve/{ => container}/element_wrapper.h | 2 +- libopenage/curve/{ => container}/iterator.cpp | 2 +- libopenage/curve/{ => container}/iterator.h | 2 +- libopenage/curve/{ => container}/map.cpp | 2 +- libopenage/curve/{ => container}/map.h | 7 +++---- .../curve/{ => container}/map_filter_iterator.cpp | 2 +- .../curve/{ => container}/map_filter_iterator.h | 4 ++-- libopenage/curve/{ => container}/queue.cpp | 2 +- libopenage/curve/{ => container}/queue.h | 8 ++++---- .../curve/{ => container}/queue_filter_iterator.cpp | 2 +- .../curve/{ => container}/queue_filter_iterator.h | 4 ++-- libopenage/curve/tests/container.cpp | 12 ++++++------ libopenage/gamestate/component/api/live.cpp | 6 +++--- libopenage/gamestate/component/api/live.h | 4 ++-- .../gamestate/component/internal/command_queue.h | 4 ++-- libopenage/gamestate/entity_factory.cpp | 4 ++-- 21 files changed, 46 insertions(+), 44 deletions(-) create mode 100644 libopenage/curve/container/CMakeLists.txt rename libopenage/curve/{ => container}/array.cpp (65%) rename libopenage/curve/{ => container}/array.h (99%) rename libopenage/curve/{ => container}/element_wrapper.cpp (68%) rename libopenage/curve/{ => container}/element_wrapper.h (96%) rename libopenage/curve/{ => container}/iterator.cpp (67%) rename libopenage/curve/{ => container}/iterator.h (97%) rename libopenage/curve/{ => container}/map.cpp (66%) rename libopenage/curve/{ => container}/map.h (97%) rename libopenage/curve/{ => container}/map_filter_iterator.cpp (68%) rename libopenage/curve/{ => container}/map_filter_iterator.h (93%) rename libopenage/curve/{ => container}/queue.cpp (58%) rename libopenage/curve/{ => container}/queue.h (98%) rename libopenage/curve/{ => container}/queue_filter_iterator.cpp (69%) rename libopenage/curve/{ => container}/queue_filter_iterator.h (93%) diff --git a/libopenage/curve/CMakeLists.txt b/libopenage/curve/CMakeLists.txt index 174498e23b..eb52858f43 100644 --- a/libopenage/curve/CMakeLists.txt +++ b/libopenage/curve/CMakeLists.txt @@ -1,19 +1,13 @@ add_sources(libopenage - array.cpp base_curve.cpp continuous.cpp discrete.cpp discrete_mod.cpp - element_wrapper.cpp interpolated.cpp - iterator.cpp keyframe.cpp keyframe_container.cpp - map.cpp - map_filter_iterator.cpp - queue.cpp - queue_filter_iterator.cpp segmented.cpp ) +add_subdirectory("container") add_subdirectory("tests") diff --git a/libopenage/curve/container/CMakeLists.txt b/libopenage/curve/container/CMakeLists.txt new file mode 100644 index 0000000000..a187e01429 --- /dev/null +++ b/libopenage/curve/container/CMakeLists.txt @@ -0,0 +1,9 @@ +add_sources(libopenage + array.cpp + element_wrapper.cpp + iterator.cpp + map.cpp + map_filter_iterator.cpp + queue.cpp + queue_filter_iterator.cpp +) diff --git a/libopenage/curve/array.cpp b/libopenage/curve/container/array.cpp similarity index 65% rename from libopenage/curve/array.cpp rename to libopenage/curve/container/array.cpp index 97e07033ce..609ee117a3 100644 --- a/libopenage/curve/array.cpp +++ b/libopenage/curve/container/array.cpp @@ -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. #include "array.h" diff --git a/libopenage/curve/array.h b/libopenage/curve/container/array.h similarity index 99% rename from libopenage/curve/array.h rename to libopenage/curve/container/array.h index 9f9bacd09c..e980d77915 100644 --- a/libopenage/curve/array.h +++ b/libopenage/curve/container/array.h @@ -4,7 +4,7 @@ #include -#include "curve/iterator.h" +#include "curve/container/iterator.h" #include "curve/keyframe_container.h" #include "event/evententity.h" diff --git a/libopenage/curve/element_wrapper.cpp b/libopenage/curve/container/element_wrapper.cpp similarity index 68% rename from libopenage/curve/element_wrapper.cpp rename to libopenage/curve/container/element_wrapper.cpp index 5d2eaa08af..bdbca7346f 100644 --- a/libopenage/curve/element_wrapper.cpp +++ b/libopenage/curve/container/element_wrapper.cpp @@ -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. #include "element_wrapper.h" diff --git a/libopenage/curve/element_wrapper.h b/libopenage/curve/container/element_wrapper.h similarity index 96% rename from libopenage/curve/element_wrapper.h rename to libopenage/curve/container/element_wrapper.h index 764b61d5cd..0032e4c6fe 100644 --- a/libopenage/curve/element_wrapper.h +++ b/libopenage/curve/container/element_wrapper.h @@ -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. #pragma once diff --git a/libopenage/curve/iterator.cpp b/libopenage/curve/container/iterator.cpp similarity index 67% rename from libopenage/curve/iterator.cpp rename to libopenage/curve/container/iterator.cpp index b449d93812..594df3a5b8 100644 --- a/libopenage/curve/iterator.cpp +++ b/libopenage/curve/container/iterator.cpp @@ -1,4 +1,4 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. +// Copyright 2017-2025 the openage authors. See copying.md for legal info. #include "iterator.h" diff --git a/libopenage/curve/iterator.h b/libopenage/curve/container/iterator.h similarity index 97% rename from libopenage/curve/iterator.h rename to libopenage/curve/container/iterator.h index d0346e1b5d..7a4fb82d6b 100644 --- a/libopenage/curve/iterator.h +++ b/libopenage/curve/container/iterator.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 diff --git a/libopenage/curve/map.cpp b/libopenage/curve/container/map.cpp similarity index 66% rename from libopenage/curve/map.cpp rename to libopenage/curve/container/map.cpp index 82b26f9ecc..a2469bfa5f 100644 --- a/libopenage/curve/map.cpp +++ b/libopenage/curve/container/map.cpp @@ -1,4 +1,4 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. +// Copyright 2017-2025 the openage authors. See copying.md for legal info. #include "map.h" diff --git a/libopenage/curve/map.h b/libopenage/curve/container/map.h similarity index 97% rename from libopenage/curve/map.h rename to libopenage/curve/container/map.h index 5a5e0a4d9b..4997824a6d 100644 --- a/libopenage/curve/map.h +++ b/libopenage/curve/container/map.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,8 +7,8 @@ #include #include -#include "curve/map_filter_iterator.h" -#include "curve/element_wrapper.h" +#include "curve/container/element_wrapper.h" +#include "curve/container/map_filter_iterator.h" #include "time/time.h" #include "util/fixed_point.h" @@ -21,7 +21,6 @@ namespace openage::curve { */ template class UnorderedMap { - /** * Data holder. Maps keys to map elements. * Map elements themselves store when they are valid. diff --git a/libopenage/curve/map_filter_iterator.cpp b/libopenage/curve/container/map_filter_iterator.cpp similarity index 68% rename from libopenage/curve/map_filter_iterator.cpp rename to libopenage/curve/container/map_filter_iterator.cpp index da10b01774..bba3ffa478 100644 --- a/libopenage/curve/map_filter_iterator.cpp +++ b/libopenage/curve/container/map_filter_iterator.cpp @@ -1,4 +1,4 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. +// Copyright 2017-2025 the openage authors. See copying.md for legal info. #include "map_filter_iterator.h" diff --git a/libopenage/curve/map_filter_iterator.h b/libopenage/curve/container/map_filter_iterator.h similarity index 93% rename from libopenage/curve/map_filter_iterator.h rename to libopenage/curve/container/map_filter_iterator.h index 3aec2a899e..c9afceee88 100644 --- a/libopenage/curve/map_filter_iterator.h +++ b/libopenage/curve/container/map_filter_iterator.h @@ -1,8 +1,8 @@ -// 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 -#include "curve/iterator.h" +#include "curve/container/iterator.h" #include "time/time.h" diff --git a/libopenage/curve/queue.cpp b/libopenage/curve/container/queue.cpp similarity index 58% rename from libopenage/curve/queue.cpp rename to libopenage/curve/container/queue.cpp index d994b6c82e..842a140045 100644 --- a/libopenage/curve/queue.cpp +++ b/libopenage/curve/container/queue.cpp @@ -1,4 +1,4 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. +// Copyright 2017-2025 the openage authors. See copying.md for legal info. #include "queue.h" diff --git a/libopenage/curve/queue.h b/libopenage/curve/container/queue.h similarity index 98% rename from libopenage/curve/queue.h rename to libopenage/curve/container/queue.h index 9314dd3a0e..fb32a53cbb 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/container/queue.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 @@ -11,9 +11,9 @@ #include "error/error.h" -#include "curve/iterator.h" -#include "curve/queue_filter_iterator.h" -#include "curve/element_wrapper.h" +#include "curve/container/element_wrapper.h" +#include "curve/container/iterator.h" +#include "curve/container/queue_filter_iterator.h" #include "event/evententity.h" #include "time/time.h" #include "util/fixed_point.h" diff --git a/libopenage/curve/queue_filter_iterator.cpp b/libopenage/curve/container/queue_filter_iterator.cpp similarity index 69% rename from libopenage/curve/queue_filter_iterator.cpp rename to libopenage/curve/container/queue_filter_iterator.cpp index fa0b3ad15a..b4ceb2b7e6 100644 --- a/libopenage/curve/queue_filter_iterator.cpp +++ b/libopenage/curve/container/queue_filter_iterator.cpp @@ -1,4 +1,4 @@ -// Copyright 2017-2018 the openage authors. See copying.md for legal info. +// Copyright 2017-2025 the openage authors. See copying.md for legal info. #include "queue_filter_iterator.h" diff --git a/libopenage/curve/queue_filter_iterator.h b/libopenage/curve/container/queue_filter_iterator.h similarity index 93% rename from libopenage/curve/queue_filter_iterator.h rename to libopenage/curve/container/queue_filter_iterator.h index cf6bc5aa2c..6b2fa471f2 100644 --- a/libopenage/curve/queue_filter_iterator.h +++ b/libopenage/curve/container/queue_filter_iterator.h @@ -1,8 +1,8 @@ -// 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 -#include "curve/iterator.h" +#include "curve/container/iterator.h" #include "time/time.h" diff --git a/libopenage/curve/tests/container.cpp b/libopenage/curve/tests/container.cpp index 92b885320d..16da2476e9 100644 --- a/libopenage/curve/tests/container.cpp +++ b/libopenage/curve/tests/container.cpp @@ -9,12 +9,12 @@ #include #include -#include "curve/array.h" -#include "curve/iterator.h" -#include "curve/map.h" -#include "curve/map_filter_iterator.h" -#include "curve/queue.h" -#include "curve/queue_filter_iterator.h" +#include "curve/container/array.h" +#include "curve/container/iterator.h" +#include "curve/container/map.h" +#include "curve/container/map_filter_iterator.h" +#include "curve/container/queue.h" +#include "curve/container/queue_filter_iterator.h" #include "event/event_loop.h" #include "testing/testing.h" diff --git a/libopenage/gamestate/component/api/live.cpp b/libopenage/gamestate/component/api/live.cpp index 01fafde5ea..f7a1f17d98 100644 --- a/libopenage/gamestate/component/api/live.cpp +++ b/libopenage/gamestate/component/api/live.cpp @@ -1,12 +1,12 @@ -// 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 "live.h" #include +#include "curve/container/iterator.h" +#include "curve/container/map_filter_iterator.h" #include "curve/discrete.h" -#include "curve/iterator.h" -#include "curve/map_filter_iterator.h" #include "gamestate/component/types.h" diff --git a/libopenage/gamestate/component/api/live.h b/libopenage/gamestate/component/api/live.h index 2e1f5e41d5..4916713cdc 100644 --- a/libopenage/gamestate/component/api/live.h +++ b/libopenage/gamestate/component/api/live.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,7 +7,7 @@ #include -#include "curve/map.h" +#include "curve/container/map.h" #include "gamestate/component/api_component.h" #include "gamestate/component/types.h" #include "time/time.h" diff --git a/libopenage/gamestate/component/internal/command_queue.h b/libopenage/gamestate/component/internal/command_queue.h index a7905c4d24..fb3179b470 100644 --- a/libopenage/gamestate/component/internal/command_queue.h +++ b/libopenage/gamestate/component/internal/command_queue.h @@ -1,10 +1,10 @@ -// 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 #include -#include "curve/queue.h" +#include "curve/container/queue.h" #include "gamestate/component/internal/commands/base_command.h" #include "gamestate/component/internal_component.h" #include "gamestate/component/types.h" diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index f2f489206c..6e1a4c825f 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "entity_factory.h" @@ -10,8 +10,8 @@ #include "error/error.h" +#include "curve/container/queue.h" #include "curve/discrete.h" -#include "curve/queue.h" #include "event/event_loop.h" #include "gamestate/activity/activity.h" #include "gamestate/activity/condition/command_in_queue.h" From 5f8690944a46c2435182c7a843b970966214962b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Xu=C3=A2n=20Minh?= Date: Thu, 28 Oct 2021 00:18:49 +0700 Subject: [PATCH 695/771] Add generic function fox SMX file --- .../convert/value_object/read/media/smx.pyx | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) diff --git a/openage/convert/value_object/read/media/smx.pyx b/openage/convert/value_object/read/media/smx.pyx index d1f61a2735..10ad0c3364 100644 --- a/openage/convert/value_object/read/media/smx.pyx +++ b/openage/convert/value_object/read/media/smx.pyx @@ -59,6 +59,325 @@ cdef public dict LAYER_TYPES = { 1: SMXLayerType.SHADOW, 2: SMXLayerType.OUTLINE, } +cdef class SMXMainLayer8to5Variant: + pass + +cdef class SMXMainLayer4plus1Variant: + pass + +cdef class SMXOutlineLayerVariant: + pass + +cdef class SMXShadowLayerVariant: + pass + + +ctypedef fused SMXLayerVariant: + SMXMainLayer8to5Variant + SMXMainLayer4plus1Variant + SMXOutlineLayerVariant + SMXShadowLayerVariant + + +cdef process_drawing_cmds(SMXLayerVariant variant, + const uint8_t[:] &data_raw, + vector[pixel] &row_data, + Py_ssize_t rowid, + Py_ssize_t first_cmd_offset, + Py_ssize_t first_color_offset, + int chunk_pos, + size_t expected_size): + """ + TODO: docstring + """ + + # position in the command array, we start at the first command of this row + cdef Py_ssize_t dpos_cmd = first_cmd_offset + + # is the end of the current row reached? + cdef bool eor = False + + cdef uint8_t cmd = 0 + cdef uint8_t lower_crumb = 0 + cdef int pixel_count = 0 + + if SMXLayerVariant is SMXMainLayer8to5Variant + # Position in the pixel data array + cdef Py_ssize_t dpos_color = first_color_offset + + # Position in the compression chunk. + cdef bool odd = chunk_pos + cdef int px_dpos = 0 # For loop iterator + + cdef vector[uint8_t] pixel_data + pixel_data.reserve(4) + + # Pixel data temporary values that need further decompression + cdef uint8_t pixel_data_odd_0 = 0 + cdef uint8_t pixel_data_odd_1 = 0 + cdef uint8_t pixel_data_odd_2 = 0 + cdef uint8_t pixel_data_odd_3 = 0 + + # Mask for even indices + # cdef uint8_t pixel_mask_even_0 = 0xFF + cdef uint8_t pixel_mask_even_1 = 0b00000011 + cdef uint8_t pixel_mask_even_2 = 0b11110000 + cdef uint8_t pixel_mask_even_3 = 0b00111111 + + if SMXLayerVariant is SMXMainLayer4plus1Variant: + # Position in the pixel data array + cdef Py_ssize_t dpos_color = first_color_offset + + # Position in the compression chunk + cdef uint8_t dpos_chunk = chunk_pos + + cdef uint8_t palette_section_block = 0 + cdef uint8_t palette_section = 0 + + if SMXLayerVariant in (SMXShadowLayerVariant, SMXOutlineLayerVariant): + cdef uint8_t nextbyte = 0 + + # work through commands till end of row. + while not eor: + if row_data.size() > expected_size: + raise Exception( + f"Only {expected_size:d} pixels should be drawn in row {rowid:d} " + f"with layer type {self.info.layer_type:#x}, but we have {row_data.size():d} " + f"already!" + ) + + # fetch drawing instruction + cmd = data_raw[dpos_cmd] + + # Last 2 bits store command type + lower_crumb = 0b00000011 & cmd + + if lower_crumb == 0b00000011: + # eor (end of row) command, this row is finished now. + eor = True + dpos_cmd += 1 + + if is SMXShadowLayerVariant: + # shadows sometimes need an extra pixel at + # the end + if row_data.size() < expected_size: + # copy the last drawn pixel + # (still stored in nextbyte) + # + # TODO: confirm that this is the + # right way to do it + row_data.push_back(pixel(color_shadow, + nextbyte, 0, 0, 0)) + continue + + elif lower_crumb == 0b00000000: + # skip command + # draw 'count' transparent pixels + # count = (cmd >> 2) + 1 + + pixel_count = (cmd >> 2) + 1 + + for _ in range(pixel_count): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) + + elif lower_crumb == 0b00000001: + # color_list command + # draw the following 'count' pixels + # pixels are stored in 5 byte chunks + # even pixel indices have their info stored + # in byte[0] - byte[3]. odd pixel indices have + # their info stored in byte[1] - byte[4]. + # count = (cmd >> 2) + 1 + + pixel_count = (cmd >> 2) + 1 + + if is SMXMainLayer8to5Variant: + for _ in range(pixel_count): + # Start fetching pixel data + if odd: + # Odd indices require manual extraction of each of the 4 values + + # Palette index. Essentially a rotation of (byte[1]byte[2]) + # by 6 to the left, then masking with 0x00FF. + pixel_data_odd_0 = data_raw[dpos_color + 1] + pixel_data_odd_1 = data_raw[dpos_color + 2] + pixel_data.push_back((pixel_data_odd_0 >> 2) | (pixel_data_odd_1 << 6)) + + # Palette section. Described in byte[2] in bits 4-5. + pixel_data.push_back((pixel_data_odd_1 >> 2) & 0x03) + + # Damage mask 1. Essentially a rotation of (byte[3]byte[4]) + # by 6 to the left, then masking with 0x00F0. + pixel_data_odd_2 = data_raw[dpos_color + 3] + pixel_data_odd_3 = data_raw[dpos_color + 4] + pixel_data.push_back(((pixel_data_odd_2 >> 2) | (pixel_data_odd_3 << 6)) & 0xF0) + + # Damage mask 2. Described in byte[4] in bits 0-5. + pixel_data.push_back((pixel_data_odd_3 >> 2) & 0x3F) + + row_data.push_back(pixel(color_standard, + pixel_data[0], + pixel_data[1], + pixel_data[2], + pixel_data[3])) + + # Go to next pixel + dpos_color += 5 + + else: + # Even indices can be read "as is". They just have to be masked. + for px_dpos in range(4): + pixel_data.push_back(data_raw[dpos_color + px_dpos]) + + row_data.push_back(pixel(color_standard, + pixel_data[0], + pixel_data[1] & pixel_mask_even_1, + pixel_data[2] & pixel_mask_even_2, + pixel_data[3] & pixel_mask_even_3)) + + odd = not odd + pixel_data.clear() + + if is SMXMainLayer4plus1Variant: + palette_section_block = data_raw[dpos_color + (4 - dpos_chunk)] + + for _ in range(pixel_count): + # Start fetching pixel data + palette_section = (palette_section_block >> (2 * dpos_chunk)) & 0x03 + row_data.push_back(pixel(color_standard, + data_raw[dpos_color], + palette_section, + 0, + 0)) + + dpos_color += 1 + dpos_chunk += 1 + + # Skip to next chunk + if dpos_chunk > 3: + dpos_chunk = 0 + dpos_color += 1 # Skip palette section block + palette_section_block = data_raw[dpos_color + 4] + + if SMXLayerVariant is SMXShadowLayerVariant: + for _ in range(pixel_count): + dpos_color += 1 + nextbyte = data_raw[dpos_color] + + row_data.push_back(pixel(color_shadow, + nextbyte, 0, 0, 0)) + + if SMXLayerVariant is SMXOutlineLayerVariant: + # we don't know the color the game wants + # so we just draw index 0 + row_data.push_back(pixel(color_outline, + 0, 0, 0, 0)) + + + elif lower_crumb == 0b00000010: + if SMXLayerVariant is SMXMainLayer8to5Variant: + # player_color command + # draw the following 'count' pixels + # pixels are stored in 5 byte chunks + # even pixel indices have their info stored + # in byte[0] - byte[3]. odd pixel indices have + # their info stored in byte[1] - byte[4]. + # count = (cmd >> 2) + 1 + + pixel_count = (cmd >> 2) + 1 + + for _ in range(pixel_count): + # Start fetching pixel data + if odd: + # Odd indices require manual extraction of each of the 4 values + + # Palette index. Essentially a rotation of (byte[1]byte[2]) + # by 6 to the left, then masking with 0x00FF. + pixel_data_odd_0 = data_raw[dpos_color + 1] + pixel_data_odd_1 = data_raw[dpos_color + 2] + pixel_data.push_back((pixel_data_odd_0 >> 2) | (pixel_data_odd_1 << 6)) + + # Palette section. Described in byte[2] in bits 4-5. + pixel_data.push_back((pixel_data_odd_1 >> 2) & 0x03) + + # Damage modifier 1. Essentially a rotation of (byte[3]byte[4]) + # by 6 to the left, then masking with 0x00F0. + pixel_data_odd_2 = data_raw[dpos_color + 3] + pixel_data_odd_3 = data_raw[dpos_color + 4] + pixel_data.push_back(((pixel_data_odd_2 >> 2) | (pixel_data_odd_3 << 6)) & 0xF0) + + # Damage modifier 2. Described in byte[4] in bits 0-5. + pixel_data.push_back((pixel_data_odd_3 >> 2) & 0x3F) + + row_data.push_back(pixel(color_player, + pixel_data[0], + pixel_data[1], + pixel_data[2], + pixel_data[3])) + + # Go to next pixel + dpos_color += 5 + + else: + # Even indices can be read "as is". They just have to be masked. + for px_dpos in range(4): + pixel_data.push_back(data_raw[dpos_color + px_dpos]) + + row_data.push_back(pixel(color_player, + pixel_data[0], + pixel_data[1] & pixel_mask_even_1, + pixel_data[2] & pixel_mask_even_2, + pixel_data[3] & pixel_mask_even_3)) + + odd = not odd + pixel_data.clear() + + + elif lower_crumb == 0b00000010: + if SMXLayerVariant is SMXMainLayer4plus1Variant: + # player_color command + # draw the following 'count' pixels + # 4 pixels are stored in every 5 byte chunk. + # palette indices are contained in byte[0] - byte[3] + # palette sections are stored in byte[4] + # count = (cmd >> 2) + 1 + + pixel_count = (cmd >> 2) + 1 + + for _ in range(pixel_count): + # Start fetching pixel data + palette_section = (palette_section_block >> (2 * dpos_chunk)) & 0x03 + row_data.push_back(pixel(color_player, + data_raw[dpos_color], + palette_section, + 0, + 0)) + + dpos_color += 1 + dpos_chunk += 1 + + # Skip to next chunk + if dpos_chunk > 3: + dpos_chunk = 0 + dpos_color += 1 # Skip palette section block + palette_section_block = data_raw[dpos_color + 4] + + if SMXLayerVariant is (SMXOutlineLayerVariant, SMXShadowLayerVariant): + pass + + else: + raise Exception( + f"unknown smx main graphics layer drawing command: " + + f"{cmd:#x} in row {rowid:d}" + ) + + # Process next command + dpos_cmd += 1 + + if SMXLayerVariant in (SMXMainLayer8to5Variant, SMXMainLayer4plus1Variant): + return dpos_cmd, dpos_color, odd, row_data + if SMXLayerVariant in (SMXOutlineLayerVariant, SMXShadowLayerVariant): + return dpos_cmd, dpos_cmd, chunk_pos, row_data class SMX: From b421336fe8a70460cf7ce45955beed6a2e8449e0 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Fri, 27 Dec 2024 15:23:22 +0000 Subject: [PATCH 696/771] compiling code --- .../convert/value_object/read/media/smx.pyx | 688 ++---------------- 1 file changed, 55 insertions(+), 633 deletions(-) diff --git a/openage/convert/value_object/read/media/smx.pyx b/openage/convert/value_object/read/media/smx.pyx index 10ad0c3364..b4cee859dc 100644 --- a/openage/convert/value_object/read/media/smx.pyx +++ b/openage/convert/value_object/read/media/smx.pyx @@ -59,28 +59,29 @@ cdef public dict LAYER_TYPES = { 1: SMXLayerType.SHADOW, 2: SMXLayerType.OUTLINE, } -cdef class SMXMainLayer8to5Variant: +cdef class SMXMainLayer8to5: pass -cdef class SMXMainLayer4plus1Variant: +cdef class SMXMainLayer4plus1: pass -cdef class SMXOutlineLayerVariant: +cdef class SMXOutlineLayer: pass -cdef class SMXShadowLayerVariant: +cdef class SMXShadowLayer: pass ctypedef fused SMXLayerVariant: - SMXMainLayer8to5Variant - SMXMainLayer4plus1Variant - SMXOutlineLayerVariant - SMXShadowLayerVariant + SMXLayer + SMXMainLayer8to5 + SMXMainLayer4plus1 + SMXOutlineLayer + SMXShadowLayer - -cdef process_drawing_cmds(SMXLayerVariant variant, - const uint8_t[:] &data_raw, +@cython.boundscheck(False) +cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant variant, + const uint8_t[::1] &data_raw, vector[pixel] &row_data, Py_ssize_t rowid, Py_ssize_t first_cmd_offset, @@ -100,49 +101,34 @@ cdef process_drawing_cmds(SMXLayerVariant variant, cdef uint8_t cmd = 0 cdef uint8_t lower_crumb = 0 cdef int pixel_count = 0 - - if SMXLayerVariant is SMXMainLayer8to5Variant + cdef Py_ssize_t dpos_color = 0 + cdef vector[uint8_t] pixel_data + # Pixel data temporary values that need further decompression + cdef uint8_t pixel_data_odd_0 = 0 + cdef uint8_t pixel_data_odd_1 = 0 + cdef uint8_t pixel_data_odd_2 = 0 + cdef uint8_t pixel_data_odd_3 = 0 + # Mask for even indices + # cdef uint8_t pixel_mask_even_0 = 0xFF + cdef uint8_t pixel_mask_even_1 = 0b00000011 + cdef uint8_t pixel_mask_even_2 = 0b11110000 + cdef uint8_t pixel_mask_even_3 = 0b00111111 + + if variant in (SMXMainLayer8to5, SMXMainLayer4plus1): # Position in the pixel data array - cdef Py_ssize_t dpos_color = first_color_offset + dpos_color = first_color_offset - # Position in the compression chunk. - cdef bool odd = chunk_pos - cdef int px_dpos = 0 # For loop iterator - - cdef vector[uint8_t] pixel_data - pixel_data.reserve(4) - - # Pixel data temporary values that need further decompression - cdef uint8_t pixel_data_odd_0 = 0 - cdef uint8_t pixel_data_odd_1 = 0 - cdef uint8_t pixel_data_odd_2 = 0 - cdef uint8_t pixel_data_odd_3 = 0 - - # Mask for even indices - # cdef uint8_t pixel_mask_even_0 = 0xFF - cdef uint8_t pixel_mask_even_1 = 0b00000011 - cdef uint8_t pixel_mask_even_2 = 0b11110000 - cdef uint8_t pixel_mask_even_3 = 0b00111111 - - if SMXLayerVariant is SMXMainLayer4plus1Variant: - # Position in the pixel data array - cdef Py_ssize_t dpos_color = first_color_offset - # Position in the compression chunk - cdef uint8_t dpos_chunk = chunk_pos - - cdef uint8_t palette_section_block = 0 - cdef uint8_t palette_section = 0 - - if SMXLayerVariant in (SMXShadowLayerVariant, SMXOutlineLayerVariant): - cdef uint8_t nextbyte = 0 + cdef uint8_t palette_section_block = 0 + cdef uint8_t palette_section = 0 + cdef uint8_t nextbyte = 0 # work through commands till end of row. while not eor: if row_data.size() > expected_size: raise Exception( f"Only {expected_size:d} pixels should be drawn in row {rowid:d} " - f"with layer type {self.info.layer_type:#x}, but we have {row_data.size():d} " + f"with layer type {variant:#x}, but we have {row_data.size():d} " f"already!" ) @@ -157,7 +143,7 @@ cdef process_drawing_cmds(SMXLayerVariant variant, eor = True dpos_cmd += 1 - if is SMXShadowLayerVariant: + if variant is SMXShadowLayer: # shadows sometimes need an extra pixel at # the end if row_data.size() < expected_size: @@ -191,10 +177,11 @@ cdef process_drawing_cmds(SMXLayerVariant variant, pixel_count = (cmd >> 2) + 1 - if is SMXMainLayer8to5Variant: + if variant is SMXMainLayer8to5: + pixel_data.reserve(4) for _ in range(pixel_count): # Start fetching pixel data - if odd: + if chunk_pos: # Odd indices require manual extraction of each of the 4 values # Palette index. Essentially a rotation of (byte[1]byte[2]) @@ -226,8 +213,8 @@ cdef process_drawing_cmds(SMXLayerVariant variant, else: # Even indices can be read "as is". They just have to be masked. - for px_dpos in range(4): - pixel_data.push_back(data_raw[dpos_color + px_dpos]) + for i in range(4): + pixel_data.push_back(data_raw[dpos_color + i]) row_data.push_back(pixel(color_standard, pixel_data[0], @@ -238,12 +225,12 @@ cdef process_drawing_cmds(SMXLayerVariant variant, odd = not odd pixel_data.clear() - if is SMXMainLayer4plus1Variant: - palette_section_block = data_raw[dpos_color + (4 - dpos_chunk)] + if variant is SMXMainLayer4plus1: + palette_section_block = data_raw[dpos_color + (4 - chunk_pos)] for _ in range(pixel_count): # Start fetching pixel data - palette_section = (palette_section_block >> (2 * dpos_chunk)) & 0x03 + palette_section = (palette_section_block >> (2 * chunk_pos)) & 0x03 row_data.push_back(pixel(color_standard, data_raw[dpos_color], palette_section, @@ -251,15 +238,15 @@ cdef process_drawing_cmds(SMXLayerVariant variant, 0)) dpos_color += 1 - dpos_chunk += 1 + chunk_pos += 1 # Skip to next chunk - if dpos_chunk > 3: - dpos_chunk = 0 + if chunk_pos > 3: + chunk_pos = 0 dpos_color += 1 # Skip palette section block palette_section_block = data_raw[dpos_color + 4] - if SMXLayerVariant is SMXShadowLayerVariant: + if variant is SMXShadowLayer: for _ in range(pixel_count): dpos_color += 1 nextbyte = data_raw[dpos_color] @@ -267,7 +254,7 @@ cdef process_drawing_cmds(SMXLayerVariant variant, row_data.push_back(pixel(color_shadow, nextbyte, 0, 0, 0)) - if SMXLayerVariant is SMXOutlineLayerVariant: + if variant is SMXOutlineLayer: # we don't know the color the game wants # so we just draw index 0 row_data.push_back(pixel(color_outline, @@ -275,7 +262,7 @@ cdef process_drawing_cmds(SMXLayerVariant variant, elif lower_crumb == 0b00000010: - if SMXLayerVariant is SMXMainLayer8to5Variant: + if variant is SMXMainLayer8to5: # player_color command # draw the following 'count' pixels # pixels are stored in 5 byte chunks @@ -334,7 +321,7 @@ cdef process_drawing_cmds(SMXLayerVariant variant, elif lower_crumb == 0b00000010: - if SMXLayerVariant is SMXMainLayer4plus1Variant: + if variant is SMXMainLayer4plus1: # player_color command # draw the following 'count' pixels # 4 pixels are stored in every 5 byte chunk. @@ -346,7 +333,7 @@ cdef process_drawing_cmds(SMXLayerVariant variant, for _ in range(pixel_count): # Start fetching pixel data - palette_section = (palette_section_block >> (2 * dpos_chunk)) & 0x03 + palette_section = (palette_section_block >> (2 * chunk_pos)) & 0x03 row_data.push_back(pixel(color_player, data_raw[dpos_color], palette_section, @@ -354,15 +341,15 @@ cdef process_drawing_cmds(SMXLayerVariant variant, 0)) dpos_color += 1 - dpos_chunk += 1 + chunk_pos += 1 # Skip to next chunk - if dpos_chunk > 3: - dpos_chunk = 0 + if chunk_pos > 3: + chunk_pos = 0 dpos_color += 1 # Skip palette section block palette_section_block = data_raw[dpos_color + 4] - if SMXLayerVariant is (SMXOutlineLayerVariant, SMXShadowLayerVariant): + if variant is (SMXOutlineLayer, SMXShadowLayer): pass else: @@ -374,9 +361,9 @@ cdef process_drawing_cmds(SMXLayerVariant variant, # Process next command dpos_cmd += 1 - if SMXLayerVariant in (SMXMainLayer8to5Variant, SMXMainLayer4plus1Variant): + if variant in (SMXMainLayer8to5, SMXMainLayer4plus1): return dpos_cmd, dpos_color, odd, row_data - if SMXLayerVariant in (SMXOutlineLayerVariant, SMXShadowLayerVariant): + if variant in (SMXOutlineLayer, SMXShadowLayer): return dpos_cmd, dpos_cmd, chunk_pos, row_data @@ -729,7 +716,7 @@ cdef class SMXLayer: # process the drawing commands for this row. next_cmd_offset, next_color_offset, chunk_pos, row_data = \ - self.process_drawing_cmds( + process_drawing_cmds(self, data_raw, row_data, rowid, @@ -757,26 +744,6 @@ cdef class SMXLayer: return next_cmd_offset, next_color_offset, chunk_pos, row_data - cdef (int, int, int, vector[pixel]) process_drawing_cmds(self, - const uint8_t[::1] &data_raw, - vector[pixel] &row_data, - Py_ssize_t rowid, - Py_ssize_t first_cmd_offset, - Py_ssize_t first_color_offset, - int chunk_pos, - size_t expected_size): - """ - Extracts pixel data from the layer data. Every layer type uses - its own implementation for better optimization. - - :param row_data: Pixel data is appended to this array. - :param rowid: Index of the current row in the layer. - :param first_cmd_offset: Offset of the first command of the current row. - :param first_color_offset: Offset of the first pixel data value of the current row. - :param chunk_pos: Current position in the compressed chunk. - :param expected_size: Expected length of row_data after encountering the EOR command. - """ - pass def get_picture_data(self, palette): """ @@ -811,551 +778,6 @@ cdef class SMXLayer: return repr(self.info) -cdef class SMXMainLayer8to5(SMXLayer): - """ - Compressed SMP layer (compression type 8to5) for the main graphics sprite. - """ - - def __init__(self, layer_header, data): - super().__init__(layer_header, data) - - @cython.boundscheck(False) - cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(self, - const uint8_t[::1] &data_raw, - vector[pixel] &row_data, - Py_ssize_t rowid, - Py_ssize_t first_cmd_offset, - Py_ssize_t first_color_offset, - int chunk_pos, - size_t expected_size): - """ - extract colors (pixels) for the drawing commands that were - compressed with 8to5 compression. - """ - # position in the command array, we start at the first command of this row - cdef Py_ssize_t dpos_cmd = first_cmd_offset - - # Position in the pixel data array - cdef Py_ssize_t dpos_color = first_color_offset - - # Position in the compression chunk. - cdef bool odd = chunk_pos - cdef int px_dpos = 0 # For loop iterator - - # is the end of the current row reached? - cdef bool eor = False - - cdef uint8_t cmd = 0 - cdef uint8_t lower_crumb = 0 - cdef int pixel_count = 0 - cdef vector[uint8_t] pixel_data - pixel_data.reserve(4) - - # Pixel data temporary values that need further decompression - cdef uint8_t pixel_data_odd_0 = 0 - cdef uint8_t pixel_data_odd_1 = 0 - cdef uint8_t pixel_data_odd_2 = 0 - cdef uint8_t pixel_data_odd_3 = 0 - - # Mask for even indices - # cdef uint8_t pixel_mask_even_0 = 0xFF - cdef uint8_t pixel_mask_even_1 = 0b00000011 - cdef uint8_t pixel_mask_even_2 = 0b11110000 - cdef uint8_t pixel_mask_even_3 = 0b00111111 - - # work through commands till end of row. - while not eor: - if row_data.size() > expected_size: - raise Exception( - f"Only {expected_size:d} pixels should be drawn in row {rowid:d} " - f"with layer type {self.info.layer_type:#x}, but we have {row_data.size():d} " - f"already!" - ) - - # fetch drawing instruction - cmd = data_raw[dpos_cmd] - - # Last 2 bits store command type - lower_crumb = 0b00000011 & cmd - - if lower_crumb == 0b00000011: - # eor (end of row) command, this row is finished now. - eor = True - dpos_cmd += 1 - - continue - - elif lower_crumb == 0b00000000: - # skip command - # draw 'count' transparent pixels - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - elif lower_crumb == 0b00000001: - # color_list command - # draw the following 'count' pixels - # pixels are stored in 5 byte chunks - # even pixel indices have their info stored - # in byte[0] - byte[3]. odd pixel indices have - # their info stored in byte[1] - byte[4]. - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - # Start fetching pixel data - if odd: - # Odd indices require manual extraction of each of the 4 values - - # Palette index. Essentially a rotation of (byte[1]byte[2]) - # by 6 to the left, then masking with 0x00FF. - pixel_data_odd_0 = data_raw[dpos_color + 1] - pixel_data_odd_1 = data_raw[dpos_color + 2] - pixel_data.push_back((pixel_data_odd_0 >> 2) | (pixel_data_odd_1 << 6)) - - # Palette section. Described in byte[2] in bits 4-5. - pixel_data.push_back((pixel_data_odd_1 >> 2) & 0x03) - - # Damage mask 1. Essentially a rotation of (byte[3]byte[4]) - # by 6 to the left, then masking with 0x00F0. - pixel_data_odd_2 = data_raw[dpos_color + 3] - pixel_data_odd_3 = data_raw[dpos_color + 4] - pixel_data.push_back(((pixel_data_odd_2 >> 2) | (pixel_data_odd_3 << 6)) & 0xF0) - - # Damage mask 2. Described in byte[4] in bits 0-5. - pixel_data.push_back((pixel_data_odd_3 >> 2) & 0x3F) - - row_data.push_back(pixel(color_standard, - pixel_data[0], - pixel_data[1], - pixel_data[2], - pixel_data[3])) - - # Go to next pixel - dpos_color += 5 - - else: - # Even indices can be read "as is". They just have to be masked. - for px_dpos in range(4): - pixel_data.push_back(data_raw[dpos_color + px_dpos]) - - row_data.push_back(pixel(color_standard, - pixel_data[0], - pixel_data[1] & pixel_mask_even_1, - pixel_data[2] & pixel_mask_even_2, - pixel_data[3] & pixel_mask_even_3)) - - odd = not odd - pixel_data.clear() - - elif lower_crumb == 0b00000010: - # player_color command - # draw the following 'count' pixels - # pixels are stored in 5 byte chunks - # even pixel indices have their info stored - # in byte[0] - byte[3]. odd pixel indices have - # their info stored in byte[1] - byte[4]. - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - # Start fetching pixel data - if odd: - # Odd indices require manual extraction of each of the 4 values - - # Palette index. Essentially a rotation of (byte[1]byte[2]) - # by 6 to the left, then masking with 0x00FF. - pixel_data_odd_0 = data_raw[dpos_color + 1] - pixel_data_odd_1 = data_raw[dpos_color + 2] - pixel_data.push_back((pixel_data_odd_0 >> 2) | (pixel_data_odd_1 << 6)) - - # Palette section. Described in byte[2] in bits 4-5. - pixel_data.push_back((pixel_data_odd_1 >> 2) & 0x03) - - # Damage modifier 1. Essentially a rotation of (byte[3]byte[4]) - # by 6 to the left, then masking with 0x00F0. - pixel_data_odd_2 = data_raw[dpos_color + 3] - pixel_data_odd_3 = data_raw[dpos_color + 4] - pixel_data.push_back(((pixel_data_odd_2 >> 2) | (pixel_data_odd_3 << 6)) & 0xF0) - - # Damage modifier 2. Described in byte[4] in bits 0-5. - pixel_data.push_back((pixel_data_odd_3 >> 2) & 0x3F) - - row_data.push_back(pixel(color_player, - pixel_data[0], - pixel_data[1], - pixel_data[2], - pixel_data[3])) - - # Go to next pixel - dpos_color += 5 - - else: - # Even indices can be read "as is". They just have to be masked. - for px_dpos in range(4): - pixel_data.push_back(data_raw[dpos_color + px_dpos]) - - row_data.push_back(pixel(color_player, - pixel_data[0], - pixel_data[1] & pixel_mask_even_1, - pixel_data[2] & pixel_mask_even_2, - pixel_data[3] & pixel_mask_even_3)) - - odd = not odd - pixel_data.clear() - - else: - raise Exception( - f"unknown smx main graphics layer drawing command: " + - f"{cmd:#x} in row {rowid:d}" - ) - - # Process next command - dpos_cmd += 1 - - return dpos_cmd, dpos_color, odd, row_data - - -cdef class SMXMainLayer4plus1(SMXLayer): - """ - Compressed SMP layer (compression type 4plus1) for the main graphics sprite. - """ - - def __init__(self, layer_header, data): - super().__init__(layer_header, data) - - @cython.boundscheck(False) - cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(self, - const uint8_t[::1] &data_raw, - vector[pixel] &row_data, - Py_ssize_t rowid, - Py_ssize_t first_cmd_offset, - Py_ssize_t first_color_offset, - int chunk_pos, - size_t expected_size): - """ - extract colors (pixels) for the drawing commands that were - compressed with 4plus1 compression. - """ - # position in the data blob, we start at the first command of this row - cdef Py_ssize_t dpos_cmd = first_cmd_offset - - # Position in the pixel data array - cdef Py_ssize_t dpos_color = first_color_offset - - # Position in the compression chunk - cdef uint8_t dpos_chunk = chunk_pos - - # is the end of the current row reached? - cdef bool eor = False - - cdef uint8_t cmd = 0 - cdef uint8_t lower_crumb = 0 - cdef int pixel_count = 0 - cdef uint8_t palette_section_block = 0 - cdef uint8_t palette_section = 0 - - # work through commands till end of row. - while not eor: - if row_data.size() > expected_size: - raise Exception( - f"Only {expected_size:d} pixels should be drawn in row {rowid:d} " + - f"with layer type {self.info.layer_type:#x}, but we have {row_data.size():d} " + - f"already!" - ) - - # fetch drawing instruction - cmd = data_raw[dpos_cmd] - - # Last 2 bits store command type - lower_crumb = 0b00000011 & cmd - - if lower_crumb == 0b00000011: - # eor (end of row) command, this row is finished now. - eor = True - dpos_cmd += 1 - - continue - - elif lower_crumb == 0b00000000: - # skip command - # draw 'count' transparent pixels - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - elif lower_crumb == 0b00000001: - # color_list command - # draw the following 'count' pixels - # 4 pixels are stored in every 5 byte chunk. - # palette indices are contained in byte[0] - byte[3] - # palette sections are stored in byte[4] - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - palette_section_block = data_raw[dpos_color + (4 - dpos_chunk)] - - for _ in range(pixel_count): - # Start fetching pixel data - palette_section = (palette_section_block >> (2 * dpos_chunk)) & 0x03 - row_data.push_back(pixel(color_standard, - data_raw[dpos_color], - palette_section, - 0, - 0)) - - dpos_color += 1 - dpos_chunk += 1 - - # Skip to next chunk - if dpos_chunk > 3: - dpos_chunk = 0 - dpos_color += 1 # Skip palette section block - palette_section_block = data_raw[dpos_color + 4] - - elif lower_crumb == 0b00000010: - # player_color command - # draw the following 'count' pixels - # 4 pixels are stored in every 5 byte chunk. - # palette indices are contained in byte[0] - byte[3] - # palette sections are stored in byte[4] - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - # Start fetching pixel data - palette_section = (palette_section_block >> (2 * dpos_chunk)) & 0x03 - row_data.push_back(pixel(color_player, - data_raw[dpos_color], - palette_section, - 0, - 0)) - - dpos_color += 1 - dpos_chunk += 1 - - # Skip to next chunk - if dpos_chunk > 3: - dpos_chunk = 0 - dpos_color += 1 # Skip palette section block - palette_section_block = data_raw[dpos_color + 4] - - else: - raise Exception( - f"unknown smx main graphics layer drawing command: " + - f"{cmd:#x} in row {rowid:d}" - ) - - # Process next command - dpos_cmd += 1 - - return dpos_cmd, dpos_color, dpos_chunk, row_data - - -cdef class SMXShadowLayer(SMXLayer): - """ - Compressed SMP layer for the shadow graphics. - """ - - def __init__(self, layer_header, data): - super().__init__(layer_header, data) - - @cython.boundscheck(False) - cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(self, - const uint8_t[::1] &data_raw, - vector[pixel] &row_data, - Py_ssize_t rowid, - Py_ssize_t first_cmd_offset, - Py_ssize_t first_color_offset, - int chunk_pos, - size_t expected_size): - """ - extract colors (pixels) for the drawing commands - found for this row in the SMX layer. - """ - # position in the data blob, we start at the first command of this row - cdef Py_ssize_t dpos = first_cmd_offset - - # is the end of the current row reached? - cdef bool eor = False - - cdef uint8_t cmd = 0 - cdef uint8_t nextbyte = 0 - cdef uint8_t lower_crumb = 0 - cdef int pixel_count = 0 - - # work through commands till end of row. - while not eor: - if row_data.size() > expected_size: - raise Exception( - f"Only {expected_size:d} pixels should be drawn ifn row {rowid:d} " + - f"with layer type {self.info.layer_type:#x}, but we have {row_data.size():d} " + - f"already!" - ) - - # fetch drawing instruction - cmd = data_raw[dpos] - - # Last 2 bits store command type - lower_crumb = 0b00000011 & cmd - - if lower_crumb == 0b00000011: - # eol (end of line) command, this row is finished now. - eor = True - dpos += 1 - - # shadows sometimes need an extra pixel at - # the end - if row_data.size() < expected_size: - # copy the last drawn pixel - # (still stored in nextbyte) - # - # TODO: confirm that this is the - # right way to do it - row_data.push_back(pixel(color_shadow, - nextbyte, 0, 0, 0)) - - continue - - elif lower_crumb == 0b00000000: - # skip command - # draw 'count' transparent pixels - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - elif lower_crumb == 0b00000001: - # color_list command - # draw the following 'count' pixels - # pixels are stored as 1 byte alpha values - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - dpos += 1 - nextbyte = data_raw[dpos] - - row_data.push_back(pixel(color_shadow, - nextbyte, 0, 0, 0)) - - else: - raise Exception( - f"unknown smp shadow layer drawing command: " + - f"{cmd:#x} in row {rowid}" - ) - - # process next command - dpos += 1 - - # end of row reached, return the created pixel array. - return dpos, dpos, chunk_pos, row_data - - -cdef class SMXOutlineLayer(SMXLayer): - """ - Compressed SMP layer for the outline graphics. - """ - - def __init__(self, layer_header, data): - super().__init__(layer_header, data) - - @cython.boundscheck(False) - cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(self, - const uint8_t[::1] &data_raw, - vector[pixel] &row_data, - Py_ssize_t rowid, - Py_ssize_t first_cmd_offset, - Py_ssize_t first_color_offset, - int chunk_pos, - size_t expected_size): - """ - extract colors (pixels) for the drawing commands - found for this row in the SMX layer. - """ - # position in the data blob, we start at the first command of this row - cdef Py_ssize_t dpos = first_cmd_offset - - # is the end of the current row reached? - cdef bool eor = False - - cdef uint8_t cmd = 0 - cdef uint8_t nextbyte = 0 - cdef uint8_t lower_crumb = 0 - cdef int pixel_count = 0 - - # work through commands till end of row. - while not eor: - if row_data.size() > expected_size: - raise Exception( - f"Only {expected_size:d} pixels should be drawn in row {rowid:d} " + - f"with layer type {self.info.layer_type:#x}, but we have {row_data.size():d} " + - f"already!" - ) - - # fetch drawing instruction - cmd = data_raw[dpos] - - # Last 2 bits store command type - lower_crumb = 0b00000011 & cmd - - # opcode: cmd, rowid: rowid - - if lower_crumb == 0b00000011: - # eol (end of line) command, this row is finished now. - eor = True - dpos += 1 - - continue - - elif lower_crumb == 0b00000000: - # skip command - # draw 'count' transparent pixels - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - elif lower_crumb == 0b00000001: - # color_list command - # draw the following 'count' pixels - # as player outline colors. - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - # we don't know the color the game wants - # so we just draw index 0 - row_data.push_back(pixel(color_outline, - 0, 0, 0, 0)) - - else: - raise Exception( - f"unknown smp outline layer drawing command: " + - f"{cmd:#x} in row {rowid}" - ) - - # process next command - dpos += 1 - - # end of row reached, return the created pixel array. - return dpos, dpos, chunk_pos, row_data @cython.boundscheck(False) From 7177fd2e9a72b0364c401fdc33fd4a87e6784e6e Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Fri, 27 Dec 2024 19:30:30 +0000 Subject: [PATCH 697/771] format --- .../convert/value_object/read/media/smx.pyx | 181 +++++++++--------- 1 file changed, 95 insertions(+), 86 deletions(-) diff --git a/openage/convert/value_object/read/media/smx.pyx b/openage/convert/value_object/read/media/smx.pyx index b4cee859dc..bcf9ec0e33 100644 --- a/openage/convert/value_object/read/media/smx.pyx +++ b/openage/convert/value_object/read/media/smx.pyx @@ -2,6 +2,9 @@ # # cython: infer_types=True +from libcpp.vector cimport vector +from libcpp cimport bool +from libc.stdint cimport uint8_t, uint16_t from enum import Enum import numpy from struct import Struct, unpack_from @@ -12,11 +15,6 @@ from .....log import spam, dbg cimport cython cimport numpy -from libc.stdint cimport uint8_t, uint16_t -from libcpp cimport bool -from libcpp.vector cimport vector - - # SMX files have little endian byte order endianness = "< " @@ -49,8 +47,8 @@ class SMXLayerType(Enum): """ SMX layer types. """ - MAIN = "main" - SHADOW = "shadow" + MAIN = "main" + SHADOW = "shadow" OUTLINE = "outline" @@ -79,15 +77,16 @@ ctypedef fused SMXLayerVariant: SMXOutlineLayer SMXShadowLayer + @cython.boundscheck(False) -cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant variant, - const uint8_t[::1] &data_raw, - vector[pixel] &row_data, - Py_ssize_t rowid, - Py_ssize_t first_cmd_offset, - Py_ssize_t first_color_offset, - int chunk_pos, - size_t expected_size): +cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant variant, + const uint8_t[::1] & data_raw, + vector[pixel] & row_data, + Py_ssize_t rowid, + Py_ssize_t first_cmd_offset, + Py_ssize_t first_color_offset, + int chunk_pos, + size_t expected_size): """ TODO: docstring """ @@ -118,11 +117,10 @@ cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant # Position in the pixel data array dpos_color = first_color_offset - cdef uint8_t palette_section_block = 0 cdef uint8_t palette_section = 0 cdef uint8_t nextbyte = 0 - + # work through commands till end of row. while not eor: if row_data.size() > expected_size: @@ -176,7 +174,7 @@ cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant # count = (cmd >> 2) + 1 pixel_count = (cmd >> 2) + 1 - + if variant is SMXMainLayer8to5: pixel_data.reserve(4) for _ in range(pixel_count): @@ -188,7 +186,8 @@ cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant # by 6 to the left, then masking with 0x00FF. pixel_data_odd_0 = data_raw[dpos_color + 1] pixel_data_odd_1 = data_raw[dpos_color + 2] - pixel_data.push_back((pixel_data_odd_0 >> 2) | (pixel_data_odd_1 << 6)) + pixel_data.push_back( + (pixel_data_odd_0 >> 2) | (pixel_data_odd_1 << 6)) # Palette section. Described in byte[2] in bits 4-5. pixel_data.push_back((pixel_data_odd_1 >> 2) & 0x03) @@ -197,16 +196,17 @@ cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant # by 6 to the left, then masking with 0x00F0. pixel_data_odd_2 = data_raw[dpos_color + 3] pixel_data_odd_3 = data_raw[dpos_color + 4] - pixel_data.push_back(((pixel_data_odd_2 >> 2) | (pixel_data_odd_3 << 6)) & 0xF0) + pixel_data.push_back( + ((pixel_data_odd_2 >> 2) | (pixel_data_odd_3 << 6)) & 0xF0) # Damage mask 2. Described in byte[4] in bits 0-5. pixel_data.push_back((pixel_data_odd_3 >> 2) & 0x3F) row_data.push_back(pixel(color_standard, - pixel_data[0], - pixel_data[1], - pixel_data[2], - pixel_data[3])) + pixel_data[0], + pixel_data[1], + pixel_data[2], + pixel_data[3])) # Go to next pixel dpos_color += 5 @@ -217,10 +217,10 @@ cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant pixel_data.push_back(data_raw[dpos_color + i]) row_data.push_back(pixel(color_standard, - pixel_data[0], - pixel_data[1] & pixel_mask_even_1, - pixel_data[2] & pixel_mask_even_2, - pixel_data[3] & pixel_mask_even_3)) + pixel_data[0], + pixel_data[1] & pixel_mask_even_1, + pixel_data[2] & pixel_mask_even_2, + pixel_data[3] & pixel_mask_even_3)) odd = not odd pixel_data.clear() @@ -230,7 +230,8 @@ cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant for _ in range(pixel_count): # Start fetching pixel data - palette_section = (palette_section_block >> (2 * chunk_pos)) & 0x03 + palette_section = ( + palette_section_block >> (2 * chunk_pos)) & 0x03 row_data.push_back(pixel(color_standard, data_raw[dpos_color], palette_section, @@ -243,7 +244,7 @@ cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant # Skip to next chunk if chunk_pos > 3: chunk_pos = 0 - dpos_color += 1 # Skip palette section block + dpos_color += 1 # Skip palette section block palette_section_block = data_raw[dpos_color + 4] if variant is SMXShadowLayer: @@ -258,9 +259,8 @@ cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant # we don't know the color the game wants # so we just draw index 0 row_data.push_back(pixel(color_outline, - 0, 0, 0, 0)) + 0, 0, 0, 0)) - elif lower_crumb == 0b00000010: if variant is SMXMainLayer8to5: # player_color command @@ -282,7 +282,8 @@ cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant # by 6 to the left, then masking with 0x00FF. pixel_data_odd_0 = data_raw[dpos_color + 1] pixel_data_odd_1 = data_raw[dpos_color + 2] - pixel_data.push_back((pixel_data_odd_0 >> 2) | (pixel_data_odd_1 << 6)) + pixel_data.push_back( + (pixel_data_odd_0 >> 2) | (pixel_data_odd_1 << 6)) # Palette section. Described in byte[2] in bits 4-5. pixel_data.push_back((pixel_data_odd_1 >> 2) & 0x03) @@ -291,16 +292,17 @@ cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant # by 6 to the left, then masking with 0x00F0. pixel_data_odd_2 = data_raw[dpos_color + 3] pixel_data_odd_3 = data_raw[dpos_color + 4] - pixel_data.push_back(((pixel_data_odd_2 >> 2) | (pixel_data_odd_3 << 6)) & 0xF0) + pixel_data.push_back( + ((pixel_data_odd_2 >> 2) | (pixel_data_odd_3 << 6)) & 0xF0) # Damage modifier 2. Described in byte[4] in bits 0-5. pixel_data.push_back((pixel_data_odd_3 >> 2) & 0x3F) row_data.push_back(pixel(color_player, - pixel_data[0], - pixel_data[1], - pixel_data[2], - pixel_data[3])) + pixel_data[0], + pixel_data[1], + pixel_data[2], + pixel_data[3])) # Go to next pixel dpos_color += 5 @@ -308,18 +310,18 @@ cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant else: # Even indices can be read "as is". They just have to be masked. for px_dpos in range(4): - pixel_data.push_back(data_raw[dpos_color + px_dpos]) + pixel_data.push_back( + data_raw[dpos_color + px_dpos]) row_data.push_back(pixel(color_player, - pixel_data[0], - pixel_data[1] & pixel_mask_even_1, - pixel_data[2] & pixel_mask_even_2, - pixel_data[3] & pixel_mask_even_3)) + pixel_data[0], + pixel_data[1] & pixel_mask_even_1, + pixel_data[2] & pixel_mask_even_2, + pixel_data[3] & pixel_mask_even_3)) odd = not odd pixel_data.clear() - elif lower_crumb == 0b00000010: if variant is SMXMainLayer4plus1: # player_color command @@ -333,12 +335,13 @@ cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant for _ in range(pixel_count): # Start fetching pixel data - palette_section = (palette_section_block >> (2 * chunk_pos)) & 0x03 + palette_section = ( + palette_section_block >> (2 * chunk_pos)) & 0x03 row_data.push_back(pixel(color_player, - data_raw[dpos_color], - palette_section, - 0, - 0)) + data_raw[dpos_color], + palette_section, + 0, + 0)) dpos_color += 1 chunk_pos += 1 @@ -346,7 +349,7 @@ cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant # Skip to next chunk if chunk_pos > 3: chunk_pos = 0 - dpos_color += 1 # Skip palette section block + dpos_color += 1 # Skip palette section block palette_section_block = data_raw[dpos_color + 4] if variant is (SMXOutlineLayer, SMXShadowLayer): @@ -356,7 +359,7 @@ cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant raise Exception( f"unknown smx main graphics layer drawing command: " + f"{cmd:#x} in row {rowid:d}" - ) + ) # Process next command dpos_cmd += 1 @@ -409,14 +412,16 @@ class SMX: """ smx_header = SMX.smx_header.unpack_from(data) - self.smp_type, version, frame_count, file_size_comp,\ + self.smp_type, version, frame_count, file_size_comp, \ file_size_uncomp, comment = smx_header dbg("SMX") dbg(" version: %s", version) dbg(" frame count: %s", frame_count) - dbg(" file size compressed: %s B", file_size_comp + 0x20) # 0x20 = SMX header size - dbg(" file size uncompressed: %s B", file_size_uncomp + 0x40) # 0x80 = SMP header size + # 0x20 = SMX header size + dbg(" file size compressed: %s B", file_size_comp + 0x20) + # 0x80 = SMP header size + dbg(" file size uncompressed: %s B", file_size_uncomp + 0x40) dbg(" comment: %s", comment.decode('ascii')) # SMX graphic frames are created from overlaying @@ -434,7 +439,7 @@ class SMX: frame_header = SMX.smx_frame_header.unpack_from( data, current_offset) - frame_type , palette_number, _ = frame_header + frame_type, palette_number, _ = frame_header current_offset += SMX.smx_frame_header.size @@ -453,7 +458,7 @@ class SMX: layer_header_data = SMX.smx_layer_header.unpack_from( data, current_offset) - width, height, hotspot_x, hotspot_y,\ + width, height, hotspot_x, hotspot_y, \ distance_next_frame, _ = layer_header_data current_offset += SMX.smx_layer_header.size @@ -463,12 +468,14 @@ class SMX: # Skip outline table current_offset += 4 * height - qdl_command_array_size = Struct("< I").unpack_from(data, current_offset)[0] + qdl_command_array_size = Struct( + "< I").unpack_from(data, current_offset)[0] current_offset += 4 # Read length of color table if layer_type is SMXLayerType.MAIN: - qdl_color_table_size = Struct("< I").unpack_from(data, current_offset)[0] + qdl_color_table_size = Struct( + "< I").unpack_from(data, current_offset)[0] current_offset += 4 qdl_color_table_offset = current_offset + qdl_command_array_size @@ -488,16 +495,20 @@ class SMX: if layer_type is SMXLayerType.MAIN: if layer_header.compression_type == 0x08: - self.main_frames.append(SMXMainLayer8to5(layer_header, data)) + self.main_frames.append( + SMXMainLayer8to5(layer_header, data)) elif layer_header.compression_type == 0x00: - self.main_frames.append(SMXMainLayer4plus1(layer_header, data)) + self.main_frames.append( + SMXMainLayer4plus1(layer_header, data)) elif layer_type is SMXLayerType.SHADOW: - self.shadow_frames.append(SMXShadowLayer(layer_header, data)) + self.shadow_frames.append( + SMXShadowLayer(layer_header, data)) elif layer_type is SMXLayerType.OUTLINE: - self.outline_frames.append(SMXOutlineLayer(layer_header, data)) + self.outline_frames.append( + SMXOutlineLayer(layer_header, data)) def get_frames(self, layer: int = 0): """ @@ -673,16 +684,17 @@ cdef class SMXLayer: # process cmd table for i in range(row_count): cmd_offset, color_offset, chunk_pos, row_data = \ - self.create_color_row(data_raw, i, cmd_offset, color_offset, chunk_pos) + self.create_color_row( + data_raw, i, cmd_offset, color_offset, chunk_pos) self.pcolor.push_back(row_data) - cdef inline (int, int, int, vector[pixel]) create_color_row(self, - const uint8_t[::1] &data_raw, - Py_ssize_t rowid, - int cmd_offset, - int color_offset, - int chunk_pos): + cdef inline(int, int, int, vector[pixel]) create_color_row(self, + const uint8_t[::1] & data_raw, + Py_ssize_t rowid, + int cmd_offset, + int color_offset, + int chunk_pos): """ Extract colors (pixels) for the given rowid. @@ -717,14 +729,14 @@ cdef class SMXLayer: # process the drawing commands for this row. next_cmd_offset, next_color_offset, chunk_pos, row_data = \ process_drawing_cmds(self, - data_raw, - row_data, - rowid, - first_cmd_offset, - first_color_offset, - chunk_pos, - pixel_count - bounds.right - ) + data_raw, + row_data, + rowid, + first_cmd_offset, + first_color_offset, + chunk_pos, + pixel_count - bounds.right + ) # finish by filling up the right transparent space for i in range(bounds.right): @@ -736,7 +748,7 @@ cdef class SMXLayer: summary = "%d/%d -> row %d, layer type %d, offset %d / %#x" % ( got, pixel_count, rowid, self.info.layer_type, first_cmd_offset, first_cmd_offset - ) + ) txt = "got %%s pixels than expected: %s, missing: %d" % ( summary, abs(pixel_count - got)) @@ -744,7 +756,6 @@ cdef class SMXLayer: return next_cmd_offset, next_color_offset, chunk_pos, row_data - def get_picture_data(self, palette): """ Convert the palette index matrix to a RGBA image. @@ -778,12 +789,10 @@ cdef class SMXLayer: return repr(self.info) - - @cython.boundscheck(False) @cython.wraparound(False) -cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix, - numpy.ndarray[numpy.uint8_t, ndim=2, mode="c"] palette): +cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] & image_matrix, + numpy.ndarray[numpy.uint8_t, ndim = 2, mode = "c"] palette): """ converts a palette index image matrix to an rgba matrix. @@ -794,7 +803,7 @@ cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix, cdef size_t height = image_matrix.size() cdef size_t width = image_matrix[0].size() - cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode="c"] array_data = \ + cdef numpy.ndarray[numpy.uint8_t, ndim = 3, mode = "c"] array_data = \ numpy.zeros((height, width, 4), dtype=numpy.uint8) cdef uint8_t[:, ::1] m_lookup = palette @@ -883,7 +892,7 @@ cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix, @cython.boundscheck(False) @cython.wraparound(False) -cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] &image_matrix): +cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] & image_matrix): """ converts the damage modifier values to an image using the RG values. @@ -893,7 +902,7 @@ cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] &image_matrix): cdef size_t height = image_matrix.size() cdef size_t width = image_matrix[0].size() - cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode="c"] array_data = \ + cdef numpy.ndarray[numpy.uint8_t, ndim = 3, mode = "c"] array_data = \ numpy.zeros((height, width, 4), dtype=numpy.uint8) cdef uint8_t r = 0 From 74cb7d0cd7802c0b3fabe6c33ff7a221253d701b Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Fri, 27 Dec 2024 20:42:22 +0000 Subject: [PATCH 698/771] class defs --- .../convert/value_object/read/media/smx.pyx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/openage/convert/value_object/read/media/smx.pyx b/openage/convert/value_object/read/media/smx.pyx index bcf9ec0e33..2cb4749091 100644 --- a/openage/convert/value_object/read/media/smx.pyx +++ b/openage/convert/value_object/read/media/smx.pyx @@ -57,17 +57,21 @@ cdef public dict LAYER_TYPES = { 1: SMXLayerType.SHADOW, 2: SMXLayerType.OUTLINE, } -cdef class SMXMainLayer8to5: - pass +cdef class SMXMainLayer8to5(SMXLayer): + def __init__(self, layer_header, data): + super().__init__(layer_header, data) -cdef class SMXMainLayer4plus1: - pass +cdef class SMXMainLayer4plus1(SMXLayer): + def __init__(self, layer_header, data): + super().__init__(layer_header, data) -cdef class SMXOutlineLayer: - pass +cdef class SMXOutlineLayer(SMXLayer): + def __init__(self, layer_header, data): + super().__init__(layer_header, data) -cdef class SMXShadowLayer: - pass +cdef class SMXShadowLayer(SMXLayer): + def __init__(self, layer_header, data): + super().__init__(layer_header, data) ctypedef fused SMXLayerVariant: From b5427b57667a46223372946e5dc5f1a17f2ff0af Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Fri, 27 Dec 2024 22:42:20 +0000 Subject: [PATCH 699/771] smp fused --- .../convert/value_object/read/media/smp.pyx | 499 ++++++------------ 1 file changed, 169 insertions(+), 330 deletions(-) diff --git a/openage/convert/value_object/read/media/smp.pyx b/openage/convert/value_object/read/media/smp.pyx index 49a2122175..9454bd9c80 100644 --- a/openage/convert/value_object/read/media/smp.pyx +++ b/openage/convert/value_object/read/media/smp.pyx @@ -253,6 +253,170 @@ class SMPLayerHeader: ) return "".join(ret) + +cdef class SMPMainLayer(SMPLayer): + def __init__(self, layer_header, data): + super().__init__(layer_header, data) + + def get_damage_mask(self): + """ + Convert the 4th pixel byte to a mask used for damaged units. + """ + return determine_damage_matrix(self.pcolor) + +cdef class SMPShadowLayer(SMPLayer): + def __init__(self, layer_header, data): + super().__init__(layer_header, data) + +cdef class SMPOutlineLayer(SMPLayer): + def __init__(self, layer_header, data): + super().__init__(layer_header, data) + + +ctypedef fused SMPLayerVariant: + SMPLayer + SMPMainLayer + SMPShadowLayer + SMPOutlineLayer + + +@cython.boundscheck(False) +cdef void process_drawing_cmds(SMPLayerVariant variant, + const uint8_t[::1] &data_raw, + vector[pixel] &row_data, + Py_ssize_t rowid, + Py_ssize_t first_cmd_offset, + size_t expected_size): + + """ + extract colors (pixels) for the drawing commands + found for this row in the SMP frame. + """ + + # position in the data blob, we start at the first command of this row + cdef Py_ssize_t dpos = first_cmd_offset + + # is the end of the current row reached? + cdef bool eor = False + + cdef uint8_t cmd = 0 + cdef uint8_t nextbyte = 0 + cdef uint8_t lower_crumb = 0 + cdef int pixel_count = 0 + + cdef vector[uint8_t] pixel_data + pixel_data.reserve(4) + + # work through commands till end of row. + while not eor: + if row_data.size() > expected_size: + raise Exception( + f"Only {expected_size:d} pixels should be drawn in row {rowid:d} " + + f"with layer type {variant.info.layer_type:#x}, but we have {row_data.size():d}" + ) + + # fetch drawing instruction + cmd = data_raw[dpos] + + # Last 2 bits store command type + lower_crumb = 0b00000011 & cmd + + # opcode: cmd, rowid: rowid + + if lower_crumb == 0b00000011: + # eol (end of line) command, this row is finished now. + eor = True + + if variant is SMPShadowLayer and row_data.size() < expected_size: + # copy the last drawn pixel + # (still stored in nextbyte) + # + # TODO: confirm that this is the + # right way to do it + row_data.push_back(pixel(color_shadow, nextbyte, 0, 0, 0)) + + continue + + elif lower_crumb == 0b00000000: + # skip command + # draw 'count' transparent pixels + # count = (cmd >> 2) + 1 + + pixel_count = (cmd >> 2) + 1 + + for _ in range(pixel_count): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) + + elif lower_crumb == 0b00000001: + # color_list command + # draw the following 'count' pixels + # pixels are stored as 4 byte palette and meta infos + # count = (cmd >> 2) + 1 + + pixel_count = (cmd >> 2) + 1 + + for _ in range(pixel_count): + if variant is SMPShadowLayer: + dpos += 1 + nextbyte = data_raw[dpos] + row_data.push_back(pixel(color_shadow, + nextbyte, 0, 0, 0)) + elif variant is SMPOutlineLayer: + # we don't know the color the game wants + # so we just draw index 0 + row_data.push_back(pixel(color_outline, + 0, 0, 0, 0)) + elif variant is SMPMainLayer: + for _ in range(4): + dpos += 1 + pixel_data.push_back(data_raw[dpos]) + + row_data.push_back(pixel(color_standard, + pixel_data[0], + pixel_data[1], + pixel_data[2], + pixel_data[3] & 0x1F)) # remove "usage" bit here + pixel_data.clear() + + elif lower_crumb == 0b00000010 and variant in (SMPMainLayer, SMPShadowLayer): + # player_color command + # draw the following 'count' pixels + # pixels are stored as 4 byte palette and meta infos + # count = (cmd >> 2) + 1 + + pixel_count = (cmd >> 2) + 1 + + for _ in range(pixel_count): + if variant is SMPShadowLayer: + dpos += 1 + nextbyte = data_raw[dpos] + row_data.push_back(pixel(color_shadow, + nextbyte, 0, 0, 0)) + else: + for _ in range(4): + dpos += 1 + pixel_data.push_back(data_raw[dpos]) + + row_data.push_back(pixel(color_player, + pixel_data[0], + pixel_data[1], + pixel_data[2], + pixel_data[3] & 0x1F)) # remove "usage" bit here + pixel_data.clear() + + else: + raise Exception( + f"unknown smp {variant.info.layer_type} layer drawing command: " + + f"{cmd:#x} in row {rowid:d}" + ) + + # process next command + dpos += 1 + + # end of row reached, return the created pixel array. + return + + cdef class SMPLayer: """ one layer inside the SMP. you can imagine it as a frame of a video. @@ -357,10 +521,11 @@ cdef class SMPLayer: row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) # process the drawing commands for this row. - self.process_drawing_cmds(data_raw, - row_data, rowid, - first_cmd_offset, - pixel_count - bounds.right) + process_drawing_cmds(self, + data_raw, + row_data, rowid, + first_cmd_offset, + pixel_count - bounds.right) # finish by filling up the right transparent space for i in range(bounds.right): @@ -383,15 +548,6 @@ cdef class SMPLayer: return row_data - @cython.boundscheck(False) - cdef void process_drawing_cmds(self, - const uint8_t[::1] &data_raw, - vector[pixel] &row_data, - Py_ssize_t rowid, - Py_ssize_t first_cmd_offset, - size_t expected_size): - pass - def get_picture_data(self, palette): """ Convert the palette index matrix to a colored image. @@ -417,323 +573,6 @@ cdef class SMPLayer: return repr(self.info) -cdef class SMPMainLayer(SMPLayer): - """ - SMPLayer for the main graphics sprite. - """ - - def __init__(self, layer_header, data): - super().__init__(layer_header, data) - - @cython.boundscheck(False) - cdef void process_drawing_cmds(self, - const uint8_t[::1] &data_raw, - vector[pixel] &row_data, - Py_ssize_t rowid, - Py_ssize_t first_cmd_offset, - size_t expected_size): - """ - extract colors (pixels) for the drawing commands - found for this row in the SMP frame. - """ - - # position in the data blob, we start at the first command of this row - cdef Py_ssize_t dpos = first_cmd_offset - - # is the end of the current row reached? - cdef bool eor = False - - cdef uint8_t cmd - cdef uint8_t nextbyte - cdef uint8_t lower_crumb - cdef int pixel_count - - cdef vector[uint8_t] pixel_data - pixel_data.reserve(4) - - # work through commands till end of row. - while not eor: - if row_data.size() > expected_size: - raise Exception( - f"Only {expected_size:d} pixels should be drawn in row {rowid:d} " + - f"with layer type {self.info.layer_type:#x}, but we have {row_data.size():d}" - ) - - # fetch drawing instruction - cmd = data_raw[dpos] - - # Last 2 bits store command type - lower_crumb = 0b00000011 & cmd - - # opcode: cmd, rowid: rowid - - if lower_crumb == 0b00000011: - # eol (end of line) command, this row is finished now. - eor = True - - continue - - elif lower_crumb == 0b00000000: - # skip command - # draw 'count' transparent pixels - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - elif lower_crumb == 0b00000001: - # color_list command - # draw the following 'count' pixels - # pixels are stored as 4 byte palette and meta infos - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - for _ in range(4): - dpos += 1 - pixel_data.push_back(data_raw[dpos]) - - row_data.push_back(pixel(color_standard, - pixel_data[0], - pixel_data[1], - pixel_data[2], - pixel_data[3] & 0x1F)) # remove "usage" bit here - - pixel_data.clear() - - elif lower_crumb == 0b00000010: - # player_color command - # draw the following 'count' pixels - # pixels are stored as 4 byte palette and meta infos - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - for _ in range(4): - dpos += 1 - pixel_data.push_back(data_raw[dpos]) - - row_data.push_back(pixel(color_player, - pixel_data[0], - pixel_data[1], - pixel_data[2], - pixel_data[3] & 0x1F)) # remove "usage" bit here - - pixel_data.clear() - - else: - raise Exception( - f"unknown smp main graphics layer drawing command: " + - f"{cmd:#x} in row {rowid:d}" - ) - - # process next command - dpos += 1 - - # end of row reached, return the created pixel array. - return - - def get_damage_mask(self): - """ - Convert the 4th pixel byte to a mask used for damaged units. - """ - return determine_damage_matrix(self.pcolor) - - -cdef class SMPShadowLayer(SMPLayer): - """ - SMPLayer for the shadow graphics. - """ - - def __init__(self, layer_header, data): - super().__init__(layer_header, data) - - @cython.boundscheck(False) - cdef void process_drawing_cmds(self, - const uint8_t[::1] &data_raw, - vector[pixel] &row_data, - Py_ssize_t rowid, - Py_ssize_t first_cmd_offset, - size_t expected_size): - """ - extract colors (pixels) for the drawing commands - found for this row in the SMP frame. - """ - - # position in the data blob, we start at the first command of this row - cdef Py_ssize_t dpos = first_cmd_offset - - # is the end of the current row reached? - cdef bool eor = False - - cdef uint8_t cmd - cdef uint8_t nextbyte - cdef uint8_t lower_crumb - cdef int pixel_count - - # work through commands till end of row. - while not eor: - if row_data.size() > expected_size: - raise Exception( - f"Only {expected_size:d} pixels should be drawn in row {rowid:d} " + - f"with layer type {self.info.layer_type:#x}, but we have {row_data.size():d} " + - f"already!" - ) - - # fetch drawing instruction - cmd = data_raw[dpos] - - # Last 2 bits store command type - lower_crumb = 0b00000011 & cmd - - # opcode: cmd, rowid: rowid - - if lower_crumb == 0b00000011: - # eol (end of line) command, this row is finished now. - eor = True - - # shadows sometimes need an extra pixel at - # the end - if row_data.size() < expected_size: - # copy the last drawn pixel - # (still stored in nextbyte) - # - # TODO: confirm that this is the - # right way to do it - row_data.push_back(pixel(color_shadow, - nextbyte, 0, 0, 0)) - - continue - - elif lower_crumb == 0b00000000: - # skip command - # draw 'count' transparent pixels - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - elif lower_crumb == 0b00000001: - # color_list command - # draw the following 'count' pixels - # pixels are stored as 1 byte alpha values - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - - dpos += 1 - nextbyte = data_raw[dpos] - - row_data.push_back(pixel(color_shadow, - nextbyte, 0, 0, 0)) - - else: - raise Exception( - f"unknown smp shadow layer drawing command: " + - f"{cmd:#x} in row {rowid:d}") - - # process next command - dpos += 1 - - # end of row reached, return the created pixel array. - return - - -cdef class SMPOutlineLayer(SMPLayer): - """ - SMPLayer for the outline graphics. - """ - - def __init__(self, layer_header, data): - super().__init__(layer_header, data) - - @cython.boundscheck(False) - cdef void process_drawing_cmds(self, - const uint8_t[::1] &data_raw, - vector[pixel] &row_data, - Py_ssize_t rowid, - Py_ssize_t first_cmd_offset, - size_t expected_size): - """ - extract colors (pixels) for the drawing commands - found for this row in the SMP frame. - """ - - # position in the data blob, we start at the first command of this row - cdef Py_ssize_t dpos = first_cmd_offset - - # is the end of the current row reached? - cdef bool eor = False - - cdef uint8_t cmd - cdef uint8_t nextbyte - cdef uint8_t lower_crumb - cdef int pixel_count - - # work through commands till end of row. - while not eor: - if row_data.size() > expected_size: - raise Exception( - f"Only {expected_size:d} pixels should be drawn in row {rowid:d} " + - f"with layer type {self.info.layer_type:#x}, but we have {row_data.size():d} " - f"already!" - ) - - # fetch drawing instruction - cmd = data_raw[dpos] - - # Last 2 bits store command type - lower_crumb = 0b00000011 & cmd - - # opcode: cmd, rowid: rowid - - if lower_crumb == 0b00000011: - # eol (end of line) command, this row is finished now. - eor = True - - continue - - elif lower_crumb == 0b00000000: - # skip command - # draw 'count' transparent pixels - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - elif lower_crumb == 0b00000001: - # color_list command - # draw the following 'count' pixels - # as player outline colors. - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - # we don't know the color the game wants - # so we just draw index 0 - row_data.push_back(pixel(color_outline, - 0, 0, 0, 0)) - - else: - raise Exception( - f"unknown smp outline layer drawing command: " + - f"{cmd:#x} in row {rowid:d}") - # process next command - dpos += 1 - - # end of row reached, return the created pixel array. - return @cython.boundscheck(False) From 536c6b8a30e862926709f6081822c19658f80f03 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Fri, 27 Dec 2024 22:42:44 +0000 Subject: [PATCH 700/771] formatting --- .../convert/value_object/read/media/smp.pyx | 71 +++++++++---------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/openage/convert/value_object/read/media/smp.pyx b/openage/convert/value_object/read/media/smp.pyx index 9454bd9c80..942ac8e028 100644 --- a/openage/convert/value_object/read/media/smp.pyx +++ b/openage/convert/value_object/read/media/smp.pyx @@ -1,7 +1,10 @@ -# Copyright 2013-2023 the openage authors. See copying.md for legal info. +# Copyright 2013-2024 the openage authors. See copying.md for legal info. # # cython: infer_types=True +from libcpp.vector cimport vector +from libcpp cimport bool +from libc.stdint cimport uint8_t, uint16_t from enum import Enum import numpy from struct import Struct, unpack_from @@ -12,11 +15,6 @@ from .....log import spam, dbg cimport cython cimport numpy -from libc.stdint cimport uint8_t, uint16_t -from libcpp cimport bool -from libcpp.vector cimport vector - - # SMP files have little endian byte order endianness = "< " @@ -50,8 +48,8 @@ class SMPLayerType(Enum): """ SMP layer types. """ - MAIN = "main" - SHADOW = "shadow" + MAIN = "main" + SHADOW = "shadow" OUTLINE = "outline" @@ -106,7 +104,7 @@ class SMP: def __init__(self, data): smp_header = SMP.smp_header.unpack_from(data) - signature, version, frame_count, facet_count, frames_per_facet,\ + signature, version, frame_count, facet_count, frames_per_facet, \ checksum, file_size, source_format, comment = smp_header dbg("SMP") @@ -159,12 +157,14 @@ class SMP: elif layer_header.layer_type == 0x04: # layer that stores a shadow - self.shadow_frames.append(SMPShadowLayer(layer_header, data)) + self.shadow_frames.append( + SMPShadowLayer(layer_header, data)) elif layer_header.layer_type == 0x08 or \ - layer_header.layer_type == 0x10: + layer_header.layer_type == 0x10: # layer that stores an outline - self.outline_frames.append(SMPOutlineLayer(layer_header, data)) + self.outline_frames.append( + SMPOutlineLayer(layer_header, data)) else: raise Exception( @@ -282,11 +282,11 @@ ctypedef fused SMPLayerVariant: @cython.boundscheck(False) cdef void process_drawing_cmds(SMPLayerVariant variant, - const uint8_t[::1] &data_raw, - vector[pixel] &row_data, - Py_ssize_t rowid, - Py_ssize_t first_cmd_offset, - size_t expected_size): + const uint8_t[::1] & data_raw, + vector[pixel] & row_data, + Py_ssize_t rowid, + Py_ssize_t first_cmd_offset, + size_t expected_size): """ extract colors (pixels) for the drawing commands @@ -360,22 +360,22 @@ cdef void process_drawing_cmds(SMPLayerVariant variant, dpos += 1 nextbyte = data_raw[dpos] row_data.push_back(pixel(color_shadow, - nextbyte, 0, 0, 0)) + nextbyte, 0, 0, 0)) elif variant is SMPOutlineLayer: # we don't know the color the game wants # so we just draw index 0 row_data.push_back(pixel(color_outline, - 0, 0, 0, 0)) + 0, 0, 0, 0)) elif variant is SMPMainLayer: for _ in range(4): dpos += 1 pixel_data.push_back(data_raw[dpos]) row_data.push_back(pixel(color_standard, - pixel_data[0], - pixel_data[1], - pixel_data[2], - pixel_data[3] & 0x1F)) # remove "usage" bit here + pixel_data[0], + pixel_data[1], + pixel_data[2], + pixel_data[3] & 0x1F)) # remove "usage" bit here pixel_data.clear() elif lower_crumb == 0b00000010 and variant in (SMPMainLayer, SMPShadowLayer): @@ -391,17 +391,17 @@ cdef void process_drawing_cmds(SMPLayerVariant variant, dpos += 1 nextbyte = data_raw[dpos] row_data.push_back(pixel(color_shadow, - nextbyte, 0, 0, 0)) + nextbyte, 0, 0, 0)) else: for _ in range(4): dpos += 1 pixel_data.push_back(data_raw[dpos]) row_data.push_back(pixel(color_player, - pixel_data[0], - pixel_data[1], - pixel_data[2], - pixel_data[3] & 0x1F)) # remove "usage" bit here + pixel_data[0], + pixel_data[1], + pixel_data[2], + pixel_data[3] & 0x1F)) # remove "usage" bit here pixel_data.clear() else: @@ -493,7 +493,7 @@ cdef class SMPLayer: self.pcolor.push_back(self.create_color_row(data_raw, i)) cdef vector[pixel] create_color_row(self, - const uint8_t[::1] &data_raw, + const uint8_t[::1] & data_raw, Py_ssize_t rowid): """ extract colors (pixels) for the given rowid. @@ -573,12 +573,10 @@ cdef class SMPLayer: return repr(self.info) - - @cython.boundscheck(False) @cython.wraparound(False) -cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix, - numpy.ndarray[numpy.uint8_t, ndim=2, mode="c"] palette): +cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] & image_matrix, + numpy.ndarray[numpy.uint8_t, ndim = 2, mode = "c"] palette): """ converts a palette index image matrix to an rgba matrix. """ @@ -586,7 +584,7 @@ cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix, cdef size_t height = image_matrix.size() cdef size_t width = image_matrix[0].size() - cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode="c"] array_data = \ + cdef numpy.ndarray[numpy.uint8_t, ndim = 3, mode = "c"] array_data = \ numpy.zeros((height, width, 4), dtype=numpy.uint8) cdef uint8_t[:, ::1] m_lookup = palette @@ -681,9 +679,10 @@ cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix, return array_data + @cython.boundscheck(False) @cython.wraparound(False) -cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] &image_matrix): +cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] & image_matrix): """ converts the damage modifier values to an image using the RG values. @@ -693,7 +692,7 @@ cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] &image_matrix): cdef size_t height = image_matrix.size() cdef size_t width = image_matrix[0].size() - cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode="c"] array_data = \ + cdef numpy.ndarray[numpy.uint8_t, ndim = 3, mode = "c"] array_data = \ numpy.zeros((height, width, 4), dtype=numpy.uint8) cdef uint8_t r From 11e2177984e99da3f97274934bf48a90fde89609 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Mon, 30 Dec 2024 02:11:25 +0000 Subject: [PATCH 701/771] adjustments --- .../convert/value_object/read/media/smp.pyx | 162 +++++++------ .../convert/value_object/read/media/smx.pyx | 222 ++++++++++-------- 2 files changed, 211 insertions(+), 173 deletions(-) diff --git a/openage/convert/value_object/read/media/smp.pyx b/openage/convert/value_object/read/media/smp.pyx index 942ac8e028..80d2844fce 100644 --- a/openage/convert/value_object/read/media/smp.pyx +++ b/openage/convert/value_object/read/media/smp.pyx @@ -258,6 +258,9 @@ cdef class SMPMainLayer(SMPLayer): def __init__(self, layer_header, data): super().__init__(layer_header, data) + for i in range(self.row_count): + self.pcolor.push_back(create_color_row(self, i)) + def get_damage_mask(self): """ Convert the 4th pixel byte to a mask used for damaged units. @@ -268,18 +271,78 @@ cdef class SMPShadowLayer(SMPLayer): def __init__(self, layer_header, data): super().__init__(layer_header, data) + for i in range(self.row_count): + self.pcolor.push_back(create_color_row(self, i)) + cdef class SMPOutlineLayer(SMPLayer): def __init__(self, layer_header, data): super().__init__(layer_header, data) + for i in range(self.row_count): + self.pcolor.push_back(create_color_row(self, i)) + ctypedef fused SMPLayerVariant: - SMPLayer SMPMainLayer SMPShadowLayer SMPOutlineLayer +@cython.boundscheck(False) +cdef vector[pixel] create_color_row(SMPLayerVariant variant, + Py_ssize_t rowid): + """ + extract colors (pixels) for the given rowid. + """ + cdef vector[pixel] row_data + cdef Py_ssize_t i + + cdef Py_ssize_t first_cmd_offset = variant.cmd_offsets[rowid] # redudent + cdef boundary_def bounds = variant.boundaries[rowid] + cdef size_t pixel_count = variant.info.size[0] + + # preallocate memory + row_data.reserve(pixel_count) + + # row is completely transparent + if bounds.full_row: + for _ in range(pixel_count): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) + + return row_data + + # start drawing the left transparent space + for i in range(bounds.left): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) + + # process the drawing commands for this row. + process_drawing_cmds(variant, variant.data_raw, + row_data, rowid, + first_cmd_offset, + pixel_count - bounds.right) + + # finish by filling up the right transparent space + for i in range(bounds.right): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) + + # verify size of generated row + if row_data.size() != pixel_count: + got = row_data.size() + summary = ( + f"{got:d}/{pixel_count:d} -> row {rowid:d}, " + f"layer type {variant.info.layer_type:x}, " + f"offset {first_cmd_offset:d} / {first_cmd_offset:#x}" + ) + message = ( + f"got {'LESS' if got < pixel_count else 'MORE'} pixels than expected: {summary}, " + f"missing: {abs(pixel_count - got):d}" + ) + + raise Exception(message) + + return row_data + + @cython.boundscheck(False) cdef void process_drawing_cmds(SMPLayerVariant variant, const uint8_t[::1] & data_raw, @@ -312,7 +375,7 @@ cdef void process_drawing_cmds(SMPLayerVariant variant, if row_data.size() > expected_size: raise Exception( f"Only {expected_size:d} pixels should be drawn in row {rowid:d} " + - f"with layer type {variant.info.layer_type:#x}, but we have {row_data.size():d}" + f"with layer type {variant.info.layer_type:#x}, but we have {row_data.size():d} already!" ) # fetch drawing instruction @@ -320,14 +383,13 @@ cdef void process_drawing_cmds(SMPLayerVariant variant, # Last 2 bits store command type lower_crumb = 0b00000011 & cmd - # opcode: cmd, rowid: rowid if lower_crumb == 0b00000011: # eol (end of line) command, this row is finished now. eor = True - if variant is SMPShadowLayer and row_data.size() < expected_size: + if SMPLayerVariant is SMPShadowLayer and row_data.size() < expected_size: # copy the last drawn pixel # (still stored in nextbyte) # @@ -356,17 +418,17 @@ cdef void process_drawing_cmds(SMPLayerVariant variant, pixel_count = (cmd >> 2) + 1 for _ in range(pixel_count): - if variant is SMPShadowLayer: + if SMPLayerVariant is SMPShadowLayer: dpos += 1 nextbyte = data_raw[dpos] row_data.push_back(pixel(color_shadow, nextbyte, 0, 0, 0)) - elif variant is SMPOutlineLayer: + elif SMPLayerVariant is SMPOutlineLayer: # we don't know the color the game wants # so we just draw index 0 row_data.push_back(pixel(color_outline, 0, 0, 0, 0)) - elif variant is SMPMainLayer: + elif SMPLayerVariant is SMPMainLayer: for _ in range(4): dpos += 1 pixel_data.push_back(data_raw[dpos]) @@ -378,7 +440,7 @@ cdef void process_drawing_cmds(SMPLayerVariant variant, pixel_data[3] & 0x1F)) # remove "usage" bit here pixel_data.clear() - elif lower_crumb == 0b00000010 and variant in (SMPMainLayer, SMPShadowLayer): + elif lower_crumb == 0b00000010 and (SMPLayerVariant is SMPMainLayer or SMPLayerVariant is SMPShadowLayer): # player_color command # draw the following 'count' pixels # pixels are stored as 4 byte palette and meta infos @@ -387,7 +449,7 @@ cdef void process_drawing_cmds(SMPLayerVariant variant, pixel_count = (cmd >> 2) + 1 for _ in range(pixel_count): - if variant is SMPShadowLayer: + if SMPLayerVariant is SMPShadowLayer: dpos += 1 nextbyte = data_raw[dpos] row_data.push_back(pixel(color_shadow, @@ -446,15 +508,20 @@ cdef class SMPLayer: # pixel matrix representing the final image cdef vector[vector[pixel]] pcolor + # memory pointer + cdef const uint8_t[::1] data_raw + + # rows of image + cdef size_t row_count + def __init__(self, layer_header, data): self.info = layer_header if not (isinstance(data, bytes) or isinstance(data, bytearray)): raise ValueError("Layer data must be some bytes object") - # memory pointer # convert the bytes obj to char* - cdef const uint8_t[::1] data_raw = data + self.data_raw = data cdef unsigned short left cdef unsigned short right @@ -462,11 +529,11 @@ cdef class SMPLayer: cdef size_t i cdef int cmd_offset - cdef size_t row_count = self.info.size[1] - self.pcolor.reserve(row_count) + self.row_count = self.info.size[1] + self.pcolor.reserve(self.row_count) # process bondary table - for i in range(row_count): + for i in range(self.row_count): outline_entry_position = (self.info.outline_table_offset + i * SMPLayer.smp_frame_row_edge.size) @@ -481,7 +548,7 @@ cdef class SMPLayer: self.boundaries.push_back(boundary_def(left, right, False)) # process cmd table - for i in range(row_count): + for i in range(self.row_count): cmd_table_position = (self.info.qdl_table_offset + i * SMPLayer.smp_command_offset.size) @@ -489,65 +556,6 @@ cdef class SMPLayer: data, cmd_table_position)[0] + self.info.frame_offset self.cmd_offsets.push_back(cmd_offset) - for i in range(row_count): - self.pcolor.push_back(self.create_color_row(data_raw, i)) - - cdef vector[pixel] create_color_row(self, - const uint8_t[::1] & data_raw, - Py_ssize_t rowid): - """ - extract colors (pixels) for the given rowid. - """ - - cdef vector[pixel] row_data - cdef Py_ssize_t i - - first_cmd_offset = self.cmd_offsets[rowid] - cdef boundary_def bounds = self.boundaries[rowid] - cdef size_t pixel_count = self.info.size[0] - - # preallocate memory - row_data.reserve(pixel_count) - - # row is completely transparent - if bounds.full_row: - for _ in range(pixel_count): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - return row_data - - # start drawing the left transparent space - for i in range(bounds.left): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - # process the drawing commands for this row. - process_drawing_cmds(self, - data_raw, - row_data, rowid, - first_cmd_offset, - pixel_count - bounds.right) - - # finish by filling up the right transparent space - for i in range(bounds.right): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - # verify size of generated row - if row_data.size() != pixel_count: - got = row_data.size() - summary = ( - f"{got:d}/{pixel_count:d} -> row {rowid:d}, " - f"layer type {self.info.layer_type:x}, " - f"offset {first_cmd_offset:d} / {first_cmd_offset:#x}" - ) - message = ( - f"got {'LESS' if got < pixel_count else 'MORE'} pixels than expected: {summary}, " - f"missing: {abs(pixel_count - got):d}" - ) - - raise Exception(message) - - return row_data - def get_picture_data(self, palette): """ Convert the palette index matrix to a colored image. @@ -576,7 +584,7 @@ cdef class SMPLayer: @cython.boundscheck(False) @cython.wraparound(False) cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] & image_matrix, - numpy.ndarray[numpy.uint8_t, ndim = 2, mode = "c"] palette): + numpy.ndarray[numpy.uint8_t, ndim= 2, mode = "c"] palette): """ converts a palette index image matrix to an rgba matrix. """ @@ -584,7 +592,7 @@ cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] & image_matrix, cdef size_t height = image_matrix.size() cdef size_t width = image_matrix[0].size() - cdef numpy.ndarray[numpy.uint8_t, ndim = 3, mode = "c"] array_data = \ + cdef numpy.ndarray[numpy.uint8_t, ndim= 3, mode = "c"] array_data = \ numpy.zeros((height, width, 4), dtype=numpy.uint8) cdef uint8_t[:, ::1] m_lookup = palette @@ -692,7 +700,7 @@ cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] & image_matrix) cdef size_t height = image_matrix.size() cdef size_t width = image_matrix[0].size() - cdef numpy.ndarray[numpy.uint8_t, ndim = 3, mode = "c"] array_data = \ + cdef numpy.ndarray[numpy.uint8_t, ndim= 3, mode = "c"] array_data = \ numpy.zeros((height, width, 4), dtype=numpy.uint8) cdef uint8_t r diff --git a/openage/convert/value_object/read/media/smx.pyx b/openage/convert/value_object/read/media/smx.pyx index 2cb4749091..fd12a20ee7 100644 --- a/openage/convert/value_object/read/media/smx.pyx +++ b/openage/convert/value_object/read/media/smx.pyx @@ -60,28 +60,116 @@ cdef public dict LAYER_TYPES = { cdef class SMXMainLayer8to5(SMXLayer): def __init__(self, layer_header, data): super().__init__(layer_header, data) + # process cmd table + for i in range(self.row_count): + cmd_offset, color_offset, chunk_pos, row_data = create_color_row( + self, i) + + self.pcolor.push_back(row_data) cdef class SMXMainLayer4plus1(SMXLayer): def __init__(self, layer_header, data): super().__init__(layer_header, data) + # process cmd table + for i in range(self.row_count): + cmd_offset, color_offset, chunk_pos, row_data = create_color_row( + self, i) + + self.pcolor.push_back(row_data) cdef class SMXOutlineLayer(SMXLayer): def __init__(self, layer_header, data): super().__init__(layer_header, data) + # process cmd table + for i in range(self.row_count): + cmd_offset, color_offset, chunk_pos, row_data = create_color_row( + self, i) + + self.pcolor.push_back(row_data) cdef class SMXShadowLayer(SMXLayer): def __init__(self, layer_header, data): super().__init__(layer_header, data) + # process cmd table + for i in range(self.row_count): + cmd_offset, color_offset, chunk_pos, row_data = create_color_row( + self, i) + + self.pcolor.push_back(row_data) ctypedef fused SMXLayerVariant: - SMXLayer SMXMainLayer8to5 SMXMainLayer4plus1 SMXOutlineLayer SMXShadowLayer +cdef inline(int, int, int, vector[pixel]) create_color_row(SMXLayerVariant variant, + Py_ssize_t rowid): + """ + Extract colors (pixels) for the given rowid. + + :param rowid: Index of the current row in the layer. + :param cmd_offset: Offset of the command table of the layer. + :param color_offset: Offset of the color table of the layer. + :param chunk_pos: Current position in the compressed chunk. + """ + + cdef vector[pixel] row_data + cdef Py_ssize_t i + + cdef int first_cmd_offset = variant.cmd_offset + cdef int first_color_offset = variant.color_offset + cdef int first_chunk_pos = variant.chunk_pos + cdef boundary_def bounds = variant.boundaries[rowid] + cdef size_t pixel_count = variant.info.size[0] + + # preallocate memory + row_data.reserve(pixel_count) + + # row is completely transparent + if bounds.full_row: + for _ in range(pixel_count): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) + + return variant.cmd_offset, variant.color_offset, variant.chunk_pos, row_data + + # start drawing the left transparent space + for i in range(bounds.left): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) + + # process the drawing commands for this row. + next_cmd_offset, next_color_offset, next_chunk_pos, row_data = \ + process_drawing_cmds(variant, + variant.data_raw, + row_data, + rowid, + first_cmd_offset, + first_color_offset, + first_chunk_pos, + pixel_count - bounds.right + ) + + # finish by filling up the right transparent space + for i in range(bounds.right): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) + + # verify size of generated row + if row_data.size() != pixel_count: + got = row_data.size() + summary = "%d/%d -> row %d, layer type %s, offset %d / %#x" % ( + got, pixel_count, rowid, variant.info.layer_type, + first_cmd_offset, first_cmd_offset + ) + txt = "got %%s pixels than expected: %s, missing: %d" % ( + summary, abs(pixel_count - got)) + + raise Exception(txt % ("LESS" if got < pixel_count else "MORE")) + + return next_cmd_offset, next_color_offset, next_chunk_pos, row_data + + @cython.boundscheck(False) cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant variant, const uint8_t[::1] & data_raw, @@ -117,7 +205,9 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v cdef uint8_t pixel_mask_even_2 = 0b11110000 cdef uint8_t pixel_mask_even_3 = 0b00111111 - if variant in (SMXMainLayer8to5, SMXMainLayer4plus1): + print(type(variant)) + + if SMXLayerVariant is SMXMainLayer8to5 or SMXLayerVariant is SMXMainLayer4plus1: # Position in the pixel data array dpos_color = first_color_offset @@ -139,13 +229,14 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v # Last 2 bits store command type lower_crumb = 0b00000011 & cmd + print(lower_crumb) if lower_crumb == 0b00000011: # eor (end of row) command, this row is finished now. eor = True dpos_cmd += 1 - if variant is SMXShadowLayer: + if SMXLayerVariant is SMXShadowLayer: # shadows sometimes need an extra pixel at # the end if row_data.size() < expected_size: @@ -179,7 +270,7 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v pixel_count = (cmd >> 2) + 1 - if variant is SMXMainLayer8to5: + if SMXLayerVariant is SMXMainLayer8to5: pixel_data.reserve(4) for _ in range(pixel_count): # Start fetching pixel data @@ -229,7 +320,7 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v odd = not odd pixel_data.clear() - if variant is SMXMainLayer4plus1: + if SMXLayerVariant is SMXMainLayer4plus1: palette_section_block = data_raw[dpos_color + (4 - chunk_pos)] for _ in range(pixel_count): @@ -251,7 +342,7 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v dpos_color += 1 # Skip palette section block palette_section_block = data_raw[dpos_color + 4] - if variant is SMXShadowLayer: + if SMXLayerVariant is SMXShadowLayer: for _ in range(pixel_count): dpos_color += 1 nextbyte = data_raw[dpos_color] @@ -259,14 +350,14 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v row_data.push_back(pixel(color_shadow, nextbyte, 0, 0, 0)) - if variant is SMXOutlineLayer: + if SMXLayerVariant is SMXOutlineLayer: # we don't know the color the game wants # so we just draw index 0 row_data.push_back(pixel(color_outline, 0, 0, 0, 0)) elif lower_crumb == 0b00000010: - if variant is SMXMainLayer8to5: + if SMXLayerVariant is SMXMainLayer8to5: # player_color command # draw the following 'count' pixels # pixels are stored in 5 byte chunks @@ -327,7 +418,7 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v pixel_data.clear() elif lower_crumb == 0b00000010: - if variant is SMXMainLayer4plus1: + if SMXLayerVariant is SMXMainLayer4plus1: # player_color command # draw the following 'count' pixels # 4 pixels are stored in every 5 byte chunk. @@ -356,7 +447,7 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v dpos_color += 1 # Skip palette section block palette_section_block = data_raw[dpos_color + 4] - if variant is (SMXOutlineLayer, SMXShadowLayer): + if SMXLayerVariant is SMXOutlineLayer or SMXLayerVariant is SMXShadowLayer: pass else: @@ -368,9 +459,9 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v # Process next command dpos_cmd += 1 - if variant in (SMXMainLayer8to5, SMXMainLayer4plus1): - return dpos_cmd, dpos_color, odd, row_data - if variant in (SMXOutlineLayer, SMXShadowLayer): + if SMXLayerVariant is SMXMainLayer8to5 or SMXLayerVariant is SMXMainLayer4plus1: + return dpos_cmd, dpos_color, chunk_pos, row_data + if SMXLayerVariant is SMXOutlineLayer or SMXLayerVariant is SMXShadowLayer: return dpos_cmd, dpos_cmd, chunk_pos, row_data @@ -640,6 +731,21 @@ cdef class SMXLayer: # pixel matrix representing the final image cdef vector[vector[pixel]] pcolor + # memory pointer + cdef const uint8_t[::1] data_raw + + # current command + cdef int cmd_offset + + # current color + cdef int color_offset + + # current chunk position + cdef int chunk_pos + + # rows + cdef size_t row_count + def __init__(self, layer_header, data): """ SMX layer definition superclass. There can be various types of @@ -655,19 +761,18 @@ cdef class SMXLayer: if not (isinstance(data, bytes) or isinstance(data, bytearray)): raise ValueError("Layer data must be some bytes object") - # memory pointer # convert the bytes obj to char* - cdef const uint8_t[::1] data_raw = data + self.data_raw = data cdef unsigned short left cdef unsigned short right cdef size_t i - cdef size_t row_count = self.info.size[1] - self.pcolor.reserve(row_count) + self.row_count = self.info.size[1] + self.pcolor.reserve(self.row_count) # process bondary table - for i in range(row_count): + for i in range(self.row_count): outline_entry_position = (self.info.outline_table_offset + i * SMXLayer.smp_layer_row_edge.size) @@ -681,84 +786,9 @@ cdef class SMXLayer: else: self.boundaries.push_back(boundary_def(left, right, False)) - cdef int cmd_offset = self.info.qdl_command_table_offset - cdef int color_offset = self.info.qdl_color_table_offset - cdef int chunk_pos = 0 - - # process cmd table - for i in range(row_count): - cmd_offset, color_offset, chunk_pos, row_data = \ - self.create_color_row( - data_raw, i, cmd_offset, color_offset, chunk_pos) - - self.pcolor.push_back(row_data) - - cdef inline(int, int, int, vector[pixel]) create_color_row(self, - const uint8_t[::1] & data_raw, - Py_ssize_t rowid, - int cmd_offset, - int color_offset, - int chunk_pos): - """ - Extract colors (pixels) for the given rowid. - - :param rowid: Index of the current row in the layer. - :param cmd_offset: Offset of the command table of the layer. - :param color_offset: Offset of the color table of the layer. - :param chunk_pos: Current position in the compressed chunk. - """ - - cdef vector[pixel] row_data - cdef Py_ssize_t i - - cdef int first_cmd_offset = cmd_offset - cdef int first_color_offset = color_offset - cdef boundary_def bounds = self.boundaries[rowid] - cdef size_t pixel_count = self.info.size[0] - - # preallocate memory - row_data.reserve(pixel_count) - - # row is completely transparent - if bounds.full_row: - for _ in range(pixel_count): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - return cmd_offset, color_offset, chunk_pos, row_data - - # start drawing the left transparent space - for i in range(bounds.left): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - # process the drawing commands for this row. - next_cmd_offset, next_color_offset, chunk_pos, row_data = \ - process_drawing_cmds(self, - data_raw, - row_data, - rowid, - first_cmd_offset, - first_color_offset, - chunk_pos, - pixel_count - bounds.right - ) - - # finish by filling up the right transparent space - for i in range(bounds.right): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - # verify size of generated row - if row_data.size() != pixel_count: - got = row_data.size() - summary = "%d/%d -> row %d, layer type %d, offset %d / %#x" % ( - got, pixel_count, rowid, self.info.layer_type, - first_cmd_offset, first_cmd_offset - ) - txt = "got %%s pixels than expected: %s, missing: %d" % ( - summary, abs(pixel_count - got)) - - raise Exception(txt % ("LESS" if got < pixel_count else "MORE")) - - return next_cmd_offset, next_color_offset, chunk_pos, row_data + self.cmd_offset = self.info.qdl_command_table_offset + self.color_offset = self.info.qdl_color_table_offset + self.chunk_pos = 0 def get_picture_data(self, palette): """ From 21af7181cb4593031f067d7861f7d8ceac6969b4 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Mon, 30 Dec 2024 04:20:01 +0000 Subject: [PATCH 702/771] adjustments --- .../convert/value_object/read/media/smp.pyx | 2 +- .../convert/value_object/read/media/smx.pyx | 57 ++++++++++--------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/openage/convert/value_object/read/media/smp.pyx b/openage/convert/value_object/read/media/smp.pyx index 80d2844fce..15cb55873e 100644 --- a/openage/convert/value_object/read/media/smp.pyx +++ b/openage/convert/value_object/read/media/smp.pyx @@ -297,7 +297,7 @@ cdef vector[pixel] create_color_row(SMPLayerVariant variant, cdef vector[pixel] row_data cdef Py_ssize_t i - cdef Py_ssize_t first_cmd_offset = variant.cmd_offsets[rowid] # redudent + cdef Py_ssize_t first_cmd_offset = variant.cmd_offsets[rowid] cdef boundary_def bounds = variant.boundaries[rowid] cdef size_t pixel_count = variant.info.size[0] diff --git a/openage/convert/value_object/read/media/smx.pyx b/openage/convert/value_object/read/media/smx.pyx index fd12a20ee7..a058c22399 100644 --- a/openage/convert/value_object/read/media/smx.pyx +++ b/openage/convert/value_object/read/media/smx.pyx @@ -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. # # cython: infer_types=True @@ -64,6 +64,9 @@ cdef class SMXMainLayer8to5(SMXLayer): for i in range(self.row_count): cmd_offset, color_offset, chunk_pos, row_data = create_color_row( self, i) + self.cmd_offset = cmd_offset + self.color_offset = color_offset + self.chunk_pos = chunk_pos self.pcolor.push_back(row_data) @@ -74,6 +77,9 @@ cdef class SMXMainLayer4plus1(SMXLayer): for i in range(self.row_count): cmd_offset, color_offset, chunk_pos, row_data = create_color_row( self, i) + self.cmd_offset = cmd_offset + self.color_offset = color_offset + self.chunk_pos = chunk_pos self.pcolor.push_back(row_data) @@ -84,6 +90,9 @@ cdef class SMXOutlineLayer(SMXLayer): for i in range(self.row_count): cmd_offset, color_offset, chunk_pos, row_data = create_color_row( self, i) + self.cmd_offset = cmd_offset + self.color_offset = color_offset + self.chunk_pos = chunk_pos self.pcolor.push_back(row_data) @@ -94,6 +103,9 @@ cdef class SMXShadowLayer(SMXLayer): for i in range(self.row_count): cmd_offset, color_offset, chunk_pos, row_data = create_color_row( self, i) + self.cmd_offset = cmd_offset + self.color_offset = color_offset + self.chunk_pos = chunk_pos self.pcolor.push_back(row_data) @@ -182,7 +194,6 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v """ TODO: docstring """ - # position in the command array, we start at the first command of this row cdef Py_ssize_t dpos_cmd = first_cmd_offset @@ -205,8 +216,6 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v cdef uint8_t pixel_mask_even_2 = 0b11110000 cdef uint8_t pixel_mask_even_3 = 0b00111111 - print(type(variant)) - if SMXLayerVariant is SMXMainLayer8to5 or SMXLayerVariant is SMXMainLayer4plus1: # Position in the pixel data array dpos_color = first_color_offset @@ -229,24 +238,22 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v # Last 2 bits store command type lower_crumb = 0b00000011 & cmd - print(lower_crumb) - if lower_crumb == 0b00000011: # eor (end of row) command, this row is finished now. + eor = True dpos_cmd += 1 - if SMXLayerVariant is SMXShadowLayer: - # shadows sometimes need an extra pixel at - # the end - if row_data.size() < expected_size: - # copy the last drawn pixel - # (still stored in nextbyte) - # - # TODO: confirm that this is the - # right way to do it - row_data.push_back(pixel(color_shadow, - nextbyte, 0, 0, 0)) + # shadows sometimes need an extra pixel at + # the end + if SMXLayerVariant is SMXShadowLayer and row_data.size() < expected_size: + # copy the last drawn pixel + # (still stored in nextbyte) + # + # TODO: confirm that this is the + # right way to do it + row_data.push_back(pixel(color_shadow, + nextbyte, 0, 0, 0)) continue elif lower_crumb == 0b00000000: @@ -344,8 +351,8 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v if SMXLayerVariant is SMXShadowLayer: for _ in range(pixel_count): - dpos_color += 1 - nextbyte = data_raw[dpos_color] + dpos_cmd += 1 + nextbyte = data_raw[dpos_cmd] row_data.push_back(pixel(color_shadow, nextbyte, 0, 0, 0)) @@ -417,8 +424,7 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v odd = not odd pixel_data.clear() - elif lower_crumb == 0b00000010: - if SMXLayerVariant is SMXMainLayer4plus1: + elif SMXLayerVariant is SMXMainLayer4plus1: # player_color command # draw the following 'count' pixels # 4 pixels are stored in every 5 byte chunk. @@ -447,9 +453,6 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v dpos_color += 1 # Skip palette section block palette_section_block = data_raw[dpos_color + 4] - if SMXLayerVariant is SMXOutlineLayer or SMXLayerVariant is SMXShadowLayer: - pass - else: raise Exception( f"unknown smx main graphics layer drawing command: " + @@ -826,7 +829,7 @@ cdef class SMXLayer: @cython.boundscheck(False) @cython.wraparound(False) cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] & image_matrix, - numpy.ndarray[numpy.uint8_t, ndim = 2, mode = "c"] palette): + numpy.ndarray[numpy.uint8_t, ndim= 2, mode = "c"] palette): """ converts a palette index image matrix to an rgba matrix. @@ -837,7 +840,7 @@ cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] & image_matrix, cdef size_t height = image_matrix.size() cdef size_t width = image_matrix[0].size() - cdef numpy.ndarray[numpy.uint8_t, ndim = 3, mode = "c"] array_data = \ + cdef numpy.ndarray[numpy.uint8_t, ndim= 3, mode = "c"] array_data = \ numpy.zeros((height, width, 4), dtype=numpy.uint8) cdef uint8_t[:, ::1] m_lookup = palette @@ -936,7 +939,7 @@ cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] & image_matrix) cdef size_t height = image_matrix.size() cdef size_t width = image_matrix[0].size() - cdef numpy.ndarray[numpy.uint8_t, ndim = 3, mode = "c"] array_data = \ + cdef numpy.ndarray[numpy.uint8_t, ndim= 3, mode = "c"] array_data = \ numpy.zeros((height, width, 4), dtype=numpy.uint8) cdef uint8_t r = 0 From f8d35e074179b561245898596cb4620de09a2e87 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Thu, 2 Jan 2025 16:19:12 +0000 Subject: [PATCH 703/771] copying.md --- .mailmap | 6 +++++- copying.md | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.mailmap b/.mailmap index 8a1a8727b1..3d8b8ec12e 100644 --- a/.mailmap +++ b/.mailmap @@ -21,4 +21,8 @@ Tobias Feldballe Jonas Borchelt Derek Frogget <114030121+derekfrogget@users.noreply.github.com> Nikhil Ghosh -David Wever <56411717+dmwever@users.noreply.github.com> \ No newline at end of file +<<<<<<< HEAD +David Wever <56411717+dmwever@users.noreply.github.com> +======= +Ngô Xuân Minh +>>>>>>> e8251ff6 (copying.md) diff --git a/copying.md b/copying.md index 53ced52632..f873817f29 100644 --- a/copying.md +++ b/copying.md @@ -160,6 +160,7 @@ _the openage authors_ are: | Alex Zhuohao He | ZzzhHe | zhuohao dawt he à outlook dawt com | | David Wever | dmwever | dmwever à crimson dawt ua dawt edu | | Michael Lynch | mtlynch | git à mtlynch dawt io | +| Ngô Xuân Minh | | xminh dawt ngo dawt 00 à gmail dawt com | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. From e6228a2e4326c5654e7968b74c1c4142da91e99b Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Thu, 9 Jan 2025 13:08:08 +0000 Subject: [PATCH 704/771] addressing comments --- .../convert/value_object/read/media/smp.pyx | 235 +++++++++--------- .../convert/value_object/read/media/smx.pyx | 182 +++++++------- 2 files changed, 211 insertions(+), 206 deletions(-) diff --git a/openage/convert/value_object/read/media/smp.pyx b/openage/convert/value_object/read/media/smp.pyx index 15cb55873e..c52aa1a194 100644 --- a/openage/convert/value_object/read/media/smp.pyx +++ b/openage/convert/value_object/read/media/smp.pyx @@ -1,10 +1,7 @@ -# Copyright 2013-2024 the openage authors. See copying.md for legal info. +# Copyright 2013-2025 the openage authors. See copying.md for legal info. # # cython: infer_types=True -from libcpp.vector cimport vector -from libcpp cimport bool -from libc.stdint cimport uint8_t, uint16_t from enum import Enum import numpy from struct import Struct, unpack_from @@ -15,6 +12,11 @@ from .....log import spam, dbg cimport cython cimport numpy +from libc.stdint cimport uint8_t, uint16_t +from libcpp cimport bool +from libcpp.vector cimport vector + + # SMP files have little endian byte order endianness = "< " @@ -48,8 +50,8 @@ class SMPLayerType(Enum): """ SMP layer types. """ - MAIN = "main" - SHADOW = "shadow" + MAIN = "main" + SHADOW = "shadow" OUTLINE = "outline" @@ -104,7 +106,7 @@ class SMP: def __init__(self, data): smp_header = SMP.smp_header.unpack_from(data) - signature, version, frame_count, facet_count, frames_per_facet, \ + signature, version, frame_count, facet_count, frames_per_facet,\ checksum, file_size, source_format, comment = smp_header dbg("SMP") @@ -157,14 +159,12 @@ class SMP: elif layer_header.layer_type == 0x04: # layer that stores a shadow - self.shadow_frames.append( - SMPShadowLayer(layer_header, data)) + self.shadow_frames.append(SMPShadowLayer(layer_header, data)) elif layer_header.layer_type == 0x08 or \ - layer_header.layer_type == 0x10: + layer_header.layer_type == 0x10: # layer that stores an outline - self.outline_frames.append( - SMPOutlineLayer(layer_header, data)) + self.outline_frames.append(SMPOutlineLayer(layer_header, data)) else: raise Exception( @@ -254,6 +254,108 @@ class SMPLayerHeader: return "".join(ret) +cdef class SMPLayer: + """ + one layer inside the SMP. you can imagine it as a frame of a video. + """ + + # struct smp_frame_row_edge { + # unsigned short left_space; + # unsigned short right_space; + # }; + smp_frame_row_edge = Struct(endianness + "H H") + + # struct smp_command_offset { + # unsigned int offset; + # } + smp_command_offset = Struct(endianness + "I") + + # layer and frame information + cdef object info + + # for each row: + # contains (left, right, full_row) number of boundary pixels + cdef vector[boundary_def] boundaries + + # stores the file offset for the first drawing command + cdef vector[int] cmd_offsets + + # pixel matrix representing the final image + cdef vector[vector[pixel]] pcolor + + # memory pointer + cdef const uint8_t[::1] data_raw + + # rows of image + cdef size_t row_count + + def __init__(self, layer_header, data): + self.info = layer_header + + if not (isinstance(data, bytes) or isinstance(data, bytearray)): + raise ValueError("Layer data must be some bytes object") + + # convert the bytes obj to char* + self.data_raw = data + + cdef unsigned short left + cdef unsigned short right + + cdef size_t i + cdef int cmd_offset + + self.row_count = self.info.size[1] + self.pcolor.reserve(self.row_count) + + # process bondary table + for i in range(self.row_count): + outline_entry_position = (self.info.outline_table_offset + + i * SMPLayer.smp_frame_row_edge.size) + + left, right = SMPLayer.smp_frame_row_edge.unpack_from( + data, outline_entry_position + ) + + # is this row completely transparent? + if left == 0xFFFF or right == 0xFFFF: + self.boundaries.push_back(boundary_def(0, 0, True)) + else: + self.boundaries.push_back(boundary_def(left, right, False)) + + # process cmd table + for i in range(self.row_count): + cmd_table_position = (self.info.qdl_table_offset + + i * SMPLayer.smp_command_offset.size) + + cmd_offset = SMPLayer.smp_command_offset.unpack_from( + data, cmd_table_position)[0] + self.info.frame_offset + self.cmd_offsets.push_back(cmd_offset) + + def get_picture_data(self, palette): + """ + Convert the palette index matrix to a colored image. + """ + return determine_rgba_matrix(self.pcolor, palette) + + def get_hotspot(self): + """ + Return the layer's hotspot (the "center" of the image) + """ + return self.info.hotspot + + def get_palette_number(self): + """ + Return the layer's palette number. + + :return: Palette number of the layer. + :rtype: int + """ + return self.pcolor[0][0].palette & 0b00111111 + + def __repr__(self): + return repr(self.info) + + cdef class SMPMainLayer(SMPLayer): def __init__(self, layer_header, data): super().__init__(layer_header, data) @@ -479,112 +581,13 @@ cdef void process_drawing_cmds(SMPLayerVariant variant, return -cdef class SMPLayer: - """ - one layer inside the SMP. you can imagine it as a frame of a video. - """ - - # struct smp_frame_row_edge { - # unsigned short left_space; - # unsigned short right_space; - # }; - smp_frame_row_edge = Struct(endianness + "H H") - - # struct smp_command_offset { - # unsigned int offset; - # } - smp_command_offset = Struct(endianness + "I") - - # layer and frame information - cdef object info - - # for each row: - # contains (left, right, full_row) number of boundary pixels - cdef vector[boundary_def] boundaries - - # stores the file offset for the first drawing command - cdef vector[int] cmd_offsets - - # pixel matrix representing the final image - cdef vector[vector[pixel]] pcolor - - # memory pointer - cdef const uint8_t[::1] data_raw - - # rows of image - cdef size_t row_count - - def __init__(self, layer_header, data): - self.info = layer_header - - if not (isinstance(data, bytes) or isinstance(data, bytearray)): - raise ValueError("Layer data must be some bytes object") - - # convert the bytes obj to char* - self.data_raw = data - - cdef unsigned short left - cdef unsigned short right - - cdef size_t i - cdef int cmd_offset - - self.row_count = self.info.size[1] - self.pcolor.reserve(self.row_count) - - # process bondary table - for i in range(self.row_count): - outline_entry_position = (self.info.outline_table_offset + - i * SMPLayer.smp_frame_row_edge.size) - - left, right = SMPLayer.smp_frame_row_edge.unpack_from( - data, outline_entry_position - ) - - # is this row completely transparent? - if left == 0xFFFF or right == 0xFFFF: - self.boundaries.push_back(boundary_def(0, 0, True)) - else: - self.boundaries.push_back(boundary_def(left, right, False)) - - # process cmd table - for i in range(self.row_count): - cmd_table_position = (self.info.qdl_table_offset + - i * SMPLayer.smp_command_offset.size) - - cmd_offset = SMPLayer.smp_command_offset.unpack_from( - data, cmd_table_position)[0] + self.info.frame_offset - self.cmd_offsets.push_back(cmd_offset) - - def get_picture_data(self, palette): - """ - Convert the palette index matrix to a colored image. - """ - return determine_rgba_matrix(self.pcolor, palette) - - def get_hotspot(self): - """ - Return the layer's hotspot (the "center" of the image) - """ - return self.info.hotspot - - def get_palette_number(self): - """ - Return the layer's palette number. - - :return: Palette number of the layer. - :rtype: int - """ - return self.pcolor[0][0].palette & 0b00111111 - def __repr__(self): - return repr(self.info) @cython.boundscheck(False) @cython.wraparound(False) -cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] & image_matrix, - numpy.ndarray[numpy.uint8_t, ndim= 2, mode = "c"] palette): +cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix, + numpy.ndarray[numpy.uint8_t, ndim=2, mode="c"] palette): """ converts a palette index image matrix to an rgba matrix. """ @@ -592,7 +595,7 @@ cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] & image_matrix, cdef size_t height = image_matrix.size() cdef size_t width = image_matrix[0].size() - cdef numpy.ndarray[numpy.uint8_t, ndim= 3, mode = "c"] array_data = \ + cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode="c"] array_data = \ numpy.zeros((height, width, 4), dtype=numpy.uint8) cdef uint8_t[:, ::1] m_lookup = palette @@ -690,7 +693,7 @@ cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] & image_matrix, @cython.boundscheck(False) @cython.wraparound(False) -cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] & image_matrix): +cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] &image_matrix): """ converts the damage modifier values to an image using the RG values. @@ -700,7 +703,7 @@ cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] & image_matrix) cdef size_t height = image_matrix.size() cdef size_t width = image_matrix[0].size() - cdef numpy.ndarray[numpy.uint8_t, ndim= 3, mode = "c"] array_data = \ + cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode="c"] array_data = \ numpy.zeros((height, width, 4), dtype=numpy.uint8) cdef uint8_t r diff --git a/openage/convert/value_object/read/media/smx.pyx b/openage/convert/value_object/read/media/smx.pyx index a058c22399..52e17428c8 100644 --- a/openage/convert/value_object/read/media/smx.pyx +++ b/openage/convert/value_object/read/media/smx.pyx @@ -1,10 +1,7 @@ -# Copyright 2019-2024 the openage authors. See copying.md for legal info. +# Copyright 2019-2025 the openage authors. See copying.md for legal info. # # cython: infer_types=True -from libcpp.vector cimport vector -from libcpp cimport bool -from libc.stdint cimport uint8_t, uint16_t from enum import Enum import numpy from struct import Struct, unpack_from @@ -15,6 +12,11 @@ from .....log import spam, dbg cimport cython cimport numpy +from libc.stdint cimport uint8_t, uint16_t +from libcpp cimport bool +from libcpp.vector cimport vector + + # SMX files have little endian byte order endianness = "< " @@ -47,8 +49,8 @@ class SMXLayerType(Enum): """ SMX layer types. """ - MAIN = "main" - SHADOW = "shadow" + MAIN = "main" + SHADOW = "shadow" OUTLINE = "outline" @@ -57,57 +59,6 @@ cdef public dict LAYER_TYPES = { 1: SMXLayerType.SHADOW, 2: SMXLayerType.OUTLINE, } -cdef class SMXMainLayer8to5(SMXLayer): - def __init__(self, layer_header, data): - super().__init__(layer_header, data) - # process cmd table - for i in range(self.row_count): - cmd_offset, color_offset, chunk_pos, row_data = create_color_row( - self, i) - self.cmd_offset = cmd_offset - self.color_offset = color_offset - self.chunk_pos = chunk_pos - - self.pcolor.push_back(row_data) - -cdef class SMXMainLayer4plus1(SMXLayer): - def __init__(self, layer_header, data): - super().__init__(layer_header, data) - # process cmd table - for i in range(self.row_count): - cmd_offset, color_offset, chunk_pos, row_data = create_color_row( - self, i) - self.cmd_offset = cmd_offset - self.color_offset = color_offset - self.chunk_pos = chunk_pos - - self.pcolor.push_back(row_data) - -cdef class SMXOutlineLayer(SMXLayer): - def __init__(self, layer_header, data): - super().__init__(layer_header, data) - # process cmd table - for i in range(self.row_count): - cmd_offset, color_offset, chunk_pos, row_data = create_color_row( - self, i) - self.cmd_offset = cmd_offset - self.color_offset = color_offset - self.chunk_pos = chunk_pos - - self.pcolor.push_back(row_data) - -cdef class SMXShadowLayer(SMXLayer): - def __init__(self, layer_header, data): - super().__init__(layer_header, data) - # process cmd table - for i in range(self.row_count): - cmd_offset, color_offset, chunk_pos, row_data = create_color_row( - self, i) - self.cmd_offset = cmd_offset - self.color_offset = color_offset - self.chunk_pos = chunk_pos - - self.pcolor.push_back(row_data) ctypedef fused SMXLayerVariant: @@ -246,15 +197,16 @@ cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant v # shadows sometimes need an extra pixel at # the end - if SMXLayerVariant is SMXShadowLayer and row_data.size() < expected_size: - # copy the last drawn pixel - # (still stored in nextbyte) - # - # TODO: confirm that this is the - # right way to do it - row_data.push_back(pixel(color_shadow, - nextbyte, 0, 0, 0)) - continue + if SMXLayerVariant is SMXShadowLayer: + if row_data.size() < expected_size: + # copy the last drawn pixel + # (still stored in nextbyte) + # + # TODO: confirm that this is the + # right way to do it + row_data.push_back(pixel(color_shadow, + nextbyte, 0, 0, 0)) + continue elif lower_crumb == 0b00000000: # skip command @@ -510,16 +462,14 @@ class SMX: """ smx_header = SMX.smx_header.unpack_from(data) - self.smp_type, version, frame_count, file_size_comp, \ + self.smp_type, version, frame_count, file_size_comp,\ file_size_uncomp, comment = smx_header dbg("SMX") dbg(" version: %s", version) dbg(" frame count: %s", frame_count) - # 0x20 = SMX header size - dbg(" file size compressed: %s B", file_size_comp + 0x20) - # 0x80 = SMP header size - dbg(" file size uncompressed: %s B", file_size_uncomp + 0x40) + dbg(" file size compressed: %s B", file_size_comp + 0x20) # 0x20 = SMX header size + dbg(" file size uncompressed: %s B", file_size_uncomp + 0x40) # 0x80 = SMP header size dbg(" comment: %s", comment.decode('ascii')) # SMX graphic frames are created from overlaying @@ -537,7 +487,7 @@ class SMX: frame_header = SMX.smx_frame_header.unpack_from( data, current_offset) - frame_type, palette_number, _ = frame_header + frame_type , palette_number, _ = frame_header current_offset += SMX.smx_frame_header.size @@ -556,7 +506,7 @@ class SMX: layer_header_data = SMX.smx_layer_header.unpack_from( data, current_offset) - width, height, hotspot_x, hotspot_y, \ + width, height, hotspot_x, hotspot_y,\ distance_next_frame, _ = layer_header_data current_offset += SMX.smx_layer_header.size @@ -566,14 +516,12 @@ class SMX: # Skip outline table current_offset += 4 * height - qdl_command_array_size = Struct( - "< I").unpack_from(data, current_offset)[0] + qdl_command_array_size = Struct("< I").unpack_from(data, current_offset)[0] current_offset += 4 # Read length of color table if layer_type is SMXLayerType.MAIN: - qdl_color_table_size = Struct( - "< I").unpack_from(data, current_offset)[0] + qdl_color_table_size = Struct("< I").unpack_from(data, current_offset)[0] current_offset += 4 qdl_color_table_offset = current_offset + qdl_command_array_size @@ -593,20 +541,16 @@ class SMX: if layer_type is SMXLayerType.MAIN: if layer_header.compression_type == 0x08: - self.main_frames.append( - SMXMainLayer8to5(layer_header, data)) + self.main_frames.append(SMXMainLayer8to5(layer_header, data)) elif layer_header.compression_type == 0x00: - self.main_frames.append( - SMXMainLayer4plus1(layer_header, data)) + self.main_frames.append(SMXMainLayer4plus1(layer_header, data)) elif layer_type is SMXLayerType.SHADOW: - self.shadow_frames.append( - SMXShadowLayer(layer_header, data)) + self.shadow_frames.append(SMXShadowLayer(layer_header, data)) elif layer_type is SMXLayerType.OUTLINE: - self.outline_frames.append( - SMXOutlineLayer(layer_header, data)) + self.outline_frames.append(SMXOutlineLayer(layer_header, data)) def get_frames(self, layer: int = 0): """ @@ -826,10 +770,68 @@ cdef class SMXLayer: return repr(self.info) +cdef class SMXMainLayer8to5(SMXLayer): + """ + Compressed SMP layer (compression type 8to5) for the main graphics sprite. + """ + + def __init__(self, layer_header, data): + super().__init__(layer_header, data) + # process cmd table + for i in range(self.row_count): + cmd_offset, color_offset, chunk_pos, row_data = create_color_row( + self, i) + self.cmd_offset = cmd_offset + self.color_offset = color_offset + self.chunk_pos = chunk_pos + + self.pcolor.push_back(row_data) + +cdef class SMXMainLayer4plus1(SMXLayer): + def __init__(self, layer_header, data): + super().__init__(layer_header, data) + # process cmd table + for i in range(self.row_count): + cmd_offset, color_offset, chunk_pos, row_data = create_color_row( + self, i) + self.cmd_offset = cmd_offset + self.color_offset = color_offset + self.chunk_pos = chunk_pos + + self.pcolor.push_back(row_data) + +cdef class SMXOutlineLayer(SMXLayer): + def __init__(self, layer_header, data): + super().__init__(layer_header, data) + # process cmd table + for i in range(self.row_count): + cmd_offset, color_offset, chunk_pos, row_data = create_color_row( + self, i) + self.cmd_offset = cmd_offset + self.color_offset = color_offset + self.chunk_pos = chunk_pos + + self.pcolor.push_back(row_data) + +cdef class SMXShadowLayer(SMXLayer): + def __init__(self, layer_header, data): + super().__init__(layer_header, data) + # process cmd table + for i in range(self.row_count): + cmd_offset, color_offset, chunk_pos, row_data = create_color_row( + self, i) + self.cmd_offset = cmd_offset + self.color_offset = color_offset + self.chunk_pos = chunk_pos + + self.pcolor.push_back(row_data) + + + @cython.boundscheck(False) @cython.wraparound(False) -cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] & image_matrix, - numpy.ndarray[numpy.uint8_t, ndim= 2, mode = "c"] palette): +cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix, + numpy.ndarray[numpy.uint8_t, ndim=2, mode="c"] palette): """ converts a palette index image matrix to an rgba matrix. @@ -840,7 +842,7 @@ cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] & image_matrix, cdef size_t height = image_matrix.size() cdef size_t width = image_matrix[0].size() - cdef numpy.ndarray[numpy.uint8_t, ndim= 3, mode = "c"] array_data = \ + cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode="c"] array_data = \ numpy.zeros((height, width, 4), dtype=numpy.uint8) cdef uint8_t[:, ::1] m_lookup = palette @@ -929,7 +931,7 @@ cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] & image_matrix, @cython.boundscheck(False) @cython.wraparound(False) -cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] & image_matrix): +cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] &image_matrix): """ converts the damage modifier values to an image using the RG values. @@ -939,7 +941,7 @@ cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] & image_matrix) cdef size_t height = image_matrix.size() cdef size_t width = image_matrix[0].size() - cdef numpy.ndarray[numpy.uint8_t, ndim= 3, mode = "c"] array_data = \ + cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode="c"] array_data = \ numpy.zeros((height, width, 4), dtype=numpy.uint8) cdef uint8_t r = 0 From c8b98a2719f5198cf9d678dbd2d95921a6528069 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Thu, 16 Jan 2025 23:26:07 +0000 Subject: [PATCH 705/771] restructure --- .../convert/value_object/read/media/smp.pyx | 423 +++++----- .../convert/value_object/read/media/smx.pyx | 794 +++++++++--------- 2 files changed, 592 insertions(+), 625 deletions(-) diff --git a/openage/convert/value_object/read/media/smp.pyx b/openage/convert/value_object/read/media/smp.pyx index c52aa1a194..9ad253c5c3 100644 --- a/openage/convert/value_object/read/media/smp.pyx +++ b/openage/convert/value_object/read/media/smp.pyx @@ -253,6 +253,11 @@ class SMPLayerHeader: ) return "".join(ret) +ctypedef fused SMPLayerVariant: + SMPMainLayer + SMPShadowLayer + SMPOutlineLayer + cdef class SMPLayer: """ @@ -283,20 +288,15 @@ cdef class SMPLayer: # pixel matrix representing the final image cdef vector[vector[pixel]] pcolor - # memory pointer - cdef const uint8_t[::1] data_raw - - # rows of image - cdef size_t row_count - - def __init__(self, layer_header, data): + def init(self, SMPLayerVariant variant, layer_header, data): self.info = layer_header if not (isinstance(data, bytes) or isinstance(data, bytearray)): raise ValueError("Layer data must be some bytes object") + # memory pointer # convert the bytes obj to char* - self.data_raw = data + cdef const uint8_t[::1] data_raw = data cdef unsigned short left cdef unsigned short right @@ -304,11 +304,11 @@ cdef class SMPLayer: cdef size_t i cdef int cmd_offset - self.row_count = self.info.size[1] - self.pcolor.reserve(self.row_count) + cdef size_t row_count = self.info.size[1] + self.pcolor.reserve(row_count) # process bondary table - for i in range(self.row_count): + for i in range(row_count): outline_entry_position = (self.info.outline_table_offset + i * SMPLayer.smp_frame_row_edge.size) @@ -323,7 +323,7 @@ cdef class SMPLayer: self.boundaries.push_back(boundary_def(left, right, False)) # process cmd table - for i in range(self.row_count): + for i in range(row_count): cmd_table_position = (self.info.qdl_table_offset + i * SMPLayer.smp_command_offset.size) @@ -331,257 +331,253 @@ cdef class SMPLayer: data, cmd_table_position)[0] + self.info.frame_offset self.cmd_offsets.push_back(cmd_offset) - def get_picture_data(self, palette): - """ - Convert the palette index matrix to a colored image. - """ - return determine_rgba_matrix(self.pcolor, palette) + for i in range(row_count): + self.pcolor.push_back(self.create_color_row(variant, data_raw, i)) - def get_hotspot(self): + cdef vector[pixel] create_color_row(self, + SMPLayerVariant variant, + const uint8_t[::1] &data_raw, + Py_ssize_t rowid): """ - Return the layer's hotspot (the "center" of the image) + extract colors (pixels) for the given rowid. """ - return self.info.hotspot - def get_palette_number(self): - """ - Return the layer's palette number. + cdef vector[pixel] row_data + cdef Py_ssize_t i - :return: Palette number of the layer. - :rtype: int - """ - return self.pcolor[0][0].palette & 0b00111111 + first_cmd_offset = self.cmd_offsets[rowid] + cdef boundary_def bounds = self.boundaries[rowid] + cdef size_t pixel_count = self.info.size[0] - def __repr__(self): - return repr(self.info) + # preallocate memory + row_data.reserve(pixel_count) + # row is completely transparent + if bounds.full_row: + for _ in range(pixel_count): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) -cdef class SMPMainLayer(SMPLayer): - def __init__(self, layer_header, data): - super().__init__(layer_header, data) + return row_data - for i in range(self.row_count): - self.pcolor.push_back(create_color_row(self, i)) + # start drawing the left transparent space + for i in range(bounds.left): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - def get_damage_mask(self): - """ - Convert the 4th pixel byte to a mask used for damaged units. - """ - return determine_damage_matrix(self.pcolor) + # process the drawing commands for this row. + self.process_drawing_cmds(variant, + data_raw, + row_data, rowid, + first_cmd_offset, + pixel_count - bounds.right) -cdef class SMPShadowLayer(SMPLayer): - def __init__(self, layer_header, data): - super().__init__(layer_header, data) + # finish by filling up the right transparent space + for i in range(bounds.right): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - for i in range(self.row_count): - self.pcolor.push_back(create_color_row(self, i)) + # verify size of generated row + if row_data.size() != pixel_count: + got = row_data.size() + summary = ( + f"{got:d}/{pixel_count:d} -> row {rowid:d}, " + f"layer type {self.info.layer_type:x}, " + f"offset {first_cmd_offset:d} / {first_cmd_offset:#x}" + ) + message = ( + f"got {'LESS' if got < pixel_count else 'MORE'} pixels than expected: {summary}, " + f"missing: {abs(pixel_count - got):d}" + ) -cdef class SMPOutlineLayer(SMPLayer): - def __init__(self, layer_header, data): - super().__init__(layer_header, data) + raise Exception(message) - for i in range(self.row_count): - self.pcolor.push_back(create_color_row(self, i)) + return row_data -ctypedef fused SMPLayerVariant: - SMPMainLayer - SMPShadowLayer - SMPOutlineLayer + @cython.boundscheck(False) + cdef void process_drawing_cmds(self, + SMPLayerVariant variant, + const uint8_t[::1] &data_raw, + vector[pixel] &row_data, + Py_ssize_t rowid, + Py_ssize_t first_cmd_offset, + size_t expected_size): + """ + extract colors (pixels) for the drawing commands + found for this row in the SMP frame. + """ + # position in the data blob, we start at the first command of this row + cdef Py_ssize_t dpos = first_cmd_offset -@cython.boundscheck(False) -cdef vector[pixel] create_color_row(SMPLayerVariant variant, - Py_ssize_t rowid): - """ - extract colors (pixels) for the given rowid. - """ - cdef vector[pixel] row_data - cdef Py_ssize_t i + # is the end of the current row reached? + cdef bool eor = False - cdef Py_ssize_t first_cmd_offset = variant.cmd_offsets[rowid] - cdef boundary_def bounds = variant.boundaries[rowid] - cdef size_t pixel_count = variant.info.size[0] + cdef uint8_t cmd + cdef uint8_t nextbyte + cdef uint8_t lower_crumb + cdef int pixel_count - # preallocate memory - row_data.reserve(pixel_count) + cdef vector[uint8_t] pixel_data + pixel_data.reserve(4) - # row is completely transparent - if bounds.full_row: - for _ in range(pixel_count): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) + # work through commands till end of row. + while not eor: + if row_data.size() > expected_size: + raise Exception( + f"Only {expected_size:d} pixels should be drawn in row {rowid:d} " + + f"with layer type {self.info.layer_type:#x}, but we have {row_data.size():d}" + ) - return row_data + # fetch drawing instruction + cmd = data_raw[dpos] - # start drawing the left transparent space - for i in range(bounds.left): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - # process the drawing commands for this row. - process_drawing_cmds(variant, variant.data_raw, - row_data, rowid, - first_cmd_offset, - pixel_count - bounds.right) - - # finish by filling up the right transparent space - for i in range(bounds.right): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - # verify size of generated row - if row_data.size() != pixel_count: - got = row_data.size() - summary = ( - f"{got:d}/{pixel_count:d} -> row {rowid:d}, " - f"layer type {variant.info.layer_type:x}, " - f"offset {first_cmd_offset:d} / {first_cmd_offset:#x}" - ) - message = ( - f"got {'LESS' if got < pixel_count else 'MORE'} pixels than expected: {summary}, " - f"missing: {abs(pixel_count - got):d}" - ) + # Last 2 bits store command type + lower_crumb = 0b00000011 & cmd - raise Exception(message) + # opcode: cmd, rowid: rowid - return row_data + if lower_crumb == 0b00000011: + # eol (end of line) command, this row is finished now. + eor = True + if SMPLayerVariant is SMPShadowLayer and row_data.size() < expected_size: + # copy the last drawn pixel + # (still stored in nextbyte) + # + # TODO: confirm that this is the + # right way to do it + row_data.push_back(pixel(color_shadow, nextbyte, 0, 0, 0)) -@cython.boundscheck(False) -cdef void process_drawing_cmds(SMPLayerVariant variant, - const uint8_t[::1] & data_raw, - vector[pixel] & row_data, - Py_ssize_t rowid, - Py_ssize_t first_cmd_offset, - size_t expected_size): + continue - """ - extract colors (pixels) for the drawing commands - found for this row in the SMP frame. - """ - - # position in the data blob, we start at the first command of this row - cdef Py_ssize_t dpos = first_cmd_offset - - # is the end of the current row reached? - cdef bool eor = False - - cdef uint8_t cmd = 0 - cdef uint8_t nextbyte = 0 - cdef uint8_t lower_crumb = 0 - cdef int pixel_count = 0 - - cdef vector[uint8_t] pixel_data - pixel_data.reserve(4) - - # work through commands till end of row. - while not eor: - if row_data.size() > expected_size: - raise Exception( - f"Only {expected_size:d} pixels should be drawn in row {rowid:d} " + - f"with layer type {variant.info.layer_type:#x}, but we have {row_data.size():d} already!" - ) + elif lower_crumb == 0b00000000: + # skip command + # draw 'count' transparent pixels + # count = (cmd >> 2) + 1 - # fetch drawing instruction - cmd = data_raw[dpos] + pixel_count = (cmd >> 2) + 1 - # Last 2 bits store command type - lower_crumb = 0b00000011 & cmd - # opcode: cmd, rowid: rowid + for _ in range(pixel_count): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - if lower_crumb == 0b00000011: - # eol (end of line) command, this row is finished now. - eor = True + elif lower_crumb == 0b00000001: + # color_list command + # draw the following 'count' pixels + # pixels are stored as 4 byte palette and meta infos + # count = (cmd >> 2) + 1 - if SMPLayerVariant is SMPShadowLayer and row_data.size() < expected_size: - # copy the last drawn pixel - # (still stored in nextbyte) - # - # TODO: confirm that this is the - # right way to do it - row_data.push_back(pixel(color_shadow, nextbyte, 0, 0, 0)) + pixel_count = (cmd >> 2) + 1 - continue + for _ in range(pixel_count): + if SMPLayerVariant is SMPShadowLayer: + dpos += 1 + nextbyte = data_raw[dpos] + row_data.push_back(pixel(color_shadow, nextbyte, 0, 0, 0)) + elif SMPLayerVariant is SMPOutlineLayer: + # we don't know the color the game wants + # so we just draw index 0 + row_data.push_back(pixel(color_outline, 0, 0, 0, 0)) + elif SMPLayerVariant is SMPMainLayer: + for _ in range(4): + dpos += 1 + pixel_data.push_back(data_raw[dpos]) + + row_data.push_back(pixel(color_standard, + pixel_data[0], + pixel_data[1], + pixel_data[2], + pixel_data[3] & 0x1F)) # remove "usage" bit here + pixel_data.clear() + + elif lower_crumb == 0b00000010 and (SMPLayerVariant is SMPMainLayer or SMPLayerVariant is SMPShadowLayer): + # player_color command + # draw the following 'count' pixels + # pixels are stored as 4 byte palette and meta infos + # count = (cmd >> 2) + 1 + + pixel_count = (cmd >> 2) + 1 + + for _ in range(pixel_count): + if SMPLayerVariant is SMPShadowLayer: + dpos += 1 + nextbyte = data_raw[dpos] + row_data.push_back(pixel(color_shadow, + nextbyte, 0, 0, 0)) + else: + for _ in range(4): + dpos += 1 + pixel_data.push_back(data_raw[dpos]) + + row_data.push_back(pixel(color_player, + pixel_data[0], + pixel_data[1], + pixel_data[2], + pixel_data[3] & 0x1F)) # remove "usage" bit here + pixel_data.clear() - elif lower_crumb == 0b00000000: - # skip command - # draw 'count' transparent pixels - # count = (cmd >> 2) + 1 + else: + raise Exception( + f"unknown smp {self.info.layer_type} layer drawing command: " + + f"{cmd:#x} in row {rowid:d}" + ) - pixel_count = (cmd >> 2) + 1 + # process next command + dpos += 1 - for _ in range(pixel_count): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) + # end of row reached, return the created pixel array. + return - elif lower_crumb == 0b00000001: - # color_list command - # draw the following 'count' pixels - # pixels are stored as 4 byte palette and meta infos - # count = (cmd >> 2) + 1 - pixel_count = (cmd >> 2) + 1 + def get_picture_data(self, palette): + """ + Convert the palette index matrix to a colored image. + """ + return determine_rgba_matrix(self.pcolor, palette) - for _ in range(pixel_count): - if SMPLayerVariant is SMPShadowLayer: - dpos += 1 - nextbyte = data_raw[dpos] - row_data.push_back(pixel(color_shadow, - nextbyte, 0, 0, 0)) - elif SMPLayerVariant is SMPOutlineLayer: - # we don't know the color the game wants - # so we just draw index 0 - row_data.push_back(pixel(color_outline, - 0, 0, 0, 0)) - elif SMPLayerVariant is SMPMainLayer: - for _ in range(4): - dpos += 1 - pixel_data.push_back(data_raw[dpos]) + def get_hotspot(self): + """ + Return the layer's hotspot (the "center" of the image) + """ + return self.info.hotspot - row_data.push_back(pixel(color_standard, - pixel_data[0], - pixel_data[1], - pixel_data[2], - pixel_data[3] & 0x1F)) # remove "usage" bit here - pixel_data.clear() + def get_palette_number(self): + """ + Return the layer's palette number. - elif lower_crumb == 0b00000010 and (SMPLayerVariant is SMPMainLayer or SMPLayerVariant is SMPShadowLayer): - # player_color command - # draw the following 'count' pixels - # pixels are stored as 4 byte palette and meta infos - # count = (cmd >> 2) + 1 + :return: Palette number of the layer. + :rtype: int + """ + return self.pcolor[0][0].palette & 0b00111111 - pixel_count = (cmd >> 2) + 1 + def __repr__(self): + return repr(self.info) - for _ in range(pixel_count): - if SMPLayerVariant is SMPShadowLayer: - dpos += 1 - nextbyte = data_raw[dpos] - row_data.push_back(pixel(color_shadow, - nextbyte, 0, 0, 0)) - else: - for _ in range(4): - dpos += 1 - pixel_data.push_back(data_raw[dpos]) - row_data.push_back(pixel(color_player, - pixel_data[0], - pixel_data[1], - pixel_data[2], - pixel_data[3] & 0x1F)) # remove "usage" bit here - pixel_data.clear() +cdef class SMPMainLayer(SMPLayer): + """ + SMPLayer for the main graphics sprite. + """ - else: - raise Exception( - f"unknown smp {variant.info.layer_type} layer drawing command: " + - f"{cmd:#x} in row {rowid:d}" - ) + def __init__(self, layer_header, data): + self.init(self ,layer_header, data) - # process next command - dpos += 1 + def get_damage_mask(self): + """ + Convert the 4th pixel byte to a mask used for damaged units. + """ + return determine_damage_matrix(self.pcolor) - # end of row reached, return the created pixel array. - return +cdef class SMPShadowLayer(SMPLayer): + """ + SMPLayer for the shadow graphics. + """ + def __init__(self, layer_header, data): + self.init(self ,layer_header, data) +cdef class SMPOutlineLayer(SMPLayer): + def __init__(self, layer_header, data): + self.init(self, layer_header, data) @cython.boundscheck(False) @@ -690,7 +686,6 @@ cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix, return array_data - @cython.boundscheck(False) @cython.wraparound(False) cdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] &image_matrix): diff --git a/openage/convert/value_object/read/media/smx.pyx b/openage/convert/value_object/read/media/smx.pyx index 52e17428c8..2e60a84a00 100644 --- a/openage/convert/value_object/read/media/smx.pyx +++ b/openage/convert/value_object/read/media/smx.pyx @@ -68,357 +68,6 @@ ctypedef fused SMXLayerVariant: SMXShadowLayer -cdef inline(int, int, int, vector[pixel]) create_color_row(SMXLayerVariant variant, - Py_ssize_t rowid): - """ - Extract colors (pixels) for the given rowid. - - :param rowid: Index of the current row in the layer. - :param cmd_offset: Offset of the command table of the layer. - :param color_offset: Offset of the color table of the layer. - :param chunk_pos: Current position in the compressed chunk. - """ - - cdef vector[pixel] row_data - cdef Py_ssize_t i - - cdef int first_cmd_offset = variant.cmd_offset - cdef int first_color_offset = variant.color_offset - cdef int first_chunk_pos = variant.chunk_pos - cdef boundary_def bounds = variant.boundaries[rowid] - cdef size_t pixel_count = variant.info.size[0] - - # preallocate memory - row_data.reserve(pixel_count) - - # row is completely transparent - if bounds.full_row: - for _ in range(pixel_count): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - return variant.cmd_offset, variant.color_offset, variant.chunk_pos, row_data - - # start drawing the left transparent space - for i in range(bounds.left): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - # process the drawing commands for this row. - next_cmd_offset, next_color_offset, next_chunk_pos, row_data = \ - process_drawing_cmds(variant, - variant.data_raw, - row_data, - rowid, - first_cmd_offset, - first_color_offset, - first_chunk_pos, - pixel_count - bounds.right - ) - - # finish by filling up the right transparent space - for i in range(bounds.right): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - # verify size of generated row - if row_data.size() != pixel_count: - got = row_data.size() - summary = "%d/%d -> row %d, layer type %s, offset %d / %#x" % ( - got, pixel_count, rowid, variant.info.layer_type, - first_cmd_offset, first_cmd_offset - ) - txt = "got %%s pixels than expected: %s, missing: %d" % ( - summary, abs(pixel_count - got)) - - raise Exception(txt % ("LESS" if got < pixel_count else "MORE")) - - return next_cmd_offset, next_color_offset, next_chunk_pos, row_data - - -@cython.boundscheck(False) -cdef inline(int, int, int, vector[pixel]) process_drawing_cmds(SMXLayerVariant variant, - const uint8_t[::1] & data_raw, - vector[pixel] & row_data, - Py_ssize_t rowid, - Py_ssize_t first_cmd_offset, - Py_ssize_t first_color_offset, - int chunk_pos, - size_t expected_size): - """ - TODO: docstring - """ - # position in the command array, we start at the first command of this row - cdef Py_ssize_t dpos_cmd = first_cmd_offset - - # is the end of the current row reached? - cdef bool eor = False - - cdef uint8_t cmd = 0 - cdef uint8_t lower_crumb = 0 - cdef int pixel_count = 0 - cdef Py_ssize_t dpos_color = 0 - cdef vector[uint8_t] pixel_data - # Pixel data temporary values that need further decompression - cdef uint8_t pixel_data_odd_0 = 0 - cdef uint8_t pixel_data_odd_1 = 0 - cdef uint8_t pixel_data_odd_2 = 0 - cdef uint8_t pixel_data_odd_3 = 0 - # Mask for even indices - # cdef uint8_t pixel_mask_even_0 = 0xFF - cdef uint8_t pixel_mask_even_1 = 0b00000011 - cdef uint8_t pixel_mask_even_2 = 0b11110000 - cdef uint8_t pixel_mask_even_3 = 0b00111111 - - if SMXLayerVariant is SMXMainLayer8to5 or SMXLayerVariant is SMXMainLayer4plus1: - # Position in the pixel data array - dpos_color = first_color_offset - - cdef uint8_t palette_section_block = 0 - cdef uint8_t palette_section = 0 - cdef uint8_t nextbyte = 0 - - # work through commands till end of row. - while not eor: - if row_data.size() > expected_size: - raise Exception( - f"Only {expected_size:d} pixels should be drawn in row {rowid:d} " - f"with layer type {variant:#x}, but we have {row_data.size():d} " - f"already!" - ) - - # fetch drawing instruction - cmd = data_raw[dpos_cmd] - - # Last 2 bits store command type - lower_crumb = 0b00000011 & cmd - if lower_crumb == 0b00000011: - # eor (end of row) command, this row is finished now. - - eor = True - dpos_cmd += 1 - - # shadows sometimes need an extra pixel at - # the end - if SMXLayerVariant is SMXShadowLayer: - if row_data.size() < expected_size: - # copy the last drawn pixel - # (still stored in nextbyte) - # - # TODO: confirm that this is the - # right way to do it - row_data.push_back(pixel(color_shadow, - nextbyte, 0, 0, 0)) - continue - - elif lower_crumb == 0b00000000: - # skip command - # draw 'count' transparent pixels - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) - - elif lower_crumb == 0b00000001: - # color_list command - # draw the following 'count' pixels - # pixels are stored in 5 byte chunks - # even pixel indices have their info stored - # in byte[0] - byte[3]. odd pixel indices have - # their info stored in byte[1] - byte[4]. - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - if SMXLayerVariant is SMXMainLayer8to5: - pixel_data.reserve(4) - for _ in range(pixel_count): - # Start fetching pixel data - if chunk_pos: - # Odd indices require manual extraction of each of the 4 values - - # Palette index. Essentially a rotation of (byte[1]byte[2]) - # by 6 to the left, then masking with 0x00FF. - pixel_data_odd_0 = data_raw[dpos_color + 1] - pixel_data_odd_1 = data_raw[dpos_color + 2] - pixel_data.push_back( - (pixel_data_odd_0 >> 2) | (pixel_data_odd_1 << 6)) - - # Palette section. Described in byte[2] in bits 4-5. - pixel_data.push_back((pixel_data_odd_1 >> 2) & 0x03) - - # Damage mask 1. Essentially a rotation of (byte[3]byte[4]) - # by 6 to the left, then masking with 0x00F0. - pixel_data_odd_2 = data_raw[dpos_color + 3] - pixel_data_odd_3 = data_raw[dpos_color + 4] - pixel_data.push_back( - ((pixel_data_odd_2 >> 2) | (pixel_data_odd_3 << 6)) & 0xF0) - - # Damage mask 2. Described in byte[4] in bits 0-5. - pixel_data.push_back((pixel_data_odd_3 >> 2) & 0x3F) - - row_data.push_back(pixel(color_standard, - pixel_data[0], - pixel_data[1], - pixel_data[2], - pixel_data[3])) - - # Go to next pixel - dpos_color += 5 - - else: - # Even indices can be read "as is". They just have to be masked. - for i in range(4): - pixel_data.push_back(data_raw[dpos_color + i]) - - row_data.push_back(pixel(color_standard, - pixel_data[0], - pixel_data[1] & pixel_mask_even_1, - pixel_data[2] & pixel_mask_even_2, - pixel_data[3] & pixel_mask_even_3)) - - odd = not odd - pixel_data.clear() - - if SMXLayerVariant is SMXMainLayer4plus1: - palette_section_block = data_raw[dpos_color + (4 - chunk_pos)] - - for _ in range(pixel_count): - # Start fetching pixel data - palette_section = ( - palette_section_block >> (2 * chunk_pos)) & 0x03 - row_data.push_back(pixel(color_standard, - data_raw[dpos_color], - palette_section, - 0, - 0)) - - dpos_color += 1 - chunk_pos += 1 - - # Skip to next chunk - if chunk_pos > 3: - chunk_pos = 0 - dpos_color += 1 # Skip palette section block - palette_section_block = data_raw[dpos_color + 4] - - if SMXLayerVariant is SMXShadowLayer: - for _ in range(pixel_count): - dpos_cmd += 1 - nextbyte = data_raw[dpos_cmd] - - row_data.push_back(pixel(color_shadow, - nextbyte, 0, 0, 0)) - - if SMXLayerVariant is SMXOutlineLayer: - # we don't know the color the game wants - # so we just draw index 0 - row_data.push_back(pixel(color_outline, - 0, 0, 0, 0)) - - elif lower_crumb == 0b00000010: - if SMXLayerVariant is SMXMainLayer8to5: - # player_color command - # draw the following 'count' pixels - # pixels are stored in 5 byte chunks - # even pixel indices have their info stored - # in byte[0] - byte[3]. odd pixel indices have - # their info stored in byte[1] - byte[4]. - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - # Start fetching pixel data - if odd: - # Odd indices require manual extraction of each of the 4 values - - # Palette index. Essentially a rotation of (byte[1]byte[2]) - # by 6 to the left, then masking with 0x00FF. - pixel_data_odd_0 = data_raw[dpos_color + 1] - pixel_data_odd_1 = data_raw[dpos_color + 2] - pixel_data.push_back( - (pixel_data_odd_0 >> 2) | (pixel_data_odd_1 << 6)) - - # Palette section. Described in byte[2] in bits 4-5. - pixel_data.push_back((pixel_data_odd_1 >> 2) & 0x03) - - # Damage modifier 1. Essentially a rotation of (byte[3]byte[4]) - # by 6 to the left, then masking with 0x00F0. - pixel_data_odd_2 = data_raw[dpos_color + 3] - pixel_data_odd_3 = data_raw[dpos_color + 4] - pixel_data.push_back( - ((pixel_data_odd_2 >> 2) | (pixel_data_odd_3 << 6)) & 0xF0) - - # Damage modifier 2. Described in byte[4] in bits 0-5. - pixel_data.push_back((pixel_data_odd_3 >> 2) & 0x3F) - - row_data.push_back(pixel(color_player, - pixel_data[0], - pixel_data[1], - pixel_data[2], - pixel_data[3])) - - # Go to next pixel - dpos_color += 5 - - else: - # Even indices can be read "as is". They just have to be masked. - for px_dpos in range(4): - pixel_data.push_back( - data_raw[dpos_color + px_dpos]) - - row_data.push_back(pixel(color_player, - pixel_data[0], - pixel_data[1] & pixel_mask_even_1, - pixel_data[2] & pixel_mask_even_2, - pixel_data[3] & pixel_mask_even_3)) - - odd = not odd - pixel_data.clear() - - elif SMXLayerVariant is SMXMainLayer4plus1: - # player_color command - # draw the following 'count' pixels - # 4 pixels are stored in every 5 byte chunk. - # palette indices are contained in byte[0] - byte[3] - # palette sections are stored in byte[4] - # count = (cmd >> 2) + 1 - - pixel_count = (cmd >> 2) + 1 - - for _ in range(pixel_count): - # Start fetching pixel data - palette_section = ( - palette_section_block >> (2 * chunk_pos)) & 0x03 - row_data.push_back(pixel(color_player, - data_raw[dpos_color], - palette_section, - 0, - 0)) - - dpos_color += 1 - chunk_pos += 1 - - # Skip to next chunk - if chunk_pos > 3: - chunk_pos = 0 - dpos_color += 1 # Skip palette section block - palette_section_block = data_raw[dpos_color + 4] - - else: - raise Exception( - f"unknown smx main graphics layer drawing command: " + - f"{cmd:#x} in row {rowid:d}" - ) - - # Process next command - dpos_cmd += 1 - - if SMXLayerVariant is SMXMainLayer8to5 or SMXLayerVariant is SMXMainLayer4plus1: - return dpos_cmd, dpos_color, chunk_pos, row_data - if SMXLayerVariant is SMXOutlineLayer or SMXLayerVariant is SMXShadowLayer: - return dpos_cmd, dpos_cmd, chunk_pos, row_data - class SMX: """ @@ -678,22 +327,7 @@ cdef class SMXLayer: # pixel matrix representing the final image cdef vector[vector[pixel]] pcolor - # memory pointer - cdef const uint8_t[::1] data_raw - - # current command - cdef int cmd_offset - - # current color - cdef int color_offset - - # current chunk position - cdef int chunk_pos - - # rows - cdef size_t row_count - - def __init__(self, layer_header, data): + def init(self, SMXLayerVariant variant, layer_header, data): """ SMX layer definition superclass. There can be various types of layers inside an SMX frame. @@ -708,18 +342,19 @@ cdef class SMXLayer: if not (isinstance(data, bytes) or isinstance(data, bytearray)): raise ValueError("Layer data must be some bytes object") + # memory pointer # convert the bytes obj to char* - self.data_raw = data + cdef const uint8_t[::1] data_raw = data cdef unsigned short left cdef unsigned short right cdef size_t i - self.row_count = self.info.size[1] - self.pcolor.reserve(self.row_count) + cdef size_t row_count = self.info.size[1] + self.pcolor.reserve(row_count) # process bondary table - for i in range(self.row_count): + for i in range(row_count): outline_entry_position = (self.info.outline_table_offset + i * SMXLayer.smp_layer_row_edge.size) @@ -733,9 +368,379 @@ cdef class SMXLayer: else: self.boundaries.push_back(boundary_def(left, right, False)) - self.cmd_offset = self.info.qdl_command_table_offset - self.color_offset = self.info.qdl_color_table_offset - self.chunk_pos = 0 + cdef int cmd_offset = self.info.qdl_command_table_offset + cdef int color_offset = self.info.qdl_color_table_offset + cdef int chunk_pos = 0 + + # process cmd table + for i in range(row_count): + cmd_offset, color_offset, chunk_pos, row_data = \ + self.create_color_row(variant, data_raw, i, cmd_offset, color_offset, chunk_pos) + + self.pcolor.push_back(row_data) + + + cdef inline (int, int, int, vector[pixel]) create_color_row(self, + SMXLayerVariant variant, + const uint8_t[::1] &data_raw, + Py_ssize_t rowid, + int cmd_offset, + int color_offset, + int chunk_pos): + """ + Extract colors (pixels) for the given rowid. + + :param rowid: Index of the current row in the layer. + :param cmd_offset: Offset of the command table of the layer. + :param color_offset: Offset of the color table of the layer. + :param chunk_pos: Current position in the compressed chunk. + """ + + cdef vector[pixel] row_data + cdef Py_ssize_t i + + cdef int first_cmd_offset = cmd_offset + cdef int first_color_offset = color_offset + cdef boundary_def bounds = self.boundaries[rowid] + cdef size_t pixel_count = self.info.size[0] + + # preallocate memory + row_data.reserve(pixel_count) + + # row is completely transparent + if bounds.full_row: + for _ in range(pixel_count): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) + + return cmd_offset, color_offset, chunk_pos, row_data + + # start drawing the left transparent space + for i in range(bounds.left): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) + + # process the drawing commands for this row. + next_cmd_offset, next_color_offset, chunk_pos, row_data = \ + self.process_drawing_cmds( + variant, + data_raw, + row_data, + rowid, + first_cmd_offset, + first_color_offset, + chunk_pos, + pixel_count - bounds.right + ) + + # finish by filling up the right transparent space + for i in range(bounds.right): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) + + # verify size of generated row + if row_data.size() != pixel_count: + got = row_data.size() + summary = "%d/%d -> row %d, layer type %d, offset %d / %#x" % ( + got, pixel_count, rowid, self.info.layer_type, + first_cmd_offset, first_cmd_offset + ) + txt = "got %%s pixels than expected: %s, missing: %d" % ( + summary, abs(pixel_count - got)) + + raise Exception(txt % ("LESS" if got < pixel_count else "MORE")) + + return next_cmd_offset, next_color_offset, chunk_pos, row_data + + + @cython.boundscheck(False) + cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(self, + SMXLayerVariant variant, + const uint8_t[::1] &data_raw, + vector[pixel] &row_data, + Py_ssize_t rowid, + Py_ssize_t first_cmd_offset, + Py_ssize_t first_color_offset, + int chunk_pos, + size_t expected_size): + """ + extract colors (pixels) for the drawing commands that were + compressed with 8to5 compression. + """ + # position in the command array, we start at the first command of this row + cdef Py_ssize_t dpos_cmd = first_cmd_offset + + # Position in the pixel data array + cdef Py_ssize_t dpos_color = first_color_offset + + # Position in the compression chunk. + cdef bool odd = chunk_pos + cdef int px_dpos = 0 # For loop iterator + + # is the end of the current row reached? + cdef bool eor = False + + cdef uint8_t cmd = 0 + cdef uint8_t lower_crumb = 0 + cdef int pixel_count = 0 + cdef vector[uint8_t] pixel_data + pixel_data.reserve(4) + + # Pixel data temporary values that need further decompression + cdef uint8_t pixel_data_odd_0 = 0 + cdef uint8_t pixel_data_odd_1 = 0 + cdef uint8_t pixel_data_odd_2 = 0 + cdef uint8_t pixel_data_odd_3 = 0 + + # Mask for even indices + # cdef uint8_t pixel_mask_even_0 = 0xFF + cdef uint8_t pixel_mask_even_1 = 0b00000011 + cdef uint8_t pixel_mask_even_2 = 0b11110000 + cdef uint8_t pixel_mask_even_3 = 0b00111111 + + if SMXLayerVariant is SMXMainLayer8to5 or SMXLayerVariant is SMXMainLayer4plus1: + # Position in the pixel data array + dpos_color = first_color_offset + + cdef uint8_t palette_section_block = 0 + cdef uint8_t palette_section = 0 + cdef uint8_t nextbyte = 0 + + # work through commands till end of row. + while not eor: + if row_data.size() > expected_size: + raise Exception( + f"Only {expected_size:d} pixels should be drawn in row {rowid:d} " + f"with layer type {self.info.layer_type:#x}, but we have {row_data.size():d} " + f"already!" + ) + + # fetch drawing instruction + cmd = data_raw[dpos_cmd] + + # Last 2 bits store command type + lower_crumb = 0b00000011 & cmd + + if lower_crumb == 0b00000011: + # eor (end of row) command, this row is finished now. + eor = True + dpos_cmd += 1 + + # shadows sometimes need an extra pixel at + # the end + if SMXLayerVariant is SMXShadowLayer: + if row_data.size() < expected_size: + # copy the last drawn pixel + # (still stored in nextbyte) + # + # TODO: confirm that this is the + # right way to do it + row_data.push_back(pixel(color_shadow, + nextbyte, 0, 0, 0)) + continue + + elif lower_crumb == 0b00000000: + # skip command + # draw 'count' transparent pixels + # count = (cmd >> 2) + 1 + + pixel_count = (cmd >> 2) + 1 + + for _ in range(pixel_count): + row_data.push_back(pixel(color_transparent, 0, 0, 0, 0)) + + elif lower_crumb == 0b00000001: + # color_list command + # draw the following 'count' pixels + # pixels are stored in 5 byte chunks + # even pixel indices have their info stored + # in byte[0] - byte[3]. odd pixel indices have + # their info stored in byte[1] - byte[4]. + # count = (cmd >> 2) + 1 + + pixel_count = (cmd >> 2) + 1 + + if SMXLayerVariant is SMXMainLayer8to5: + pixel_data.reserve(4) + for _ in range(pixel_count): + # Start fetching pixel data + if odd: + # Odd indices require manual extraction of each of the 4 values + + # Palette index. Essentially a rotation of (byte[1]byte[2]) + # by 6 to the left, then masking with 0x00FF. + pixel_data_odd_0 = data_raw[dpos_color + 1] + pixel_data_odd_1 = data_raw[dpos_color + 2] + pixel_data.push_back((pixel_data_odd_0 >> 2) | (pixel_data_odd_1 << 6)) + + # Palette section. Described in byte[2] in bits 4-5. + pixel_data.push_back((pixel_data_odd_1 >> 2) & 0x03) + + # Damage mask 1. Essentially a rotation of (byte[3]byte[4]) + # by 6 to the left, then masking with 0x00F0. + pixel_data_odd_2 = data_raw[dpos_color + 3] + pixel_data_odd_3 = data_raw[dpos_color + 4] + pixel_data.push_back(((pixel_data_odd_2 >> 2) | (pixel_data_odd_3 << 6)) & 0xF0) + + # Damage mask 2. Described in byte[4] in bits 0-5. + pixel_data.push_back((pixel_data_odd_3 >> 2) & 0x3F) + + row_data.push_back(pixel(color_standard, + pixel_data[0], + pixel_data[1], + pixel_data[2], + pixel_data[3])) + + # Go to next pixel + dpos_color += 5 + + else: + # Even indices can be read "as is". They just have to be masked. + for i in range(4): + pixel_data.push_back(data_raw[dpos_color + i]) + + row_data.push_back(pixel(color_standard, + pixel_data[0], + pixel_data[1] & pixel_mask_even_1, + pixel_data[2] & pixel_mask_even_2, + pixel_data[3] & pixel_mask_even_3)) + + odd = not odd + pixel_data.clear() + + if SMXLayerVariant is SMXMainLayer4plus1: + palette_section_block = data_raw[dpos_color + (4 - chunk_pos)] + + for _ in range(pixel_count): + # Start fetching pixel data + palette_section = ( + palette_section_block >> (2 * chunk_pos)) & 0x03 + row_data.push_back(pixel(color_standard, + data_raw[dpos_color], + palette_section, + 0, + 0)) + + dpos_color += 1 + chunk_pos += 1 + + # Skip to next chunk + if chunk_pos > 3: + chunk_pos = 0 + dpos_color += 1 # Skip palette section block + palette_section_block = data_raw[dpos_color + 4] + + if SMXLayerVariant is SMXShadowLayer: + for _ in range(pixel_count): + dpos_cmd += 1 + nextbyte = data_raw[dpos_cmd] + + row_data.push_back(pixel(color_shadow, + nextbyte, 0, 0, 0)) + + if SMXLayerVariant is SMXOutlineLayer: + # we don't know the color the game wants + # so we just draw index 0 + row_data.push_back(pixel(color_outline, + 0, 0, 0, 0)) + + elif lower_crumb == 0b00000010: + if SMXLayerVariant is SMXMainLayer8to5: + # player_color command + # draw the following 'count' pixels + # pixels are stored in 5 byte chunks + # even pixel indices have their info stored + # in byte[0] - byte[3]. odd pixel indices have + # their info stored in byte[1] - byte[4]. + # count = (cmd >> 2) + 1 + + pixel_count = (cmd >> 2) + 1 + + for _ in range(pixel_count): + # Start fetching pixel data + if odd: + # Odd indices require manual extraction of each of the 4 values + + # Palette index. Essentially a rotation of (byte[1]byte[2]) + # by 6 to the left, then masking with 0x00FF. + pixel_data_odd_0 = data_raw[dpos_color + 1] + pixel_data_odd_1 = data_raw[dpos_color + 2] + pixel_data.push_back((pixel_data_odd_0 >> 2) | (pixel_data_odd_1 << 6)) + + # Palette section. Described in byte[2] in bits 4-5. + pixel_data.push_back((pixel_data_odd_1 >> 2) & 0x03) + + # Damage modifier 1. Essentially a rotation of (byte[3]byte[4]) + # by 6 to the left, then masking with 0x00F0. + pixel_data_odd_2 = data_raw[dpos_color + 3] + pixel_data_odd_3 = data_raw[dpos_color + 4] + pixel_data.push_back(((pixel_data_odd_2 >> 2) | (pixel_data_odd_3 << 6)) & 0xF0) + + # Damage modifier 2. Described in byte[4] in bits 0-5. + pixel_data.push_back((pixel_data_odd_3 >> 2) & 0x3F) + + row_data.push_back(pixel(color_player, + pixel_data[0], + pixel_data[1], + pixel_data[2], + pixel_data[3])) + + # Go to next pixel + dpos_color += 5 + + else: + # Even indices can be read "as is". They just have to be masked. + for px_dpos in range(4): + pixel_data.push_back(data_raw[dpos_color + px_dpos]) + + row_data.push_back(pixel(color_player, + pixel_data[0], + pixel_data[1] & pixel_mask_even_1, + pixel_data[2] & pixel_mask_even_2, + pixel_data[3] & pixel_mask_even_3)) + + odd = not odd + pixel_data.clear() + + elif SMXLayerVariant is SMXMainLayer4plus1: + # player_color command + # draw the following 'count' pixels + # 4 pixels are stored in every 5 byte chunk. + # palette indices are contained in byte[0] - byte[3] + # palette sections are stored in byte[4] + # count = (cmd >> 2) + 1 + + pixel_count = (cmd >> 2) + 1 + + for _ in range(pixel_count): + # Start fetching pixel data + palette_section = (palette_section_block >> (2 * chunk_pos)) & 0x03 + row_data.push_back(pixel(color_player, + data_raw[dpos_color], + palette_section, + 0, + 0)) + + dpos_color += 1 + chunk_pos += 1 + + # Skip to next chunk + if chunk_pos > 3: + chunk_pos = 0 + dpos_color += 1 # Skip palette section block + palette_section_block = data_raw[dpos_color + 4] + + else: + raise Exception( + f"unknown smx main graphics layer drawing command: " + + f"{cmd:#x} in row {rowid:d}" + ) + + # Process next command + dpos_cmd += 1 + + if SMXLayerVariant is SMXMainLayer8to5 or SMXLayerVariant is SMXMainLayer4plus1: + return dpos_cmd, dpos_color, chunk_pos, row_data + elif SMXLayerVariant is SMXOutlineLayer or SMXLayerVariant is SMXShadowLayer: + return dpos_cmd, dpos_cmd, chunk_pos, row_data + def get_picture_data(self, palette): """ @@ -776,55 +781,22 @@ cdef class SMXMainLayer8to5(SMXLayer): """ def __init__(self, layer_header, data): - super().__init__(layer_header, data) - # process cmd table - for i in range(self.row_count): - cmd_offset, color_offset, chunk_pos, row_data = create_color_row( - self, i) - self.cmd_offset = cmd_offset - self.color_offset = color_offset - self.chunk_pos = chunk_pos - - self.pcolor.push_back(row_data) + self.init(self, layer_header, data) cdef class SMXMainLayer4plus1(SMXLayer): def __init__(self, layer_header, data): - super().__init__(layer_header, data) - # process cmd table - for i in range(self.row_count): - cmd_offset, color_offset, chunk_pos, row_data = create_color_row( - self, i) - self.cmd_offset = cmd_offset - self.color_offset = color_offset - self.chunk_pos = chunk_pos + self.init(self, layer_header, data) - self.pcolor.push_back(row_data) cdef class SMXOutlineLayer(SMXLayer): def __init__(self, layer_header, data): - super().__init__(layer_header, data) - # process cmd table - for i in range(self.row_count): - cmd_offset, color_offset, chunk_pos, row_data = create_color_row( - self, i) - self.cmd_offset = cmd_offset - self.color_offset = color_offset - self.chunk_pos = chunk_pos + self.init(self, layer_header, data) - self.pcolor.push_back(row_data) cdef class SMXShadowLayer(SMXLayer): def __init__(self, layer_header, data): - super().__init__(layer_header, data) - # process cmd table - for i in range(self.row_count): - cmd_offset, color_offset, chunk_pos, row_data = create_color_row( - self, i) - self.cmd_offset = cmd_offset - self.color_offset = color_offset - self.chunk_pos = chunk_pos + self.init(self, layer_header, data) - self.pcolor.push_back(row_data) From 64782e68bbc14c99b2b82bed5d370ce031178d6d Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Sun, 19 Jan 2025 22:06:07 +0000 Subject: [PATCH 706/771] split conditionals --- .../convert/value_object/read/media/smp.pyx | 65 ++++++++++--------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/openage/convert/value_object/read/media/smp.pyx b/openage/convert/value_object/read/media/smp.pyx index 9ad253c5c3..40787250ec 100644 --- a/openage/convert/value_object/read/media/smp.pyx +++ b/openage/convert/value_object/read/media/smp.pyx @@ -439,13 +439,14 @@ cdef class SMPLayer: # eol (end of line) command, this row is finished now. eor = True - if SMPLayerVariant is SMPShadowLayer and row_data.size() < expected_size: - # copy the last drawn pixel - # (still stored in nextbyte) - # - # TODO: confirm that this is the - # right way to do it - row_data.push_back(pixel(color_shadow, nextbyte, 0, 0, 0)) + if SMPLayerVariant is SMPShadowLayer: + if row_data.size() < expected_size: + # copy the last drawn pixel + # (still stored in nextbyte) + # + # TODO: confirm that this is the + # right way to do it + row_data.push_back(pixel(color_shadow, nextbyte, 0, 0, 0)) continue @@ -488,31 +489,37 @@ cdef class SMPLayer: pixel_data[3] & 0x1F)) # remove "usage" bit here pixel_data.clear() - elif lower_crumb == 0b00000010 and (SMPLayerVariant is SMPMainLayer or SMPLayerVariant is SMPShadowLayer): - # player_color command - # draw the following 'count' pixels - # pixels are stored as 4 byte palette and meta infos - # count = (cmd >> 2) + 1 + elif lower_crumb == 0b00000010: + if (SMPLayerVariant is SMPMainLayer or SMPLayerVariant is SMPShadowLayer): + # player_color command + # draw the following 'count' pixels + # pixels are stored as 4 byte palette and meta infos + # count = (cmd >> 2) + 1 - pixel_count = (cmd >> 2) + 1 + pixel_count = (cmd >> 2) + 1 - for _ in range(pixel_count): - if SMPLayerVariant is SMPShadowLayer: - dpos += 1 - nextbyte = data_raw[dpos] - row_data.push_back(pixel(color_shadow, - nextbyte, 0, 0, 0)) - else: - for _ in range(4): + for _ in range(pixel_count): + if SMPLayerVariant is SMPShadowLayer: dpos += 1 - pixel_data.push_back(data_raw[dpos]) - - row_data.push_back(pixel(color_player, - pixel_data[0], - pixel_data[1], - pixel_data[2], - pixel_data[3] & 0x1F)) # remove "usage" bit here - pixel_data.clear() + nextbyte = data_raw[dpos] + row_data.push_back(pixel(color_shadow, + nextbyte, 0, 0, 0)) + else: + for _ in range(4): + dpos += 1 + pixel_data.push_back(data_raw[dpos]) + + row_data.push_back(pixel(color_player, + pixel_data[0], + pixel_data[1], + pixel_data[2], + pixel_data[3] & 0x1F)) # remove "usage" bit here + pixel_data.clear() + else: + raise Exception( + f"unknown smp {self.info.layer_type} layer drawing command: " + + f"{cmd:#x} in row {rowid:d}" + ) else: raise Exception( From 71391d3ff2f63554e9bdf2bc686c859885a5c3b1 Mon Sep 17 00:00:00 2001 From: jere8184 Date: Fri, 24 Jan 2025 21:00:46 +0000 Subject: [PATCH 707/771] Update .mailmap --- .mailmap | 3 --- 1 file changed, 3 deletions(-) diff --git a/.mailmap b/.mailmap index 3d8b8ec12e..5acbd1b581 100644 --- a/.mailmap +++ b/.mailmap @@ -21,8 +21,5 @@ Tobias Feldballe Jonas Borchelt Derek Frogget <114030121+derekfrogget@users.noreply.github.com> Nikhil Ghosh -<<<<<<< HEAD David Wever <56411717+dmwever@users.noreply.github.com> -======= Ngô Xuân Minh ->>>>>>> e8251ff6 (copying.md) From 68e500b65de7416b051cc030c9d437948d32b2e6 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 27 Jan 2025 03:22:14 +0100 Subject: [PATCH 708/771] convert: Make fused type work without inheritance. --- .../convert/value_object/read/media/smx.pyx | 58 ++++++++----------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/openage/convert/value_object/read/media/smx.pyx b/openage/convert/value_object/read/media/smx.pyx index 2e60a84a00..7fb6a39f7f 100644 --- a/openage/convert/value_object/read/media/smx.pyx +++ b/openage/convert/value_object/read/media/smx.pyx @@ -61,12 +61,24 @@ cdef public dict LAYER_TYPES = { } +cdef class SMXMainLayer8to5: + pass + +cdef class SMXMainLayer4plus1: + pass + +cdef class SMXShadowLayer: + pass + +cdef class SMXOutlineLayer: + pass + + ctypedef fused SMXLayerVariant: SMXMainLayer8to5 SMXMainLayer4plus1 - SMXOutlineLayer SMXShadowLayer - + SMXOutlineLayer class SMX: @@ -190,16 +202,17 @@ class SMX: if layer_type is SMXLayerType.MAIN: if layer_header.compression_type == 0x08: - self.main_frames.append(SMXMainLayer8to5(layer_header, data)) + self.main_frames.append(SMXLayer(SMXMainLayer8to5(), layer_header, data)) elif layer_header.compression_type == 0x00: - self.main_frames.append(SMXMainLayer4plus1(layer_header, data)) + self.main_frames.append(SMXLayer(SMXMainLayer4plus1(), layer_header, data)) elif layer_type is SMXLayerType.SHADOW: - self.shadow_frames.append(SMXShadowLayer(layer_header, data)) + self.shadow_frames.append(SMXLayer(SMXShadowLayer(), layer_header, data)) elif layer_type is SMXLayerType.OUTLINE: - self.outline_frames.append(SMXOutlineLayer(layer_header, data)) + # self.outline_frames.append(SMXLayer(SMXOutlineLayer(), layer_header, data)) + pass def get_frames(self, layer: int = 0): """ @@ -327,6 +340,9 @@ cdef class SMXLayer: # pixel matrix representing the final image cdef vector[vector[pixel]] pcolor + def __init__(self, variant, layer_header, data) -> None: + self.init(variant, layer_header, data) + def init(self, SMXLayerVariant variant, layer_header, data): """ SMX layer definition superclass. There can be various types of @@ -376,7 +392,6 @@ cdef class SMXLayer: for i in range(row_count): cmd_offset, color_offset, chunk_pos, row_data = \ self.create_color_row(variant, data_raw, i, cmd_offset, color_offset, chunk_pos) - self.pcolor.push_back(row_data) @@ -438,8 +453,8 @@ cdef class SMXLayer: # verify size of generated row if row_data.size() != pixel_count: got = row_data.size() - summary = "%d/%d -> row %d, layer type %d, offset %d / %#x" % ( - got, pixel_count, rowid, self.info.layer_type, + summary = "%d/%d -> row %d, layer type %s, offset %d / %#x" % ( + got, pixel_count, rowid, repr(self.info.layer_type), first_cmd_offset, first_cmd_offset ) txt = "got %%s pixels than expected: %s, missing: %d" % ( @@ -775,31 +790,6 @@ cdef class SMXLayer: return repr(self.info) -cdef class SMXMainLayer8to5(SMXLayer): - """ - Compressed SMP layer (compression type 8to5) for the main graphics sprite. - """ - - def __init__(self, layer_header, data): - self.init(self, layer_header, data) - -cdef class SMXMainLayer4plus1(SMXLayer): - def __init__(self, layer_header, data): - self.init(self, layer_header, data) - - -cdef class SMXOutlineLayer(SMXLayer): - def __init__(self, layer_header, data): - self.init(self, layer_header, data) - - -cdef class SMXShadowLayer(SMXLayer): - def __init__(self, layer_header, data): - self.init(self, layer_header, data) - - - - @cython.boundscheck(False) @cython.wraparound(False) cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix, From 09cfc3d20b08a442e46d67a11ee956f931ed9143 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 27 Jan 2025 03:47:49 +0100 Subject: [PATCH 709/771] convert: Fix missing loop in outline layer parsing. --- openage/convert/value_object/read/media/smx.pyx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openage/convert/value_object/read/media/smx.pyx b/openage/convert/value_object/read/media/smx.pyx index 7fb6a39f7f..565dcb611e 100644 --- a/openage/convert/value_object/read/media/smx.pyx +++ b/openage/convert/value_object/read/media/smx.pyx @@ -211,8 +211,7 @@ class SMX: self.shadow_frames.append(SMXLayer(SMXShadowLayer(), layer_header, data)) elif layer_type is SMXLayerType.OUTLINE: - # self.outline_frames.append(SMXLayer(SMXOutlineLayer(), layer_header, data)) - pass + self.outline_frames.append(SMXLayer(SMXOutlineLayer(), layer_header, data)) def get_frames(self, layer: int = 0): """ @@ -653,8 +652,9 @@ cdef class SMXLayer: if SMXLayerVariant is SMXOutlineLayer: # we don't know the color the game wants # so we just draw index 0 - row_data.push_back(pixel(color_outline, - 0, 0, 0, 0)) + for _ in range(pixel_count): + row_data.push_back(pixel(color_outline, + 0, 0, 0, 0)) elif lower_crumb == 0b00000010: if SMXLayerVariant is SMXMainLayer8to5: From 3b82ce2d37aa1d5b80779ffa5c303669c0fc01ea Mon Sep 17 00:00:00 2001 From: Leo Peng Date: Fri, 31 Jan 2025 11:54:47 +0800 Subject: [PATCH 710/771] doc: fix incorrect fontconfig path in Windows build guide --- doc/build_instructions/windows_msvc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/build_instructions/windows_msvc.md b/doc/build_instructions/windows_msvc.md index af889d8b99..e31bcf79cd 100644 --- a/doc/build_instructions/windows_msvc.md +++ b/doc/build_instructions/windows_msvc.md @@ -82,7 +82,7 @@ _Note:_ If you want to download and build Nyan automatically add `-DDOWNLOAD_NYA - Select all `ttf\DejaVuSerif*.ttf` files, right click and click `Install for all users`. _Note:_ This will require administrator rights. - - Set the `FONTCONFIG_PATH` environment variable to `\installed\\tools\fontconfig\fonts\`. + - Set the `FONTCONFIG_PATH` environment variable to `\installed\\tools\fontconfig\fonts\` or `\installed\\etc\fonts\`. - Copy `fontconfig\57-dejavu-serif.conf` to `%FONTCONFIG_PATH%\conf.d`. - [Optional] Set the `AGE2DIR` environment variable to the AoE 2 installation directory. - Set `QML2_IMPORT_PATH` to `\installed\\qml` or for prebuilt Qt `\\\qml` From 64206f07597b454b594be89cd99c738ff03ba318 Mon Sep 17 00:00:00 2001 From: Jonas Jelten Date: Thu, 6 Feb 2025 22:09:22 +0100 Subject: [PATCH 711/771] readme: add kevin badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4cd98b9070..c0bcf4a02c 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ If you're interested, we wrote detailed explanations on our blog: [Part 1](https | Operating System | Build status | | :-----------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -| Debian Sid | [Todo: Kevin #11] | +| Debian Sid | [![Kevin CI status](https://cidata.sft.lol/openage/branches/master/status.svg)](/kevinfile) | | Ubuntu 24.04 LTS | [![Ubuntu 24.04 build status](https://github.com/SFTTech/openage/actions/workflows/ubuntu-24.04.yml/badge.svg?branch=master)](https://github.com/SFTtech/openage/actions/workflows/ubuntu-24.04.yml) | | macOS | [![macOS build status](https://github.com/SFTtech/openage/workflows/macOS-CI/badge.svg)](https://github.com/SFTtech/openage/actions?query=workflow%3AmacOS-CI) | | Windows Server 2019 | [![Windows Server 2019 build status](https://github.com/SFTtech/openage/actions/workflows/windows-server-2019.yml/badge.svg?branch=master)](https://github.com/SFTtech/openage/actions/workflows/windows-server-2019.yml) | From cb5f61e3b25c269bf54c69e163c2faccf8487f7b Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 9 Feb 2025 21:31:49 +0100 Subject: [PATCH 712/771] convert: Make fused type work without inheritance. --- .../convert/value_object/read/media/smp.pyx | 66 +++++++++---------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/openage/convert/value_object/read/media/smp.pyx b/openage/convert/value_object/read/media/smp.pyx index 40787250ec..a67efd87bb 100644 --- a/openage/convert/value_object/read/media/smp.pyx +++ b/openage/convert/value_object/read/media/smp.pyx @@ -62,6 +62,24 @@ cdef public dict LAYER_TYPES = { } +cdef class SMPMainLayer: + pass + + +cdef class SMPShadowLayer: + pass + + +cdef class SMPOutlineLayer: + pass + + +ctypedef fused SMPLayerVariant: + SMPMainLayer + SMPShadowLayer + SMPOutlineLayer + + class SMP: """ Class for reading/converting the SMP image format (successor of SLP). @@ -155,16 +173,16 @@ class SMP: if layer_header.layer_type == 0x02: # layer that store the main graphic - self.main_frames.append(SMPMainLayer(layer_header, data)) + self.main_frames.append(SMPLayer(SMPMainLayer(), layer_header, data)) elif layer_header.layer_type == 0x04: # layer that stores a shadow - self.shadow_frames.append(SMPShadowLayer(layer_header, data)) + self.shadow_frames.append(SMPLayer(SMPShadowLayer(), layer_header, data)) elif layer_header.layer_type == 0x08 or \ layer_header.layer_type == 0x10: # layer that stores an outline - self.outline_frames.append(SMPOutlineLayer(layer_header, data)) + self.outline_frames.append(SMPLayer(SMPOutlineLayer(), layer_header, data)) else: raise Exception( @@ -253,11 +271,6 @@ class SMPLayerHeader: ) return "".join(ret) -ctypedef fused SMPLayerVariant: - SMPMainLayer - SMPShadowLayer - SMPOutlineLayer - cdef class SMPLayer: """ @@ -288,6 +301,9 @@ cdef class SMPLayer: # pixel matrix representing the final image cdef vector[vector[pixel]] pcolor + def __init__(self, variant, layer_header, data) -> None: + self.init(variant, layer_header, data) + def init(self, SMPLayerVariant variant, layer_header, data): self.info = layer_header @@ -540,6 +556,12 @@ cdef class SMPLayer: """ return determine_rgba_matrix(self.pcolor, palette) + def get_damage_mask(self): + """ + Convert the 4th pixel byte to a mask used for damaged units. + """ + return determine_damage_matrix(self.pcolor) + def get_hotspot(self): """ Return the layer's hotspot (the "center" of the image) @@ -559,34 +581,6 @@ cdef class SMPLayer: return repr(self.info) -cdef class SMPMainLayer(SMPLayer): - """ - SMPLayer for the main graphics sprite. - """ - - def __init__(self, layer_header, data): - self.init(self ,layer_header, data) - - def get_damage_mask(self): - """ - Convert the 4th pixel byte to a mask used for damaged units. - """ - return determine_damage_matrix(self.pcolor) - - -cdef class SMPShadowLayer(SMPLayer): - """ - SMPLayer for the shadow graphics. - """ - - def __init__(self, layer_header, data): - self.init(self ,layer_header, data) - -cdef class SMPOutlineLayer(SMPLayer): - def __init__(self, layer_header, data): - self.init(self, layer_header, data) - - @cython.boundscheck(False) @cython.wraparound(False) cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix, From 6f8689d617ce6e08e30c4838db28b6586f5a98ca Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 9 Feb 2025 21:56:20 +0100 Subject: [PATCH 713/771] convert: Add comments to SMP/SMX code. --- .../convert/value_object/read/media/smp.pyx | 137 ++++++++++++++---- .../convert/value_object/read/media/smx.pyx | 111 ++++++++------ 2 files changed, 180 insertions(+), 68 deletions(-) diff --git a/openage/convert/value_object/read/media/smp.pyx b/openage/convert/value_object/read/media/smp.pyx index a67efd87bb..03de7dd200 100644 --- a/openage/convert/value_object/read/media/smp.pyx +++ b/openage/convert/value_object/read/media/smp.pyx @@ -22,6 +22,7 @@ from libcpp.vector cimport vector endianness = "< " +# Boundary of a row in a SMP layer. cdef struct boundary_def: Py_ssize_t left Py_ssize_t right @@ -63,17 +64,27 @@ cdef public dict LAYER_TYPES = { cdef class SMPMainLayer: + """ + Main graphic layer of an SMP. Stores the color information. + """ pass cdef class SMPShadowLayer: + """ + Shadow layer of an SMP. + """ pass cdef class SMPOutlineLayer: + """ + Outline layer of an SMP. + """ pass +# fused type for the layer variants ctypedef fused SMPLayerVariant: SMPMainLayer SMPShadowLayer @@ -83,7 +94,7 @@ ctypedef fused SMPLayerVariant: class SMP: """ Class for reading/converting the SMP image format (successor of SLP). - This format is used to store all graphics within AoE2: Definitive Edition. + This format is used to store all graphics within AoE2: Definitive Edition (Beta). """ # struct smp_header { @@ -122,15 +133,23 @@ class SMP: # }; smp_layer_header = Struct(endianness + "i i i i I I I I") - def __init__(self, data): + def __init__(self, data: bytes) -> None: + """ + Read the SMP file and store the frames in the object. + + :param data: SMP file data. + """ smp_header = SMP.smp_header.unpack_from(data) signature, version, frame_count, facet_count, frames_per_facet,\ checksum, file_size, source_format, comment = smp_header dbg("SMP") + spam(" signature: %s", signature.decode('ascii')) + spam(" version: %s", version) dbg(" frame count: %s", frame_count) dbg(" facet count: %s", facet_count) dbg(" facets per animation: %s", frames_per_facet) + spam(" checksum: %s", checksum) dbg(" file size: %s B", file_size) dbg(" source format: %s", source_format) dbg(" comment: %s", comment.decode('ascii')) @@ -188,9 +207,10 @@ class SMP: raise Exception( f"unknown layer type: {layer_header.layer_type:#x} at offset {layer_header_offset:#x}" ) + spam(layer_header) - def get_frames(self, layer: int = 0): + def get_frames(self, layer: int = 0) -> list[SMPLayer]: """ Get the frames in the SMP. @@ -198,7 +218,6 @@ class SMP: - 0 = main graphics - 1 = shadow graphics - 2 = outline - :type layer: int """ cdef list frames @@ -221,7 +240,7 @@ class SMP: return frames - def __str__(self): + def __str__(self) -> str: ret = list() ret.extend([repr(self), "\n", SMPLayerHeader.repr_header(), "\n"]) @@ -229,15 +248,39 @@ class SMP: ret.extend([repr(frame), "\n"]) return "".join(ret) - def __repr__(self): + def __repr__(self) -> str: return f"SMP image<{len(self.main_frames):d} frames>" class SMPLayerHeader: - def __init__(self, width, height, hotspot_x, - hotspot_y, layer_type, outline_table_offset, - qdl_table_offset, flags, - frame_offset): + """ + Header of a layer in the SMP file. + """ + def __init__( + self, + width: int, + height: int, + hotspot_x: int, + hotspot_y: int, + layer_type: int, + outline_table_offset: int, + qdl_table_offset: int, + flags: int, + frame_offset: int + ) -> None: + """ + Create a SMP layer header. + + :param width: Width of the layer sprites. + :param height: Height of the layer sprites. + :param hotspot_x: X coordinate of the anchor point. + :param hotspot_y: Y coordinate of the anchor point. + :param layer_type: Type of the layer. + :param outline_table_offset: Offset of the outline table. + :param qdl_table_offset: Offset of the pixel command table. + :param flags: Flags of the layer. + :param frame_offset: Offset of the frame. + """ self.size = (width, height) self.hotspot = (hotspot_x, hotspot_y) @@ -252,16 +295,19 @@ class SMPLayerHeader: # the absolute offset of the frame self.frame_offset = frame_offset + # flags + self.flags = flags + self.palette_number = -1 @staticmethod - def repr_header(): + def repr_header() -> str: return ("width x height | hotspot x/y | " "layer type | " "offset (outline table|qdl table)" ) - def __repr__(self): + def __repr__(self) -> str: ret = ( "% 5d x% 7d | " % self.size, "% 4d /% 5d | " % self.hotspot, @@ -274,7 +320,7 @@ class SMPLayerHeader: cdef class SMPLayer: """ - one layer inside the SMP. you can imagine it as a frame of a video. + Layer inside the SMP. you can imagine it as a frame of an animation. """ # struct smp_frame_row_edge { @@ -301,10 +347,29 @@ cdef class SMPLayer: # pixel matrix representing the final image cdef vector[vector[pixel]] pcolor - def __init__(self, variant, layer_header, data) -> None: + def __init__( + self, + variant, # this argument must not be typed because cython can't handle it + layer_header: SMPLayerHeader, + data: bytes + ) -> None: + """ + Create a SMP layer. + + :param variant: Type of the layer. + :param layer_header: Header information of the layer. + :param data: Layer data. + """ self.init(variant, layer_header, data) - def init(self, SMPLayerVariant variant, layer_header, data): + def init(self, SMPLayerVariant variant, layer_header: SMPLayerHeader, data: bytes) -> None: + """ + Read the pixel information of the SMP layer. + + :param variant: Type of the layer. + :param layer_header: Header information of the layer. + :param data: Layer data. + """ self.info = layer_header if not (isinstance(data, bytes) or isinstance(data, bytearray)): @@ -355,7 +420,11 @@ cdef class SMPLayer: const uint8_t[::1] &data_raw, Py_ssize_t rowid): """ - extract colors (pixels) for the given rowid. + Extract colors (pixels) for a pixel row in the layer. + + :param variant: Type of the layer. + :param data_raw: Raw data of the layer. + :param rowid: Index of the current row in the layer. """ cdef vector[pixel] row_data @@ -417,8 +486,14 @@ cdef class SMPLayer: Py_ssize_t first_cmd_offset, size_t expected_size): """ - extract colors (pixels) for the drawing commands - found for this row in the SMP frame. + Extract colors (pixels) from the drawing commands for a row in the layer. + + :param variant: Type of the layer. + :param data_raw: Raw data of the layer. + :param row_data: Stores the extracted pixels. May be prefilled with transparent pixels. + :param rowid: Index of the current row in the layer. + :param first_cmd_offset: Offset of the first drawing command in the data. + :param expected_size: Expected number of pixels in the row. """ # position in the data blob, we start at the first command of this row @@ -550,34 +625,41 @@ cdef class SMPLayer: return - def get_picture_data(self, palette): + def get_picture_data(self, palette) -> numpy.ndarray: """ - Convert the palette index matrix to a colored image. + Convert the palette index matrix to a RGBA image. + + :param palette: Color palette used for pixels in the sprite. + :type palette: .colortable.ColorTable + :return: Array of RGBA values. """ return determine_rgba_matrix(self.pcolor, palette) - def get_damage_mask(self): + def get_damage_mask(self) -> numpy.ndarray: """ Convert the 4th pixel byte to a mask used for damaged units. + + :return: Damage mask of the layer. """ return determine_damage_matrix(self.pcolor) - def get_hotspot(self): + def get_hotspot(self) -> tuple[int, int]: """ - Return the layer's hotspot (the "center" of the image) + Return the layer's hotspot (the "center" of the image). + + :return: Hotspot of the layer. """ return self.info.hotspot - def get_palette_number(self): + def get_palette_number(self) -> int: """ Return the layer's palette number. :return: Palette number of the layer. - :rtype: int """ return self.pcolor[0][0].palette & 0b00111111 - def __repr__(self): + def __repr__(self) -> str: return repr(self.info) @@ -587,6 +669,9 @@ cdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix, numpy.ndarray[numpy.uint8_t, ndim=2, mode="c"] palette): """ converts a palette index image matrix to an rgba matrix. + + :param image_matrix: A 2-dimensional array of SMP pixels. + :param palette: Color palette used for normal pixels in the sprite. """ cdef size_t height = image_matrix.size() diff --git a/openage/convert/value_object/read/media/smx.pyx b/openage/convert/value_object/read/media/smx.pyx index 565dcb611e..887440cf7b 100644 --- a/openage/convert/value_object/read/media/smx.pyx +++ b/openage/convert/value_object/read/media/smx.pyx @@ -21,6 +21,8 @@ from libcpp.vector cimport vector # SMX files have little endian byte order endianness = "< " + +# Boundary of a row in a SMX layer. cdef struct boundary_def: Py_ssize_t left Py_ssize_t right @@ -62,18 +64,31 @@ cdef public dict LAYER_TYPES = { cdef class SMXMainLayer8to5: + """ + Main graphics layer of an SMX (compressed with 8to5). + """ pass cdef class SMXMainLayer4plus1: + """ + Main graphics layer of an SMX (compressed with 4plus1). + """ pass cdef class SMXShadowLayer: + """ + Shadow layer of an SMX. + """ pass cdef class SMXOutlineLayer: + """ + Outline layer of an SMX. + """ pass +# fused type for SMX layer variants ctypedef fused SMXLayerVariant: SMXMainLayer8to5 SMXMainLayer4plus1 @@ -114,14 +129,12 @@ class SMX: # }; smx_layer_header = Struct(endianness + "H H h h I i") - def __init__(self, data): + def __init__(self, data: bytes): """ - Read an SMX image file. + Read an SMX image file and store the frames in the object. - :param data: File content as bytes. - :type data: bytes, bytearray + :param data: SMX file data. """ - smx_header = SMX.smx_header.unpack_from(data) self.smp_type, version, frame_count, file_size_comp,\ file_size_uncomp, comment = smx_header @@ -213,7 +226,7 @@ class SMX: elif layer_type is SMXLayerType.OUTLINE: self.outline_frames.append(SMXLayer(SMXOutlineLayer(), layer_header, data)) - def get_frames(self, layer: int = 0): + def get_frames(self, layer: int = 0) -> list[SMXLayer]: """ Get the frames in the SMX. @@ -221,7 +234,6 @@ class SMX: - 0 = main graphics - 1 = shadow graphics - 2 = outline - :type layer: int """ cdef list frames @@ -257,16 +269,23 @@ class SMX: class SMXLayerHeader: - def __init__(self, layer_type, frame_type, - palette_number, - width, height, hotspot_x, hotspot_y, - outline_table_offset, - qdl_command_table_offset, - qdl_color_table_offset): + def __init__( + self, + layer_type: SMXLayerType, + frame_type: int, + palette_number: int, + width: int, + height: int, + hotspot_x: int, + hotspot_y: int, + outline_table_offset: int, + qdl_command_table_offset: int, + qdl_color_table_offset: int + ) -> None: """ Stores the header of a layer including additional info about its frame. - :param layer_type: Type of layer. Either main. shadow or outline. + :param layer_type: Type of layer. :param frame_type: Type of the frame the layer belongs to. :param palette_number: Palette number used for pixels in the frame. :param width: Width of layer in pixels. @@ -276,16 +295,6 @@ class SMXLayerHeader: :param outline_table_offset: Absolute position of the layer's outline table in the file. :param qdl_command_table_offset: Absolute position of the layer's command table in the file. :param qdl_color_table_offset: Absolute position of the layer's pixel data table in the file. - :type layer_type: str - :type frame_type: int - :type palette_number: int - :type width: int - :type height: int - :type hotspot_x: int - :type hotspot_y: int - :type outline_table_offset: int - :type qdl_command_table_offset: int - :type qdl_color_table_offset: int """ self.size = (width, height) @@ -301,12 +310,12 @@ class SMXLayerHeader: self.qdl_color_table_offset = qdl_color_table_offset @staticmethod - def repr_header(): + def repr_header() -> str: return ("layer type | width x height | " "hotspot x/y | " ) - def __repr__(self): + def __repr__(self) -> str: ret = ( "% s | " % self.layer_type, "% 5d x% 7d | " % self.size, @@ -320,7 +329,7 @@ class SMXLayerHeader: cdef class SMXLayer: """ - one layer inside the compressed SMP. + Layer inside the compressed SMX. """ # struct smp_layer_row_edge { @@ -339,18 +348,28 @@ cdef class SMXLayer: # pixel matrix representing the final image cdef vector[vector[pixel]] pcolor - def __init__(self, variant, layer_header, data) -> None: + def __init__( + self, + variant, # this argument must not be typed because cython can't handle it + layer_header: SMXLayerHeader, + data: bytes + ) -> None: + """ + Create a SMX layer. + + :param variant: Type of the layer. + :param layer_header: Header information of the layer. + :param data: Layer data. + """ self.init(variant, layer_header, data) - def init(self, SMXLayerVariant variant, layer_header, data): + def init(self, SMXLayerVariant variant, layer_header: SMXLayerHeader, data: bytes) -> None: """ - SMX layer definition superclass. There can be various types of - layers inside an SMX frame. + SMX layer definition. There can be various types of layers inside an SMX frame. + :param variant: Type of the layer. :param layer_header: Header definition of the layer. :param data: File content as bytes. - :type layer_header: SMXLayerHeader - :type data: bytes, bytearray """ self.info = layer_header @@ -402,8 +421,10 @@ cdef class SMXLayer: int color_offset, int chunk_pos): """ - Extract colors (pixels) for the given rowid. + Extract colors (pixels) for a pixel row in the layer. + :param variant: Type of the layer. + :param data_raw: Raw data of the layer. :param rowid: Index of the current row in the layer. :param cmd_offset: Offset of the command table of the layer. :param color_offset: Offset of the color table of the layer. @@ -477,6 +498,15 @@ cdef class SMXLayer: """ extract colors (pixels) for the drawing commands that were compressed with 8to5 compression. + + :param variant: Type of the layer. + :param data_raw: Raw data of the layer. + :param row_data: Stores the extracted pixels. May be prefilled with transparent pixels. + :param rowid: Row index. + :param first_cmd_offset: Offset of the first drawing command in the data. + :param first_color_offset: Offset of the first color command in the data. + :param chunk_pos: Current position in the compressed chunk. + :param expected_size: Expected number of pixels in the row. """ # position in the command array, we start at the first command of this row cdef Py_ssize_t dpos_cmd = first_cmd_offset @@ -757,32 +787,29 @@ cdef class SMXLayer: return dpos_cmd, dpos_cmd, chunk_pos, row_data - def get_picture_data(self, palette): + def get_picture_data(self, palette) -> numpy.ndarray: """ Convert the palette index matrix to a RGBA image. - :param main_palette: Color palette used for pixels in the sprite. - :type main_palette: .colortable.ColorTable + :param palette: Color palette used for pixels in the sprite. + :type palette: .colortable.ColorTable :return: Array of RGBA values. - :rtype: numpy.ndarray """ return determine_rgba_matrix(self.pcolor, palette) - def get_hotspot(self): + def get_hotspot(self) -> tuple[int, int]: """ Return the layer's hotspot (the "center" of the image). :return: Hotspot of the layer. - :rtype: tuple """ return self.info.hotspot - def get_palette_number(self): + def get_palette_number(self) -> int: """ Return the layer's palette number. :return: Palette number of the layer. - :rtype: int """ return self.info.palette_number From cfc21ff1f2369c7e6f53d26cdd8c5f92ae3b293b Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Wed, 22 Jan 2025 19:13:06 +0000 Subject: [PATCH 714/771] remove OOAPI specifier from enum to allow windows debug builds to link successfully --- libopenage/util/enum.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libopenage/util/enum.h b/libopenage/util/enum.h index 394db95b93..c6a9b4666b 100644 --- a/libopenage/util/enum.h +++ b/libopenage/util/enum.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 @@ -44,7 +44,7 @@ namespace util { * NumericType numeric */ template -class OAAPI EnumValue { +class EnumValue { public: constexpr EnumValue(const char *value_name, NumericType numeric_value) : name(value_name), numeric(numeric_value) {} @@ -124,7 +124,7 @@ class OAAPI EnumValue { * bool operator >=(Enum[DerivedType] other) except + */ template -class OAAPI Enum { +class Enum { using this_type = Enum; public: From 423cfed131040ccf278eb55872d2cf5da9c041eb Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Fri, 24 Jan 2025 12:26:48 +0000 Subject: [PATCH 715/771] working wondows debug build --- buildsystem/cythonize.py | 19 +++++++++++++++++++ buildsystem/python.cmake | 6 +++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/buildsystem/cythonize.py b/buildsystem/cythonize.py index 9e8ba8e234..89b499a29f 100755 --- a/buildsystem/cythonize.py +++ b/buildsystem/cythonize.py @@ -88,13 +88,32 @@ def cythonize_wrapper(modules, **kwargs): with redirect_stdout(cython_filter): if src_modules: cythonize(src_modules, **kwargs) + windows_include_python_debug_build_wrapper(src_modules, bin_dir) if bin_modules: os.chdir(bin_dir) cythonize(bin_modules, **kwargs) + windows_include_python_debug_build_wrapper(bin_modules, bin_dir) os.chdir(src_dir) +def windows_include_python_debug_build_wrapper(modules, path): + for module in modules: + module = str(module) + if (path): + module = path + "\\" + module + module = module.removesuffix(".py").removesuffix(".pyx") + module = module + ".cpp" + with open(module, "r") as file: + text = file.read() + text = text.replace("#include \"Python.h\"", + "#ifdef _DEBUG\n#define _DEBUG_WAS_DEFINED\n#undef _DEBUG\n#endif\n\ + #include \"Python.h\"\n#ifdef _DEBUG_WAS_DEFINED\n#define _DEBUG\n\ + #undef _DEBUG_WAS_DEFINED\n#endif", 1) + with open(module, "w") as file: + file.write(text) + + def main(): """ CLI entry point """ cli = argparse.ArgumentParser() diff --git a/buildsystem/python.cmake b/buildsystem/python.cmake index f5c4d84de5..a8e1fed72e 100644 --- a/buildsystem/python.cmake +++ b/buildsystem/python.cmake @@ -128,8 +128,7 @@ function(add_cython_modules) if(MINGW) set_target_properties("${TARGETNAME}" PROPERTIES LINK_FLAGS "-municode") endif() - - target_link_libraries("${TARGETNAME}" PRIVATE ${PYEXT_LIBRARY}) + target_link_libraries("${TARGETNAME}" PRIVATE C:/vcpkg/installed/x64-windows/lib/python311.lib) else() set_property(GLOBAL APPEND PROPERTY SFT_CYTHON_MODULES "${source}") add_library("${TARGETNAME}" MODULE "${CPPNAME}") @@ -140,7 +139,7 @@ function(add_cython_modules) ) if(WIN32) - target_link_libraries("${TARGETNAME}" PRIVATE ${PYEXT_LIBRARY}) + target_link_libraries("${TARGETNAME}" PRIVATE C:/vcpkg/installed/x64-windows/lib/python311.lib) endif() endif() @@ -514,6 +513,7 @@ function(python_finalize) ${INPLACEMODULES_LISTFILE} "$" ) + message(hello jeremiah, ${INPLACEMODULES_LISTFILE}) set(INPLACEMODULES_TIMEFILE "${CMAKE_BINARY_DIR}/py/inplacemodules_timefile") add_custom_command(OUTPUT "${INPLACEMODULES_TIMEFILE}" COMMAND ${INPLACEMODULES_INVOCATION} From 7eca2beb3a3d622b3150fdfd517c7491b48f09f9 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Fri, 24 Jan 2025 12:33:16 +0000 Subject: [PATCH 716/771] working windows debug build --- buildsystem/HandlePythonOptions.cmake | 13 ++++++++++- buildsystem/cythonize.py | 33 +++++++++++++++++++-------- buildsystem/python.cmake | 12 ++++++---- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/buildsystem/HandlePythonOptions.cmake b/buildsystem/HandlePythonOptions.cmake index ef7da71f9b..d6888c0673 100644 --- a/buildsystem/HandlePythonOptions.cmake +++ b/buildsystem/HandlePythonOptions.cmake @@ -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. # finds the python interpreter, install destination and extension flags. @@ -39,6 +39,17 @@ if(PYTHON_VER VERSION_GREATER_EQUAL 3.8 AND PYTHON_VERSION VERSION_LESS 3.9) endif() set(PYEXT_LIBRARY "${PYTHON_LIBRARIES}") + +#Windows always uses optimized version of Python lib +if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + #get index of string "optimized" and increment it by 1 so index points at the path of the optimized lib + list (FIND PYEXT_LIBRARY "optimized" _index) + if (${_index} GREATER -1) + MATH(EXPR _index "${_index}+1") + list(GET PYEXT_LIBRARY ${_index} PYEXT_LIBRARY) + endif() +endif() + set(PYEXT_INCLUDE_DIRS "${PYTHON_INCLUDE_DIRS};${NUMPY_INCLUDE_DIR}") if(NOT CMAKE_PY_INSTALL_PREFIX) diff --git a/buildsystem/cythonize.py b/buildsystem/cythonize.py index 89b499a29f..5d7cd492fe 100755 --- a/buildsystem/cythonize.py +++ b/buildsystem/cythonize.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2015-2021 the openage authors. See copying.md for legal info. +# Copyright 2015-2025 the openage authors. See copying.md for legal info. """ Runs Cython on all modules that were listed via add_cython_module. @@ -73,7 +73,7 @@ def remove_if_exists(filename): filename.unlink() -def cythonize_wrapper(modules, **kwargs): +def cythonize_wrapper(modules, force_optimized_lib = False, **kwargs): """ Calls cythonize, filtering useless warnings """ bin_dir, bin_modules = kwargs['build_dir'], [] src_dir, src_modules = Path.cwd(), [] @@ -88,29 +88,40 @@ def cythonize_wrapper(modules, **kwargs): with redirect_stdout(cython_filter): if src_modules: cythonize(src_modules, **kwargs) - windows_include_python_debug_build_wrapper(src_modules, bin_dir) + if sys.platform == 'win32' and force_optimized_lib: + windows_use_optimized_lib_python(src_modules, bin_dir) if bin_modules: os.chdir(bin_dir) cythonize(bin_modules, **kwargs) - windows_include_python_debug_build_wrapper(bin_modules, bin_dir) + if sys.platform == 'win32' and force_optimized_lib: + windows_use_optimized_lib_python(bin_modules, bin_dir) os.chdir(src_dir) -def windows_include_python_debug_build_wrapper(modules, path): +def windows_use_optimized_lib_python(modules, path): + """ + Add an #ifdef statement in cythonized .cpp files to temporarily undefine _DEBUG before + #include "Python.h" + + This function modifies the generated C++ files from Cython to prevent linking to + the debug version of the Python library on Windows. The debug version of the + Python library cannot import Python libraries that contain extension modules. + """ + for module in modules: module = str(module) - if (path): + if path: module = path + "\\" + module module = module.removesuffix(".py").removesuffix(".pyx") module = module + ".cpp" - with open(module, "r") as file: + with open(module, "r", encoding='utf8') as file: text = file.read() text = text.replace("#include \"Python.h\"", "#ifdef _DEBUG\n#define _DEBUG_WAS_DEFINED\n#undef _DEBUG\n#endif\n\ #include \"Python.h\"\n#ifdef _DEBUG_WAS_DEFINED\n#define _DEBUG\n\ #undef _DEBUG_WAS_DEFINED\n#endif", 1) - with open(module, "w") as file: + with open(module, "w", encoding='utf8') as file: file.write(text) @@ -144,6 +155,8 @@ def main(): )) cli.add_argument("--threads", type=int, default=cpu_count(), help="number of compilation threads to use") + cli.add_argument("--force_optimized_lib", action="store_true", + help= "edit compiled .cpp files to link to optimized version of python libary") args = cli.parse_args() # cython emits warnings on using absolute paths to modules @@ -178,12 +191,12 @@ def main(): # writing funny lines at the head of each file. cythonize_args['language'] = 'c++' - cythonize_wrapper(modules, **cythonize_args) + cythonize_wrapper(modules, args.force_optimized_lib, **cythonize_args) # build standalone executables that embed the py interpreter Options.embed = "main" - cythonize_wrapper(embedded_modules, **cythonize_args) + cythonize_wrapper(embedded_modules, args.force_optimized_lib, **cythonize_args) # verify depends from Cython.Build.Dependencies import _dep_tree diff --git a/buildsystem/python.cmake b/buildsystem/python.cmake index a8e1fed72e..a46f7106f3 100644 --- a/buildsystem/python.cmake +++ b/buildsystem/python.cmake @@ -1,4 +1,4 @@ -# Copyright 2014-2020 the openage authors. See copying.md for legal info. +# Copyright 2014-2025 the openage authors. See copying.md for legal info. # provides macros for defining python extension modules and pxdgen sources. # and a 'finalize' function that must be called in the end. @@ -128,7 +128,8 @@ function(add_cython_modules) if(MINGW) set_target_properties("${TARGETNAME}" PROPERTIES LINK_FLAGS "-municode") endif() - target_link_libraries("${TARGETNAME}" PRIVATE C:/vcpkg/installed/x64-windows/lib/python311.lib) + + target_link_libraries("${TARGETNAME}" PRIVATE ${PYEXT_LIBRARY}) else() set_property(GLOBAL APPEND PROPERTY SFT_CYTHON_MODULES "${source}") add_library("${TARGETNAME}" MODULE "${CPPNAME}") @@ -139,7 +140,7 @@ function(add_cython_modules) ) if(WIN32) - target_link_libraries("${TARGETNAME}" PRIVATE C:/vcpkg/installed/x64-windows/lib/python311.lib) + target_link_libraries("${TARGETNAME}" PRIVATE ${PYEXT_LIBRARY}) endif() endif() @@ -374,6 +375,9 @@ function(python_finalize) # cythonize (.pyx -> .cpp) + if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + set(force_optimized_lib "--force_optimized_lib") + endif() get_property(cython_modules GLOBAL PROPERTY SFT_CYTHON_MODULES) write_on_change("${CMAKE_BINARY_DIR}/py/cython_modules" "${cython_modules}") @@ -392,6 +396,7 @@ function(python_finalize) "${CMAKE_BINARY_DIR}/py/cython_modules" "${CMAKE_BINARY_DIR}/py/cython_modules_embed" "${CMAKE_BINARY_DIR}/py/pxd_list" + ${force_optimized_lib} "--build-dir" "${CMAKE_BINARY_DIR}" COMMAND "${CMAKE_COMMAND}" -E touch "${CYTHONIZE_TIMEFILE}" DEPENDS @@ -513,7 +518,6 @@ function(python_finalize) ${INPLACEMODULES_LISTFILE} "$" ) - message(hello jeremiah, ${INPLACEMODULES_LISTFILE}) set(INPLACEMODULES_TIMEFILE "${CMAKE_BINARY_DIR}/py/inplacemodules_timefile") add_custom_command(OUTPUT "${INPLACEMODULES_TIMEFILE}" COMMAND ${INPLACEMODULES_INVOCATION} From de9ab1db44d7abbb32408754f3476d95f8b8432a Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Fri, 24 Jan 2025 18:10:33 +0000 Subject: [PATCH 717/771] CI-CD:use debug config for CI --- .github/workflows/windows-server-2019.yml | 2 +- .github/workflows/windows-server-2022.yml | 2 +- buildsystem/HandlePythonOptions.cmake | 1 + buildsystem/cythonize.py | 30 +++++++++++++++++------ buildsystem/python.cmake | 5 +--- 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/.github/workflows/windows-server-2019.yml b/.github/workflows/windows-server-2019.yml index bf3ff7856b..406d303f27 100644 --- a/.github/workflows/windows-server-2019.yml +++ b/.github/workflows/windows-server-2019.yml @@ -51,7 +51,7 @@ jobs: mkdir build cd build cmake -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TRY_COMPILE_CONFIGURATION=Release -DCMAKE_CXX_FLAGS='/Zc:__cplusplus /permissive- /EHsc' -DCMAKE_EXE_LINKER_FLAGS='' -DCMAKE_MODULE_LINKER_FLAGS='' -DCMAKE_SHARED_LINKER_FLAGS='' -DDOWNLOAD_NYAN=YES -DCXX_OPTIMIZATION_LEVEL=auto -DCXX_SANITIZE_FATAL=False -DCXX_SANITIZE_MODE=none -DWANT_BACKTRACE=if_available -DWANT_GPERFTOOLS_PROFILER=if_available -DWANT_GPERFTOOLS_TCMALLOC=False -DWANT_INOTIFY=if_available -DWANT_NCURSES=if_available -DWANT_OPENGL=if_available -DWANT_VULKAN=if_available -DFLEX_EXECUTABLE="$FLEX_PATH" -G "Visual Studio 16 2019" -A x64 ../source - cmake --build . --config RelWithDebInfo -- -nologo -maxCpuCount + cmake --build . --config Debug -- -nologo -maxCpuCount shell: pwsh - name: Package run: | diff --git a/.github/workflows/windows-server-2022.yml b/.github/workflows/windows-server-2022.yml index a721db5a52..10a6a52c59 100644 --- a/.github/workflows/windows-server-2022.yml +++ b/.github/workflows/windows-server-2022.yml @@ -51,7 +51,7 @@ jobs: mkdir build cd build cmake -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TRY_COMPILE_CONFIGURATION=Release -DCMAKE_CXX_FLAGS='/Zc:__cplusplus /permissive- /EHsc' -DCMAKE_EXE_LINKER_FLAGS='' -DCMAKE_MODULE_LINKER_FLAGS='' -DCMAKE_SHARED_LINKER_FLAGS='' -DDOWNLOAD_NYAN=YES -DCXX_OPTIMIZATION_LEVEL=auto -DCXX_SANITIZE_FATAL=False -DCXX_SANITIZE_MODE=none -DWANT_BACKTRACE=if_available -DWANT_GPERFTOOLS_PROFILER=if_available -DWANT_GPERFTOOLS_TCMALLOC=False -DWANT_INOTIFY=if_available -DWANT_NCURSES=if_available -DWANT_OPENGL=if_available -DWANT_VULKAN=if_available -DFLEX_EXECUTABLE="$FLEX_PATH" -G "Visual Studio 17 2022" -A x64 ../source - cmake --build . --config RelWithDebInfo -- -nologo -maxCpuCount + cmake --build . --config Debug -- -nologo -maxCpuCount shell: pwsh - name: Package run: | diff --git a/buildsystem/HandlePythonOptions.cmake b/buildsystem/HandlePythonOptions.cmake index d6888c0673..20a35f3d61 100644 --- a/buildsystem/HandlePythonOptions.cmake +++ b/buildsystem/HandlePythonOptions.cmake @@ -47,6 +47,7 @@ if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") if (${_index} GREATER -1) MATH(EXPR _index "${_index}+1") list(GET PYEXT_LIBRARY ${_index} PYEXT_LIBRARY) + set(force_optimized_lib_flag "--force_optimized_lib") endif() endif() diff --git a/buildsystem/cythonize.py b/buildsystem/cythonize.py index 5d7cd492fe..e7ac86ae6f 100755 --- a/buildsystem/cythonize.py +++ b/buildsystem/cythonize.py @@ -89,17 +89,17 @@ def cythonize_wrapper(modules, force_optimized_lib = False, **kwargs): if src_modules: cythonize(src_modules, **kwargs) if sys.platform == 'win32' and force_optimized_lib: - windows_use_optimized_lib_python(src_modules, bin_dir) + win_use_optimized_lib_python(src_modules, bin_dir) if bin_modules: os.chdir(bin_dir) cythonize(bin_modules, **kwargs) if sys.platform == 'win32' and force_optimized_lib: - windows_use_optimized_lib_python(bin_modules, bin_dir) + win_use_optimized_lib_python(bin_modules, bin_dir) os.chdir(src_dir) -def windows_use_optimized_lib_python(modules, path): +def win_use_optimized_lib_python(modules, path): """ Add an #ifdef statement in cythonized .cpp files to temporarily undefine _DEBUG before #include "Python.h" @@ -107,6 +107,8 @@ def windows_use_optimized_lib_python(modules, path): This function modifies the generated C++ files from Cython to prevent linking to the debug version of the Python library on Windows. The debug version of the Python library cannot import Python libraries that contain extension modules. + see: https://github.com/python/cpython/issues/127619 (To unserstand the problem) + see: https://stackoverflow.com/a/59566420 (To understand the soloution) """ for module in modules: @@ -117,10 +119,24 @@ def windows_use_optimized_lib_python(modules, path): module = module + ".cpp" with open(module, "r", encoding='utf8') as file: text = file.read() - text = text.replace("#include \"Python.h\"", - "#ifdef _DEBUG\n#define _DEBUG_WAS_DEFINED\n#undef _DEBUG\n#endif\n\ - #include \"Python.h\"\n#ifdef _DEBUG_WAS_DEFINED\n#define _DEBUG\n\ - #undef _DEBUG_WAS_DEFINED\n#endif", 1) + if not text.count("OPENAGE: UNDEF_DEBUG_INSERTED"): + text = text.replace( + '#include "Python.h"', + ( + "\n\n// OPENAGE: UNDEF_DEBUG_INSERTED\n" + "// Avoid linking to the debug version of the Python library on Windows\n\n" + "#ifdef _DEBUG\n" + "#define _DEBUG_WAS_DEFINED\n" + "#undef _DEBUG\n#endif\n" + "#include \"Python.h\"\n" + "#ifdef _DEBUG_WAS_DEFINED\n" + "#define _DEBUG\n" + "#undef _DEBUG_WAS_DEFINED\n" + "#endif\n\n" + "// OPENAGE: UNDEF_DEBUG_INSERTED\n\n" + ), + 1 + ) with open(module, "w", encoding='utf8') as file: file.write(text) diff --git a/buildsystem/python.cmake b/buildsystem/python.cmake index a46f7106f3..90117baadc 100644 --- a/buildsystem/python.cmake +++ b/buildsystem/python.cmake @@ -375,9 +375,6 @@ function(python_finalize) # cythonize (.pyx -> .cpp) - if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") - set(force_optimized_lib "--force_optimized_lib") - endif() get_property(cython_modules GLOBAL PROPERTY SFT_CYTHON_MODULES) write_on_change("${CMAKE_BINARY_DIR}/py/cython_modules" "${cython_modules}") @@ -396,7 +393,7 @@ function(python_finalize) "${CMAKE_BINARY_DIR}/py/cython_modules" "${CMAKE_BINARY_DIR}/py/cython_modules_embed" "${CMAKE_BINARY_DIR}/py/pxd_list" - ${force_optimized_lib} + ${force_optimized_lib_flag} "--build-dir" "${CMAKE_BINARY_DIR}" COMMAND "${CMAKE_COMMAND}" -E touch "${CYTHONIZE_TIMEFILE}" DEPENDS From 11bdc78f487ad1d4ad43c000370aed2d003a10a9 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Mon, 20 Jan 2025 16:06:22 +0000 Subject: [PATCH 718/771] ArrayIterator inherit from CurveIterator --- libopenage/curve/container/array.h | 78 +++++++++++++++------------- libopenage/curve/tests/container.cpp | 3 +- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/libopenage/curve/container/array.h b/libopenage/curve/container/array.h index e980d77915..e3d69af553 100644 --- a/libopenage/curve/container/array.h +++ b/libopenage/curve/container/array.h @@ -26,6 +26,10 @@ class Array : event::EventEntity { public: /// Underlying container type. using container_t = std::array, Size>; + + /// Uderlying iterator type + using const_iterator = typename std::array, Size>::const_iterator; + /// Index type to access elements in the container. using index_t = typename container_t::size_type; @@ -167,61 +171,60 @@ class Array : event::EventEntity { return this->_idstr; } - /** - * Array::Iterator is used to iterate over KeyframeContainers contained in a curve at a given time. - */ - class Iterator { - public: - Iterator(Array *curve, const time::time_t &time = time::TIME_MAX, size_t offset = 0) : - curve(curve), time(time), offset(offset) {}; + class ArrayIterator : public CurveIterator> { + public: /** - * returns a copy of the keyframe at the current offset and time + * Construct the iterator from its boundary conditions: time and container */ - const T operator*() { - return this->curve->frame(this->time, this->offset).second; + ArrayIterator(const const_iterator &base, + const Array *base_container, + const time::time_t &to) : + CurveIterator>(base, base_container, -time::TIME_MAX, to) { } - /** - * increments the Iterator to point at the next KeyframeContainer - */ - void operator++() { - this->offset++; - } - /** - * Compare two Iterators by their offset - */ - bool operator!=(const Array::Iterator &rhs) const { - return this->offset != rhs.offset; + virtual bool valid() const override { + if (this->container->end().get_base() != this->get_base() && this->get_base()->begin()->time() <= this->to) { + return true; + } + return false; } - private: /** - * the curve object that this iterator, iterates over + * Get the keyFrame with a time <= this->to from the KeyframeContainer + * that this iterator currently points at */ - Array *curve; - - /** - * time at which this iterator is iterating over - */ - time::time_t time; + const T &value() const override { + const auto &key_frame_container = *this->get_base(); + size_t hint_index = std::distance(this->container->begin().get_base(), this->get_base()); + size_t e = key_frame_container.last(this->to, last_elements[hint_index]); + this->last_elements[hint_index] = e; + return key_frame_container.get(e).val(); + } /** - * used to index the Curve::Array pointed to by this iterator + * Cache hints for containers. Stores the index of the last keyframe accessed in each container. + * + * hints is used to speed up the search for keyframes. + * + * mutable as hints are updated by const read-only functions. */ - size_t offset; + mutable std::array::elem_ptr, Size> last_elements = {}; }; + /** * iterator pointing to a keyframe of the first KeyframeContainer in the curve at a given time */ - Iterator begin(const time::time_t &time = time::TIME_MIN); + ArrayIterator begin(const time::time_t &time = time::TIME_MIN) const; + /** * iterator pointing after the last KeyframeContainer in the curve at a given time */ - Iterator end(const time::time_t &time = time::TIME_MIN); + ArrayIterator end(const time::time_t &time = time::TIME_MAX) const; + private: /** @@ -382,13 +385,14 @@ void Array::sync(const Array &other, } template -typename Array::Iterator Array::begin(const time::time_t &time) { - return Array::Iterator(this, time); +auto Array::begin(const time::time_t &t) const -> ArrayIterator { + return ArrayIterator(this->containers.begin(), this, t); } + template -typename Array::Iterator Array::end(const time::time_t &time) { - return Array::Iterator(this, time, this->containers.size()); +auto Array::end(const time::time_t &t) const -> ArrayIterator { + return ArrayIterator(this->containers.end(), this, t); } } // namespace curve diff --git a/libopenage/curve/tests/container.cpp b/libopenage/curve/tests/container.cpp index 16da2476e9..a19542dd8c 100644 --- a/libopenage/curve/tests/container.cpp +++ b/libopenage/curve/tests/container.cpp @@ -336,7 +336,8 @@ void test_array() { TESTEQUALS(next_frame.second, 40); // value // Test begin and end - auto it = a.begin(1); + + auto it = a.begin(openage::time::time_t(1)); TESTEQUALS(*it, 4); ++it; TESTEQUALS(*it, 5); From f1ca4190ca20fa14ea404cf0df5a963a624f2594 Mon Sep 17 00:00:00 2001 From: jere8184 Date: Tue, 21 Jan 2025 03:18:50 +0000 Subject: [PATCH 719/771] Update container.cpp --- libopenage/curve/tests/container.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libopenage/curve/tests/container.cpp b/libopenage/curve/tests/container.cpp index a19542dd8c..d7d6825de0 100644 --- a/libopenage/curve/tests/container.cpp +++ b/libopenage/curve/tests/container.cpp @@ -337,7 +337,7 @@ void test_array() { // Test begin and end - auto it = a.begin(openage::time::time_t(1)); + auto it = a.begin(1); TESTEQUALS(*it, 4); ++it; TESTEQUALS(*it, 5); From 768bd99fc64f7ce224112b1f4198cf0faa16eb8d Mon Sep 17 00:00:00 2001 From: jere8184 Date: Tue, 21 Jan 2025 03:20:10 +0000 Subject: [PATCH 720/771] Update container.cpp --- libopenage/curve/tests/container.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libopenage/curve/tests/container.cpp b/libopenage/curve/tests/container.cpp index d7d6825de0..16da2476e9 100644 --- a/libopenage/curve/tests/container.cpp +++ b/libopenage/curve/tests/container.cpp @@ -336,7 +336,6 @@ void test_array() { TESTEQUALS(next_frame.second, 40); // value // Test begin and end - auto it = a.begin(1); TESTEQUALS(*it, 4); ++it; From 03a5f8257e423fbcc92411b77808cc30d6436371 Mon Sep 17 00:00:00 2001 From: haytham918 Date: Mon, 18 Nov 2024 17:32:50 -0500 Subject: [PATCH 721/771] Add and Test Clang-tidy --- buildsystem/codecompliance/__main__.py | 11 ++++ buildsystem/codecompliance/clangtidy.py | 67 +++++++++++++++++++++++++ copying.md | 1 + 3 files changed, 79 insertions(+) create mode 100644 buildsystem/codecompliance/clangtidy.py diff --git a/buildsystem/codecompliance/__main__.py b/buildsystem/codecompliance/__main__.py index 2a4f8b4084..a8373478b9 100644 --- a/buildsystem/codecompliance/__main__.py +++ b/buildsystem/codecompliance/__main__.py @@ -57,6 +57,8 @@ def parse_args(): help="increase program verbosity") cli.add_argument("-q", "--quiet", action="count", default=0, help="decrease program verbosity") + cli.add_argument("--clangtidy", action="store_true", + help="Check the C++ code with clang-tidy.") args = cli.parse_args() process_args(args, cli.error) @@ -92,6 +94,7 @@ def process_args(args, error): args.pystyle = True args.pylint = True args.test_git_change_years = True + args.clangtidy = True if not any((args.headerguards, args.legal, args.authors, args.pystyle, args.cppstyle, args.cython, args.test_git_change_years, @@ -127,6 +130,10 @@ def process_args(args, error): if args.pylint: if not importlib.util.find_spec('pylint'): error("pylint python module required for linting") + + if args.clangtidy: + if not shutil.which('clang-tidy'): + error("--clang-tidy requires clang-tidy to be installed") def get_changed_files(gitref): @@ -264,6 +271,10 @@ def find_all_issues(args, check_files=None): from .modes import find_issues yield from find_issues(check_files, ('openage', 'buildsystem', 'libopenage', 'etc/gdb_pretty')) + + if args.clangtidy: + from .clangtidy import find_issues + yield from find_issues(check_files, ('libopenage', )) if __name__ == '__main__': diff --git a/buildsystem/codecompliance/clangtidy.py b/buildsystem/codecompliance/clangtidy.py new file mode 100644 index 0000000000..affa76bd9e --- /dev/null +++ b/buildsystem/codecompliance/clangtidy.py @@ -0,0 +1,67 @@ +# Copyright 2024-2024 the openage authors. See copying.md for legal info. + +""" +Checks clang-tidy errors on cpp files +""" + +import subprocess +from .cppstyle import filter_file_list +from .util import findfiles + + +def find_issues(check_files, dirnames): + """ + Invoke clang-tidy to check C++ files for issues. + Yields issues found by clang-tidy in real-time. + """ + # Specify the checks to include + checks_to_include = [ + 'clang-analyzer-*', + 'bugprone-*', + 'concurrency-*', + 'performance-*' + ] + # Create the checks string + checks = ','.join(checks_to_include) + + # Invocation command + invocation = ['clang-tidy', f'-checks=-*,{checks}'] + + if check_files is not None: + filenames = list(filter_file_list(check_files, dirnames)) + else: + filenames = list(filter_file_list(findfiles(dirnames), dirnames)) + + if not filenames: + print("No files to check.") + return # No files to check + + for filename in filenames: + # Run clang-tidy for each file + print(f"Starting clang-tidy check on file: {filename}") + try: + process = subprocess.Popen( + invocation + [filename], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # Stream output in real-time + while True: + output = process.stdout.readline() + if output: + yield ("clang-tidy output", output.strip(), None) + elif process.poll() is not None: + break + + # Capture remaining errors (if any) + for error_line in process.stderr: + yield ("clang-tidy error", error_line.strip(), None) + + except Exception as exc: + yield ( + "clang-tidy error", + f"An error occurred while running clang-tidy on {filename}: {str(exc)}", + None + ) \ No newline at end of file diff --git a/copying.md b/copying.md index f873817f29..3938eaa8e0 100644 --- a/copying.md +++ b/copying.md @@ -161,6 +161,7 @@ _the openage authors_ are: | David Wever | dmwever | dmwever à crimson dawt ua dawt edu | | Michael Lynch | mtlynch | git à mtlynch dawt io | | Ngô Xuân Minh | | xminh dawt ngo dawt 00 à gmail dawt com | +| Haytham Tang | haytham918 | yunxuant à umich dawt edu | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. From cb9b76220059d286b389fd79c1efbc46487c513a Mon Sep 17 00:00:00 2001 From: haytham918 Date: Mon, 18 Nov 2024 18:11:54 -0500 Subject: [PATCH 722/771] Some style fix for clangtidy.py --- buildsystem/codecompliance/__main__.py | 8 +++---- buildsystem/codecompliance/clangtidy.py | 31 ++++++++++++------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/buildsystem/codecompliance/__main__.py b/buildsystem/codecompliance/__main__.py index a8373478b9..b72edcd2ef 100644 --- a/buildsystem/codecompliance/__main__.py +++ b/buildsystem/codecompliance/__main__.py @@ -57,8 +57,8 @@ def parse_args(): help="increase program verbosity") cli.add_argument("-q", "--quiet", action="count", default=0, help="decrease program verbosity") - cli.add_argument("--clangtidy", action="store_true", - help="Check the C++ code with clang-tidy.") + cli.add_argument("--clangtidy", action="store_true", + help="Check the C++ code with clang-tidy.") args = cli.parse_args() process_args(args, cli.error) @@ -98,7 +98,7 @@ def process_args(args, error): if not any((args.headerguards, args.legal, args.authors, args.pystyle, args.cppstyle, args.cython, args.test_git_change_years, - args.pylint, args.filemodes, args.textfiles)): + args.pylint, args.filemodes, args.textfiles, args.clangtidy)): error("no checks were specified") has_git = bool(shutil.which('git')) @@ -130,7 +130,6 @@ def process_args(args, error): if args.pylint: if not importlib.util.find_spec('pylint'): error("pylint python module required for linting") - if args.clangtidy: if not shutil.which('clang-tidy'): error("--clang-tidy requires clang-tidy to be installed") @@ -271,7 +270,6 @@ def find_all_issues(args, check_files=None): from .modes import find_issues yield from find_issues(check_files, ('openage', 'buildsystem', 'libopenage', 'etc/gdb_pretty')) - if args.clangtidy: from .clangtidy import find_issues yield from find_issues(check_files, ('libopenage', )) diff --git a/buildsystem/codecompliance/clangtidy.py b/buildsystem/codecompliance/clangtidy.py index affa76bd9e..7eda5cd72a 100644 --- a/buildsystem/codecompliance/clangtidy.py +++ b/buildsystem/codecompliance/clangtidy.py @@ -22,7 +22,7 @@ def find_issues(check_files, dirnames): 'performance-*' ] # Create the checks string - checks = ','.join(checks_to_include) + checks = ', '.join(checks_to_include) # Invocation command invocation = ['clang-tidy', f'-checks=-*,{checks}'] @@ -40,28 +40,27 @@ def find_issues(check_files, dirnames): # Run clang-tidy for each file print(f"Starting clang-tidy check on file: {filename}") try: - process = subprocess.Popen( + with subprocess.Popen( invocation + [filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True - ) - - # Stream output in real-time - while True: - output = process.stdout.readline() - if output: - yield ("clang-tidy output", output.strip(), None) - elif process.poll() is not None: - break + ) as process: + # Stream output in real-time + while True: + output = process.stdout.readline() + if output: + yield ("clang-tidy output", output.strip(), None) + elif process.poll() is not None: + break - # Capture remaining errors (if any) - for error_line in process.stderr: - yield ("clang-tidy error", error_line.strip(), None) + # Capture remaining errors (if any) + for error_line in process.stderr: + yield ("clang-tidy error", error_line.strip(), None) - except Exception as exc: + except subprocess.SubprocessError as exc: yield ( "clang-tidy error", f"An error occurred while running clang-tidy on {filename}: {str(exc)}", None - ) \ No newline at end of file + ) From 6306530cece676969c857b644c413ade29d1fb70 Mon Sep 17 00:00:00 2001 From: haytham918 Date: Mon, 18 Nov 2024 18:24:49 -0500 Subject: [PATCH 723/771] Add clang-tidy to dependency list of doc/buildings.md --- doc/building.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/building.md b/doc/building.md index 619ad7a423..8c11b8d967 100644 --- a/doc/building.md +++ b/doc/building.md @@ -57,6 +57,7 @@ Dependency list: CR qt6 >=6.2 (Core, Quick, QuickControls, Multimedia modules) CR toml11 CR O vulkan + S clang-tidy A An installed version of any of the following (wine is your friend). Other versions _might_ work: From 401abc2cc2ab2f233ba2f8e4d0f478f2337cf023 Mon Sep 17 00:00:00 2001 From: haytham918 Date: Tue, 19 Nov 2024 13:23:35 -0500 Subject: [PATCH 724/771] Clang-tidy follows alphabetical sorting in building.md, I think? --- doc/building.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/building.md b/doc/building.md index 8c11b8d967..6ed51cb4a6 100644 --- a/doc/building.md +++ b/doc/building.md @@ -39,6 +39,7 @@ Dependency list: CR opengl >=3.3 CR libepoxy CR libpng + S clang-tidy R dejavu font CR eigen >=3 CR freetype2 @@ -57,7 +58,6 @@ Dependency list: CR qt6 >=6.2 (Core, Quick, QuickControls, Multimedia modules) CR toml11 CR O vulkan - S clang-tidy A An installed version of any of the following (wine is your friend). Other versions _might_ work: From a8ee46e99281440a0e5a8ddd736eacac70f86e9b Mon Sep 17 00:00:00 2001 From: haytham918 Date: Wed, 27 Nov 2024 04:44:24 -0500 Subject: [PATCH 725/771] Add comments in clang-tidy.py --- buildsystem/codecompliance/clangtidy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/buildsystem/codecompliance/clangtidy.py b/buildsystem/codecompliance/clangtidy.py index 7eda5cd72a..d4e1b5586a 100644 --- a/buildsystem/codecompliance/clangtidy.py +++ b/buildsystem/codecompliance/clangtidy.py @@ -15,6 +15,7 @@ def find_issues(check_files, dirnames): Yields issues found by clang-tidy in real-time. """ # Specify the checks to include + # 4 checks we focus on checks_to_include = [ 'clang-analyzer-*', 'bugprone-*', @@ -27,6 +28,7 @@ def find_issues(check_files, dirnames): # Invocation command invocation = ['clang-tidy', f'-checks=-*,{checks}'] + # Use utility functions from util.py and cppstyle.py if check_files is not None: filenames = list(filter_file_list(check_files, dirnames)) else: @@ -58,6 +60,7 @@ def find_issues(check_files, dirnames): for error_line in process.stderr: yield ("clang-tidy error", error_line.strip(), None) + # Handle exception except subprocess.SubprocessError as exc: yield ( "clang-tidy error", From 8debe0c2cce93cec23515dce7f207b2a11f989be Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 10 Feb 2025 00:49:52 +0100 Subject: [PATCH 726/771] buildsys: Add new sanity check category 'checkmerge' in makefile. --- Makefile | 14 +++++---- buildsystem/codecompliance/__main__.py | 39 ++++++++++++++++---------- kevinfile | 2 +- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index ef481521b2..d0dc0585f8 100644 --- a/Makefile +++ b/Makefile @@ -128,22 +128,26 @@ mrproperer: mrproper checkfast: python3 -m buildsystem.codecompliance --fast -.PHONY: checkall -checkall: - python3 -m buildsystem.codecompliance --all +.PHONY: checkmerge +checkmerge: + python3 -m buildsystem.codecompliance --merge .PHONY: checkchanged checkchanged: - python3 -m buildsystem.codecompliance --all --only-changed-files=origin/master + python3 -m buildsystem.codecompliance --merge --only-changed-files=origin/master .PHONY: checkuncommited checkuncommited: - python3 -m buildsystem.codecompliance --all --only-changed-files=HEAD + python3 -m buildsystem.codecompliance --merge --only-changed-files=HEAD .PHONY: checkpy checkpy: python3 -m buildsystem.codecompliance --pystyle --pylint +.PHONY: checkall +checkall: + python3 -m buildsystem.codecompliance --all + .PHONY: help help: $(BUILDDIR)/Makefile @echo "openage Makefile" diff --git a/buildsystem/codecompliance/__main__.py b/buildsystem/codecompliance/__main__.py index b72edcd2ef..4d3237e36a 100644 --- a/buildsystem/codecompliance/__main__.py +++ b/buildsystem/codecompliance/__main__.py @@ -1,4 +1,4 @@ -# Copyright 2014-2024 the openage authors. See copying.md for legal info. +# Copyright 2014-2025 the openage authors. See copying.md for legal info. """ Entry point for the code compliance checker. @@ -18,16 +18,24 @@ def parse_args(): """ Returns the raw argument namespace. """ cli = argparse.ArgumentParser() - cli.add_argument("--fast", action="store_true", - help="do all checks that can be performed quickly") - cli.add_argument("--all", action="store_true", - help="do all checks, even the really slow ones") + check_types = cli.add_mutually_exclusive_group() + check_types.add_argument("--fast", action="store_true", + help="do all checks that can be performed quickly") + check_types.add_argument("--merge", action="store_true", + help="do all checks that are required before merges to master") + check_types.add_argument("--all", action="store_true", + help="do all checks, even the really slow ones") + cli.add_argument("--only-changed-files", metavar='GITREF', help=("slow checks are only done on files that have " "changed since GITREF.")) cli.add_argument("--authors", action="store_true", help=("check whether all git authors are in copying.md. " "repo must be a git repository.")) + cli.add_argument("--clang-tidy", action="store_true", + help=("Check the C++ code with clang-tidy. Make sure you have build the " + "project with ./configure --clang-tidy or have set " + "CMAKE_CXX_CLANG_TIDY for your CMake build.")) cli.add_argument("--cppstyle", action="store_true", help="check the cpp code style") cli.add_argument("--cython", action="store_true", @@ -57,8 +65,6 @@ def parse_args(): help="increase program verbosity") cli.add_argument("-q", "--quiet", action="count", default=0, help="decrease program verbosity") - cli.add_argument("--clangtidy", action="store_true", - help="Check the C++ code with clang-tidy.") args = cli.parse_args() process_args(args, cli.error) @@ -78,7 +84,7 @@ def process_args(args, error): # set up log level log_setup(args.verbose - args.quiet) - if args.fast or args.all: + if args.fast or args.merge or args.all: # enable "fast" tests args.authors = True args.cppstyle = True @@ -88,17 +94,19 @@ def process_args(args, error): args.filemodes = True args.textfiles = True - if args.all: - # enable tests that take a bit longer - + if args.merge or args.all: + # enable tests that are required before merging to master args.pystyle = True args.pylint = True args.test_git_change_years = True - args.clangtidy = True + + if args.all: + # enable tests that take a bit longer + args.clang_tidy = True if not any((args.headerguards, args.legal, args.authors, args.pystyle, args.cppstyle, args.cython, args.test_git_change_years, - args.pylint, args.filemodes, args.textfiles, args.clangtidy)): + args.pylint, args.filemodes, args.textfiles, args.clang_tidy)): error("no checks were specified") has_git = bool(shutil.which('git')) @@ -130,7 +138,8 @@ def process_args(args, error): if args.pylint: if not importlib.util.find_spec('pylint'): error("pylint python module required for linting") - if args.clangtidy: + + if args.clang_tidy: if not shutil.which('clang-tidy'): error("--clang-tidy requires clang-tidy to be installed") @@ -270,7 +279,7 @@ def find_all_issues(args, check_files=None): from .modes import find_issues yield from find_issues(check_files, ('openage', 'buildsystem', 'libopenage', 'etc/gdb_pretty')) - if args.clangtidy: + if args.clang_tidy: from .clangtidy import find_issues yield from find_issues(check_files, ('libopenage', )) diff --git a/kevinfile b/kevinfile index 7a7353bbcd..e4acd0e036 100644 --- a/kevinfile +++ b/kevinfile @@ -6,7 +6,7 @@ sanity_check: - skip (? if job != "debian" ?) - make checkall + make checkmerge # Various optimisation options can affect warnings compiler generates. # Make sure both release and debug are tested. Arch job has more checks, From e9543ab8ec9daff327abda6ba11211c66148391f Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Fri, 24 Jan 2025 18:13:43 +0000 Subject: [PATCH 727/771] fix paring heap UB --- libopenage/datastructure/pairing_heap.h | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/libopenage/datastructure/pairing_heap.h b/libopenage/datastructure/pairing_heap.h index dd348d8c55..8cf1731713 100644 --- a/libopenage/datastructure/pairing_heap.h +++ b/libopenage/datastructure/pairing_heap.h @@ -1,4 +1,4 @@ -// Copyright 2014-2024 the openage authors. See copying.md for legal info. +// Copyright 2014-2025 the openage authors. See copying.md for legal info. #pragma once @@ -404,7 +404,7 @@ class PairingHeap final { * erase all elements on the heap. */ void clear() { - auto delete_node = [](element_t node) { delete node; }; + auto delete_node = [](element_t& node) { delete node; node = nullptr; }; this->iter_all(delete_node); this->root_node = nullptr; this->node_count = 0; @@ -586,7 +586,7 @@ class PairingHeap final { * @param func Function to apply to each node. */ template - void iter_all(const std::function &func) const { + void iter_all(const std::function &func) { this->walk_tree(this->root_node, func); } @@ -599,21 +599,18 @@ class PairingHeap final { * @param func Function to apply to each node. */ template - void walk_tree(const element_t &start, - const std::function &func) const { + void walk_tree(element_t &start, + const std::function &func) { if constexpr (not reverse) { func(start); } if (start) { auto node = start->first_child; - while (true) { - if (not node) { - break; - } - + while (node) { this->walk_tree(node, func); - node = node->next_sibling; + if (node) + node = node->next_sibling; } if constexpr (reverse) { func(start); From aef6cf01ecf64bb1c9e3b10f4ed9b1621dcf0fd0 Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Sun, 26 Jan 2025 16:05:47 +0000 Subject: [PATCH 728/771] add iterator to paring heap --- libopenage/datastructure/pairing_heap.h | 206 +++++++++++++++++++++++- libopenage/datastructure/tests.cpp | 10 +- 2 files changed, 207 insertions(+), 9 deletions(-) diff --git a/libopenage/datastructure/pairing_heap.h b/libopenage/datastructure/pairing_heap.h index 8cf1731713..df8d1ad66d 100644 --- a/libopenage/datastructure/pairing_heap.h +++ b/libopenage/datastructure/pairing_heap.h @@ -17,6 +17,7 @@ */ #include +#include #include #include #include @@ -36,6 +37,9 @@ template class PairingHeap; +template +class PairingHeapIterator; + template > class PairingHeapNode { @@ -43,6 +47,7 @@ class PairingHeapNode { using this_type = PairingHeapNode; friend PairingHeap; + friend PairingHeapIterator; T data; compare cmp; @@ -186,6 +191,166 @@ class PairingHeapNode { }; +/** + * @brief Iterator class for PairingHeap. + * + * This class provides a bidirectional iterator for the PairingHeap data structure. + * It allows traversal of the heap in both forward and backward directions. + * It is depth-first traversal. + * + * @tparam T The type of elements stored in the heap. + * @tparam compare The comparison functor used to order the elements. + */ +template > +class PairingHeapIterator { +public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = T *; + using reference = T &; + + /** + * @brief Constructs an iterator starting at the given node. + * + * @param node The starting node for the iterator. + */ + PairingHeapIterator(PairingHeapNode *node) : + current(node) {} + + /** + * @brief Dereference operator. + * + * @return A reference to the data stored in the current node. + */ + reference operator*() const { + return current->data; + } + + /** + * @brief Member access operator. + * + * @return A pointer to the data stored in the current node. + */ + pointer operator->() const { + return &(current->data); + } + + + /** + * @brief Get current node. + * + * @return The current node. + */ + PairingHeapNode *node() { + return current; + } + + + /** + * @brief Pre-increment operator. + * + * Moves the iterator to the next node in the heap. + * + * @return A reference to the incremented iterator. + */ + PairingHeapIterator &operator++() { + if (current->first_child) { + current = current->first_child; + } + else if (current->next_sibling) { + current = current->next_sibling; + } + else { + while (current->parent && !current->parent->next_sibling) { + current = current->parent; + } + if (current->parent) { + current = current->parent->next_sibling; + } + else { + current = nullptr; + } + } + return *this; + } + + /** + * @brief Post-increment operator. + * + * Moves the iterator to the next node in the heap. + * + * @return A copy of the iterator before incrementing. + */ + PairingHeapIterator operator++(int) { + PairingHeapIterator tmp = *this; + ++(*this); + return tmp; + } + + /** + * @brief Pre-decrement operator. + * + * Moves the iterator to the previous node in the heap. + * + * @return A reference to the decremented iterator. + */ + PairingHeapIterator &operator--() { + if (current->prev_sibling) { + current = current->prev_sibling; + while (current->first_child) { + current = current->first_child; + while (current->next_sibling) { + current = current->next_sibling; + } + } + } + else if (current->parent) { + current = current->parent; + } + return *this; + } + + /** + * @brief Post-decrement operator. + * + * Moves the iterator to the previous node in the heap. + * + * @return A copy of the iterator before decrementing. + */ + PairingHeapIterator operator--(int) { + PairingHeapIterator tmp = *this; + --(*this); + return tmp; + } + + /** + * @brief Equality comparison operator. + * + * @param a The first iterator to compare. + * @param b The second iterator to compare. + * @return True if both iterators point to the same node, false otherwise. + */ + friend bool operator==(const PairingHeapIterator &a, const PairingHeapIterator &b) { + return a.current == b.current; + } + + /** + * @brief Inequality comparison operator. + * + * @param a The first iterator to compare. + * @param b The second iterator to compare. + * @return True if the iterators point to different nodes, false otherwise. + */ + friend bool operator!=(const PairingHeapIterator &a, const PairingHeapIterator &b) { + return a.current != b.current; + } + +private: + PairingHeapNode *current; ///< Pointer to the current node in the heap. +}; + + /** * (Quite) efficient heap implementation. */ @@ -196,6 +361,7 @@ class PairingHeap final { public: using element_t = heapnode_t *; using this_type = PairingHeap; + using iterator = PairingHeapIterator; /** * create a empty heap. @@ -404,11 +570,24 @@ class PairingHeap final { * erase all elements on the heap. */ void clear() { - auto delete_node = [](element_t& node) { delete node; node = nullptr; }; - this->iter_all(delete_node); + std::vector to_delete; + to_delete.reserve(this->size()); + + // collect all node pointers to delete + for (iterator it = this->begin(); it != this->end(); it++) { + to_delete.push_back(it.node()); + } + + // delete all nodes + for (element_t node : to_delete) { + delete node; + } + + // reset heap state to empty this->root_node = nullptr; this->node_count = 0; #if OPENAGE_PAIRINGHEAP_DEBUG + // clear the node set for debugging this->nodes.clear(); #endif } @@ -586,10 +765,18 @@ class PairingHeap final { * @param func Function to apply to each node. */ template - void iter_all(const std::function &func) { + void iter_all(const std::function &func) const { this->walk_tree(this->root_node, func); } + iterator begin() const { + return iterator(this->root_node); + } + + iterator end() const { + return iterator(nullptr); + } + private: /** * Apply the given function to all nodes in the tree. @@ -599,18 +786,21 @@ class PairingHeap final { * @param func Function to apply to each node. */ template - void walk_tree(element_t &start, - const std::function &func) { + void walk_tree(const element_t &start, + const std::function &func) const { if constexpr (not reverse) { func(start); } if (start) { auto node = start->first_child; - while (node) { + while (true) { + if (not node) { + break; + } + this->walk_tree(node, func); - if (node) - node = node->next_sibling; + node = node->next_sibling; } if constexpr (reverse) { func(start); diff --git a/libopenage/datastructure/tests.cpp b/libopenage/datastructure/tests.cpp index c375f78c17..40aa516951 100644 --- a/libopenage/datastructure/tests.cpp +++ b/libopenage/datastructure/tests.cpp @@ -1,4 +1,4 @@ -// Copyright 2014-2024 the openage authors. See copying.md for legal info. +// Copyright 2014-2025 the openage authors. See copying.md for legal info. #include "tests.h" @@ -135,6 +135,14 @@ void pairing_heap_3() { heap.push(heap_elem{3}); heap.push(heap_elem{4}); heap.push(heap_elem{5}); + + size_t i = 0; + std::array expected{0, 5, 4, 3, 2, 1}; + for (auto &elem : heap) { + TESTEQUALS(elem.data, expected.at(i)); + i++; + } + heap.pop(); // trigger pairing heap.clear(); From 961d18717bf64c8c094137fb76ed118bb267860d Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 2 Mar 2025 22:45:40 +0100 Subject: [PATCH 729/771] doc: Change mentions of 'checkall' to 'checkmerge'. --- .github/PULL_REQUEST_TEMPLATES/pull_request_template.md | 2 +- Makefile | 1 + doc/buildsystem.md | 2 +- doc/contributing.md | 4 ++-- doc/ide/configs/vscode/tasks.json | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATES/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATES/pull_request_template.md index ed3dfa558f..537cbfb04a 100644 --- a/.github/PULL_REQUEST_TEMPLATES/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATES/pull_request_template.md @@ -5,7 +5,7 @@ - [ ] I have read the [contribution guide](doc/contributing.md) - [ ] I have added my info to [copying.md](copying.md) (only first time contributors) -- [ ] I have run `make checkall` and fixed all mentioned problems +- [ ] I have run `make checkmerge` and fixed all mentioned problems ### Description diff --git a/Makefile b/Makefile index d0dc0585f8..4fd6e2fe5a 100644 --- a/Makefile +++ b/Makefile @@ -179,6 +179,7 @@ help: $(BUILDDIR)/Makefile @echo "tests -> run the tests (py + cpp)" @echo "" @echo "checkall -> full code compliance check" + @echo "checkmerge -> code compliance check for merging to master" @echo "checkfast -> fast checks only" @echo "checkchanged -> full check for all files changed since origin/master" @echo "checkuncommited -> full check for all currently uncommited files" diff --git a/doc/buildsystem.md b/doc/buildsystem.md index 57dcdaf1d7..65b9acba5b 100644 --- a/doc/buildsystem.md +++ b/doc/buildsystem.md @@ -42,7 +42,7 @@ Additional recipes: - `doc` (generate docs via Doxygen) - `test` (runs the various tests) - various cleaning recipes: `cleanelf`, `cleancodegen`, `cleancython`, `cleaninsourcebuild`, `cleanpxdgen`, `cleanbuilddirs`, `mrproper`, `mrproperer` - - various compliance checkers: `checkfast`, `checkall`, ... + - various compliance checkers: `checkfast`, `checkmerge`, `checkall`, ... Phases diff --git a/doc/contributing.md b/doc/contributing.md index 6498b10f95..326c1d1e98 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -52,7 +52,7 @@ tl;dr - Add yourself to `copying.md` - `git add libopenage/unit/tentacle_monster.cpp` - `git commit -m "engine: fixed vomiting animation of tentacle monster"` -- `make checkall` +- `make checkmerge` - `make test` - `git push origin tentacle-monster-fix` - Create a pull request and look at the CI output @@ -101,7 +101,7 @@ Before making a pull request, it's good to review these things: - Run `make test` to check whether any functionality has been broken - [Check your whitespaces](https://github.com/SFTtech/openage/blob/master/doc/code_style/tabs_n_spaces.md) - [Read all the codestyle docs]( https://github.com/SFTtech/openage/tree/master/doc/code_style) -- Before pushing, run `make checkall`. If that fails, the automatic buildbot will reject your code. +- Before pushing, run `make checkmerge`. If that fails, the automatic buildbot will reject your code. - If this is your first contribution, add yourself to the authors list in [copying.md](/copying.md). - Commit messages should be meaningful, they should say in a sentence (or very little text) what changes it has without requiring to read the entire diff. [tpope knows this very well!](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) diff --git a/doc/ide/configs/vscode/tasks.json b/doc/ide/configs/vscode/tasks.json index 9ac35659b0..2a43dbf10e 100644 --- a/doc/ide/configs/vscode/tasks.json +++ b/doc/ide/configs/vscode/tasks.json @@ -51,7 +51,7 @@ "type": "shell", "command": "make", "args": [ - "checkall" + "checkmerge" ], "group": "build", "presentation": { From 7a81292421409b67b3a918ed1780c5697f43088b Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Wed, 5 Mar 2025 22:39:18 +0000 Subject: [PATCH 730/771] ci-cd: always force link to release python lib when configuring windows debug build --- buildsystem/HandlePythonOptions.cmake | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/buildsystem/HandlePythonOptions.cmake b/buildsystem/HandlePythonOptions.cmake index 20a35f3d61..7ab80d3bdc 100644 --- a/buildsystem/HandlePythonOptions.cmake +++ b/buildsystem/HandlePythonOptions.cmake @@ -39,7 +39,7 @@ if(PYTHON_VER VERSION_GREATER_EQUAL 3.8 AND PYTHON_VERSION VERSION_LESS 3.9) endif() set(PYEXT_LIBRARY "${PYTHON_LIBRARIES}") - +message("PYTHON_LIBRARIES: " "${PYTHON_LIBRARIES}") #Windows always uses optimized version of Python lib if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") #get index of string "optimized" and increment it by 1 so index points at the path of the optimized lib @@ -47,8 +47,10 @@ if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") if (${_index} GREATER -1) MATH(EXPR _index "${_index}+1") list(GET PYEXT_LIBRARY ${_index} PYEXT_LIBRARY) - set(force_optimized_lib_flag "--force_optimized_lib") + message("force linking to python release lib + instead of debug lib when cythonising") endif() + set(force_optimized_lib_flag "--force_optimized_lib") endif() set(PYEXT_INCLUDE_DIRS "${PYTHON_INCLUDE_DIRS};${NUMPY_INCLUDE_DIR}") From 4d138bcc370571d5095f21700d3abf6d678172bf Mon Sep 17 00:00:00 2001 From: Jeremiah Morgan Date: Wed, 5 Mar 2025 22:39:18 +0000 Subject: [PATCH 731/771] ci-cd: always force link to release python lib when configuring windows debug build --- .github/workflows/windows-server-2019.yml | 2 +- .github/workflows/windows-server-2022.yml | 2 +- buildsystem/HandlePythonOptions.cmake | 3 +-- buildsystem/modules/FindPython.cmake | 13 ++++++++++++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/windows-server-2019.yml b/.github/workflows/windows-server-2019.yml index 406d303f27..06f9fa0df4 100644 --- a/.github/workflows/windows-server-2019.yml +++ b/.github/workflows/windows-server-2019.yml @@ -50,7 +50,7 @@ jobs: $FLEX_PATH = (Get-ChildItem ./download -Recurse -Force -Filter 'win_flex.exe')[0].FullName mkdir build cd build - cmake -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TRY_COMPILE_CONFIGURATION=Release -DCMAKE_CXX_FLAGS='/Zc:__cplusplus /permissive- /EHsc' -DCMAKE_EXE_LINKER_FLAGS='' -DCMAKE_MODULE_LINKER_FLAGS='' -DCMAKE_SHARED_LINKER_FLAGS='' -DDOWNLOAD_NYAN=YES -DCXX_OPTIMIZATION_LEVEL=auto -DCXX_SANITIZE_FATAL=False -DCXX_SANITIZE_MODE=none -DWANT_BACKTRACE=if_available -DWANT_GPERFTOOLS_PROFILER=if_available -DWANT_GPERFTOOLS_TCMALLOC=False -DWANT_INOTIFY=if_available -DWANT_NCURSES=if_available -DWANT_OPENGL=if_available -DWANT_VULKAN=if_available -DFLEX_EXECUTABLE="$FLEX_PATH" -G "Visual Studio 16 2019" -A x64 ../source + cmake -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TRY_COMPILE_CONFIGURATION=Debug -DCMAKE_CXX_FLAGS='/Zc:__cplusplus /permissive- /EHsc' -DCMAKE_EXE_LINKER_FLAGS='' -DCMAKE_MODULE_LINKER_FLAGS='' -DCMAKE_SHARED_LINKER_FLAGS='' -DDOWNLOAD_NYAN=YES -DCXX_OPTIMIZATION_LEVEL=auto -DCXX_SANITIZE_FATAL=False -DCXX_SANITIZE_MODE=none -DWANT_BACKTRACE=if_available -DWANT_GPERFTOOLS_PROFILER=if_available -DWANT_GPERFTOOLS_TCMALLOC=False -DWANT_INOTIFY=if_available -DWANT_NCURSES=if_available -DWANT_OPENGL=if_available -DWANT_VULKAN=if_available -DFLEX_EXECUTABLE="$FLEX_PATH" -G "Visual Studio 16 2019" -A x64 ../source cmake --build . --config Debug -- -nologo -maxCpuCount shell: pwsh - name: Package diff --git a/.github/workflows/windows-server-2022.yml b/.github/workflows/windows-server-2022.yml index 10a6a52c59..0ee700e8f3 100644 --- a/.github/workflows/windows-server-2022.yml +++ b/.github/workflows/windows-server-2022.yml @@ -50,7 +50,7 @@ jobs: $FLEX_PATH = (Get-ChildItem ./download -Recurse -Force -Filter 'win_flex.exe')[0].FullName mkdir build cd build - cmake -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TRY_COMPILE_CONFIGURATION=Release -DCMAKE_CXX_FLAGS='/Zc:__cplusplus /permissive- /EHsc' -DCMAKE_EXE_LINKER_FLAGS='' -DCMAKE_MODULE_LINKER_FLAGS='' -DCMAKE_SHARED_LINKER_FLAGS='' -DDOWNLOAD_NYAN=YES -DCXX_OPTIMIZATION_LEVEL=auto -DCXX_SANITIZE_FATAL=False -DCXX_SANITIZE_MODE=none -DWANT_BACKTRACE=if_available -DWANT_GPERFTOOLS_PROFILER=if_available -DWANT_GPERFTOOLS_TCMALLOC=False -DWANT_INOTIFY=if_available -DWANT_NCURSES=if_available -DWANT_OPENGL=if_available -DWANT_VULKAN=if_available -DFLEX_EXECUTABLE="$FLEX_PATH" -G "Visual Studio 17 2022" -A x64 ../source + cmake -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TRY_COMPILE_CONFIGURATION=Debug -DCMAKE_CXX_FLAGS='/Zc:__cplusplus /permissive- /EHsc' -DCMAKE_EXE_LINKER_FLAGS='' -DCMAKE_MODULE_LINKER_FLAGS='' -DCMAKE_SHARED_LINKER_FLAGS='' -DDOWNLOAD_NYAN=YES -DCXX_OPTIMIZATION_LEVEL=auto -DCXX_SANITIZE_FATAL=False -DCXX_SANITIZE_MODE=none -DWANT_BACKTRACE=if_available -DWANT_GPERFTOOLS_PROFILER=if_available -DWANT_GPERFTOOLS_TCMALLOC=False -DWANT_INOTIFY=if_available -DWANT_NCURSES=if_available -DWANT_OPENGL=if_available -DWANT_VULKAN=if_available -DFLEX_EXECUTABLE="$FLEX_PATH" -G "Visual Studio 17 2022" -A x64 ../source cmake --build . --config Debug -- -nologo -maxCpuCount shell: pwsh - name: Package diff --git a/buildsystem/HandlePythonOptions.cmake b/buildsystem/HandlePythonOptions.cmake index 7ab80d3bdc..13ac8a62b8 100644 --- a/buildsystem/HandlePythonOptions.cmake +++ b/buildsystem/HandlePythonOptions.cmake @@ -47,9 +47,8 @@ if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") if (${_index} GREATER -1) MATH(EXPR _index "${_index}+1") list(GET PYEXT_LIBRARY ${_index} PYEXT_LIBRARY) - message("force linking to python release lib - instead of debug lib when cythonising") endif() + message("force linking to python release lib, instead of debug lib when cythonising") set(force_optimized_lib_flag "--force_optimized_lib") endif() diff --git a/buildsystem/modules/FindPython.cmake b/buildsystem/modules/FindPython.cmake index c2e03011d0..61d5494191 100644 --- a/buildsystem/modules/FindPython.cmake +++ b/buildsystem/modules/FindPython.cmake @@ -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. # Find Python # ~~~~~~~~~~~ @@ -78,6 +78,12 @@ set(PYTHON_MIN_VERSION_HEX "${BIT_SHIFT_HEX}") # there's a static_assert that tests the Python version. # that way, we verify the interpreter and the library version. # (the interpreter provided us the library location) + +if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + set(TEMP_CMAKE_TRY_COMPILE_CONFIGURATION ${CMAKE_TRY_COMPILE_CONFIGURATION}) + set(CMAKE_TRY_COMPILE_CONFIGURATION "Release") +endif() + try_compile(PYTHON_TEST_RESULT "${CMAKE_BINARY_DIR}" SOURCES "${CMAKE_CURRENT_LIST_DIR}/FindPython_test.cpp" @@ -87,6 +93,11 @@ try_compile(PYTHON_TEST_RESULT OUTPUT_VARIABLE PYTHON_TEST_OUTPUT ) +if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + set(CMAKE_TRY_COMPILE_CONFIGURATION ${TEMP_CMAKE_TRY_COMPILE_CONFIGURATION}) +endif() + + if(NOT PYTHON_TEST_RESULT) message(STATUS "!! No suitable Python interpreter was found !! From 81b0443ee23cd983bec510513d88e24803e58aa4 Mon Sep 17 00:00:00 2001 From: anatriaslabella Date: Mon, 17 Feb 2025 21:10:51 -0500 Subject: [PATCH 732/771] Added sin, cos, and tan operations to fixed-point implementation --- copying.md | 1 + libopenage/util/fixed_point.h | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/copying.md b/copying.md index 3938eaa8e0..dd71a8dc82 100644 --- a/copying.md +++ b/copying.md @@ -162,6 +162,7 @@ _the openage authors_ are: | Michael Lynch | mtlynch | git à mtlynch dawt io | | Ngô Xuân Minh | | xminh dawt ngo dawt 00 à gmail dawt com | | Haytham Tang | haytham918 | yunxuant à umich dawt edu | +| Ana Trias-Labellarte | anatriaslabella | ana dawt triaslabella à ufl dawt edu | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. diff --git a/libopenage/util/fixed_point.h b/libopenage/util/fixed_point.h index 4e97e77323..e9f86ce0ef 100644 --- a/libopenage/util/fixed_point.h +++ b/libopenage/util/fixed_point.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 @@ -451,6 +451,18 @@ class FixedPoint { constexpr double atan2(const FixedPoint &n) { return std::atan2(this->to_double(), n.to_double()); } + + constexpr double sin() { + return std::sin(this->to_double()); + } + + constexpr double cos() { + return std::cos(this->to_double()); + } + + constexpr double tan() { + return std::tan(this->to_double()); + } }; @@ -567,6 +579,21 @@ constexpr double atan2(openage::util::FixedPoint x, openage::util::FixedPo return x.atan2(y); } +template +constexpr double sin(openage::util::FixedPoint n) { + return n.sin(); +} + +template +constexpr double cos(openage::util::FixedPoint n) { + return n.cos(); +} + +template +constexpr double tan(openage::util::FixedPoint n) { + return n.tan(); +} + template constexpr openage::util::FixedPoint min(openage::util::FixedPoint x, openage::util::FixedPoint y) { return openage::util::FixedPoint::from_raw_value( From 247910482b937bd39369efe92c66919e53d81bef Mon Sep 17 00:00:00 2001 From: Eelco Empting Date: Wed, 12 Mar 2025 20:34:26 +0100 Subject: [PATCH 733/771] Add new template parameter & tests; prettier test failures --- libopenage/testing/testing.h | 37 +++++-- libopenage/util/fixed_point.h | 149 ++++++++++++++------------- libopenage/util/fixed_point_test.cpp | 36 ++++++- 3 files changed, 142 insertions(+), 80 deletions(-) diff --git a/libopenage/testing/testing.h b/libopenage/testing/testing.h index 50e03338e0..83f0efaab9 100644 --- a/libopenage/testing/testing.h +++ b/libopenage/testing/testing.h @@ -1,4 +1,4 @@ -// Copyright 2014-2024 the openage authors. See copying.md for legal info. +// Copyright 2014-2025 the openage authors. See copying.md for legal info. #pragma once @@ -51,7 +51,9 @@ bool fail(const log::message &msg); try { \ auto &&test_result_left = (left); \ if (test_result_left != (right)) { \ - TESTFAILMSG("unexpected value: " << (test_result_left)); \ + TESTFAILMSG(__FILE__ << ":" << __LINE__ << ": Expected " \ + << test_result_left << " and " \ + << (right) << " to be equal"); \ } \ } \ catch (::openage::testing::TestError & e) { \ @@ -63,6 +65,28 @@ bool fail(const log::message &msg); } \ while (0) +/** + * Asserts that the left expression does not equal the right expression, + * and that no exception is thrown. + */ +#define TESTNOTEQUALS(left, right) \ + do { \ + try { \ + auto &&test_result_left = (left); \ + if (test_result_left == (right)) { \ + TESTFAILMSG(__FILE__ << ":" << __LINE__ << ": Expected " \ + << test_result_left << " and " \ + << (right) << " to be NOT equal"); \ + } \ + } \ + catch (::openage::testing::TestError & e) { \ + throw; \ + } \ + catch (::openage::error::Error & e) { \ + TESTFAILMSG("unexpected exception: " << e); \ + } \ + } \ + while (0) /** * Asserts that the left expression equals the right expression, @@ -72,8 +96,9 @@ bool fail(const log::message &msg); do { \ try { \ auto &&test_result_left = (left); \ - if ((test_result_left < (right - epsilon)) or (test_result_left > (right + epsilon))) { \ - TESTFAILMSG("unexpected value: " << (test_result_left)); \ + auto &&test_result_right = (right); \ + if ((test_result_left < (test_result_right - epsilon)) or (test_result_left > (test_result_right + epsilon))) { \ + TESTFAILMSG(__FILE__ << ":" << __LINE__ << ": Expected " << (test_result_left) << " and " << (test_result_right) << " to be equal"); \ } \ } \ catch (::openage::testing::TestError & e) { \ @@ -99,7 +124,7 @@ bool fail(const log::message &msg); expr_has_thrown = true; \ } \ if (not expr_has_thrown) { \ - TESTFAILMSG("no exception"); \ + TESTFAILMSG(__FILE__ << ":" << __LINE__ << ": no exception"); \ } \ } \ while (0) @@ -114,7 +139,7 @@ bool fail(const log::message &msg); expression; \ } \ catch (::openage::error::Error & e) { \ - TESTFAILMSG("unexpected exception"); \ + TESTFAILMSG(__FILE__ << ":" << __LINE__ << ": unexpected exception"); \ } \ } \ while (0) diff --git a/libopenage/util/fixed_point.h b/libopenage/util/fixed_point.h index e9f86ce0ef..2d6a7eb084 100644 --- a/libopenage/util/fixed_point.h +++ b/libopenage/util/fixed_point.h @@ -85,14 +85,16 @@ 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; - using this_type = FixedPoint; + using this_type = FixedPoint; using unsigned_int_type = typename std::make_unsigned::type; + using unsigned_intermediate_type = typename std::make_unsigned::type; + using same_type_but_unsigned = FixedPoint; + fractional_bits, typename FixedPoint::unsigned_intermediate_type>; private: // Helper function to create the scaling factors that are used below. @@ -264,14 +266,14 @@ class FixedPoint { /** * Factory function to get a fixed-point number from a fixed-point number of different type. */ - template other_fractional_bits)>::type * = nullptr> - static constexpr FixedPoint from_fixedpoint(const FixedPoint &other) { + 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> - static constexpr FixedPoint from_fixedpoint(const FixedPoint &other) { + 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))); } @@ -380,14 +382,14 @@ class FixedPoint { return FixedPoint::this_type::from_raw_value(-this->raw_value); } - template - constexpr double hypot(const FixedPoint rhs) { + template + constexpr double hypot(const FixedPoint rhs) { return std::hypot(this->to_double(), rhs.to_double()); } - template - constexpr FixedPoint hypotfp(const FixedPoint rhs) { - return FixedPoint(this->hypot(rhs)); + template + constexpr FixedPoint hypotfp(const FixedPoint rhs) { + return FixedPoint(this->hypot(rhs)); } // Basic operators @@ -471,42 +473,42 @@ class FixedPoint { /** * FixedPoint + FixedPoint */ -template -constexpr FixedPoint operator+(const FixedPoint &lhs, const FixedPoint &rhs) { - return FixedPoint::from_raw_value(lhs.get_raw_value() + rhs.get_raw_value()); +template +constexpr FixedPoint operator+(const FixedPoint &lhs, const FixedPoint &rhs) { + return FixedPoint::from_raw_value(lhs.get_raw_value() + rhs.get_raw_value()); } /** * FixedPoint + double */ -template -constexpr FixedPoint operator+(const FixedPoint &lhs, const double &rhs) { - return FixedPoint{lhs} + FixedPoint::from_double(rhs); +template +constexpr FixedPoint operator+(const FixedPoint &lhs, const double &rhs) { + return FixedPoint{lhs} + FixedPoint::from_double(rhs); } /** * FixedPoint - FixedPoint */ -template -constexpr FixedPoint operator-(const FixedPoint &lhs, const FixedPoint &rhs) { - return FixedPoint::from_raw_value(lhs.get_raw_value() - rhs.get_raw_value()); +template +constexpr FixedPoint operator-(const FixedPoint &lhs, const FixedPoint &rhs) { + return FixedPoint::from_raw_value(lhs.get_raw_value() - rhs.get_raw_value()); } /** * FixedPoint - double */ -template -constexpr FixedPoint operator-(const FixedPoint &lhs, const double &rhs) { - return FixedPoint{lhs} - FixedPoint::from_double(rhs); +template +constexpr FixedPoint operator-(const FixedPoint &lhs, const double &rhs) { + return FixedPoint{lhs} - FixedPoint::from_double(rhs); } /** * FixedPoint * N */ -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); +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); } /* @@ -523,40 +525,41 @@ typename std::enable_if::value, FixedPoint>::type co * => a * a will overflow because: * a.rawvalue == 2^(16+16) == 2^32 * -> a.rawvalue * a.rawvalue == 2^64 => pwnt + * + * Use a larger intermediate type to prevent overflow */ -// template -// constexpr FixedPoint operator*(const FixedPoint lhs, const FixedPoint rhs) { -// I ret = 0; -// if (not __builtin_mul_overflow(lhs.get_raw_value(), rhs.get_raw_value(), &ret)) { -// throw std::overflow_error("FixedPoint multiplication overflow"); -// } +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; -// return FixedPoint::from_raw_value(ret); -// } + return FixedPoint::from_raw_value(static_cast(ret)); +} /** * FixedPoint / FixedPoint */ -template -constexpr FixedPoint operator/(const FixedPoint lhs, const FixedPoint rhs) { - return FixedPoint::from_raw_value(div(lhs.get_raw_value(), rhs.get_raw_value()) << F); +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)); } /** * FixedPoint / N */ -template -constexpr FixedPoint operator/(const FixedPoint lhs, const N &rhs) { - return FixedPoint::from_raw_value(div(lhs.get_raw_value(), static_cast(rhs))); +template +constexpr FixedPoint operator/(const FixedPoint lhs, const N &rhs) { + return FixedPoint::from_raw_value(div(lhs.get_raw_value(), static_cast(rhs))); } /** * FixedPoint % FixedPoint (modulo) */ -template -constexpr FixedPoint operator%(const FixedPoint lhs, const FixedPoint rhs) { +template +constexpr FixedPoint operator%(const FixedPoint lhs, const FixedPoint rhs) { auto div = (lhs / rhs); auto n = div.to_int(); return lhs - (rhs * n); @@ -569,71 +572,71 @@ constexpr FixedPoint operator%(const FixedPoint lhs, const FixedPoin // std function overloads namespace std { -template -constexpr double sqrt(openage::util::FixedPoint n) { +template +constexpr double sqrt(openage::util::FixedPoint n) { return n.sqrt(); } -template -constexpr double atan2(openage::util::FixedPoint x, openage::util::FixedPoint y) { +template +constexpr double atan2(openage::util::FixedPoint x, openage::util::FixedPoint y) { return x.atan2(y); } -template -constexpr double sin(openage::util::FixedPoint n) { +template +constexpr double sin(openage::util::FixedPoint n) { return n.sin(); } -template -constexpr double cos(openage::util::FixedPoint n) { +template +constexpr double cos(openage::util::FixedPoint n) { return n.cos(); } -template -constexpr double tan(openage::util::FixedPoint n) { +template +constexpr double tan(openage::util::FixedPoint n) { return n.tan(); } -template -constexpr openage::util::FixedPoint min(openage::util::FixedPoint x, openage::util::FixedPoint y) { - return openage::util::FixedPoint::from_raw_value( +template +constexpr openage::util::FixedPoint min(openage::util::FixedPoint x, openage::util::FixedPoint y) { + return openage::util::FixedPoint::from_raw_value( std::min(x.get_raw_value(), y.get_raw_value())); } -template -constexpr openage::util::FixedPoint max(openage::util::FixedPoint x, openage::util::FixedPoint y) { - return openage::util::FixedPoint::from_raw_value( +template +constexpr openage::util::FixedPoint max(openage::util::FixedPoint x, openage::util::FixedPoint y) { + return openage::util::FixedPoint::from_raw_value( std::max(x.get_raw_value(), y.get_raw_value())); } -template -constexpr openage::util::FixedPoint abs(openage::util::FixedPoint n) { - return openage::util::FixedPoint::from_raw_value( +template +constexpr openage::util::FixedPoint abs(openage::util::FixedPoint n) { + return openage::util::FixedPoint::from_raw_value( std::abs(n.get_raw_value())); } -template -constexpr double hypot(openage::util::FixedPoint x, openage::util::FixedPoint y) { +template +constexpr double hypot(openage::util::FixedPoint x, openage::util::FixedPoint y) { return x.hypot(y); } -template -struct hash> { - constexpr size_t operator()(const openage::util::FixedPoint &n) const { +template +struct hash> { + constexpr size_t operator()(const openage::util::FixedPoint &n) const { return std::hash{}(n.raw_value); } }; -template -struct numeric_limits> { - constexpr static openage::util::FixedPoint min() { - return openage::util::FixedPoint::min_value(); +template +struct numeric_limits> { + constexpr static openage::util::FixedPoint min() { + return openage::util::FixedPoint::min_value(); } - constexpr static openage::util::FixedPoint max() { - return openage::util::FixedPoint::max_value(); + constexpr static openage::util::FixedPoint max() { + return openage::util::FixedPoint::max_value(); } }; diff --git a/libopenage/util/fixed_point_test.cpp b/libopenage/util/fixed_point_test.cpp index 7a7fb2bc0c..21fd788259 100644 --- a/libopenage/util/fixed_point_test.cpp +++ b/libopenage/util/fixed_point_test.cpp @@ -1,4 +1,4 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. +// Copyright 2016-2025 the openage authors. See copying.md for legal info. #include "fixed_point.h" @@ -122,6 +122,40 @@ void fixed_point() { TESTEQUALS_FLOAT(TestTypeShort::e().to_double(), math::E, 1e-3); TESTEQUALS_FLOAT(TestTypeShort::pi().to_double(), math::PI, 1e-3); + { + using S = FixedPoint; + using T = FixedPoint; + + auto a = S::from_int(16U); + TESTNOTEQUALS((a*a).to_int(), 256U); + + auto b = T::from_int(16U); + TESTEQUALS((b*b).to_int(), 256U); + + auto c = T::from_int(17U); + TESTEQUALS((c*c).to_int(), 289U); + } + { + using S = FixedPoint; + auto a = S::from_int(256); + auto b = S::from_int(8); + TESTNOTEQUALS((a/b).to_int(), 32); + + + using T = FixedPoint; + auto c = T::from_int(256); + auto d = T::from_int(8); + TESTEQUALS((c/d).to_int(), 32); + } + { + using T = FixedPoint; + auto a = T::from_double(4.75); + auto b = T::from_double(3.5); + auto c = -a; + TESTEQUALS_FLOAT((a/b).to_double(), 4.75/3.5, 0.1); + TESTEQUALS_FLOAT((c/b).to_double(), -4.75/3.5, 0.1); + } + } }}} // openage::util::tests From 5ad6d7282d857c4ce149cd802a5311f8050119e3 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sat, 15 Mar 2025 11:34:04 +0100 Subject: [PATCH 734/771] Added name to copying.md --- copying.md | 1 + 1 file changed, 1 insertion(+) diff --git a/copying.md b/copying.md index dd71a8dc82..862f18bb39 100644 --- a/copying.md +++ b/copying.md @@ -163,6 +163,7 @@ _the openage authors_ are: | Ngô Xuân Minh | | xminh dawt ngo dawt 00 à gmail dawt com | | Haytham Tang | haytham918 | yunxuant à umich dawt edu | | Ana Trias-Labellarte | anatriaslabella | ana dawt triaslabella à ufl dawt edu | +| Eelco Empting | Eeelco | me à eelco dawt de | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. From cc6bddfe300c2526f66d99a04ba3aaf9a41f7f69 Mon Sep 17 00:00:00 2001 From: Jordan Sutton Date: Mon, 17 Mar 2025 06:10:05 -0600 Subject: [PATCH 735/771] Update missing CMAKE_CXX_COMPILER env in ubuntu dockerfile --- copying.md | 1 + packaging/docker/devenv/Dockerfile.ubuntu.2404 | 2 ++ 2 files changed, 3 insertions(+) diff --git a/copying.md b/copying.md index 862f18bb39..8daf89e0a9 100644 --- a/copying.md +++ b/copying.md @@ -164,6 +164,7 @@ _the openage authors_ are: | Haytham Tang | haytham918 | yunxuant à umich dawt edu | | Ana Trias-Labellarte | anatriaslabella | ana dawt triaslabella à ufl dawt edu | | Eelco Empting | Eeelco | me à eelco dawt de | +| Jordan Sutton | jsutCodes | jsutcodes à gmial dawt com | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. diff --git a/packaging/docker/devenv/Dockerfile.ubuntu.2404 b/packaging/docker/devenv/Dockerfile.ubuntu.2404 index d3d492f6af..5a135fe2a2 100644 --- a/packaging/docker/devenv/Dockerfile.ubuntu.2404 +++ b/packaging/docker/devenv/Dockerfile.ubuntu.2404 @@ -37,3 +37,5 @@ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y sudo \ qml6-module-qtquick3d-spatialaudio \ && sudo apt-get clean \ && truncate -s 0 ~/.bash_history + +ENV CMAKE_CXX_COMPILER=/usr/bin/g++ From 36727b6c64a9b18ccab06691d05213a5e4468641 Mon Sep 17 00:00:00 2001 From: Jordan Sutton Date: Mon, 17 Mar 2025 08:56:17 -0600 Subject: [PATCH 736/771] Remove -11 argument Ubuntu 24.04 LTS installs with base of g++-13 and gcc-13. Unsure if this is the correct path. Could either move forward with 13 or can change to manually install the 11 version in the Docker Build proccess. --- .github/workflows/ubuntu-24.04.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ubuntu-24.04.yml b/.github/workflows/ubuntu-24.04.yml index 6fa9145279..2fd07b7030 100644 --- a/.github/workflows/ubuntu-24.04.yml +++ b/.github/workflows/ubuntu-24.04.yml @@ -41,7 +41,7 @@ jobs: - name: Build openage run: | sudo docker run --rm -v "$(pwd)":/mnt/openage -w /mnt/openage openage-devenv:latest \ - bash -c 'mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=$(which gcc-11) -DCMAKE_CXX_COMPILER=$(which g++-11) -DCMAKE_CXX_FLAGS='' -DCMAKE_EXE_LINKER_FLAGS='' -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_MODULE_LINKER_FLAGS='' -DCMAKE_SHARED_LINKER_FLAGS='' -DDOWNLOAD_NYAN=YES -DCXX_OPTIMIZATION_LEVEL=auto -DCXX_SANITIZE_FATAL=False -DCXX_SANITIZE_MODE=none -DWANT_BACKTRACE=if_available -DWANT_GPERFTOOLS_PROFILER=if_available -DWANT_GPERFTOOLS_TCMALLOC=False -DWANT_INOTIFY=if_available -DWANT_NCURSES=if_available -DWANT_OPENGL=if_available -DWANT_VULKAN=if_available -G Ninja .. && cmake --build . --parallel $(nproc) -- -k1' + bash -c 'mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=$(which gcc) -DCMAKE_CXX_COMPILER=$(which g++) -DCMAKE_CXX_FLAGS='' -DCMAKE_EXE_LINKER_FLAGS='' -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_MODULE_LINKER_FLAGS='' -DCMAKE_SHARED_LINKER_FLAGS='' -DDOWNLOAD_NYAN=YES -DCXX_OPTIMIZATION_LEVEL=auto -DCXX_SANITIZE_FATAL=False -DCXX_SANITIZE_MODE=none -DWANT_BACKTRACE=if_available -DWANT_GPERFTOOLS_PROFILER=if_available -DWANT_GPERFTOOLS_TCMALLOC=False -DWANT_INOTIFY=if_available -DWANT_NCURSES=if_available -DWANT_OPENGL=if_available -DWANT_VULKAN=if_available -G Ninja .. && cmake --build . --parallel $(nproc) -- -k1' - name: Compress build artifacts run: | mkdir -p /tmp/openage From 9b9cf3ee81abedd1f0f6d6eee1c26251f5a4b7d9 Mon Sep 17 00:00:00 2001 From: Jordan Sutton Date: Mon, 17 Mar 2025 09:14:29 -0600 Subject: [PATCH 737/771] Undo unecessary ENV command --- packaging/docker/devenv/Dockerfile.ubuntu.2404 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packaging/docker/devenv/Dockerfile.ubuntu.2404 b/packaging/docker/devenv/Dockerfile.ubuntu.2404 index 5a135fe2a2..3ca06676f1 100644 --- a/packaging/docker/devenv/Dockerfile.ubuntu.2404 +++ b/packaging/docker/devenv/Dockerfile.ubuntu.2404 @@ -36,6 +36,4 @@ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y sudo \ qt6-multimedia-dev \ qml6-module-qtquick3d-spatialaudio \ && sudo apt-get clean \ - && truncate -s 0 ~/.bash_history - -ENV CMAKE_CXX_COMPILER=/usr/bin/g++ + && truncate -s 0 ~/.bash_history \ No newline at end of file From 11fb72283b0c9a5be449c17c3aa629da5bb22ed2 Mon Sep 17 00:00:00 2001 From: Jordan Sutton Date: Mon, 17 Mar 2025 09:23:27 -0600 Subject: [PATCH 738/771] Update copying.md --- copying.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copying.md b/copying.md index 8daf89e0a9..9d8f6703e1 100644 --- a/copying.md +++ b/copying.md @@ -164,7 +164,7 @@ _the openage authors_ are: | Haytham Tang | haytham918 | yunxuant à umich dawt edu | | Ana Trias-Labellarte | anatriaslabella | ana dawt triaslabella à ufl dawt edu | | Eelco Empting | Eeelco | me à eelco dawt de | -| Jordan Sutton | jsutCodes | jsutcodes à gmial dawt com | +| Jordan Sutton | jsutCodes | jsutcodes à gmail dawt com | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. From d255e7ec598b542137e744448ba835c8caa204a7 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 31 Mar 2025 02:27:13 +0200 Subject: [PATCH 739/771] doc: Add 'vulkan' package to Windows vcpkg install instructions. --- doc/build_instructions/windows_msvc.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/build_instructions/windows_msvc.md b/doc/build_instructions/windows_msvc.md index e31bcf79cd..43def4cbd0 100644 --- a/doc/build_instructions/windows_msvc.md +++ b/doc/build_instructions/windows_msvc.md @@ -47,7 +47,11 @@ _Note:_ Also ensure that `python` and `python3` both point to the correct and th vcpkg install dirent eigen3 fontconfig freetype harfbuzz libepoxy libogg libpng opus opusfile qtbase qtdeclarative qtmultimedia toml11 - _Note:_ The `qt6` port in vcpkg has been split into multiple packages, build times are acceptable now. +If you also want Vulkan graphics support, you should also run this command: + + vcpkg install vulkan + + _Note:_ The `qt` port in vcpkg has been split into multiple packages, build times are acceptable now. If you want, you can still use [the prebuilt version](https://www.qt.io/download-open-source/) instead. If you do so, include `-DCMAKE_PREFIX_PATH=` in the cmake configure command. From 932358c39c0b95990f649ca1073a8d3bebfb12e7 Mon Sep 17 00:00:00 2001 From: heinezen Date: Mon, 31 Mar 2025 02:40:48 +0200 Subject: [PATCH 740/771] doc: Reformat Windows build docs Markdown. --- doc/build_instructions/windows_msvc.md | 122 +++++++++++++------------ 1 file changed, 65 insertions(+), 57 deletions(-) diff --git a/doc/build_instructions/windows_msvc.md b/doc/build_instructions/windows_msvc.md index 43def4cbd0..9dc072db5a 100644 --- a/doc/build_instructions/windows_msvc.md +++ b/doc/build_instructions/windows_msvc.md @@ -17,104 +17,112 @@ __NOTE:__ You need to manually make sure and doublecheck if the system you are b --> ## Setting up the build environment - You will need to download and install the following manually. - Those who already have the latest stable versions of these programs can skip this: - - [Visual Studio Buildtools](https://aka.ms/vs/17/release/vs_BuildTools.exe) - - With the "Visual C++ Buildtools" workload. +You will need to download and install the following manually. +Those who already have the latest stable versions of these programs can skip this: - _NOTE:_ If you are searching for an IDE for development you can get an overview [here](https://en.wikipedia.org/wiki/Comparison_of_integrated_development_environments#C/C++). - We've also written some [instructions for developing with different IDEs](/doc/ide/README.md). +- [Visual Studio Buildtools](https://aka.ms/vs/17/release/vs_BuildTools.exe) + - With the "Visual C++ Buildtools" workload. + _NOTE:_ If you are searching for an IDE for development you can get an overview [here](https://en.wikipedia.org/wiki/Comparison_of_integrated_development_environments#C/C++). We've also written some [instructions for developing with different IDEs](/doc/ide/README.md). - - [Python 3](https://www.python.org/downloads/windows/) - - With the "pip" option enabled. We use `pip` to install other dependencies. - - With the "Precompile standard library" option enabled. - - With the "Download debug binaries (...)" option enabled. - - If in doubt, run the installer again and choose "Modify". - - You are going to need the 64-bit version of python if you are planning to build the 64-bit version of openage, and vice versa. +- [Python 3](https://www.python.org/downloads/windows/) + - With the *pip* option enabled. We use `pip` to install other dependencies. + - With the *Precompile standard library* option enabled. + - With the *Download debug binaries (...)* option enabled. + - If in doubt, run the installer again and choose *Modify*. + - You are going to need the 64-bit version of python if you are planning to build the 64-bit version of openage, and vice versa. - [CMake](https://cmake.org/download/) ### Python Modules - Open a command prompt at `/Scripts` - - pip install cython numpy lz4 toml pillow pygments pyreadline3 mako +Open a command prompt at `/Scripts` +```ps +pip install cython numpy lz4 toml pillow pygments pyreadline3 mako +``` _Note:_ Make sure the Python 3 instance you're installing these scripts for is the one you call `python` in CMD + _Note:_ Also ensure that `python` and `python3` both point to the correct and the same version of Python 3 ### vcpkg packages - Set up [vcpkg](https://github.com/Microsoft/vcpkg#quick-start). Open a command prompt at `` +Set up [vcpkg](https://github.com/Microsoft/vcpkg#quick-start). Open a command prompt at `` - vcpkg install dirent eigen3 fontconfig freetype harfbuzz libepoxy libogg libpng opus opusfile qtbase qtdeclarative qtmultimedia toml11 +```ps +vcpkg install dirent eigen3 fontconfig freetype harfbuzz libepoxy libogg libpng opus opusfile qtbase qtdeclarative qtmultimedia toml11 +``` If you also want Vulkan graphics support, you should also run this command: - vcpkg install vulkan +```ps +vcpkg install vulkan +``` - _Note:_ The `qt` port in vcpkg has been split into multiple packages, build times are acceptable now. - If you want, you can still use [the prebuilt version](https://www.qt.io/download-open-source/) instead. - If you do so, include `-DCMAKE_PREFIX_PATH=` in the cmake configure command. +_Note:_ The `qt` port in vcpkg has been split into multiple packages, build times are acceptable now. If you want, you can still use [the prebuilt version](https://www.qt.io/download-open-source/) instead. If you do so, include `-DCMAKE_PREFIX_PATH=` in the cmake configure command. - _Note:_ If you are planning to build the 64-bit version of openage, you are going to need 64-bit libraries. - Add command line option `--triplet x64-windows` to the above command or add the environment variable `VCPKG_DEFAULT_TRIPLET=x64-windows` to build x64 libraries. [See here](https://github.com/Microsoft/vcpkg/issues/1254) +_Note:_ If you are planning to build the 64-bit version of openage, you are going to need 64-bit libraries. Add command line option `--triplet x64-windows` to the above command or add the environment variable `VCPKG_DEFAULT_TRIPLET=x64-windows` to build x64 libraries. [See here](https://github.com/Microsoft/vcpkg/issues/1254) ## Building openage - Note that openage doesn't support completely out-of-source-tree builds yet. - We will, however, use a separate `build` directory to build the binaries. +Note that openage doesn't support completely out-of-source-tree builds yet. We will, however, use a separate `build` directory to build the binaries. -_Note:_ You will also need to set up [the dependencies for Nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md#windows), which is mainly [flex](https://sourceforge.net/projects/winflexbison/) +_Note:_ You will also need to set up [the dependencies for nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md#windows), which is mainly [flex](https://sourceforge.net/projects/winflexbison/). Open a command prompt at ``: - mkdir build - cd build - cmake -DCMAKE_TOOLCHAIN_FILE=\scripts\buildsystems\vcpkg.cmake .. - cmake --build . --config RelWithDebInfo -- /nologo /m /v:m +```ps +mkdir build +cd build +cmake -DCMAKE_TOOLCHAIN_FILE=\scripts\buildsystems\vcpkg.cmake .. +cmake --build . --config RelWithDebInfo -- /nologo /m /v:m +``` _Note:_ If you want to build the x64 version, please add `-G "Visual Studio 17 2022" -A x64` (for VS2022) to the first cmake command. _Note:_ If you want to download and build Nyan automatically add `-DDOWNLOAD_NYAN=YES -DFLEX_EXECUTABLE=` to the first cmake command. ## Running openage (in devmode) - While this is straightforward on other platforms, there is still stuff to do to run openage on Windows: - - Install the [DejaVu Book Font](https://dejavu-fonts.github.io/Download.html). - - Download and extract the latest `dejavu-fonts-ttf` tarball/zip file. - - Select all `ttf\DejaVuSerif*.ttf` files, right click and click `Install for all users`. - - _Note:_ This will require administrator rights. - - Set the `FONTCONFIG_PATH` environment variable to `\installed\\tools\fontconfig\fonts\` or `\installed\\etc\fonts\`. - - Copy `fontconfig\57-dejavu-serif.conf` to `%FONTCONFIG_PATH%\conf.d`. - - [Optional] Set the `AGE2DIR` environment variable to the AoE 2 installation directory. - - Set `QML2_IMPORT_PATH` to `\installed\\qml` or for prebuilt Qt `\\\qml` - - openage needs these DLL files to run: - - `openage.dll` (Usually in `\build\libopenage\`.) - - `nyan.dll` (The location depends on the procedure chosen to get nyan.) - - DLLs from vcpkg-installed dependencies. Normally, these DLLs should be copied to `\build\libopenage\` during the build process. If they are not, you can find them in `\installed\\bin`. - - If prebuilt QT6 was installed, the original location of QT6 DLLs is `\bin`. - - - Now, to run the openage: - - Open a CMD window in `\build\` and run `python -m openage main` - - Execute`\build\run.exe` every time after that and enjoy! +While this is straightforward on other platforms, there is still stuff to do to run openage on Windows: + +- Install the [DejaVu Book Font](https://dejavu-fonts.github.io/Download.html). + - Download and extract the latest `dejavu-fonts-ttf` tarball/zip file. + - Select all `ttf\DejaVuSerif*.ttf` files, right click and click `Install for all users`. + _Note:_ This will require administrator rights. + + - Set the `FONTCONFIG_PATH` environment variable to `\installed\\tools\fontconfig\fonts\` or `\installed\\etc\fonts\`. + - Copy `fontconfig\57-dejavu-serif.conf` to `%FONTCONFIG_PATH%\conf.d`. +- [Optional] Set the `AGE2DIR` environment variable to the AoE 2 installation directory. +- Set `QML2_IMPORT_PATH` to `\installed\\qml` or for prebuilt Qt `\\\qml` +- openage needs these DLL files to run: + - `openage.dll` (Usually in `\build\libopenage\`.) + - `nyan.dll` (The location depends on the procedure chosen to get nyan.) + - DLLs from vcpkg-installed dependencies. Normally, these DLLs should be copied to `\build\libopenage\` during the build process. If they are not, you can find them in `\installed\\bin`. + - If prebuilt QT6 was installed, the original location of QT6 DLLs is `\bin`. + +Now, to run openage: + +- Open a CMD window in `\build\` and run `python -m openage main` +- Execute`\build\run.exe` every time after that and enjoy! ## Packaging - - Install [NSIS](https://sourceforge.net/projects/nsis/files/latest/download). - - Depending on the way you installed Qt (vcpkg/pre-built) you need to edit the following line in `\buildsystem\templates\ForwardVariables.cmake.in`: +- Install [NSIS](https://sourceforge.net/projects/nsis/files/latest/download). +- Depending on the way you installed Qt (vcpkg/pre-built) you need to edit the following line in `\buildsystem\templates\ForwardVariables.cmake.in`: + ``` - # Use windeploy for packaging qt-prebuilt, standard value '1' for windeploy, '0' for vcpkg - set(use_windeployqt 1) +# Use windeploy for packaging qt-prebuilt, standard value '1' for windeploy, '0' for vcpkg +set(use_windeployqt 1) ``` - Open a command prompt at `\build` (or use the one from the building step): +Open a command prompt at `\build` (or use the one from the building step): +```ps cpack -C RelWithDebInfo +``` - The installer (`openage--.exe`) will be generated in the same directory.
+The installer (`openage--.exe`) will be generated in the same directory.
- _Hint:_ Append `-V` to the `cpack` command for verbose output (it takes time to package all dependencies). +_Note:_ Append `-V` to the `cpack` command for verbose output (it takes time to package all dependencies). - _Hint:_ you can set with the environment variable `TARGET_PLATFORM` (e.g. amd64, x86). +_Note:_ you can set with the environment variable `TARGET_PLATFORM` (e.g. amd64, x86). From 777163d8535b6c8cd21a3a23bd5aac79b149631e Mon Sep 17 00:00:00 2001 From: danielwieczorek Date: Sun, 6 Apr 2025 14:37:17 +0200 Subject: [PATCH 741/771] Update new contributor Add new contributor into copying.md and mailmap Issue: https://github.com/SFTtech/openage/issues/1766 --- copying.md | 1 + 1 file changed, 1 insertion(+) diff --git a/copying.md b/copying.md index 9d8f6703e1..d8430fd215 100644 --- a/copying.md +++ b/copying.md @@ -165,6 +165,7 @@ _the openage authors_ are: | Ana Trias-Labellarte | anatriaslabella | ana dawt triaslabella à ufl dawt edu | | Eelco Empting | Eeelco | me à eelco dawt de | | Jordan Sutton | jsutCodes | jsutcodes à gmail dawt com | +| Daniel Wieczorek | Danio | danielwieczorek96 à gmail dawt com | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. From 4cf5f6fd5588cd2fbb0f45e73ea878749919a6c6 Mon Sep 17 00:00:00 2001 From: danielwieczorek Date: Sun, 6 Apr 2025 14:16:51 +0200 Subject: [PATCH 742/771] Fix assets conversion bug. Add running with docker docs Using Cython < 3.0.10 leads to the runtime errors on assets conversion. Cython >= 3.0.10 shall be used. Update docker build file and add chapter in documentation about running image with Docker. Issue: https://github.com/SFTtech/openage/issues/1766 --- CMakeLists.txt | 4 +- README.md | 4 +- buildsystem/HandlePythonOptions.cmake | 16 ++++- doc/build_instructions/docker.md | 70 +++++++++++++++++++ doc/build_instructions/ubuntu.md | 14 ++-- doc/building.md | 3 +- .../docker/devenv/Dockerfile.ubuntu.2404 | 6 +- 7 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 doc/build_instructions/docker.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 47484acb06..7a6a785bf4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # Python and Cython requirements set(PYTHON_MIN_VERSION 3.9) -set(CYTHON_MIN_VERSION 0.29.31) +set(CYTHON_MIN_VERSION 3.0.10) +set(CYTHON_MIN_VERSION_FALLBACK 0.29.31) +set(CYTHON_MAX_VERSION_FALLBACK 3.0.7) # CMake policies foreach(pol diff --git a/README.md b/README.md index c0bcf4a02c..8666083783 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,9 @@ Quickstart make ``` +**Alternative approach:** +You can build and run the project using Docker. See [Running with docker](/doc/build_instructions/docker.md) for more details. + * **I compiled everything. Now how do I run it?** * Execute `cd bin && ./run main`. * [The convert script](/doc/media_convert.md) will transform original assets into openage formats, which are a lot saner and more moddable. @@ -145,7 +148,6 @@ To turn them off, use `./bin/run --dont-segfault --no-errors --dont-eat-dog`. If this still does not help, try our [troubleshooting guide](/doc/troubleshooting.md), the [contact section](#contact) or the [bug tracker](https://github.com/SFTtech/openage/issues). - Contributing ============ diff --git a/buildsystem/HandlePythonOptions.cmake b/buildsystem/HandlePythonOptions.cmake index 13ac8a62b8..4c5988efad 100644 --- a/buildsystem/HandlePythonOptions.cmake +++ b/buildsystem/HandlePythonOptions.cmake @@ -4,7 +4,17 @@ # the Python version number requirement is in modules/FindPython_test.cpp find_package(Python ${PYTHON_MIN_VERSION} REQUIRED) -find_package(Cython ${CYTHON_MIN_VERSION} REQUIRED) + +find_package(Cython ${CYTHON_MIN_VERSION}) +if(NOT CYTHON_FOUND) + message("Checking for alternative Cython fallback version (>=${CYTHON_MIN_VERSION_FALLBACK} AND <=${CYTHON_MAX_VERSION_FALLBACK})") + find_package(Cython ${CYTHON_MIN_VERSION_FALLBACK} QUIET) + if(CYTHON_VERSION VERSION_LESS ${CYTHON_MIN_VERSION} AND CYTHON_VERSION VERSION_GREATER ${CYTHON_MAX_VERSION_FALLBACK}) + message(FATAL_ERROR "Cython version ${CYTHON_VERSION} is not compatible") + else() + message("Compatible Cython version ${CYTHON_VERSION} found") + endif() +endif() py_get_config_var(EXT_SUFFIX PYEXT_SUFFIX) if(MINGW) @@ -43,8 +53,8 @@ message("PYTHON_LIBRARIES: " "${PYTHON_LIBRARIES}") #Windows always uses optimized version of Python lib if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") #get index of string "optimized" and increment it by 1 so index points at the path of the optimized lib - list (FIND PYEXT_LIBRARY "optimized" _index) - if (${_index} GREATER -1) + list(FIND PYEXT_LIBRARY "optimized" _index) + if(${_index} GREATER -1) MATH(EXPR _index "${_index}+1") list(GET PYEXT_LIBRARY ${_index} PYEXT_LIBRARY) endif() diff --git a/doc/build_instructions/docker.md b/doc/build_instructions/docker.md new file mode 100644 index 0000000000..6a431f0c63 --- /dev/null +++ b/doc/build_instructions/docker.md @@ -0,0 +1,70 @@ +### Running with Docker + +Docker simplifies the setup process by providing a consistent development environment. It allows you to build and run the project without manually installing dependencies. Currently provided [Dockerfile.ubuntu.2404](../../packaging/docker/devenv/Dockerfile.ubuntu.2404) supports Ubuntu 24.04 as the base operating system. + +#### Prerequisites + +- **Docker**: Ensure Docker is installed on your machine. Follow the [official documentation](https://docs.docker.com/) for installation instructions. +- **Display Server**: This guide supports both **X11** and **Wayland** display servers for GUI applications. +- **Tested Configuration**: These instructions were tested on Ubuntu 24.04 running on WSL2 on Windows 11. + +#### Steps to Build and Run + +1. **Enable GUI Support** + + Depending on your display server, follow the appropriate steps: + + - **For X11**: + Allow the Docker container to access your X server: + ```bash + xhost +local:root + ``` + + - **For Wayland**: + Allow the Docker container to access your Wayland socket: + ```bash + sudo chmod a+rw /run/user/$(id -u)/wayland-0 + ``` + +2. **Build the Docker Image** + + Build the Docker image using the provided Dockerfile: + ```bash + sudo docker build -t openage -f packaging/docker/devenv/Dockerfile.ubuntu.2404 . + ``` + +3. **Run the Docker Container** + + Start the Docker container with the appropriate configuration for your display server: + + - **For X11**: + ```bash + docker run -it \ + -e DISPLAY=$DISPLAY \ + -v /tmp/.X11-unix:/tmp/.X11-unix \ + -v $HOME/.Xauthority:/root/.Xauthority \ + --network host openage + ``` + + - **For Wayland**: + ```bash + docker run -it \ + -e XDG_RUNTIME_DIR=/tmp \ + -e QT_QPA_PLATFORM=wayland \ + -e WAYLAND_DISPLAY=$WAYLAND_DISPLAY \ + -v $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY:/tmp/$WAYLAND_DISPLAY \ + --user=$(id -u):$(id -g) \ + --network host openage + ``` + +4. **Follow the Regular Setup** + +Once inside the container, follow the regular setup described in the [Development](../building.md#development) chapter. You can skip dependency installation since the Docker image already includes all required dependencies. + +#### Notes + +- **X11 vs. Wayland**: Ensure you know which display server your system is using. Most modern Linux distributions default to Wayland, but X11 is still widely used. +- **Permissions**: For Wayland, you may need to adjust permissions for the Wayland socket (`/run/user/$(id -u)/wayland-0`) to allow Docker access. +- **GUI Applications**: These configurations enable GUI applications to run inside the Docker container. + +By following these steps, you can build and run the `openage` project in a Dockerized environment with support X11 or Wayland display servers. diff --git a/doc/build_instructions/ubuntu.md b/doc/build_instructions/ubuntu.md index d46ebd159f..46cd975fb3 100644 --- a/doc/build_instructions/ubuntu.md +++ b/doc/build_instructions/ubuntu.md @@ -2,19 +2,23 @@ Run the following commands: - - `sudo apt-get update` - - `sudo apt-get install g++ cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-dev libtoml11-dev python3-dev python3-mako python3-numpy python3-lz4 python3-pil python3-pip python3-pygments python3-toml qml6-module-qtquick-controls qt6-declarative-dev qt6-multimedia-dev qml6-module-qtquick3d-spatialaudio` +```bash + sudo apt-get update + sudo apt-get install g++ cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-dev libtoml11-dev python3-dev python3-mako python3-numpy python3-lz4 python3-pil python3-pip python3-pygments python3-toml qml6-module-qtquick-controls qt6-declarative-dev qt6-multimedia-dev qml6-module-qtquick3d-spatialaudio + ``` You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies. -# Additional steps for Ubuntu 22.04 LTS +# Additional steps for Ubuntu 22.04 LTS & 24.04 LTS -The available system version of Cython is too old in Ubuntu 22.04. You have to get the correct version +The available system version of Cython is too old in Ubuntu 22.04 & 24.04. You have to get the correct version from pip: -``` +```bash pip3 install cython --break-system-packages ``` +Please note that project requires at least **Cython 3.0.10**. + # Linux Mint Issue Linux Mint has a [problem with `toml11`](https://github.com/SFTtech/openage/issues/1601), since CMake can't find it. To solve this, download the [toml11.zip](https://github.com/SFTtech/openage/files/13401192/toml11.zip), after, put the files in the `/usr/lib/x86_64-linux-gnu/cmake/toml11` path. (if the `toml11` directory doesn't exist, create it) diff --git a/doc/building.md b/doc/building.md index 6ed51cb4a6..923fde5507 100644 --- a/doc/building.md +++ b/doc/building.md @@ -29,7 +29,7 @@ Dependency list: C gcc >=10 or clang >=10 CRA python >=3.9 - C cython >=0.29.31 + C cython >=3.0.10 OR (>=0.29.31 AND <=3.0.7) C cmake >=3.16 A numpy A lz4 @@ -161,7 +161,6 @@ The reference package is [created for Gentoo](https://github.com/SFTtech/gentoo- - Use `make install DESTDIR=/tmp/your_temporary_packaging_dir`, which will then be packed/installed by your package manager. - ### Troubleshooting - I wanna see compiler invocations diff --git a/packaging/docker/devenv/Dockerfile.ubuntu.2404 b/packaging/docker/devenv/Dockerfile.ubuntu.2404 index 3ca06676f1..f57dd43321 100644 --- a/packaging/docker/devenv/Dockerfile.ubuntu.2404 +++ b/packaging/docker/devenv/Dockerfile.ubuntu.2404 @@ -36,4 +36,8 @@ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y sudo \ qt6-multimedia-dev \ qml6-module-qtquick3d-spatialaudio \ && sudo apt-get clean \ - && truncate -s 0 ~/.bash_history \ No newline at end of file + && truncate -s 0 ~/.bash_history + +# At least cython >= 3.0.10 < 4.0.0 is required to avoid runtime errors +# TODO: Remove this line once cython is upgraded in Ubuntu 24.04.3 (expected around August 2025) +RUN pip install "cython>=3.0.10,<4.0.0" --break-system-packages \ No newline at end of file From 6d5ee2bb253f897bd1c1b88a75132f9567ec1221 Mon Sep 17 00:00:00 2001 From: heinezen Date: Sun, 20 Apr 2025 02:01:46 +0200 Subject: [PATCH 743/771] etc: Fix import of gdb printing Python module. --- etc/gdb_pretty/printers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index fe6d43e397..9ae29539ce 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.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. """ Pretty printers for GDB. @@ -6,6 +6,7 @@ import re import gdb # type: ignore +import gdb.printing # type: ignore # TODO: Printers should inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. From 8ae9de699784defb318cc4f6250eccaed9b44983 Mon Sep 17 00:00:00 2001 From: bytegrrrl Date: Sun, 13 Apr 2025 08:19:45 -0400 Subject: [PATCH 744/771] Add pure FixedPoint sqrt implementation and tests --- libopenage/util/fixed_point.h | 32 ++++++++++++++++++++++- libopenage/util/fixed_point_test.cpp | 38 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/libopenage/util/fixed_point.h b/libopenage/util/fixed_point.h index 2d6a7eb084..337c3ceb4e 100644 --- a/libopenage/util/fixed_point.h +++ b/libopenage/util/fixed_point.h @@ -3,6 +3,7 @@ #pragma once #include +#include #include #include #include @@ -446,8 +447,37 @@ class FixedPoint { return is; } + /** + * Pure FixedPoint sqrt implementation using Heron's Algorithm. + * + * Note that this function is undefined for negative values. + */ constexpr double sqrt() { - return std::sqrt(this->to_double()); + // Zero can cause issues later, so deal with now. + if (this->raw_value == 0) { + return 0.0; + } + + // A greater shift = more precision, but can overflow the intermediate type if too large. + size_t max_shift = std::countl_zero((unsigned_intermediate_type)this->raw_value) - 1; + size_t shift = max_shift > fractional_bits ? fractional_bits : max_shift; + shift &= ~1; + + // We can't use the safe shift since the shift value is unknown at compile time. + intermediate_type n = (intermediate_type)this->raw_value << shift; + intermediate_type guess = (intermediate_type)1 << fractional_bits; + + for (size_t _i = 0; _i < fractional_bits; _i++) { + intermediate_type prev = guess; + guess = (guess + n / guess) / 2; + if (guess == prev) + break; + } + + // The sqrt operation halves the number of bits, so we'll we'll have to calculate a shift back + size_t unshift = fractional_bits - (shift + fractional_bits) / 2; + + return from_raw_value(guess << unshift).to_double(); } constexpr double atan2(const FixedPoint &n) { diff --git a/libopenage/util/fixed_point_test.cpp b/libopenage/util/fixed_point_test.cpp index 21fd788259..f5951ac267 100644 --- a/libopenage/util/fixed_point_test.cpp +++ b/libopenage/util/fixed_point_test.cpp @@ -156,6 +156,44 @@ void fixed_point() { TESTEQUALS_FLOAT((c/b).to_double(), -4.75/3.5, 0.1); } + // Pure FixedPoint sqrt tests + { + using T = FixedPoint; + TESTEQUALS_FLOAT(T(41231.131).sqrt(), 203.0545025356, 1e-7); + TESTEQUALS_FLOAT(T(547965.116).sqrt(), 740.2466588915, 1e-7); + + TESTEQUALS_FLOAT(T(2).sqrt(), T::sqrt_2(), 1e-9); + TESTEQUALS_FLOAT(2 / T::pi().sqrt(), T::inv2_sqrt_pi(), 1e-9); + + // Powers of two (anything over 2^15 will overflow (2^16)^2 = 2^32 >). + for (size_t i = 0; i < 15; i++) { + int64_t value = 1 << i; + TESTEQUALS_FLOAT(T(value * value).sqrt(), value, 1e-7); + } + + for (size_t i = 0; i < 100; i++) { + double value = 14.25 * i; + TESTEQUALS_FLOAT(T(value * value).sqrt(), value, 1e-7); + } + + // This one can go up to 2^63, but that would take years. + for (uint32_t i = 0; i < (1u << 16); i++) { + T value = T::from_raw_value(i * i); + TESTEQUALS_FLOAT(value.sqrt(), std::sqrt(value.to_double()), 1e-7); + } + + // We lose some precision when raw_type == intermediate_type + for (uint64_t i = 1; i < (1ul << 63); i = (i << 1) ^ i) { + T value = T::from_raw_value(i * i); + TESTEQUALS_FLOAT(value.sqrt(), std::sqrt(value.to_double()), 1e-4); + } + + using FP16_16 = FixedPoint; + for (uint32_t i = 0; i < (1u << 16); i++) { + FP16_16 value = FP16_16::from_raw_value(i); + TESTEQUALS_FLOAT(value.sqrt(), std::sqrt(value.to_double()), 1e-4); + } + } } }}} // openage::util::tests From 9890d12319de2c4a5425895d00a06f1b2fc511c0 Mon Sep 17 00:00:00 2001 From: bytegrrrl Date: Sun, 13 Apr 2025 19:26:52 -0400 Subject: [PATCH 745/771] Update copying.md --- copying.md | 1 + 1 file changed, 1 insertion(+) diff --git a/copying.md b/copying.md index d8430fd215..8c61b294e2 100644 --- a/copying.md +++ b/copying.md @@ -166,6 +166,7 @@ _the openage authors_ are: | Eelco Empting | Eeelco | me à eelco dawt de | | Jordan Sutton | jsutCodes | jsutcodes à gmail dawt com | | Daniel Wieczorek | Danio | danielwieczorek96 à gmail dawt com | +| | bytegrrrl | bytegrrrl à proton dawt me | If you're a first-time committer, add yourself to the above list. This is not just for legal reasons, but also to keep an overview of all those nicknames. From a51a90e3b1ef56c7167e4fa1ac19f14b3be0bda4 Mon Sep 17 00:00:00 2001 From: bytegrrrl Date: Mon, 21 Apr 2025 03:13:39 -0400 Subject: [PATCH 746/771] Update FixedPoint::sqrt() to return a FixedPoint --- libopenage/util/fixed_point.h | 32 ++++++++++++++++++++-------- libopenage/util/fixed_point_test.cpp | 18 +++++++++++----- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/libopenage/util/fixed_point.h b/libopenage/util/fixed_point.h index 337c3ceb4e..6cab0167f7 100644 --- a/libopenage/util/fixed_point.h +++ b/libopenage/util/fixed_point.h @@ -451,33 +451,47 @@ class FixedPoint { * Pure FixedPoint sqrt implementation using Heron's Algorithm. * * Note that this function is undefined for negative values. + * + * There's a small loss in precision depending on the value of fractional_bits and the position of + * the most significant bit: if the integer portion is very large, we won't have as much (absolute) + * precision. Ideally you would want the intermediate_type to be twice the size of raw_type to avoid + * any losses. */ - constexpr double sqrt() { + constexpr FixedPoint sqrt() { // Zero can cause issues later, so deal with now. if (this->raw_value == 0) { return 0.0; } + // Check for negative values + ENSURE(std::is_unsigned() or std::is_signed() and this->raw_value > 0, "FixedPoint::sqrt() is undefined for negative values."); + // A greater shift = more precision, but can overflow the intermediate type if too large. - size_t max_shift = std::countl_zero((unsigned_intermediate_type)this->raw_value) - 1; + size_t max_shift = std::countl_zero(static_cast(this->raw_value)) - 1; size_t shift = max_shift > fractional_bits ? fractional_bits : max_shift; - shift &= ~1; + + // shift + fractional bits must be an even number + if ((shift + fractional_bits) % 2) { + shift -= 1; + } // We can't use the safe shift since the shift value is unknown at compile time. - intermediate_type n = (intermediate_type)this->raw_value << shift; - intermediate_type guess = (intermediate_type)1 << fractional_bits; + intermediate_type n = static_cast(this->raw_value) << shift; + intermediate_type guess = static_cast(1) << fractional_bits; - for (size_t _i = 0; _i < fractional_bits; _i++) { + for (size_t i = 0; i < fractional_bits; i++) { intermediate_type prev = guess; guess = (guess + n / guess) / 2; - if (guess == prev) + if (guess == prev) { break; + + } } // The sqrt operation halves the number of bits, so we'll we'll have to calculate a shift back size_t unshift = fractional_bits - (shift + fractional_bits) / 2; - return from_raw_value(guess << unshift).to_double(); + return from_raw_value(guess << unshift); } constexpr double atan2(const FixedPoint &n) { @@ -604,7 +618,7 @@ namespace std { template constexpr double sqrt(openage::util::FixedPoint n) { - return n.sqrt(); + return static_cast(n.sqrt()); } template diff --git a/libopenage/util/fixed_point_test.cpp b/libopenage/util/fixed_point_test.cpp index f5951ac267..64ba8f6b2c 100644 --- a/libopenage/util/fixed_point_test.cpp +++ b/libopenage/util/fixed_point_test.cpp @@ -163,7 +163,7 @@ void fixed_point() { TESTEQUALS_FLOAT(T(547965.116).sqrt(), 740.2466588915, 1e-7); TESTEQUALS_FLOAT(T(2).sqrt(), T::sqrt_2(), 1e-9); - TESTEQUALS_FLOAT(2 / T::pi().sqrt(), T::inv2_sqrt_pi(), 1e-9); + TESTEQUALS_FLOAT(2 / std::sqrt(T::pi()), T::inv2_sqrt_pi(), 1e-9); // Powers of two (anything over 2^15 will overflow (2^16)^2 = 2^32 >). for (size_t i = 0; i < 15; i++) { @@ -177,23 +177,31 @@ void fixed_point() { } // This one can go up to 2^63, but that would take years. - for (uint32_t i = 0; i < (1u << 16); i++) { + for (uint32_t i = 0; i < 65536; i++) { T value = T::from_raw_value(i * i); TESTEQUALS_FLOAT(value.sqrt(), std::sqrt(value.to_double()), 1e-7); } // We lose some precision when raw_type == intermediate_type - for (uint64_t i = 1; i < (1ul << 63); i = (i << 1) ^ i) { + for (uint64_t i = 1; i < std::numeric_limits::max(); i = (i * 2) ^ i) { T value = T::from_raw_value(i * i); + if (value < 0) { + value = -value; + } TESTEQUALS_FLOAT(value.sqrt(), std::sqrt(value.to_double()), 1e-4); } using FP16_16 = FixedPoint; - for (uint32_t i = 0; i < (1u << 16); i++) { + for (uint32_t i = 0; i < 65536; i++) { FP16_16 value = FP16_16::from_raw_value(i); TESTEQUALS_FLOAT(value.sqrt(), std::sqrt(value.to_double()), 1e-4); } + + + // Test with negative number + TESTTHROWS((FixedPoint::from_float(-3.25).sqrt())); + TESTNOEXCEPT((FixedPoint::from_float(3.25).sqrt())); + TESTNOEXCEPT((FixedPoint::from_float(-3.25).sqrt())); } } - }}} // openage::util::tests From 4a7d664ce0104ef3b966f15c5b1f90329b7099dc Mon Sep 17 00:00:00 2001 From: bytegrrrl Date: Mon, 21 Apr 2025 04:55:08 -0400 Subject: [PATCH 747/771] Fix sqrt test cases --- libopenage/util/fixed_point.h | 6 +++--- libopenage/util/fixed_point_test.cpp | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/libopenage/util/fixed_point.h b/libopenage/util/fixed_point.h index 6cab0167f7..2b3a97797a 100644 --- a/libopenage/util/fixed_point.h +++ b/libopenage/util/fixed_point.h @@ -460,11 +460,11 @@ class FixedPoint { constexpr FixedPoint sqrt() { // Zero can cause issues later, so deal with now. if (this->raw_value == 0) { - return 0.0; + return zero(); } - // Check for negative values - ENSURE(std::is_unsigned() or std::is_signed() and this->raw_value > 0, "FixedPoint::sqrt() is undefined for negative values."); + // Check for negative values + ENSURE(std::is_unsigned() or std::is_signed() and this->raw_value > 0, "FixedPoint::sqrt() is undefined for negative values."); // A greater shift = more precision, but can overflow the intermediate type if too large. size_t max_shift = std::countl_zero(static_cast(this->raw_value)) - 1; diff --git a/libopenage/util/fixed_point_test.cpp b/libopenage/util/fixed_point_test.cpp index 64ba8f6b2c..b3690b8d15 100644 --- a/libopenage/util/fixed_point_test.cpp +++ b/libopenage/util/fixed_point_test.cpp @@ -192,7 +192,7 @@ void fixed_point() { } using FP16_16 = FixedPoint; - for (uint32_t i = 0; i < 65536; i++) { + for (uint32_t i = 1; i < 65536; i++) { FP16_16 value = FP16_16::from_raw_value(i); TESTEQUALS_FLOAT(value.sqrt(), std::sqrt(value.to_double()), 1e-4); } @@ -204,4 +204,5 @@ void fixed_point() { TESTNOEXCEPT((FixedPoint::from_float(-3.25).sqrt())); } } + }}} // openage::util::tests From 55196f1f92492794a12423ece919d6153f66a852 Mon Sep 17 00:00:00 2001 From: bytegrrrl Date: Tue, 22 Apr 2025 00:34:11 -0400 Subject: [PATCH 748/771] Fix formatting --- libopenage/util/fixed_point.h | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/libopenage/util/fixed_point.h b/libopenage/util/fixed_point.h index 2b3a97797a..d8d54e3550 100644 --- a/libopenage/util/fixed_point.h +++ b/libopenage/util/fixed_point.h @@ -451,11 +451,11 @@ class FixedPoint { * Pure FixedPoint sqrt implementation using Heron's Algorithm. * * Note that this function is undefined for negative values. - * - * There's a small loss in precision depending on the value of fractional_bits and the position of - * the most significant bit: if the integer portion is very large, we won't have as much (absolute) - * precision. Ideally you would want the intermediate_type to be twice the size of raw_type to avoid - * any losses. + * + * There's a small loss in precision depending on the value of fractional_bits and the position of + * the most significant bit: if the integer portion is very large, we won't have as much (absolute) + * precision. Ideally you would want the intermediate_type to be twice the size of raw_type to avoid + * any losses. */ constexpr FixedPoint sqrt() { // Zero can cause issues later, so deal with now. @@ -464,16 +464,16 @@ class FixedPoint { } // Check for negative values - ENSURE(std::is_unsigned() or std::is_signed() and this->raw_value > 0, "FixedPoint::sqrt() is undefined for negative values."); + ENSURE(std::is_unsigned() or (std::is_signed() and this->raw_value > 0), "FixedPoint::sqrt() is undefined for negative values."); // A greater shift = more precision, but can overflow the intermediate type if too large. size_t max_shift = std::countl_zero(static_cast(this->raw_value)) - 1; size_t shift = max_shift > fractional_bits ? fractional_bits : max_shift; - // shift + fractional bits must be an even number - if ((shift + fractional_bits) % 2) { - shift -= 1; - } + // shift + fractional bits must be an even number + if ((shift + fractional_bits) % 2) { + shift -= 1; + } // We can't use the safe shift since the shift value is unknown at compile time. intermediate_type n = static_cast(this->raw_value) << shift; @@ -484,8 +484,7 @@ class FixedPoint { guess = (guess + n / guess) / 2; if (guess == prev) { break; - - } + } } // The sqrt operation halves the number of bits, so we'll we'll have to calculate a shift back From 292d6dadd5ecb7977aad6f4ba0b615ac4cc0fac4 Mon Sep 17 00:00:00 2001 From: bytegrrrl Date: Wed, 23 Apr 2025 18:20:19 -0400 Subject: [PATCH 749/771] Optimize FixedPoint::sqrt() signedness check Co-authored-by: Christoph Heine <6852422+heinezen@users.noreply.github.com> --- libopenage/util/fixed_point.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libopenage/util/fixed_point.h b/libopenage/util/fixed_point.h index d8d54e3550..c751238cdf 100644 --- a/libopenage/util/fixed_point.h +++ b/libopenage/util/fixed_point.h @@ -464,7 +464,9 @@ class FixedPoint { } // Check for negative values - ENSURE(std::is_unsigned() or (std::is_signed() and this->raw_value > 0), "FixedPoint::sqrt() is undefined for negative values."); + if constexpr (std::is_signed()) { + ENSURE(this->raw_value > 0, "FixedPoint::sqrt() is undefined for negative values."); + } // A greater shift = more precision, but can overflow the intermediate type if too large. size_t max_shift = std::countl_zero(static_cast(this->raw_value)) - 1; From 8586c3239091dfe7a31b6fbcc213ed3e37f1cedb Mon Sep 17 00:00:00 2001 From: Andreas Date: Tue, 13 May 2025 19:53:20 +0200 Subject: [PATCH 750/771] Fix configuration step for macos clang is mainly for building c files, while it can build c++ it does not link c++ libraries. --- doc/build_instructions/macos.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/build_instructions/macos.md b/doc/build_instructions/macos.md index e38dee9932..72458f6269 100644 --- a/doc/build_instructions/macos.md +++ b/doc/build_instructions/macos.md @@ -38,7 +38,7 @@ We advise against using the clang version that comes with macOS (Apple Clang) as ``` # on Intel macOS, llvm is by default in /usr/local/Cellar/llvm/bin/ # on ARM macOS, llvm is by default in /opt/homebrew/Cellar/llvm/bin/ -./configure --compiler="$(brew --prefix llvm)/bin/clang" --download-nyan +./configure --compiler="$(brew --prefix llvm)/bin/clang++" --download-nyan ``` Afterwards, trigger the build using `make`: From 85298c11f75da40ed07e2ed46b631234e5059694 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Sun, 29 Dec 2024 15:25:11 -0500 Subject: [PATCH 751/771] add world shader commands implementation --- .../renderer/stages/world/CMakeLists.txt | 1 + .../stages/world/world_shader_commands.cpp | 73 ++++++++++++ .../stages/world/world_shader_commands.h | 108 ++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 libopenage/renderer/stages/world/world_shader_commands.cpp create mode 100644 libopenage/renderer/stages/world/world_shader_commands.h diff --git a/libopenage/renderer/stages/world/CMakeLists.txt b/libopenage/renderer/stages/world/CMakeLists.txt index 811fd929ed..d86d7aa44c 100644 --- a/libopenage/renderer/stages/world/CMakeLists.txt +++ b/libopenage/renderer/stages/world/CMakeLists.txt @@ -2,4 +2,5 @@ add_sources(libopenage object.cpp render_entity.cpp render_stage.cpp + world_shader_commands.cpp ) diff --git a/libopenage/renderer/stages/world/world_shader_commands.cpp b/libopenage/renderer/stages/world/world_shader_commands.cpp new file mode 100644 index 0000000000..e7b31f87c1 --- /dev/null +++ b/libopenage/renderer/stages/world/world_shader_commands.cpp @@ -0,0 +1,73 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "world_shader_commands.h" + +#include "error/error.h" +#include "log/log.h" + +namespace openage::renderer::world { + +bool WorldShaderCommands::add_command(uint8_t alpha, const std::string &code, const std::string &description) { + if (!validate_alpha(alpha)) { + log::log(ERR << "Invalid alpha value: " << int(alpha)); + return false; + } + if (!validate_code(code)) { + log::log(ERR << "Invalid command code"); + return false; + } + + commands_map[alpha] = {alpha, code, description}; + return true; +} + +bool WorldShaderCommands::remove_command(uint8_t alpha) { + if (!validate_alpha(alpha)) { + return false; + } + commands_map.erase(alpha); + return true; +} + +bool WorldShaderCommands::has_command(uint8_t alpha) const { + return commands_map.contains(alpha); +} + +std::string WorldShaderCommands::integrate_command(const std::string &base_shader) { + std::string final_shader = base_shader; + std::string commands_code = generate_command_code(); + + // Find the insertion point + size_t insert_point = final_shader.find(COMMAND_MARKER); + if (insert_point == std::string::npos) { + throw Error(MSG(err) << "Failed to find command insertion point in shader."); + } + + // Replace the insertion point with the generated command code + final_shader.replace(insert_point, strlen(COMMAND_MARKER), commands_code); + + return final_shader; +} + +std::string WorldShaderCommands::generate_command_code() const { + std::string result = ""; + + for (const auto &[alpha, command] : commands_map) { + result += " case " + std::to_string(alpha) + ":\n"; + result += " // " + command.description + "\n"; + result += " " + command.code + "\n"; + result += " break;\n\n"; + } + + return result; +} + +bool WorldShaderCommands::validate_alpha(uint8_t alpha) const { + return alpha % 2 == 0 && alpha >= 0 && alpha <= 254; +} + +bool WorldShaderCommands::validate_code(const std::string &code) const { + return !code.empty(); +} + +} // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/world_shader_commands.h b/libopenage/renderer/stages/world/world_shader_commands.h new file mode 100644 index 0000000000..d025735db2 --- /dev/null +++ b/libopenage/renderer/stages/world/world_shader_commands.h @@ -0,0 +1,108 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include + +namespace openage { +namespace renderer { +namespace world { + +/** + * Represents a single shader command that can be used in the world fragment shader. + * Commands are identified by their alpha values and contain GLSL code snippets + * that define custom rendering behavior. + */ +struct ShaderCommand { + // Command identifier ((must be even, range 0-254)) + uint8_t alpha; + // GLSL code snippet that defines the command's behavior + std::string code; + // Documentation (optional) + std::string description; +}; + +/** + * Manages shader commands for the world fragment shader. + * Provides functionality to add, remove, and integrate commands into the base shader. + * Commands are inserted at a predefined marker in the shader code. + */ +class WorldShaderCommands { +public: + // Marker in shader code where commands will be inserted + static constexpr const char *COMMAND_MARKER = "//@INSERT_COMMANDS@"; + + /** + * Add a new shader command. + * + * @param alpha Command identifier (must be even, range 0-254) + * @param code GLSL code snippet defining the command's behavior + * @param description Human-readable description of the command's purpose + * + * @return true if command was added successfully, false if validation failed + */ + bool add_command(uint8_t alpha, const std::string &code, const std::string &description = ""); + + /** + * Remove a command. + * + * @param alpha Command identifier (even values 0-254) + */ + bool remove_command(uint8_t alpha); + + /** + * Check if a command is registered. + * + * @param alpha Command identifier to check + * + * @return true if command is registered + */ + bool has_command(uint8_t alpha) const; + + /** + * Integrate registered commands into the base shader code. + * + * @param base_shader Original shader code containing the command marker + * + * @return Complete shader code with commands integrated at the marker position + * + * @throws Error if command marker is not found in the base shader + */ + std::string integrate_command(const std::string &base_shader); + +private: + /** + * Generate GLSL code for all registered commands. + * + * @return String containing case statements for each command + */ + std::string generate_command_code() const; + + /** + * Validate a command identifier. + * + * @param alpha Command identifier to validate + * + * @return true if alpha is even and within valid range (0-254) + */ + bool validate_alpha(uint8_t alpha) const; + + /** + * Validate command GLSL code. + * + * @param code GLSL code snippet to validate + * + * @return true if code is not empty (additional validation could be added) + */ + + bool validate_code(const std::string &code) const; + + // Map of command identifiers to their respective commands + std::map commands_map; +}; + +} // namespace world +} // namespace renderer +} // namespace openage From 856b901ce907db8d191f54f71f6e15f31ae9f916 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Sun, 29 Dec 2024 15:26:41 -0500 Subject: [PATCH 752/771] use world shader commands in render_stage & update world2d.frag --- assets/shaders/world2d.frag.glsl | 10 +------ .../renderer/stages/world/render_stage.cpp | 28 +++++++++++++++++-- .../renderer/stages/world/render_stage.h | 13 +++++++++ 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/assets/shaders/world2d.frag.glsl b/assets/shaders/world2d.frag.glsl index 98d323e0f6..f9de0354ba 100644 --- a/assets/shaders/world2d.frag.glsl +++ b/assets/shaders/world2d.frag.glsl @@ -26,15 +26,7 @@ void main() { // do not save the ID return; - case 254: - col = vec4(1.0f, 0.0f, 0.0f, 1.0f); - break; - case 252: - col = vec4(0.0f, 1.0f, 0.0f, 1.0f); - break; - case 250: - col = vec4(0.0f, 0.0f, 1.0f, 1.0f); - break; + //@INSERT_COMMANDS@ default: col = tex_val; break; diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index d8d93279af..3962daa170 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -127,12 +127,18 @@ void WorldRenderStage::initialize_render_pass(size_t width, vert_shader_file.read()); vert_shader_file.close(); + // Initialize shader command system before loading fragment shader + this->shader_commands = std::make_unique(); + this->init_shader_commands(); + auto frag_shader_file = (shaderdir / "world2d.frag.glsl").open(); + auto base_shader = frag_shader_file.read(); + frag_shader_file.close(); + auto frag_shader_src = renderer::resources::ShaderSource( resources::shader_lang_t::glsl, resources::shader_stage_t::fragment, - frag_shader_file.read()); - frag_shader_file.close(); + this->shader_commands->integrate_command(base_shader)); 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)); @@ -156,4 +162,22 @@ void WorldRenderStage::init_uniform_ids() { WorldObject::anchor_offset = this->display_shader->get_uniform_id("anchor_offset"); } +void WorldRenderStage::init_shader_commands() { + // Register default shader commands + this->shader_commands->add_command( + 254, + "col = vec4(1.0f, 0.0f, 0.0f, 1.0f);", + "Red tint command"); + this->shader_commands->add_command( + 252, + "col = vec4(0.0f, 1.0f, 0.0f, 1.0f);", + "Green tint command"); + this->shader_commands->add_command( + 250, + "col = vec4(0.0f, 0.0f, 1.0f, 1.0f);", + "Blue tint command"); + + // Additional commands can be added here +} + } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index f1256f3b57..f048a1cbee 100644 --- a/libopenage/renderer/stages/world/render_stage.h +++ b/libopenage/renderer/stages/world/render_stage.h @@ -7,6 +7,7 @@ #include #include "util/path.h" +#include "world_shader_commands.h" namespace openage { @@ -111,6 +112,12 @@ class WorldRenderStage { */ void init_uniform_ids(); + /** + * Initialize the shader command system and register default commands. + * This must be called before initializing the shader program. + */ + void init_shader_commands(); + /** * Reference to the openage renderer. */ @@ -174,6 +181,12 @@ class WorldRenderStage { * Mutex for protecting threaded access. */ std::shared_mutex mutex; + + /** + * Shader command system for the world fragment shader. + * Manages custom rendering behaviors through alpha channel commands. + */ + std::unique_ptr shader_commands; }; } // namespace world } // namespace renderer From 72921d00481cceac079169bce1af67f14e48be4e Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Sun, 29 Dec 2024 16:03:08 -0500 Subject: [PATCH 753/771] fix a cstring error --- libopenage/renderer/stages/world/world_shader_commands.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libopenage/renderer/stages/world/world_shader_commands.cpp b/libopenage/renderer/stages/world/world_shader_commands.cpp index e7b31f87c1..b734d8d86a 100644 --- a/libopenage/renderer/stages/world/world_shader_commands.cpp +++ b/libopenage/renderer/stages/world/world_shader_commands.cpp @@ -2,6 +2,8 @@ #include "world_shader_commands.h" +#include + #include "error/error.h" #include "log/log.h" @@ -44,7 +46,7 @@ std::string WorldShaderCommands::integrate_command(const std::string &base_shade } // Replace the insertion point with the generated command code - final_shader.replace(insert_point, strlen(COMMAND_MARKER), commands_code); + final_shader.replace(insert_point, std::strlen(COMMAND_MARKER), commands_code); return final_shader; } From 78e4a1fd99fd9b551dece6ad70c71d3b2a7dca93 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Sun, 19 Jan 2025 20:08:28 -0500 Subject: [PATCH 754/771] reset world2d shader and render_entity --- assets/shaders/world2d.frag.glsl | 12 ++++++-- .../renderer/stages/world/render_stage.cpp | 30 ++----------------- .../renderer/stages/world/render_stage.h | 15 +--------- 3 files changed, 14 insertions(+), 43 deletions(-) diff --git a/assets/shaders/world2d.frag.glsl b/assets/shaders/world2d.frag.glsl index f9de0354ba..143d3f0980 100644 --- a/assets/shaders/world2d.frag.glsl +++ b/assets/shaders/world2d.frag.glsl @@ -26,10 +26,18 @@ void main() { // do not save the ID return; - //@INSERT_COMMANDS@ + case 254: + col = vec4(1.0f, 0.0f, 0.0f, 1.0f); + break; + case 252: + col = vec4(0.0f, 1.0f, 0.0f, 1.0f); + break; + case 250: + col = vec4(0.0f, 0.0f, 1.0f, 1.0f); + break; default: col = tex_val; break; } id = u_id; -} +} \ No newline at end of file diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 3962daa170..e5b0774788 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -127,18 +127,12 @@ void WorldRenderStage::initialize_render_pass(size_t width, vert_shader_file.read()); vert_shader_file.close(); - // Initialize shader command system before loading fragment shader - this->shader_commands = std::make_unique(); - this->init_shader_commands(); - auto frag_shader_file = (shaderdir / "world2d.frag.glsl").open(); - auto base_shader = frag_shader_file.read(); - frag_shader_file.close(); - auto frag_shader_src = renderer::resources::ShaderSource( resources::shader_lang_t::glsl, resources::shader_stage_t::fragment, - this->shader_commands->integrate_command(base_shader)); + frag_shader_file.read()); + frag_shader_file.close(); 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)); @@ -162,22 +156,4 @@ void WorldRenderStage::init_uniform_ids() { WorldObject::anchor_offset = this->display_shader->get_uniform_id("anchor_offset"); } -void WorldRenderStage::init_shader_commands() { - // Register default shader commands - this->shader_commands->add_command( - 254, - "col = vec4(1.0f, 0.0f, 0.0f, 1.0f);", - "Red tint command"); - this->shader_commands->add_command( - 252, - "col = vec4(0.0f, 1.0f, 0.0f, 1.0f);", - "Green tint command"); - this->shader_commands->add_command( - 250, - "col = vec4(0.0f, 0.0f, 1.0f, 1.0f);", - "Blue tint command"); - - // Additional commands can be added here -} - -} // namespace openage::renderer::world +} // namespace openage::renderer::world \ No newline at end of file diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index f048a1cbee..894ab52ff0 100644 --- a/libopenage/renderer/stages/world/render_stage.h +++ b/libopenage/renderer/stages/world/render_stage.h @@ -7,7 +7,6 @@ #include #include "util/path.h" -#include "world_shader_commands.h" namespace openage { @@ -112,12 +111,6 @@ class WorldRenderStage { */ void init_uniform_ids(); - /** - * Initialize the shader command system and register default commands. - * This must be called before initializing the shader program. - */ - void init_shader_commands(); - /** * Reference to the openage renderer. */ @@ -181,13 +174,7 @@ class WorldRenderStage { * Mutex for protecting threaded access. */ std::shared_mutex mutex; - - /** - * Shader command system for the world fragment shader. - * Manages custom rendering behaviors through alpha channel commands. - */ - std::unique_ptr shader_commands; }; } // namespace world } // namespace renderer -} // namespace openage +} // namespace openage \ No newline at end of file From 26c60ac7efeb847eb615f042c4c8ea9d8ffde482 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Thu, 23 Jan 2025 17:08:08 -0500 Subject: [PATCH 755/771] implement more controllable shader command template --- .../renderer/stages/world/render_stage.cpp | 60 +++++- .../renderer/stages/world/render_stage.h | 39 +++- .../stages/world/world_shader_commands.cpp | 173 +++++++++++++----- .../stages/world/world_shader_commands.h | 102 ++++------- 4 files changed, 269 insertions(+), 105 deletions(-) diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index e5b0774788..cb2c863870 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/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" @@ -12,6 +12,7 @@ #include "renderer/resources/texture_info.h" #include "renderer/shader_program.h" #include "renderer/stages/world/object.h" +#include "renderer/stages/world/world_shader_commands.h" #include "renderer/texture.h" #include "renderer/window.h" #include "time/clock.h" @@ -46,6 +47,30 @@ WorldRenderStage::WorldRenderStage(const std::shared_ptr &window, log::log(INFO << "Created render stage 'World'"); } +WorldRenderStage::WorldRenderStage(const std::shared_ptr &window, + const std::shared_ptr &renderer, + const std::shared_ptr &camera, + const util::Path &shaderdir, + const util::Path &configdir, + const std::shared_ptr &asset_manager, + const std::shared_ptr clock) : + renderer{renderer}, + camera{camera}, + asset_manager{asset_manager}, + render_objects{}, + clock{clock}, + default_geometry{this->renderer->add_mesh_geometry(WorldObject::get_mesh())} { + auto size = window->get_size(); + this->initialize_render_pass_with_shader_commands(size[0], size[1], shaderdir, configdir); + this->init_uniform_ids(); + + window->add_resize_callback([this](size_t width, size_t height, double /*scale*/) { + this->resize(width, height); + }); + + log::log(INFO << "Created render stage 'World' with shader command"); +} + std::shared_ptr WorldRenderStage::get_render_pass() { return this->render_pass; } @@ -156,4 +181,37 @@ void WorldRenderStage::init_uniform_ids() { WorldObject::anchor_offset = this->display_shader->get_uniform_id("anchor_offset"); } +void WorldRenderStage::initialize_render_pass_with_shader_commands(size_t width, size_t height, const util::Path &shaderdir, const util::Path &config_path) { + auto vert_shader_file = (shaderdir / "demo_7_world.vert.glsl").open(); + auto vert_shader_src = renderer::resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::vertex, + vert_shader_file.read()); + vert_shader_file.close(); + + auto frag_shader_file = (shaderdir / "demo_7_world.frag.glsl").open(); + log::log(INFO << "Loading shader commands config from: " << (shaderdir / "demo_7_display.frag.glsl")); + this->shader_template = std::make_shared(frag_shader_file.read()); + if (not this->shader_template->load_commands(config_path / "world_commands.config")) { + log::log(ERR << "Failed to load shader commands configuration for world stage"); + return; + } + + auto frag_shader_src = renderer::resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::fragment, + this->shader_template->generate_source()); + frag_shader_file.close(); + + 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->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}); + this->render_pass = this->renderer->add_render_pass({}, fbo); +} + } // namespace openage::renderer::world \ No newline at end of file diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index 894ab52ff0..2c751be50d 100644 --- a/libopenage/renderer/stages/world/render_stage.h +++ b/libopenage/renderer/stages/world/render_stage.h @@ -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. #pragma once @@ -33,6 +33,7 @@ class AssetManager; namespace world { class RenderEntity; class WorldObject; +class ShaderCommandTemplate; /** * Renderer for drawing and displaying entities in the game world (units, buildings, etc.) @@ -60,6 +61,26 @@ class WorldRenderStage { const util::Path &shaderdir, const std::shared_ptr &asset_manager, const std::shared_ptr clock); + + /** + * Create a new render stage for the game world with shader command. + * + * @param window openage window targeted for rendering. + * @param renderer openage low-level renderer. + * @param camera Camera used for the rendered scene. + * @param shaderdir Directory containing the shader source files. + * @param configdir Directory containing the config for shader command. + * @param asset_manager Asset manager for loading resources. + * @param clock Simulation clock for timing animations. + */ + WorldRenderStage(const std::shared_ptr &window, + const std::shared_ptr &renderer, + const std::shared_ptr &camera, + const util::Path &shaderdir, + const util::Path &configdir, + const std::shared_ptr &asset_manager, + const std::shared_ptr clock); + ~WorldRenderStage() = default; /** @@ -111,6 +132,17 @@ class WorldRenderStage { */ void init_uniform_ids(); + /** + * Initialize render pass with shader commands. + * This is an alternative to initialize_render_pass() that uses configurable shader commands. + * + * @param width Width of the FBO. + * @param height Height of the FBO. + * @param shaderdir Directory containing shader files. + * @param configdir Directory containing configuration file. + */ + void initialize_render_pass_with_shader_commands(size_t width, size_t height, const util::Path &shaderdir, const util::Path &config_path); + /** * Reference to the openage renderer. */ @@ -131,6 +163,11 @@ class WorldRenderStage { */ std::shared_ptr render_pass; + /** + * Template for the world shader program. + */ + std::shared_ptr shader_template; + /** * Render entities requested by the game world. */ diff --git a/libopenage/renderer/stages/world/world_shader_commands.cpp b/libopenage/renderer/stages/world/world_shader_commands.cpp index b734d8d86a..9cc10f5f87 100644 --- a/libopenage/renderer/stages/world/world_shader_commands.cpp +++ b/libopenage/renderer/stages/world/world_shader_commands.cpp @@ -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. #include "world_shader_commands.h" @@ -9,67 +9,158 @@ namespace openage::renderer::world { -bool WorldShaderCommands::add_command(uint8_t alpha, const std::string &code, const std::string &description) { - if (!validate_alpha(alpha)) { - log::log(ERR << "Invalid alpha value: " << int(alpha)); +ShaderCommandTemplate::ShaderCommandTemplate(const std::string &template_code) : + template_code{template_code} {} + +bool ShaderCommandTemplate::load_commands(const util::Path &config_path) { + try { + log::log(INFO << "Loading shader commands config from: " << config_path); + auto config_file = config_path.open(); + std::string line; + std::stringstream ss(config_file.read()); + + ShaderCommandConfig current_command; + // if true, we are reading the code block for the current command. + bool reading_code = false; + std::string code_block; + + while (std::getline(ss, line)) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + // Trim whitespace from line + line = trim(line); + log::log(INFO << "Parsing line: " << line); + + // Skip empty lines and comments + if (line.empty() || line[0] == '#') { + continue; + } + + if (reading_code) { + if (line == "}") { + reading_code = false; + current_command.code = code_block; + + // Generate and add snippet + std::string snippet = generate_snippet(current_command); + add_snippet(current_command.placeholder_id, snippet); + commands.push_back(current_command); + + // Reset for next command + code_block.clear(); + } + else { + code_block += line + "\n"; + } + continue; + } + + if (line == "[COMMAND]") { + current_command = ShaderCommandConfig{}; + continue; + } + + // Parse key-value pairs + size_t pos = line.find('='); + if (pos != std::string::npos) { + std::string key = trim(line.substr(0, pos)); + std::string value = trim(line.substr(pos + 1)); + + if (key == "placeholder") { + current_command.placeholder_id = value; + } + else if (key == "alpha") { + uint8_t alpha = static_cast(std::stoi(value)); + if (alpha % 2 == 0 && alpha >= 0 && alpha <= 254) { + current_command.alpha = alpha; + } + else { + log::log(ERR << "Invalid alpha value for command: " << alpha); + return false; + } + } + else if (key == "description") { + current_command.description = value; + } + else if (key == "code") { + if (value == "{") { + reading_code = true; + code_block.clear(); + } + } + } + } + + return true; + } + catch (const std::exception &e) { + log::log(ERR << "Failed to load shader commands: " << e.what()); return false; } - if (!validate_code(code)) { - log::log(ERR << "Invalid command code"); +} + +bool ShaderCommandTemplate::add_snippet(const std::string &placeholder_id, const std::string &snippet) { + if (snippet.empty()) { + log::log(ERR << "Empty snippet for placeholder: " << placeholder_id); return false; } - commands_map[alpha] = {alpha, code, description}; - return true; -} + if (placeholder_id.empty()) { + log::log(ERR << "Empty placeholder ID for snippet"); + return false; + } -bool WorldShaderCommands::remove_command(uint8_t alpha) { - if (!validate_alpha(alpha)) { + // Check if the placeholder exists in the template + std::string placeholder = "//@" + placeholder_id + "@"; + if (template_code.find(placeholder) == std::string::npos) { + log::log(ERR << "Placeholder not found in template: " << placeholder_id); return false; } - commands_map.erase(alpha); + + // Store the snippet + snippets[placeholder_id].push_back(snippet); return true; } -bool WorldShaderCommands::has_command(uint8_t alpha) const { - return commands_map.contains(alpha); +std::string ShaderCommandTemplate::generate_snippet(const ShaderCommandConfig &command) { + return "case " + std::to_string(command.alpha) + ":\n" + + "\t\t// " + command.description + "\n" + + "\t\t" + command.code + "\t\tbreak;\n"; } -std::string WorldShaderCommands::integrate_command(const std::string &base_shader) { - std::string final_shader = base_shader; - std::string commands_code = generate_command_code(); +std::string ShaderCommandTemplate::generate_source() const { + std::string result = template_code; - // Find the insertion point - size_t insert_point = final_shader.find(COMMAND_MARKER); - if (insert_point == std::string::npos) { - throw Error(MSG(err) << "Failed to find command insertion point in shader."); - } - - // Replace the insertion point with the generated command code - final_shader.replace(insert_point, std::strlen(COMMAND_MARKER), commands_code); + // Process each placeholder + for (const auto &[placeholder_id, snippet_list] : snippets) { + std::string combined_snippets; - return final_shader; -} + // Combine all snippets for this placeholder + for (const auto &snippet : snippet_list) { + combined_snippets += snippet; + } -std::string WorldShaderCommands::generate_command_code() const { - std::string result = ""; + // Find and replace the placeholder + std::string placeholder = "//@" + placeholder_id + "@"; + size_t pos = result.find(placeholder); + if (pos == std::string::npos) { + throw Error(MSG(err) << "Placeholder disappeared from template: " << placeholder_id); + } - for (const auto &[alpha, command] : commands_map) { - result += " case " + std::to_string(alpha) + ":\n"; - result += " // " + command.description + "\n"; - result += " " + command.code + "\n"; - result += " break;\n\n"; + // Replace placeholder with combined snippets + result.replace(pos, placeholder.length(), combined_snippets); } return result; } -bool WorldShaderCommands::validate_alpha(uint8_t alpha) const { - return alpha % 2 == 0 && alpha >= 0 && alpha <= 254; -} - -bool WorldShaderCommands::validate_code(const std::string &code) const { - return !code.empty(); +std::string ShaderCommandTemplate::trim(const std::string &str) const { + size_t first = str.find_first_not_of(" \t"); + if (first == std::string::npos) { + return ""; + } + size_t last = str.find_last_not_of(" \t"); + return str.substr(first, (last - first + 1)); } - } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/world_shader_commands.h b/libopenage/renderer/stages/world/world_shader_commands.h index d025735db2..9f6a158a25 100644 --- a/libopenage/renderer/stages/world/world_shader_commands.h +++ b/libopenage/renderer/stages/world/world_shader_commands.h @@ -1,11 +1,14 @@ -// Copyright 2024-2024 the openage authors. See copying.md for legal info. +// Copyright 2024-2025 the openage authors. See copying.md for legal info. #pragma once #include #include +#include #include +#include "util/path.h" + namespace openage { namespace renderer { namespace world { @@ -15,94 +18,69 @@ namespace world { * Commands are identified by their alpha values and contain GLSL code snippets * that define custom rendering behavior. */ -struct ShaderCommand { - // Command identifier ((must be even, range 0-254)) +struct ShaderCommandConfig { + /// ID of the placeholder where this snippet should be inserted + std::string placeholder_id; + /// Command identifier ((must be even, range 0-254)) uint8_t alpha; - // GLSL code snippet that defines the command's behavior + /// GLSL code snippet that defines the command's behavior std::string code; - // Documentation (optional) + /// Documentation (optional) std::string description; }; /** - * Manages shader commands for the world fragment shader. - * Provides functionality to add, remove, and integrate commands into the base shader. - * Commands are inserted at a predefined marker in the shader code. + * Manages shader templates and their code snippets. + * Allows loading configurable shader commands and generating + * complete shader source code. */ -class WorldShaderCommands { +class ShaderCommandTemplate { public: - // Marker in shader code where commands will be inserted - static constexpr const char *COMMAND_MARKER = "//@INSERT_COMMANDS@"; - /** - * Add a new shader command. - * - * @param alpha Command identifier (must be even, range 0-254) - * @param code GLSL code snippet defining the command's behavior - * @param description Human-readable description of the command's purpose + * Create a shader template from source code of shader. * - * @return true if command was added successfully, false if validation failed + * @param template_code Source code containing placeholders. */ - bool add_command(uint8_t alpha, const std::string &code, const std::string &description = ""); + explicit ShaderCommandTemplate(const std::string &template_code); /** - * Remove a command. + * Load commands from a configuration file. * - * @param alpha Command identifier (even values 0-254) + * @param config_path Path to the command configuration file. + * @return true if commands were loaded successfully. */ - bool remove_command(uint8_t alpha); + bool load_commands(const util::Path &config_path); /** - * Check if a command is registered. + * Add a single code snippet to the template. * - * @param alpha Command identifier to check - * - * @return true if command is registered + * @param placeholder_id Where to insert the snippet. + * @param snippet Code to insert. + * @return true if snippet was added successfully. */ - bool has_command(uint8_t alpha) const; + bool add_snippet(const std::string &placeholder_id, const std::string &snippet); /** - * Integrate registered commands into the base shader code. - * - * @param base_shader Original shader code containing the command marker - * - * @return Complete shader code with commands integrated at the marker position + * Generate final shader source code with all snippets inserted. * - * @throws Error if command marker is not found in the base shader + * @return Complete shader code. + * @throws Error if any required placeholders are missing snippets. */ - std::string integrate_command(const std::string &base_shader); + std::string generate_source() const; private: - /** - * Generate GLSL code for all registered commands. - * - * @return String containing case statements for each command - */ - std::string generate_command_code() const; + // Generate a single code snippet for a command. + std::string generate_snippet(const ShaderCommandConfig &command); + // Helper function to trim whitespace from a string + std::string trim(const std::string &str) const; - /** - * Validate a command identifier. - * - * @param alpha Command identifier to validate - * - * @return true if alpha is even and within valid range (0-254) - */ - bool validate_alpha(uint8_t alpha) const; - - /** - * Validate command GLSL code. - * - * @param code GLSL code snippet to validate - * - * @return true if code is not empty (additional validation could be added) - */ - - bool validate_code(const std::string &code) const; - - // Map of command identifiers to their respective commands - std::map commands_map; + // Original template code with placeholders + std::string template_code; + // Mapping of placeholder IDs to their code snippets + std::map> snippets; + // Loaded command configurations + std::vector commands; }; - } // namespace world } // namespace renderer } // namespace openage From 474a713d22bca29ae158225e849de983acd1140a Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Thu, 23 Jan 2025 17:08:55 -0500 Subject: [PATCH 756/771] add demo7 to test shader command --- assets/test/shaders/demo_7_world.frag.glsl | 34 +++++++ assets/test/shaders/demo_7_world.vert.glsl | 101 ++++++++++++++++++++ assets/test/shaders/world_commands.config | 23 +++++ libopenage/renderer/demo/CMakeLists.txt | 1 + libopenage/renderer/demo/demo_7.cpp | 106 +++++++++++++++++++++ libopenage/renderer/demo/demo_7.h | 22 +++++ libopenage/renderer/demo/tests.cpp | 7 +- 7 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 assets/test/shaders/demo_7_world.frag.glsl create mode 100644 assets/test/shaders/demo_7_world.vert.glsl create mode 100644 assets/test/shaders/world_commands.config create mode 100644 libopenage/renderer/demo/demo_7.cpp create mode 100644 libopenage/renderer/demo/demo_7.h diff --git a/assets/test/shaders/demo_7_world.frag.glsl b/assets/test/shaders/demo_7_world.frag.glsl new file mode 100644 index 0000000000..d6ffd22029 --- /dev/null +++ b/assets/test/shaders/demo_7_world.frag.glsl @@ -0,0 +1,34 @@ +#version 330 + +in vec2 vert_uv; + +layout(location = 0) out vec4 col; +layout(location = 1) out uint id; + +uniform sampler2D tex; +uniform uint u_id; + +// position (top left corner) and size: (x, y, width, height) +uniform vec4 tile_params; + +vec2 uv = vec2( + vert_uv.x * tile_params.z + tile_params.x, + vert_uv.y *tile_params.w + tile_params.y); + +void main() { + vec4 tex_val = texture(tex, uv); + int alpha = int(round(tex_val.a * 255)); + switch (alpha) { + case 0: + col = tex_val; + discard; + + // do not save the ID + return; + //@COMMAND_SWITCH@ + default: + col = tex_val; + break; + } + id = u_id; +} \ No newline at end of file diff --git a/assets/test/shaders/demo_7_world.vert.glsl b/assets/test/shaders/demo_7_world.vert.glsl new file mode 100644 index 0000000000..7987d40ec3 --- /dev/null +++ b/assets/test/shaders/demo_7_world.vert.glsl @@ -0,0 +1,101 @@ +#version 330 + +layout(location=0) in vec2 v_position; +layout(location=1) in vec2 uv; + +out vec2 vert_uv; + +// camera parameters for transforming the object position +// and scaling the subtex to the correct size +layout (std140) uniform camera { + // view matrix (world to view space) + mat4 view; + // projection matrix (view to clip space) + mat4 proj; + // inverse zoom factor (1.0 / zoom) + // high zoom = upscale subtex + // low zoom = downscale subtex + float inv_zoom; + // inverse viewport size (1.0 / viewport size) + vec2 inv_viewport_size; +}; + +// can be used to move the object position in world space _before_ +// it's transformed to clip space +// this is usually unnecessary because we want to draw the +// subtex where the object is, so this can be set to the identity matrix +uniform mat4 model; + +// position of the object in world space +uniform vec3 obj_world_position; + +// flip the subtexture horizontally/vertically +uniform bool flip_x; +uniform bool flip_y; + +// parameters for scaling and moving the subtex +// to the correct position in clip space + +// animation scalefactor +// scales the vertex positions so that they +// match the subtex dimensions +// +// high animation scale = downscale subtex +// low animation scale = upscale subtex +uniform float scale; + +// size of the subtex (in pixels) +uniform vec2 subtex_size; + +// offset of the subtex anchor point +// from the subtex center (in pixels) +// used to move the subtex so that the anchor point +// is at the object position +uniform vec2 anchor_offset; + +void main() { + // translate the position of the object from world space to clip space + // this is the position where we want to draw the subtex in 2D + vec4 obj_clip_pos = proj * view * model * vec4(obj_world_position, 1.0); + + // subtex has to be scaled to account for the zoom factor + // and the animation scale factor. essentially this is (animation scale / zoom). + float zoom_scale = scale * inv_zoom; + + // Scale the subtex vertices + // we have to account for the viewport size to get the correct dimensions + // and then scale the subtex to the zoom factor to get the correct size + vec2 vert_scale = zoom_scale * subtex_size * inv_viewport_size; + + // Scale the anchor offset with the same method as above + // to get the correct anchor position in the viewport + vec2 anchor_scale = zoom_scale * anchor_offset * inv_viewport_size; + + // if the subtex is flipped, we also need to flip the anchor offset + // essentially, we invert the coordinates for the flipped axis + float anchor_x = float(flip_x) * -1.0 * anchor_scale.x + float(!flip_x) * anchor_scale.x; + float anchor_y = float(flip_y) * -1.0 * anchor_scale.y + float(!flip_y) * anchor_scale.y; + + // offset the clip position by the offset of the subtex anchor + // imagine this as pinning the subtex to the object position at the subtex anchor point + obj_clip_pos += vec4(anchor_x, anchor_y, 0.0, 0.0); + + // create a move matrix for positioning the vertices + // uses the vert scale and the transformed object position in clip space + mat4 move = mat4(vert_scale.x, 0.0, 0.0, 0.0, + 0.0, vert_scale.y, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + obj_clip_pos.x, obj_clip_pos.y, obj_clip_pos.z, 1.0); + + // calculate the final vertex position + gl_Position = move * vec4(v_position, 0.0, 1.0); + + // if the subtex is flipped, we also need to flip the uv tex coordinates + // essentially, we invert the coordinates for the flipped axis + + // !flip_x is default because OpenGL uses bottom-left as its origin + float uv_x = float(!flip_x) * uv.x + float(flip_x) * (1.0 - uv.x); + float uv_y = float(flip_y) * uv.y + float(!flip_y) * (1.0 - uv.y); + + vert_uv = vec2(uv_x, uv_y); +} diff --git a/assets/test/shaders/world_commands.config b/assets/test/shaders/world_commands.config new file mode 100644 index 0000000000..16b6a9d399 --- /dev/null +++ b/assets/test/shaders/world_commands.config @@ -0,0 +1,23 @@ +[COMMAND] +placeholder=COMMAND_SWITCH +alpha=254 +description=Red tint +code={ +col = vec4(1.0, 0.0, 0.0, 1.0) * tex_val; +} + +[COMMAND] +placeholder=COMMAND_SWITCH +alpha=252 +description=Green tint +code={ +col = vec4(0.0, 1.0, 0.0, 1.0) * tex_val; +} + +[COMMAND] +placeholder=COMMAND_SWITCH +alpha=250 +description=Blue tint +code={ +col = vec4(0.0, 0.0, 1.0, 1.0) * tex_val; +} \ No newline at end of file diff --git a/libopenage/renderer/demo/CMakeLists.txt b/libopenage/renderer/demo/CMakeLists.txt index fb93e5279b..7567731afd 100644 --- a/libopenage/renderer/demo/CMakeLists.txt +++ b/libopenage/renderer/demo/CMakeLists.txt @@ -6,6 +6,7 @@ add_sources(libopenage demo_4.cpp demo_5.cpp demo_6.cpp + demo_7.cpp stresstest_0.cpp stresstest_1.cpp tests.cpp diff --git a/libopenage/renderer/demo/demo_7.cpp b/libopenage/renderer/demo/demo_7.cpp new file mode 100644 index 0000000000..a3a21bf94d --- /dev/null +++ b/libopenage/renderer/demo/demo_7.cpp @@ -0,0 +1,106 @@ +// demo_shader_commands.h +#pragma once + +#include "util/path.h" + +#include +#include + +#include "coord/tile.h" +#include "renderer/camera/camera.h" +#include "renderer/gui/integration/public/gui_application_with_logger.h" +#include "renderer/opengl/window.h" +#include "renderer/render_factory.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" +#include "renderer/resources/assets/asset_manager.h" +#include "renderer/resources/shader_source.h" +#include "renderer/stages/camera/manager.h" +#include "renderer/stages/screen/render_stage.h" +#include "renderer/stages/skybox/render_stage.h" +#include "renderer/stages/terrain/render_entity.h" +#include "renderer/stages/terrain/render_stage.h" +#include "renderer/stages/world/render_entity.h" +#include "renderer/stages/world/render_stage.h" +#include "renderer/uniform_buffer.h" +#include "time/clock.h" + +namespace openage::renderer::tests { + +void renderer_demo_7(const util::Path &path) { + // Basic setup + auto qtapp = std::make_shared(); + window_settings settings; + settings.width = 800; + settings.height = 600; + settings.debug = true; + + auto window = std::make_shared("Shader Commands Demo", settings); + auto renderer = window->make_renderer(); + auto camera = std::make_shared(renderer, window->get_size()); + auto clock = std::make_shared(); + auto asset_manager = std::make_shared( + renderer, + path["assets"]["test"]); + auto cam_manager = std::make_shared(camera); + + auto shaderdir = path / "assets" / "test" / "shaders"; + + std::vector> + render_passes{}; + + // Initialize world renderer with shader commands + auto world_renderer = std::make_shared( + window, + renderer, + camera, + shaderdir, + shaderdir, // Temporarily, Shader commands config has the same path with shaders for this demo + asset_manager, + clock); + + render_passes.push_back(world_renderer->get_render_pass()); + + auto screen_renderer = std::make_shared( + window, + renderer, + path["assets"]["shaders"]); + std::vector> targets{}; + for (auto &pass : render_passes) { + targets.push_back(pass->get_target()); + } + screen_renderer->set_render_targets(targets); + + render_passes.push_back(screen_renderer->get_render_pass()); + + auto render_factory = std::make_shared(nullptr, world_renderer); + + auto entity1 = render_factory->add_world_render_entity(); + entity1->update(0, coord::phys3(0.0f, 0.0f, 0.0f), "./textures/test_gaben.sprite"); + + auto entity2 = render_factory->add_world_render_entity(); + entity2->update(1, coord::phys3(3.0f, 0.0f, 0.0f), "./textures/test_gaben.sprite"); + + auto entity3 = render_factory->add_world_render_entity(); + entity3->update(2, coord::phys3(-3.0f, 0.0f, 0.0f), "./textures/test_gaben.sprite"); + + // Main loop + while (not window->should_close()) { + qtapp->process_events(); + + // Update camera matrices + cam_manager->update(); + + world_renderer->update(); + + for (auto &pass : render_passes) { + renderer->render(pass); + } + + renderer->check_error(); + + window->update(); + } +} + +} // namespace openage::renderer::tests \ No newline at end of file diff --git a/libopenage/renderer/demo/demo_7.h b/libopenage/renderer/demo/demo_7.h new file mode 100644 index 0000000000..45c192523d --- /dev/null +++ b/libopenage/renderer/demo/demo_7.h @@ -0,0 +1,22 @@ +// Copyright 2025-2025 the openage authors. See copying.md for legal info. + +#pragma once + +#include "util/path.h" + +namespace openage::renderer::tests { + +/** + * Show off the render stages in the level 2 renderer and the camera + * system. + * - Window creation + * - Creating a camera + * - Initializing the level 2 render stages: skybox, terrain, world, screen + * - Adding renderables to the render stages via the render factory + * - Moving camera with mouse/keyboard callbacks + * + * @param path Path to the project rootdir. + */ +void renderer_demo_7(const util::Path &path); + +} // namespace openage::renderer::tests diff --git a/libopenage/renderer/demo/tests.cpp b/libopenage/renderer/demo/tests.cpp index d3fb0e3c21..bab82af2e3 100644 --- a/libopenage/renderer/demo/tests.cpp +++ b/libopenage/renderer/demo/tests.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 "tests.h" @@ -12,6 +12,7 @@ #include "renderer/demo/demo_4.h" #include "renderer/demo/demo_5.h" #include "renderer/demo/demo_6.h" +#include "renderer/demo/demo_7.h" #include "renderer/demo/stresstest_0.h" #include "renderer/demo/stresstest_1.h" @@ -47,6 +48,10 @@ void renderer_demo(int demo_id, const util::Path &path) { renderer_demo_6(path); break; + case 7: + renderer_demo_7(path); + break; + default: log::log(MSG(err) << "Unknown renderer demo requested: " << demo_id << "."); break; From 720f07541459016dad8f6cc0d7893655e991c1e0 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Thu, 23 Jan 2025 17:20:42 -0500 Subject: [PATCH 757/771] update to pass sanity_check --- libopenage/renderer/demo/demo_7.cpp | 7 ++++--- libopenage/renderer/stages/world/render_stage.cpp | 2 +- libopenage/renderer/stages/world/render_stage.h | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/libopenage/renderer/demo/demo_7.cpp b/libopenage/renderer/demo/demo_7.cpp index a3a21bf94d..470ded48f1 100644 --- a/libopenage/renderer/demo/demo_7.cpp +++ b/libopenage/renderer/demo/demo_7.cpp @@ -1,5 +1,6 @@ -// demo_shader_commands.h -#pragma once +// Copyright 2025-2025 the openage authors. See copying.md for legal info. + +#include "demo_7.h" #include "util/path.h" @@ -103,4 +104,4 @@ void renderer_demo_7(const util::Path &path) { } } -} // namespace openage::renderer::tests \ No newline at end of file +} // namespace openage::renderer::tests diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index cb2c863870..7514f2ca7e 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -214,4 +214,4 @@ void WorldRenderStage::initialize_render_pass_with_shader_commands(size_t width, this->render_pass = this->renderer->add_render_pass({}, fbo); } -} // namespace openage::renderer::world \ No newline at end of file +} // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index 2c751be50d..9fc4cdbf06 100644 --- a/libopenage/renderer/stages/world/render_stage.h +++ b/libopenage/renderer/stages/world/render_stage.h @@ -214,4 +214,4 @@ class WorldRenderStage { }; } // namespace world } // namespace renderer -} // namespace openage \ No newline at end of file +} // namespace openage From 1f507077a6dfeb9b26b95edcb87faf0098f11e83 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Mon, 27 Jan 2025 20:03:29 -0500 Subject: [PATCH 758/771] Revert file(s) to pre-PR state --- assets/shaders/world2d.frag.glsl | 2 +- assets/test/shaders/world_commands.config | 23 ------- .../renderer/stages/world/render_stage.cpp | 60 +------------------ .../renderer/stages/world/render_stage.h | 39 +----------- 4 files changed, 3 insertions(+), 121 deletions(-) delete mode 100644 assets/test/shaders/world_commands.config diff --git a/assets/shaders/world2d.frag.glsl b/assets/shaders/world2d.frag.glsl index 143d3f0980..98d323e0f6 100644 --- a/assets/shaders/world2d.frag.glsl +++ b/assets/shaders/world2d.frag.glsl @@ -40,4 +40,4 @@ void main() { break; } id = u_id; -} \ No newline at end of file +} diff --git a/assets/test/shaders/world_commands.config b/assets/test/shaders/world_commands.config deleted file mode 100644 index 16b6a9d399..0000000000 --- a/assets/test/shaders/world_commands.config +++ /dev/null @@ -1,23 +0,0 @@ -[COMMAND] -placeholder=COMMAND_SWITCH -alpha=254 -description=Red tint -code={ -col = vec4(1.0, 0.0, 0.0, 1.0) * tex_val; -} - -[COMMAND] -placeholder=COMMAND_SWITCH -alpha=252 -description=Green tint -code={ -col = vec4(0.0, 1.0, 0.0, 1.0) * tex_val; -} - -[COMMAND] -placeholder=COMMAND_SWITCH -alpha=250 -description=Blue tint -code={ -col = vec4(0.0, 0.0, 1.0, 1.0) * tex_val; -} \ No newline at end of file diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 7514f2ca7e..d8d93279af 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -1,4 +1,4 @@ -// Copyright 2022-2025 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #include "render_stage.h" @@ -12,7 +12,6 @@ #include "renderer/resources/texture_info.h" #include "renderer/shader_program.h" #include "renderer/stages/world/object.h" -#include "renderer/stages/world/world_shader_commands.h" #include "renderer/texture.h" #include "renderer/window.h" #include "time/clock.h" @@ -47,30 +46,6 @@ WorldRenderStage::WorldRenderStage(const std::shared_ptr &window, log::log(INFO << "Created render stage 'World'"); } -WorldRenderStage::WorldRenderStage(const std::shared_ptr &window, - const std::shared_ptr &renderer, - const std::shared_ptr &camera, - const util::Path &shaderdir, - const util::Path &configdir, - const std::shared_ptr &asset_manager, - const std::shared_ptr clock) : - renderer{renderer}, - camera{camera}, - asset_manager{asset_manager}, - render_objects{}, - clock{clock}, - default_geometry{this->renderer->add_mesh_geometry(WorldObject::get_mesh())} { - auto size = window->get_size(); - this->initialize_render_pass_with_shader_commands(size[0], size[1], shaderdir, configdir); - this->init_uniform_ids(); - - window->add_resize_callback([this](size_t width, size_t height, double /*scale*/) { - this->resize(width, height); - }); - - log::log(INFO << "Created render stage 'World' with shader command"); -} - std::shared_ptr WorldRenderStage::get_render_pass() { return this->render_pass; } @@ -181,37 +156,4 @@ void WorldRenderStage::init_uniform_ids() { WorldObject::anchor_offset = this->display_shader->get_uniform_id("anchor_offset"); } -void WorldRenderStage::initialize_render_pass_with_shader_commands(size_t width, size_t height, const util::Path &shaderdir, const util::Path &config_path) { - auto vert_shader_file = (shaderdir / "demo_7_world.vert.glsl").open(); - auto vert_shader_src = renderer::resources::ShaderSource( - resources::shader_lang_t::glsl, - resources::shader_stage_t::vertex, - vert_shader_file.read()); - vert_shader_file.close(); - - auto frag_shader_file = (shaderdir / "demo_7_world.frag.glsl").open(); - log::log(INFO << "Loading shader commands config from: " << (shaderdir / "demo_7_display.frag.glsl")); - this->shader_template = std::make_shared(frag_shader_file.read()); - if (not this->shader_template->load_commands(config_path / "world_commands.config")) { - log::log(ERR << "Failed to load shader commands configuration for world stage"); - return; - } - - auto frag_shader_src = renderer::resources::ShaderSource( - resources::shader_lang_t::glsl, - resources::shader_stage_t::fragment, - this->shader_template->generate_source()); - frag_shader_file.close(); - - 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->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}); - this->render_pass = this->renderer->add_render_pass({}, fbo); -} - } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index 9fc4cdbf06..f1256f3b57 100644 --- a/libopenage/renderer/stages/world/render_stage.h +++ b/libopenage/renderer/stages/world/render_stage.h @@ -1,4 +1,4 @@ -// Copyright 2022-2025 the openage authors. See copying.md for legal info. +// Copyright 2022-2024 the openage authors. See copying.md for legal info. #pragma once @@ -33,7 +33,6 @@ class AssetManager; namespace world { class RenderEntity; class WorldObject; -class ShaderCommandTemplate; /** * Renderer for drawing and displaying entities in the game world (units, buildings, etc.) @@ -61,26 +60,6 @@ class WorldRenderStage { const util::Path &shaderdir, const std::shared_ptr &asset_manager, const std::shared_ptr clock); - - /** - * Create a new render stage for the game world with shader command. - * - * @param window openage window targeted for rendering. - * @param renderer openage low-level renderer. - * @param camera Camera used for the rendered scene. - * @param shaderdir Directory containing the shader source files. - * @param configdir Directory containing the config for shader command. - * @param asset_manager Asset manager for loading resources. - * @param clock Simulation clock for timing animations. - */ - WorldRenderStage(const std::shared_ptr &window, - const std::shared_ptr &renderer, - const std::shared_ptr &camera, - const util::Path &shaderdir, - const util::Path &configdir, - const std::shared_ptr &asset_manager, - const std::shared_ptr clock); - ~WorldRenderStage() = default; /** @@ -132,17 +111,6 @@ class WorldRenderStage { */ void init_uniform_ids(); - /** - * Initialize render pass with shader commands. - * This is an alternative to initialize_render_pass() that uses configurable shader commands. - * - * @param width Width of the FBO. - * @param height Height of the FBO. - * @param shaderdir Directory containing shader files. - * @param configdir Directory containing configuration file. - */ - void initialize_render_pass_with_shader_commands(size_t width, size_t height, const util::Path &shaderdir, const util::Path &config_path); - /** * Reference to the openage renderer. */ @@ -163,11 +131,6 @@ class WorldRenderStage { */ std::shared_ptr render_pass; - /** - * Template for the world shader program. - */ - std::shared_ptr shader_template; - /** * Render entities requested by the game world. */ From 32a9d670bf29385a4d1e8ccc2fa88601c67a5e70 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Tue, 28 Jan 2025 17:20:12 -0500 Subject: [PATCH 759/771] Rename files --- .../{demo_7_world.frag.glsl => demo_7_shader_command.frag.glsl} | 0 .../{demo_7_world.vert.glsl => demo_7_shader_command.vert.glsl} | 0 .../world/{world_shader_commands.cpp => shader_template.cpp} | 0 .../stages/world/{world_shader_commands.h => shader_template.h} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename assets/test/shaders/{demo_7_world.frag.glsl => demo_7_shader_command.frag.glsl} (100%) rename assets/test/shaders/{demo_7_world.vert.glsl => demo_7_shader_command.vert.glsl} (100%) rename libopenage/renderer/stages/world/{world_shader_commands.cpp => shader_template.cpp} (100%) rename libopenage/renderer/stages/world/{world_shader_commands.h => shader_template.h} (100%) diff --git a/assets/test/shaders/demo_7_world.frag.glsl b/assets/test/shaders/demo_7_shader_command.frag.glsl similarity index 100% rename from assets/test/shaders/demo_7_world.frag.glsl rename to assets/test/shaders/demo_7_shader_command.frag.glsl diff --git a/assets/test/shaders/demo_7_world.vert.glsl b/assets/test/shaders/demo_7_shader_command.vert.glsl similarity index 100% rename from assets/test/shaders/demo_7_world.vert.glsl rename to assets/test/shaders/demo_7_shader_command.vert.glsl diff --git a/libopenage/renderer/stages/world/world_shader_commands.cpp b/libopenage/renderer/stages/world/shader_template.cpp similarity index 100% rename from libopenage/renderer/stages/world/world_shader_commands.cpp rename to libopenage/renderer/stages/world/shader_template.cpp diff --git a/libopenage/renderer/stages/world/world_shader_commands.h b/libopenage/renderer/stages/world/shader_template.h similarity index 100% rename from libopenage/renderer/stages/world/world_shader_commands.h rename to libopenage/renderer/stages/world/shader_template.h From 06bbfd12eb79b694ad2fda50113aad4e83f8d03b Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Tue, 28 Jan 2025 17:34:46 -0500 Subject: [PATCH 760/771] Simplify shader template system with file-based snippets --- .../shaders/demo_7_shader_command.frag.glsl | 36 +--- .../shaders/demo_7_shader_command.vert.glsl | 100 +--------- .../shaders/demo_7_snippets/alpha.snippet | 1 + .../shaders/demo_7_snippets/color.snippet | 3 + libopenage/renderer/demo/demo_7.cpp | 112 +++++------- libopenage/renderer/demo/demo_7.h | 12 +- .../renderer/stages/world/CMakeLists.txt | 2 +- .../renderer/stages/world/shader_template.cpp | 171 ++++-------------- .../renderer/stages/world/shader_template.h | 48 ++--- 9 files changed, 114 insertions(+), 371 deletions(-) create mode 100644 assets/test/shaders/demo_7_snippets/alpha.snippet create mode 100644 assets/test/shaders/demo_7_snippets/color.snippet diff --git a/assets/test/shaders/demo_7_shader_command.frag.glsl b/assets/test/shaders/demo_7_shader_command.frag.glsl index d6ffd22029..76e50ffd78 100644 --- a/assets/test/shaders/demo_7_shader_command.frag.glsl +++ b/assets/test/shaders/demo_7_shader_command.frag.glsl @@ -1,34 +1,12 @@ #version 330 -in vec2 vert_uv; +in vec2 tex_coord; +out vec4 frag_color; -layout(location = 0) out vec4 col; -layout(location = 1) out uint id; - -uniform sampler2D tex; -uniform uint u_id; - -// position (top left corner) and size: (x, y, width, height) -uniform vec4 tile_params; - -vec2 uv = vec2( - vert_uv.x * tile_params.z + tile_params.x, - vert_uv.y *tile_params.w + tile_params.y); +uniform float time; void main() { - vec4 tex_val = texture(tex, uv); - int alpha = int(round(tex_val.a * 255)); - switch (alpha) { - case 0: - col = tex_val; - discard; - - // do not save the ID - return; - //@COMMAND_SWITCH@ - default: - col = tex_val; - break; - } - id = u_id; -} \ No newline at end of file + // PLACEHOLDER: color + // PLACEHOLDER: alpha + frag_color = vec4(r, g, b, alpha); +} diff --git a/assets/test/shaders/demo_7_shader_command.vert.glsl b/assets/test/shaders/demo_7_shader_command.vert.glsl index 7987d40ec3..caefe64535 100644 --- a/assets/test/shaders/demo_7_shader_command.vert.glsl +++ b/assets/test/shaders/demo_7_shader_command.vert.glsl @@ -1,101 +1,9 @@ #version 330 -layout(location=0) in vec2 v_position; -layout(location=1) in vec2 uv; - -out vec2 vert_uv; - -// camera parameters for transforming the object position -// and scaling the subtex to the correct size -layout (std140) uniform camera { - // view matrix (world to view space) - mat4 view; - // projection matrix (view to clip space) - mat4 proj; - // inverse zoom factor (1.0 / zoom) - // high zoom = upscale subtex - // low zoom = downscale subtex - float inv_zoom; - // inverse viewport size (1.0 / viewport size) - vec2 inv_viewport_size; -}; - -// can be used to move the object position in world space _before_ -// it's transformed to clip space -// this is usually unnecessary because we want to draw the -// subtex where the object is, so this can be set to the identity matrix -uniform mat4 model; - -// position of the object in world space -uniform vec3 obj_world_position; - -// flip the subtexture horizontally/vertically -uniform bool flip_x; -uniform bool flip_y; - -// parameters for scaling and moving the subtex -// to the correct position in clip space - -// animation scalefactor -// scales the vertex positions so that they -// match the subtex dimensions -// -// high animation scale = downscale subtex -// low animation scale = upscale subtex -uniform float scale; - -// size of the subtex (in pixels) -uniform vec2 subtex_size; - -// offset of the subtex anchor point -// from the subtex center (in pixels) -// used to move the subtex so that the anchor point -// is at the object position -uniform vec2 anchor_offset; +in vec2 position; +out vec2 tex_coord; void main() { - // translate the position of the object from world space to clip space - // this is the position where we want to draw the subtex in 2D - vec4 obj_clip_pos = proj * view * model * vec4(obj_world_position, 1.0); - - // subtex has to be scaled to account for the zoom factor - // and the animation scale factor. essentially this is (animation scale / zoom). - float zoom_scale = scale * inv_zoom; - - // Scale the subtex vertices - // we have to account for the viewport size to get the correct dimensions - // and then scale the subtex to the zoom factor to get the correct size - vec2 vert_scale = zoom_scale * subtex_size * inv_viewport_size; - - // Scale the anchor offset with the same method as above - // to get the correct anchor position in the viewport - vec2 anchor_scale = zoom_scale * anchor_offset * inv_viewport_size; - - // if the subtex is flipped, we also need to flip the anchor offset - // essentially, we invert the coordinates for the flipped axis - float anchor_x = float(flip_x) * -1.0 * anchor_scale.x + float(!flip_x) * anchor_scale.x; - float anchor_y = float(flip_y) * -1.0 * anchor_scale.y + float(!flip_y) * anchor_scale.y; - - // offset the clip position by the offset of the subtex anchor - // imagine this as pinning the subtex to the object position at the subtex anchor point - obj_clip_pos += vec4(anchor_x, anchor_y, 0.0, 0.0); - - // create a move matrix for positioning the vertices - // uses the vert scale and the transformed object position in clip space - mat4 move = mat4(vert_scale.x, 0.0, 0.0, 0.0, - 0.0, vert_scale.y, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - obj_clip_pos.x, obj_clip_pos.y, obj_clip_pos.z, 1.0); - - // calculate the final vertex position - gl_Position = move * vec4(v_position, 0.0, 1.0); - - // if the subtex is flipped, we also need to flip the uv tex coordinates - // essentially, we invert the coordinates for the flipped axis - - // !flip_x is default because OpenGL uses bottom-left as its origin - float uv_x = float(!flip_x) * uv.x + float(flip_x) * (1.0 - uv.x); - float uv_y = float(flip_y) * uv.y + float(!flip_y) * (1.0 - uv.y); - - vert_uv = vec2(uv_x, uv_y); + tex_coord = position.xy * 0.5 + 0.5; + gl_Position = vec4(position.xy, 0.0, 1.0); } diff --git a/assets/test/shaders/demo_7_snippets/alpha.snippet b/assets/test/shaders/demo_7_snippets/alpha.snippet new file mode 100644 index 0000000000..603c275311 --- /dev/null +++ b/assets/test/shaders/demo_7_snippets/alpha.snippet @@ -0,0 +1 @@ +float alpha = (sin(time) + 1.0) * 0.5; diff --git a/assets/test/shaders/demo_7_snippets/color.snippet b/assets/test/shaders/demo_7_snippets/color.snippet new file mode 100644 index 0000000000..4c0204e773 --- /dev/null +++ b/assets/test/shaders/demo_7_snippets/color.snippet @@ -0,0 +1,3 @@ +float r = 0.5 + 0.5 * sin(time); +float g = 0.5 + 0.5 * sin(time + 2.0); +float b = 0.5 + 0.5 * sin(time + 4.0); diff --git a/libopenage/renderer/demo/demo_7.cpp b/libopenage/renderer/demo/demo_7.cpp index 470ded48f1..5f964b1034 100644 --- a/libopenage/renderer/demo/demo_7.cpp +++ b/libopenage/renderer/demo/demo_7.cpp @@ -4,27 +4,15 @@ #include "util/path.h" -#include -#include - -#include "coord/tile.h" -#include "renderer/camera/camera.h" +#include "renderer/demo/util.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" -#include "renderer/render_factory.h" #include "renderer/render_pass.h" #include "renderer/render_target.h" -#include "renderer/resources/assets/asset_manager.h" +#include "renderer/resources/mesh_data.h" #include "renderer/resources/shader_source.h" -#include "renderer/stages/camera/manager.h" -#include "renderer/stages/screen/render_stage.h" -#include "renderer/stages/skybox/render_stage.h" -#include "renderer/stages/terrain/render_entity.h" -#include "renderer/stages/terrain/render_stage.h" -#include "renderer/stages/world/render_entity.h" -#include "renderer/stages/world/render_stage.h" -#include "renderer/uniform_buffer.h" -#include "time/clock.h" +#include "renderer/shader_program.h" +#include "renderer/stages/world/shader_template.h" namespace openage::renderer::tests { @@ -36,72 +24,62 @@ void renderer_demo_7(const util::Path &path) { settings.height = 600; settings.debug = true; - auto window = std::make_shared("Shader Commands Demo", settings); - auto renderer = window->make_renderer(); - auto camera = std::make_shared(renderer, window->get_size()); - auto clock = std::make_shared(); - auto asset_manager = std::make_shared( - renderer, - path["assets"]["test"]); - auto cam_manager = std::make_shared(camera); + opengl::GlWindow window("Shader Commands Demo", settings); + auto renderer = window.make_renderer(); auto shaderdir = path / "assets" / "test" / "shaders"; - std::vector> - render_passes{}; - - // Initialize world renderer with shader commands - auto world_renderer = std::make_shared( - window, - renderer, - camera, - shaderdir, - shaderdir, // Temporarily, Shader commands config has the same path with shaders for this demo - asset_manager, - clock); - - render_passes.push_back(world_renderer->get_render_pass()); - - auto screen_renderer = std::make_shared( - window, - renderer, - path["assets"]["shaders"]); - std::vector> targets{}; - for (auto &pass : render_passes) { - targets.push_back(pass->get_target()); - } - screen_renderer->set_render_targets(targets); + // Initialize shader templlalte + world::ShaderTemplate frag_template(shaderdir / "demo_7_shader_command.frag.glsl"); - render_passes.push_back(screen_renderer->get_render_pass()); + // Load snippets from a snippet directory + frag_template.load_snippets(shaderdir / "demo_7_snippets"); - auto render_factory = std::make_shared(nullptr, world_renderer); + auto vert_shader_file = (shaderdir / "demo_7_shader_command.vert.glsl").open(); + auto vert_shader_src = resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::vertex, + vert_shader_file.read()); + vert_shader_file.close(); - auto entity1 = render_factory->add_world_render_entity(); - entity1->update(0, coord::phys3(0.0f, 0.0f, 0.0f), "./textures/test_gaben.sprite"); + auto frag_shader_src = resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::fragment, + frag_template.generate_source()); - auto entity2 = render_factory->add_world_render_entity(); - entity2->update(1, coord::phys3(3.0f, 0.0f, 0.0f), "./textures/test_gaben.sprite"); + auto shader = renderer->add_shader({vert_shader_src, frag_shader_src}); - auto entity3 = render_factory->add_world_render_entity(); - entity3->update(2, coord::phys3(-3.0f, 0.0f, 0.0f), "./textures/test_gaben.sprite"); + // Create a simple quad for rendering + auto quad = renderer->add_mesh_geometry(resources::MeshData::make_quad()); - // Main loop - while (not window->should_close()) { - qtapp->process_events(); + auto uniforms = shader->new_uniform_input("time", 0.0f); + + Renderable display_obj{ + uniforms, + quad, + false, + false, + }; - // Update camera matrices - cam_manager->update(); + if (not check_uniform_completeness({display_obj})) { + log::log(WARN << "Uniforms not complete."); + } - world_renderer->update(); + auto pass = renderer->add_render_pass({display_obj}, renderer->get_display_target()); - for (auto &pass : render_passes) { - renderer->render(pass); - } + // Main loop + float time = 0.0f; + while (not window.should_close()) { + time += 0.016f; + uniforms->update("time", time); - renderer->check_error(); + renderer->render(pass); + window.update(); + qtapp->process_events(); - window->update(); + renderer->check_error(); } + window.close(); } } // namespace openage::renderer::tests diff --git a/libopenage/renderer/demo/demo_7.h b/libopenage/renderer/demo/demo_7.h index 45c192523d..7642fe06c8 100644 --- a/libopenage/renderer/demo/demo_7.h +++ b/libopenage/renderer/demo/demo_7.h @@ -7,13 +7,13 @@ namespace openage::renderer::tests { /** - * Show off the render stages in the level 2 renderer and the camera - * system. + * Demonstrate the shader template system for shader generation. * - Window creation - * - Creating a camera - * - Initializing the level 2 render stages: skybox, terrain, world, screen - * - Adding renderables to the render stages via the render factory - * - Moving camera with mouse/keyboard callbacks + * - Create a shader template + * - Load shader snippets (command) from files + * - Generate shader sources from the template + * - Creating a render pass + * - Creating a renderable from a mesh * * @param path Path to the project rootdir. */ diff --git a/libopenage/renderer/stages/world/CMakeLists.txt b/libopenage/renderer/stages/world/CMakeLists.txt index d86d7aa44c..c43ac917b9 100644 --- a/libopenage/renderer/stages/world/CMakeLists.txt +++ b/libopenage/renderer/stages/world/CMakeLists.txt @@ -2,5 +2,5 @@ add_sources(libopenage object.cpp render_entity.cpp render_stage.cpp - world_shader_commands.cpp + shader_template.cpp ) diff --git a/libopenage/renderer/stages/world/shader_template.cpp b/libopenage/renderer/stages/world/shader_template.cpp index 9cc10f5f87..04adbfbcab 100644 --- a/libopenage/renderer/stages/world/shader_template.cpp +++ b/libopenage/renderer/stages/world/shader_template.cpp @@ -1,6 +1,6 @@ // Copyright 2024-2025 the openage authors. See copying.md for legal info. -#include "world_shader_commands.h" +#include "shader_template.h" #include @@ -9,158 +9,57 @@ namespace openage::renderer::world { -ShaderCommandTemplate::ShaderCommandTemplate(const std::string &template_code) : - template_code{template_code} {} - -bool ShaderCommandTemplate::load_commands(const util::Path &config_path) { - try { - log::log(INFO << "Loading shader commands config from: " << config_path); - auto config_file = config_path.open(); - std::string line; - std::stringstream ss(config_file.read()); - - ShaderCommandConfig current_command; - // if true, we are reading the code block for the current command. - bool reading_code = false; - std::string code_block; - - while (std::getline(ss, line)) { - if (!line.empty() && line.back() == '\r') { - line.pop_back(); - } - // Trim whitespace from line - line = trim(line); - log::log(INFO << "Parsing line: " << line); - - // Skip empty lines and comments - if (line.empty() || line[0] == '#') { - continue; - } - - if (reading_code) { - if (line == "}") { - reading_code = false; - current_command.code = code_block; - - // Generate and add snippet - std::string snippet = generate_snippet(current_command); - add_snippet(current_command.placeholder_id, snippet); - commands.push_back(current_command); - - // Reset for next command - code_block.clear(); - } - else { - code_block += line + "\n"; - } - continue; - } - - if (line == "[COMMAND]") { - current_command = ShaderCommandConfig{}; - continue; - } - - // Parse key-value pairs - size_t pos = line.find('='); - if (pos != std::string::npos) { - std::string key = trim(line.substr(0, pos)); - std::string value = trim(line.substr(pos + 1)); +ShaderTemplate::ShaderTemplate(const util::Path &template_path) { + auto file = template_path.open(); + this->template_code = file.read(); + file.close(); +} - if (key == "placeholder") { - current_command.placeholder_id = value; - } - else if (key == "alpha") { - uint8_t alpha = static_cast(std::stoi(value)); - if (alpha % 2 == 0 && alpha >= 0 && alpha <= 254) { - current_command.alpha = alpha; - } - else { - log::log(ERR << "Invalid alpha value for command: " << alpha); - return false; - } - } - else if (key == "description") { - current_command.description = value; - } - else if (key == "code") { - if (value == "{") { - reading_code = true; - code_block.clear(); - } - } - } +void ShaderTemplate::load_snippets(const util::Path &snippet_path) { + // load config here + util::Path snippet_path_copy = snippet_path; + for (const auto &entry : snippet_path_copy.iterdir()) { + if (entry.get_name().ends_with(".snippet")) { + add_snippet(entry); } - - return true; - } - catch (const std::exception &e) { - log::log(ERR << "Failed to load shader commands: " << e.what()); - return false; } } -bool ShaderCommandTemplate::add_snippet(const std::string &placeholder_id, const std::string &snippet) { - if (snippet.empty()) { - log::log(ERR << "Empty snippet for placeholder: " << placeholder_id); - return false; - } - - if (placeholder_id.empty()) { - log::log(ERR << "Empty placeholder ID for snippet"); - return false; - } - - // Check if the placeholder exists in the template - std::string placeholder = "//@" + placeholder_id + "@"; - if (template_code.find(placeholder) == std::string::npos) { - log::log(ERR << "Placeholder not found in template: " << placeholder_id); - return false; - } +void ShaderTemplate::add_snippet(const util::Path &snippet_path) { + std::string file_name = snippet_path.get_name(); + std::string name = file_name.substr(0, file_name.find_last_of('.')); + auto file = snippet_path.open(); + std::string content = file.read(); + file.close(); - // Store the snippet - snippets[placeholder_id].push_back(snippet); - return true; + snippets[name] = content; } -std::string ShaderCommandTemplate::generate_snippet(const ShaderCommandConfig &command) { - return "case " + std::to_string(command.alpha) + ":\n" - + "\t\t// " + command.description + "\n" - + "\t\t" + command.code + "\t\tbreak;\n"; -} - -std::string ShaderCommandTemplate::generate_source() const { +std::string ShaderTemplate::generate_source() const { std::string result = template_code; // Process each placeholder - for (const auto &[placeholder_id, snippet_list] : snippets) { - std::string combined_snippets; + for (const auto &[name, snippet_code] : snippets) { + std::string placeholder = "// PLACEHOLDER: " + name; + size_t pos = result.find(placeholder); - // Combine all snippets for this placeholder - for (const auto &snippet : snippet_list) { - combined_snippets += snippet; + if (pos != std::string::npos) { + result.replace(pos, placeholder.length(), snippet_code); } - - // Find and replace the placeholder - std::string placeholder = "//@" + placeholder_id + "@"; - size_t pos = result.find(placeholder); - if (pos == std::string::npos) { - throw Error(MSG(err) << "Placeholder disappeared from template: " << placeholder_id); + else { + log::log(WARN << "Placeholder not found in template: " << name); } + } - // Replace placeholder with combined snippets - result.replace(pos, placeholder.length(), combined_snippets); + // Check if all placeholders were replaced + size_t placeholder_pos = result.find("// PLACEHOLDER:"); + if (placeholder_pos != std::string::npos) { + size_t line_end = result.find('\n', placeholder_pos); + std::string missing = result.substr(placeholder_pos, + line_end - placeholder_pos); + throw Error(MSG(err) << "Missing snippet for placeholder: " << missing); } return result; } - -std::string ShaderCommandTemplate::trim(const std::string &str) const { - size_t first = str.find_first_not_of(" \t"); - if (first == std::string::npos) { - return ""; - } - size_t last = str.find_last_not_of(" \t"); - return str.substr(first, (last - first + 1)); -} } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/shader_template.h b/libopenage/renderer/stages/world/shader_template.h index 9f6a158a25..0b5dc89f80 100644 --- a/libopenage/renderer/stages/world/shader_template.h +++ b/libopenage/renderer/stages/world/shader_template.h @@ -13,52 +13,35 @@ namespace openage { namespace renderer { namespace world { -/** - * Represents a single shader command that can be used in the world fragment shader. - * Commands are identified by their alpha values and contain GLSL code snippets - * that define custom rendering behavior. - */ -struct ShaderCommandConfig { - /// ID of the placeholder where this snippet should be inserted - std::string placeholder_id; - /// Command identifier ((must be even, range 0-254)) - uint8_t alpha; - /// GLSL code snippet that defines the command's behavior - std::string code; - /// Documentation (optional) - std::string description; -}; - /** * Manages shader templates and their code snippets. * Allows loading configurable shader commands and generating * complete shader source code. */ -class ShaderCommandTemplate { +class ShaderTemplate { public: /** * Create a shader template from source code of shader. * - * @param template_code Source code containing placeholders. + * @param template_path Path to the template file. */ - explicit ShaderCommandTemplate(const std::string &template_code); + explicit ShaderTemplate(const util::Path &template_path); /** - * Load commands from a configuration file. + * Load all snippets from a JSON config file. * - * @param config_path Path to the command configuration file. - * @return true if commands were loaded successfully. + * @param config_path Path to JSON config file. + * @param base_path Base path for resolving relative snippet paths. */ - bool load_commands(const util::Path &config_path); + void load_snippets(const util::Path &snippet_path); /** - * Add a single code snippet to the template. + * Add a single code snippet to snippets map. * - * @param placeholder_id Where to insert the snippet. - * @param snippet Code to insert. - * @return true if snippet was added successfully. + * @param name Snippet identifier. + * @param snippet_path Path to the snippet file. */ - bool add_snippet(const std::string &placeholder_id, const std::string &snippet); + void add_snippet(const util::Path &snippet_path); /** * Generate final shader source code with all snippets inserted. @@ -69,17 +52,10 @@ class ShaderCommandTemplate { std::string generate_source() const; private: - // Generate a single code snippet for a command. - std::string generate_snippet(const ShaderCommandConfig &command); - // Helper function to trim whitespace from a string - std::string trim(const std::string &str) const; - // Original template code with placeholders std::string template_code; // Mapping of placeholder IDs to their code snippets - std::map> snippets; - // Loaded command configurations - std::vector commands; + std::map snippets; }; } // namespace world } // namespace renderer From b07e6bbabfb5ceed41dfff9c1d13227aa44fdfed Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Sun, 20 Apr 2025 15:26:43 -0400 Subject: [PATCH 761/771] update according to review --- libopenage/renderer/demo/demo_7.cpp | 7 ++--- .../renderer/stages/world/shader_template.cpp | 26 ++++++++++++------- .../renderer/stages/world/shader_template.h | 7 ++--- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/libopenage/renderer/demo/demo_7.cpp b/libopenage/renderer/demo/demo_7.cpp index 5f964b1034..9377053bdc 100644 --- a/libopenage/renderer/demo/demo_7.cpp +++ b/libopenage/renderer/demo/demo_7.cpp @@ -29,7 +29,7 @@ void renderer_demo_7(const util::Path &path) { auto shaderdir = path / "assets" / "test" / "shaders"; - // Initialize shader templlalte + // Initialize shader template world::ShaderTemplate frag_template(shaderdir / "demo_7_shader_command.frag.glsl"); // Load snippets from a snippet directory @@ -42,10 +42,7 @@ void renderer_demo_7(const util::Path &path) { vert_shader_file.read()); vert_shader_file.close(); - auto frag_shader_src = resources::ShaderSource( - resources::shader_lang_t::glsl, - resources::shader_stage_t::fragment, - frag_template.generate_source()); + auto frag_shader_src = frag_template.generate_source(); auto shader = renderer->add_shader({vert_shader_src, frag_shader_src}); diff --git a/libopenage/renderer/stages/world/shader_template.cpp b/libopenage/renderer/stages/world/shader_template.cpp index 04adbfbcab..641e312f1e 100644 --- a/libopenage/renderer/stages/world/shader_template.cpp +++ b/libopenage/renderer/stages/world/shader_template.cpp @@ -26,25 +26,25 @@ void ShaderTemplate::load_snippets(const util::Path &snippet_path) { } void ShaderTemplate::add_snippet(const util::Path &snippet_path) { - std::string file_name = snippet_path.get_name(); - std::string name = file_name.substr(0, file_name.find_last_of('.')); auto file = snippet_path.open(); std::string content = file.read(); file.close(); + std::string name = snippet_path.get_stem(); + snippets[name] = content; } -std::string ShaderTemplate::generate_source() const { - std::string result = template_code; +renderer::resources::ShaderSource ShaderTemplate::generate_source() const { + std::string result_src = template_code; // Process each placeholder for (const auto &[name, snippet_code] : snippets) { std::string placeholder = "// PLACEHOLDER: " + name; - size_t pos = result.find(placeholder); + size_t pos = result_src.find(placeholder); if (pos != std::string::npos) { - result.replace(pos, placeholder.length(), snippet_code); + result_src.replace(pos, placeholder.length(), snippet_code); } else { log::log(WARN << "Placeholder not found in template: " << name); @@ -52,14 +52,20 @@ std::string ShaderTemplate::generate_source() const { } // Check if all placeholders were replaced - size_t placeholder_pos = result.find("// PLACEHOLDER:"); + size_t placeholder_pos = result_src.find("// PLACEHOLDER:"); if (placeholder_pos != std::string::npos) { - size_t line_end = result.find('\n', placeholder_pos); - std::string missing = result.substr(placeholder_pos, - line_end - placeholder_pos); + size_t line_end = result_src.find('\n', placeholder_pos); + std::string missing = result_src.substr(placeholder_pos, + line_end - placeholder_pos); throw Error(MSG(err) << "Missing snippet for placeholder: " << missing); } + auto result = resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::fragment, + std::move(result_src)); + return result; } + } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/shader_template.h b/libopenage/renderer/stages/world/shader_template.h index 0b5dc89f80..117236f51c 100644 --- a/libopenage/renderer/stages/world/shader_template.h +++ b/libopenage/renderer/stages/world/shader_template.h @@ -7,6 +7,7 @@ #include #include +#include "renderer/resources/shader_source.h" #include "util/path.h" namespace openage { @@ -49,12 +50,12 @@ class ShaderTemplate { * @return Complete shader code. * @throws Error if any required placeholders are missing snippets. */ - std::string generate_source() const; + renderer::resources::ShaderSource generate_source() const; private: - // Original template code with placeholders + /// Original template code with placeholders std::string template_code; - // Mapping of placeholder IDs to their code snippets + /// Mapping of placeholder IDs to their code snippets std::map snippets; }; } // namespace world From baabe59d1e4627ada0e55f115ef2c371df573c76 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Sat, 3 May 2025 18:19:51 -0400 Subject: [PATCH 762/771] Optimize ShaderTemplate by precomputing placeholder positions --- .../renderer/stages/world/shader_template.cpp | 38 ++++++++++--------- .../renderer/stages/world/shader_template.h | 14 +++++-- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/libopenage/renderer/stages/world/shader_template.cpp b/libopenage/renderer/stages/world/shader_template.cpp index 641e312f1e..6514204162 100644 --- a/libopenage/renderer/stages/world/shader_template.cpp +++ b/libopenage/renderer/stages/world/shader_template.cpp @@ -13,6 +13,20 @@ ShaderTemplate::ShaderTemplate(const util::Path &template_path) { auto file = template_path.open(); this->template_code = file.read(); file.close(); + + std::string marker = "// PLACEHOLDER: "; + size_t pos = 0; + + while ((pos = this->template_code.find(marker, pos)) != std::string::npos) { + size_t name_start = pos + marker.length(); + size_t line_end = this->template_code.find('\n', name_start); + std::string name = this->template_code.substr(name_start, line_end - name_start); + // Trim trailing whitespace (space, tab, carriage return, etc.) + name.erase(name.find_last_not_of(" \t\r\n") + 1); + + this->placeholders.push_back({name, pos, line_end - pos}); + pos = line_end; + } } void ShaderTemplate::load_snippets(const util::Path &snippet_path) { @@ -38,28 +52,18 @@ void ShaderTemplate::add_snippet(const util::Path &snippet_path) { renderer::resources::ShaderSource ShaderTemplate::generate_source() const { std::string result_src = template_code; - // Process each placeholder - for (const auto &[name, snippet_code] : snippets) { - std::string placeholder = "// PLACEHOLDER: " + name; - size_t pos = result_src.find(placeholder); - - if (pos != std::string::npos) { - result_src.replace(pos, placeholder.length(), snippet_code); + // Replace placeholders in reverse order (to avoid offset issues) + for (auto it = placeholders.rbegin(); it != placeholders.rend(); ++it) { + const auto &ph = *it; + auto snippet_it = snippets.find(ph.name); + if (snippet_it != snippets.end()) { + result_src.replace(ph.position, ph.length, snippet_it->second); } else { - log::log(WARN << "Placeholder not found in template: " << name); + throw Error(MSG(err) << "Missing snippet for placeholder: " << ph.name); } } - // Check if all placeholders were replaced - size_t placeholder_pos = result_src.find("// PLACEHOLDER:"); - if (placeholder_pos != std::string::npos) { - size_t line_end = result_src.find('\n', placeholder_pos); - std::string missing = result_src.substr(placeholder_pos, - line_end - placeholder_pos); - throw Error(MSG(err) << "Missing snippet for placeholder: " << missing); - } - auto result = resources::ShaderSource( resources::shader_lang_t::glsl, resources::shader_stage_t::fragment, diff --git a/libopenage/renderer/stages/world/shader_template.h b/libopenage/renderer/stages/world/shader_template.h index 117236f51c..542ca8ed08 100644 --- a/libopenage/renderer/stages/world/shader_template.h +++ b/libopenage/renderer/stages/world/shader_template.h @@ -29,10 +29,9 @@ class ShaderTemplate { explicit ShaderTemplate(const util::Path &template_path); /** - * Load all snippets from a JSON config file. + * Load all snippets from a directory of .snippet files. * - * @param config_path Path to JSON config file. - * @param base_path Base path for resolving relative snippet paths. + * @param snippet_path Path to directory containing snippet files. */ void load_snippets(const util::Path &snippet_path); @@ -57,6 +56,15 @@ class ShaderTemplate { std::string template_code; /// Mapping of placeholder IDs to their code snippets std::map snippets; + + /// Info about a placeholder found in the template + struct Placeholder { + std::string name; + size_t position; + size_t length; + }; + + std::vector placeholders; }; } // namespace world } // namespace renderer From 55af0a50070f31910b12d67f5d7f0931fcef8fb4 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 14 May 2025 07:45:23 +0200 Subject: [PATCH 763/771] renderer: Move shader template to resources namespace. --- libopenage/renderer/demo/demo_7.cpp | 5 +++-- libopenage/renderer/resources/CMakeLists.txt | 1 + .../renderer/{stages/world => resources}/shader_template.cpp | 2 +- .../renderer/{stages/world => resources}/shader_template.h | 4 ++-- libopenage/renderer/stages/world/CMakeLists.txt | 1 - 5 files changed, 7 insertions(+), 6 deletions(-) rename libopenage/renderer/{stages/world => resources}/shader_template.cpp (98%) rename libopenage/renderer/{stages/world => resources}/shader_template.h (95%) diff --git a/libopenage/renderer/demo/demo_7.cpp b/libopenage/renderer/demo/demo_7.cpp index 9377053bdc..8c804a52d9 100644 --- a/libopenage/renderer/demo/demo_7.cpp +++ b/libopenage/renderer/demo/demo_7.cpp @@ -11,8 +11,9 @@ #include "renderer/render_target.h" #include "renderer/resources/mesh_data.h" #include "renderer/resources/shader_source.h" +#include "renderer/resources/shader_template.h" #include "renderer/shader_program.h" -#include "renderer/stages/world/shader_template.h" + namespace openage::renderer::tests { @@ -30,7 +31,7 @@ void renderer_demo_7(const util::Path &path) { auto shaderdir = path / "assets" / "test" / "shaders"; // Initialize shader template - world::ShaderTemplate frag_template(shaderdir / "demo_7_shader_command.frag.glsl"); + resources::ShaderTemplate frag_template(shaderdir / "demo_7_shader_command.frag.glsl"); // Load snippets from a snippet directory frag_template.load_snippets(shaderdir / "demo_7_snippets"); diff --git a/libopenage/renderer/resources/CMakeLists.txt b/libopenage/renderer/resources/CMakeLists.txt index c5946910e5..16a0e5eb5f 100644 --- a/libopenage/renderer/resources/CMakeLists.txt +++ b/libopenage/renderer/resources/CMakeLists.txt @@ -4,6 +4,7 @@ add_sources(libopenage mesh_data.cpp palette_info.cpp shader_source.cpp + shader_template.cpp texture_data.cpp texture_info.cpp texture_subinfo.cpp diff --git a/libopenage/renderer/stages/world/shader_template.cpp b/libopenage/renderer/resources/shader_template.cpp similarity index 98% rename from libopenage/renderer/stages/world/shader_template.cpp rename to libopenage/renderer/resources/shader_template.cpp index 6514204162..c328ba82bc 100644 --- a/libopenage/renderer/stages/world/shader_template.cpp +++ b/libopenage/renderer/resources/shader_template.cpp @@ -7,7 +7,7 @@ #include "error/error.h" #include "log/log.h" -namespace openage::renderer::world { +namespace openage::renderer::resources { ShaderTemplate::ShaderTemplate(const util::Path &template_path) { auto file = template_path.open(); diff --git a/libopenage/renderer/stages/world/shader_template.h b/libopenage/renderer/resources/shader_template.h similarity index 95% rename from libopenage/renderer/stages/world/shader_template.h rename to libopenage/renderer/resources/shader_template.h index 542ca8ed08..210061ef0c 100644 --- a/libopenage/renderer/stages/world/shader_template.h +++ b/libopenage/renderer/resources/shader_template.h @@ -12,7 +12,7 @@ namespace openage { namespace renderer { -namespace world { +namespace resources { /** * Manages shader templates and their code snippets. @@ -57,7 +57,7 @@ class ShaderTemplate { /// Mapping of placeholder IDs to their code snippets std::map snippets; - /// Info about a placeholder found in the template + /// Info about a placeholder found in the template struct Placeholder { std::string name; size_t position; diff --git a/libopenage/renderer/stages/world/CMakeLists.txt b/libopenage/renderer/stages/world/CMakeLists.txt index c43ac917b9..811fd929ed 100644 --- a/libopenage/renderer/stages/world/CMakeLists.txt +++ b/libopenage/renderer/stages/world/CMakeLists.txt @@ -2,5 +2,4 @@ add_sources(libopenage object.cpp render_entity.cpp render_stage.cpp - shader_template.cpp ) From e92676677605346269730fa45058e8e25136cba7 Mon Sep 17 00:00:00 2001 From: heinezen Date: Wed, 14 May 2025 08:53:32 +0200 Subject: [PATCH 764/771] doc: Add documentation for shader templates. --- doc/code/renderer/demos.md | 13 +++ doc/code/renderer/images/demo_7.mp4 | Bin 0 -> 1009185 bytes doc/code/renderer/level1.md | 76 ++++++++++++++++++ .../renderer/resources/shader_template.h | 2 + 4 files changed, 91 insertions(+) create mode 100644 doc/code/renderer/images/demo_7.mp4 diff --git a/doc/code/renderer/demos.md b/doc/code/renderer/demos.md index 3bb7b266f3..3be974d628 100644 --- a/doc/code/renderer/demos.md +++ b/doc/code/renderer/demos.md @@ -144,6 +144,19 @@ This demo shows how to use [frustum culling](level2.md#frustum-culling) in the r ![Demo 6](/doc/code/renderer/images/demo_6.png) +### Demo 6 + +This demo shows how to use [shader templating](level1.md#shader-templates) in the renderer. + +```bash +./bin/run test --demo renderer.tests.renderer_demo 6 +``` + +**Result:** + +![Demo 7](/doc/code/renderer/images/demo_7.mp4) + + ## Stresstests ### Stresstest 0 diff --git a/doc/code/renderer/images/demo_7.mp4 b/doc/code/renderer/images/demo_7.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..fa26db0f866371a6c0d4a93cd781640039332f2d GIT binary patch literal 1009185 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
i3^BWB-elac=O%v$E)P>=Zgh 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 literal 0 HcmV?d00001 diff --git a/doc/code/renderer/images/demo_4.png b/doc/code/renderer/images/demo_4.png new file mode 100644 index 0000000000000000000000000000000000000000..17a90580f9d3f3f878967c1478c82130d1196333 GIT binary patch literal 13191 zcmcI~bzD^2`u8RTR1ic(35g>i0+P~g;Lr{wjYx=;(#?PZ2NjSR8iqjyhL9YjVL(b6 z8M+3gySv^!p8J08`JH<|_Z)A`A2Vy#Uh%Bw`}EqOn(9haXBf@^06+z~e+LQxq@(~q zB0xa~_LT4x4gr9$I^>Rmwg=JT01;WeJmsk_wH5#AjzCM_JCKl=onO<_k-(Re$_wE5 zKYlbLs#w6jhmQIKw$kK;Rs~7m&@&}tDH8C{=a!vqyOiJ)E7L(SJTcgDw2n>iyGYs7 zu28eqZ@skvcEOB}F&!0w0Sv)J^rTD==xL1-PZOrXHg?{;GGkQ?pbC)p9jqYvb-qJ9 zeB#7;eyPXCyQKX_ihDaF1_11a7jDS|$`nEAEtGX_8Ku{9IXBHUQ%=7;EzkQh<;$%) zDUP=fbViqr-FFuEOsT^|mVHs>#$)?_yK~Q%B|1HUWdF_$rkOlL(kIFsqI+K^eGy+$ z8%(Hg5P?JhKyLlC9Kv7i6(<+xN!;X;D`ScHINimkw%hz<1LAjq;eKzy9W~7>&3eX(x zcm?C)wC6-kJC&g%B_*qAeoT4BA08eqy1&2gC)zHjmaCeJyhO507swctoK2#rmUV?D z-p9?uWA~N$ZxrriV=x;HwY9Z8$k)RXdl#gXsC}uosC`S$q`o5JS)RaGpi8)Xoctm|Eq8+#{R6PmSM+!Q_nVq!bDcnFXKz>|-;xw(1iXte44 z$Ot#ZONaC0+lFLG{AW^i$v5A;dE>+LV_3>JPEO8hT$$(7XnBOz#Z|n}Tff<03`w#T zW|05xL{*KQ)sWG znZr7Mk=Q`>7sNoSwZH#Kzj!$6^$%9c!`Y*o?cZr;6lI?dls7Omuw#E{HkzZ)D~V`%T|Yiwm@Wnp78M^u%!2FUMxS@8d~qI}R*eJBuq z!}LnVa10US0Z2ToWMq5W%`&lWZf=gm%XEUsEE~ogr^huEyS>NS^ynaaE%8J=sm#kG zmXAbXONUq#Tpxfbt?cg)-L%c4UdqOhm=R8q)eV#j0jv zCMa&*(j_N#ELUXB(7%!aS_l<~^I}HUMNPIlS&Hx8-(V$z zpUKotQPxh&g8p*)jx;|1an3b)^+< zW_r4-t1HDNF(Dzpu<(15X7qAR4PankplNKJv+vZ_*7l}5-`UkQXl%@+r?)rLZ?V&N zV`gUN*=KU10^X!;U2W}*YH-CX%*=g9YWA*9K0a?EBhSx6O(W8f)sp0sdwcqO545yC zN?S$lu3Qa*V);*;0M}(+4=d;&CQ`-*GLs; zjw4P7V5w%dvGz@yn??QGri0&RW;?wr}o)YK_kTia4BR>{ftpDn>aUWO_wm9X%$|j$VMKPmKmVq)}}Pz)2!bFDfc(?&L(<-ri0@K~Ymz_hKJ= zfed%y!i6g=EPC1bl*JAX4sloV7ldFF-kx9a_&iF_%op();(n(2yPU z6#N<*`rs0=mX_Ajb*mJnix*RMUx|r|n$OMk!w7q*?Q0=hDxiC@Nq%oJ*(x{W#Jc^s2KE)Tqhj6>dD741O@Lt zJpZ}nQn;m#ilX9YtZXtXMc_SJVr<{J4k8^Log^*~x!O)uvN+p>rp4*JtDDD2Z~$v5 zo~5*`>@-wU6ZZMDBANZ+#}{97a`4DuhljyTmtTL zu##$ru%D)2XA5Pe$W$Y~P8-BSn(`8+sHgxi(VV(SLBrh@P`7m;l5te&RfcZ5pVHx!sF;y1chnCL=8kDv0r5C9B^!Nl|gJ z0h!T>x38M5*fGOuvY^JwrCn0lR18U`Y17xQU%M1)c260Tj*gCkQfIZaw8Z(6=5BE6 z^~bIf!@M6^a_MX0U=ft7qii2kfJBJ-mZR4K6Sen%{#duhNJa3HZ_*zNI(*@B0 z##`S#+q$&YRu&g2FMZ3-=5dCdDFoHhntpP^_N8`SfIH>xV;i6QW?Uq>+c9#DcI`K zY46^#QIN^X%91@Ve)sO3P^4twLV7x9Yv8t>Tuhc|zzwX{Am@{r*Y}x?Oie>90D;TP z##C*YC?t};lm#?x3jO1|yK#L^f%iyhS}z7NGBP4|6IvX6eXEA4yyvJXDcRTAjy{Qx zO)2c$W}dP72y=CIew>(?I7^+vrl82`RBg`sl7_uyE~@q1ZuZHEgod27#G2gE>Q^Zd z5+fs;L-#v=^i9`xBlaVK4)&Kciaz4n$_EDrR0Ce*4`dD2o0Cw<$qbuYThXHzPq#p> zbY-;{WVDbh4c4sX@;q~LQhY6^Kwoh=f0V_4qw^B6_B19gDT%VlStg?kbN4$avI9D` zkR{t26_s2t_z7qQSIFJyIqrYIon`yHiyo*a%ADRvHa0PFlIcxO>=ZPhbwT7$kcP72 zGgIDgoMyX2#YI}0H=`eoj+WJvyRT}I)8kiKwYTR<3p{vN3(c$-r1!g+aU10H%M|0~ z@85q}yq;fGWk1de6%!OZKTLw^J1X?tSzBAXOe8_nzqG_RfBZd`2B- zw5c1zcyQ+%Pwjp)OUob_y@IU6|2o zX=oHF*eEjk`uiU|Qz}C(y6;LzOEWQ}?;hQf)?kzsQ%YW0S^^!=t)6AmH`na!>;}Gk zxu4O~ZKbr}uA*`_XLzhpk81bcTa83%%%t`Ky?YIL)FgyB>h!fjzHeh=$xF-2LyL!FrH`|-Fyy@5#(qeD3L4Gj&H^<)tJz>`VDQVgI_onPcGxejI<5|~ z)Q5?kWT;Ewj}w*Q2B7)9gwmj3dwTI$qc!bS7?qvnH94rGO3g3&HJ6@+>~}n5$3IH$ zk3hSXp|%x3w$m6I8opDj%Fb>DgNP;J+f5{VQ>0#`Q7-41n3#6y+}^=e1&j%TMPW5L z7x#U}TZ-y_!UpEXB>5=>JpSmt&64MlpF!J)$g<-XQi~a_-Ctnf*hrdUj*-E^f-5gx zya-YyBkaRm)2-0-MEg_pGfDD-^GbeCEC{G)5M8mAw;3C58tg2tqj@gJC`c!{Ewu9lrZ9d(EKBHza#WN zZ~m`{{@uF&Bl=+o`oClNpD^No0NMZk^uLMj@AmocH&ZTy8CvxG;{Ek)b}8s|i}oHw zT?GgPg&gb}W8Z8Mjxcr+w)(Q`94%aDF?SYrUiKE30wT&!xNsrCUmoD+(5?0uj~=3@2p3iu*Yrr zvBkr)R0sZfj{53lJX%v(@`k;h_uzgEvnQX1S8~(7Y}Vcw4YOk-&{ZkhtOT8Hzbq}t zU-?bJrb147HrV>`$gkv(cS<&9@rBr~bf~-*Le0|3KGIXgg&-!t&~@ZUS*l^+;57m{*;VAU5ofY=77)2@mQ4&SvJp7= zM=JgS+fCw{Of73i_~6AjeA4{N6;*dyL4SQ`YY;ro1KkuxRDu?UpG!lW@Hy|sEn!L-sns;UO2!(aB!4DY? zIlOyBQ##iu<+Qt6AhH%+Vw7-}86$@f$j!oXd46D(e_bWh?5#D6S?L^`IpzpcWf@U* ze=N*#PDe+!N&r!(6TL(Xu59KAKS=Gun#+SHEzbJ(EULXWe~&Ui=7s7PudI=n?ZB&Q zvX9-E)RvtM8M3rKB3$O#b&gK~-amx(cXIou&8kC3phq842v_9aZyB@Z9qm#Hjeo>% zjrUu_vZy2#6!B^43bhhe#`>~``{_}-8ow2G#0|F;8G~D7W*AYrWDtj zA9?=Y9NIX`;_#^`lO4~QHN5YmX(WBv05jrZVOmWg<0WHYDWXl)3 z@Se$v=NVI7{k7L2WAT@lZ(!OEuxhz!%`?s&&dDT!k9Ly}iq3(;^cVHeQV$q!>HSnW zy=9H7UsRPlxO^EpAK_@)!GiJ-N$aC)M8lKRgsD^cT*Em$^&KeXVsBX2UC8b*hht+c z1W(B>MmQ}>U?a_0wkm8!Gh;ceBhX*^>~zsphnnzDJ9{%ypu)mf^OO6lkC$xBpFfh- zs6Lv$I21~RU2UdL575eVlI+J$K&qw1g7vs07?Yp8hRE_X+jBpgRa~Dkjhr?(M~v1; z?|BM$REeGf#{X#)@XyZ*JGsjbRN{@UMy&5XiK89W9>ga?&{^FvOJA?^T)v6!*>4mO zdnx%Ea%=)`xVW3LGvc+F9J2ByY`pqfMCGQ=miMxHd@s9P)Q#-HocT(T=Cz{980Wza zF@N6I7(;HS#ekyu9u)+p3HK%FwnX%#UCYO1O7OHb_LqJ;>2k){cYgDZkb!c*u^U&&n0QB< zTQu!x7NSgD%wBR+Q?6+_Q6n?EBn)-*unJkso4ZZT>^<{6B`Z6sbm9Bh$o1py7O1jX zw%6)@9S`c+J7yEr_-Jhj1m`?2heTuy?~#dnW<`7-ukrnILqyTkV!>p<1h;5XXFNNP zjTHyMvatwcB`N2sz3cB!+yReB>^vhmxIwQott-V{fFACQ91JR&>pVFKLUM08VDq{;c))9a5(UN^6e&>Zg_u|z?6&MuIf?#^L%FL1j{HowNTg|hFY zbGZBY^Oqb|I1D+N8CYBS#-*w=s#o#uz2eunV2ZebCiy7_=ANwrR*zl$&-CiZcpTR6 zCXadDx*u>93ny`8Obd1BZ8kzD7LOdq#EX17{Fw9cYSPP-ueT$TI_HP1l#K?&+B~Eo zBr2KCLR5?XwljkpE4keygd;O!byg#~9}Pu@ZKz zBCjHVPusa=f68jv>1a>wS(iZ^e&Z?WuF2Hi zdo}V9cqa6hJkRf3)234ukr~{ABdvVPY$JWHR-_B1Dm<2yuFa(*sS_>QZlXNRoDDVK zR*rrOwG>N4CAN<>>_yG;t8yW|7JQ%@(3Fb!+P3I(@K-md)QrvM4dBRM!ceFUnoGYs&wN=H7G1@ zrg5Hpr(!8ww`bYPnz~pzB|l>>qn0g+yisMsm6J&fRG4sST={tweKOlJFOZmM$i2mF z_+dmMK0Oqbi6{x$2?`6#d4WGBLHmElZFf*>r6gN%Q?*m=m(6wgD7SBWm#Guz2Zo0c zb~zf*r#6pyn(iS)3Iya5H!Hq%I!Gr|-uuwZm8xv(HL6Z`*@^@Z|9hnNr&eSfJ9i2& z*lxuf9a>>yIdexU<34O0Ma9kSbT7F?;#?X?y;t^uS@Ks zA_W&nKq9r?_b;SpWkoTBHJ16<+_E=#cqKK|RxoAPa+Jxz+CW3=DZZ#<^aCWpz!Scl>!LN!3-{DY`03mOyczih%)5@*UaHnv6*|%rslmmj7yWC?#{ z*wSjGXpUpJ+q74)mo>7)r}jFP%-b8Lk&JaJh$PkcDRwmcBN@Q;XGy~EH2X(WzWXd7 zQZ%wO*?Z)mY@C@@WMib`*q{8!nvPIVI1)2QzozX(G=$uetNKhkJ7i=FHbFQ0Mu44)XF4wA6)8 zspj$%5IcN?tq@KN-cmkDCLJZ2~i#9^dB|&9KO&{4~-7mdcl9VX?hqM zC54=9ATo$NLFYX)W9lYM#B}Xy+8!Dmk)&v+QE7a8~ z>&b+g=V)0bY^$QTIz$9B7TVHA?6?LOUAu!FN|dvI1ektKT^iVB^qrFa=5JZbVoRki zmcldq_Nk!=!->i#woy>*Vd`zq8MW6ow{_Bd3APHxMG8&3pQkmYIV2q4M<0QjW`pxvtaes^EtynsqgyYLACXfiq#r-J^&j&W-q@EvkOGMM#| zT>?pVvma+vZJPikAL~}T2I8x2WA0ask{R_=e$#}rRWP%+j-7qODN$Sy4b4L>alPW)0*lNx5p`=>UD$v{y+?@)R`%ux7<}FNWNHD(8T0t4^kL%q`@A>Aw zgR3`WHf%16FxS%uTC_?57HsSvpoo5C*oT?52^5)cqgw?%!iUDw5;7?PVyKBn42Ti=Tsd=&6*-#+Kyr2d-0$>_r*6r4KZ) zTSF}@ANj7uCa)EDs=q7iRhQil*+Jix5*$B#5(jnk9}{S0kx{;lhEBQs8xfvJ|5;!} zUKEWeN;7m7>YtmwmoG7*p6QifX9s#<^ew{)8;5GoTWMr3;wh)CaKVERu^nk2_E_?| zDDhHwW1}nbn@bY(a(H;5k3|#|q2SBetel_^N&x8X2m}@{&TTtexUq=^Iur6R59xL( zmhw`}u+wn&aoe^ex9h=GG2_HUT<}qyLiLfD+)dE%TX0|__0j^6S5{a1yp*fv%IYfj zSg3F{{i!W;Mwfr;kn;RM*9Zwj9o^N7R@Y$v06M7Euqw)%+BS8m$yWA4Enz8Rxghqm z2ne!VJv-R&gLJu*M4A;DM*m@4>(*k+{Jn{YSQYYn`hNS@(}KwtFc9YZWzj%a94i3m zsB`qEcP%WmdxK7xWakhOG_qZBRP@MMvboN1q3nFmOjBkE&S>SPiAksL4oWRkMXwJd zGt#nUw>RIL&cY8uSiPdJK>|X@Lkx)Z{fv752za_$w@82tgzCa!!~m13(^`$L@{<0F zanj&2rIr~qt)krB#bYS5g8NyTSj+Uhh}xUW^N--Kxl0&+pwpfd}#HNJVgFVGTl)+)Zo{b^szRrln2 zL-he_M#b+#2VN~cj2-m}GmL>7Qxl@jtR%>7o~_-L^sbS|I*qPkM~|Yp=nm3d3Ep=x zM@nlk1ypl3Q={cabEvELD~ho>3Dah7nbtm9a^$AC z(?HEKB{85mrFw>HVPrdbMm2b_A$k2m6#zudPM#)&Y<~>P{uQe>3lE5D2o;jTIOF*O zjkwCqZqo#2m4IV|X!P+Nu`1h&dV8FWtdBRG(*dcDJ4dJ>z;0wis&7>cNG+_HLY#O{ zfjOJgWb7CtDRi44vb68}ote$z%-U2?!oak`NfXFE49EwhN+lRAvvJ z^-t5XKT3KH=EFIlM%I0E+Gzc2fXLQ(&eB-j+7pg;yBOb?ENMdB8W@o3#Dn*}nRt^u zOziZ&Wwz9#zp&G9S0O*F ziS~Xt1+nVqr&%BWkB|FPNyrD^Z-q4*@>L*4$>n|oIs;r;V2S1DYb<}d)qgVJFGBcl zYVN;~{{MvN-|BX4%nG0k61?}nxaENP50y<^LI<$I>Mjpo*s}43vMAXmE|mvyQ(C^_ zNx!ptV0Pb;U&MbU!j9;-rT(Ik{^ySPN4of@#nOLsz(3#eztsD0()6d(#@YVimR4Sa z1cYoc4?W0BvFXFt2O3RARH9()Qr>OI)1?u`sMoZ=(6&H>Zc%@4*) z!aGUi6C^2ou5Ng?H3_b)8MBx>#E6-JFW|$j5%tHpwpq4$wnYe{f1!|W^P&0TwbteM zxcUyw?!^b=;?t}Ym4t#`pgbey9;NtQnq6k^tu2i)T5(y=F1YN@VVB33&Z0GJ;}Dxw zC`c1f{PnRxo3Eqas|6W~qL32O<^4lO;;jb(Dnpytv`;&mZZOtW25=k&v8TUZxJ1Ji zl=&)!zq5r|7}beHP98VyBrEt7u)9tkaPgGWm4}ZxBPiRj@cjB04o;C@t(e*C0XO=W>^nw4ykaP9{wY4~$b5?SbfwP}byB}iD$?G3Sw!Byg? zt@S7Cx9pE`Gd`VQfeiqvC`lqvEGvAxxZ0GlL;ePtJi_vu^Y#JrD_z#2hnO8+jeb)q zuy1w?zqRi}6Nl9!U;{ha4Bes%tidu8#>+G9@)Lf4|G@%u9cTiz|dLeh!@8l|) G1^z#*=R$D+ literal 0 HcmV?d00001 diff --git a/doc/code/renderer/images/demo_5.png b/doc/code/renderer/images/demo_5.png new file mode 100644 index 0000000000000000000000000000000000000000..b24588a0ce71dab9175f97e8ed116d583b442bf0 GIT binary patch literal 82570 zcmZ6y1z42b);~Oigp$(TARx`qDbn3t0@5hW(1?$8cXxMpNl8nNbTdc|-SG`P=bZn0 zeRExeH}_t>etWI8w;?J@(&(>=UjqODbXl2?Y5)KtA^?EEj)DYx(!^Z50D$|TD*I7f z!vpT%kNE4K>{vf0l{%ghNSf9MWhl|9eK3DSeq3TJIO~*(@*ZVnNzd`TNu|z{JA*eI= zWnp*M1~6?*d)w)zV{~y!jQF33f;>X`fIn{nzUNoqz%g2DwSsLLG9gYaL3;)-EBre; zIxx@9&e$yO=`RX$Tt9WoY}UO;Jm^||B((v7j1S(ZvFnDe*mGe>PD8G9iAKr8>11>T?@dyfqcEwGM?B)K; z*@!hsKYoHOmf>bD1D<}$_?%H0nU+B^5e{tIpl1#HXzISuBa9lf;Uh&FBIS;eq5Y;- zqVd~2lYC;*in}yHZo49U`IlO^rz@q&1}oy*IO;1TzF6k5hQ-4l`>joyC@%!e!cTpS zL74=j43m0gE&>&#gQz>#9+AFXPr_LfqH!ldF7Iw)?hpq|Pw?UAmfBInU^^&jc3t@5%Iv5Odv8`;1XO9%0DH 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 x*IUX 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 bl*PNZVcA@<^++?|kxKJ{-u>cx&))wZKC3Zy literal 0 HcmV?d00001 diff --git a/doc/code/renderer/images/demo_6.png b/doc/code/renderer/images/demo_6.png new file mode 100644 index 0000000000000000000000000000000000000000..8b0b06496cf3a74f668b09ce2d68cca2481ac6d4 GIT binary patch literal 167586 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≠~Q; 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 literal 0 HcmV?d00001 diff --git a/doc/code/renderer/images/stresstest_0.png b/doc/code/renderer/images/stresstest_0.png new file mode 100644 index 0000000000000000000000000000000000000000..21cbb8934eb94d98e906b308476de179acb127ba GIT binary patch literal 268551 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Μ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