Compare commits

...

33 commits

Author SHA1 Message Date
Cayleb-Ordo
058f0fba61 Add CMake Configuration for ActorEditor
Name the project AtlasMisc as the atlas Object is shared.

Not working as of 04.06.2026:
- Linking to wxWidgets find_package target
  - Terrain.obj : `error LNK2019: Verweis auf nicht aufgel├Âstes externes Symbol ""int const wxEVT_NULL" (?wxEVT_NULL@@3HB)" in Funktion ""void __cdecl dynamic initializer for 'private: static struct wxEventTableEntry const * const TerrainSidebar::sm_eventTableEntries''(void)" (??__E?sm_eventTableEntries@TerrainSidebar@@0QBUwxEventTableEntry@@B@@YAXXZ)".`

Working:
- Using find_package extra variables to find most of the windows ext. libs
- excluding SDL2 and Boost, which use the custom find_prebuild_library function
2026-08-04 13:11:24 +02:00
Cayleb-Ordo
f98e0248c4 Add the doxygen cmake file to the CMake Project
Make the docs its own target instead of a project. Dont add the
new docs target to ALL. Add an option to disable generating the
docs.
Update doxygen_awesome to v2.4.2.
Update README to reflect new changes.
2026-08-04 13:11:24 +02:00
Cayleb-Ordo
dc427f19ba Add a module to fetch and setup Windows prebuild libraries
This module downloads the given svn revision based on the current
architecture selected. It also sets up the `CMAKE_PREFIX_PATH`.
Add new option to disable fetching, which assumes that the libraries
are already in the correct folders.
2026-08-04 13:11:24 +02:00
Cayleb-Ordo
6bc6459519 Add Root CMakeLists.txt and custom cmake modules
CMake is de facto the industrie standard for building C/C++ Code.
It is cross platform, supports presets and packaging for
Linux Distributions can be made with CPack. This also eliminates
the dependency to build and patch premake. Furthermore a lot of
IDE's are supported.

Add a BuildFlags CMake module with an INTERFACE library encapsulating
relevant build Flags and definitions. To provide support for multi
configuration generators use generator expressions as much as possible.
Add a (currently) empty 0ad-Functions module for later usage.
2026-08-04 13:11:24 +02:00
Dunedan
6fdcd68172
Update the actions used by the lint workflow
Some checks are pending
checkrefs / lfscheck (push) Waiting to run
checkrefs / checkrefs (push) Waiting to run
lint / cppcheck (push) Waiting to run
lint / copyright (push) Waiting to run
lint / jenkinsfiles (push) Waiting to run
pre-commit / build (push) Waiting to run
2026-08-04 10:09:22 +02:00
Dunedan
e4f4030a5e
Update the actions used by the checkrefs workflow
The workaround for the authentication problems isn't necessary with the
new version of the checkout action anymore.
2026-08-04 10:09:09 +02:00
Ralph Sennhauser
2fb6347125
Delete .already-built file prior to new build
In case the build fails this will allow other Jenkins jobs to restore
the old working state themselves.

Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
2026-08-03 20:54:04 +02:00
phosit
7aa756f6a4
Remove line numbers from cppcheck supression list
Some checks are pending
checkrefs / lfscheck (push) Waiting to run
checkrefs / checkrefs (push) Waiting to run
lint / cppcheck (push) Waiting to run
lint / copyright (push) Waiting to run
lint / jenkinsfiles (push) Waiting to run
pre-commit / build (push) Waiting to run
They have to be changed to ofthen when code gets moved.
2026-08-02 14:03:28 +02:00
phosit
e712601602
PreInitGame and InitGame in the Loader
This makes the frame before the simulation starts less stuttery. Doing
it in the Loader also allows to make the progressbar more acurate.
2026-08-02 14:03:28 +02:00
Dunedan
865ca4cd55
Update the pre-commit hooks
Some checks failed
checkrefs / lfscheck (push) Has been cancelled
checkrefs / checkrefs (push) Has been cancelled
lint / cppcheck (push) Has been cancelled
lint / copyright (push) Has been cancelled
lint / jenkinsfiles (push) Has been cancelled
pre-commit / build (push) Has been cancelled
2026-08-01 16:19:18 +02:00
Dunedan
dcb7cef308
Update Github actions used for pre-commit action 2026-08-01 16:19:03 +02:00
vyordan
71be79791f Use turn_id_t (int32_t) for turn-count across simulation, network and replay
Some checks failed
checkrefs / lfscheck (push) Has been cancelled
checkrefs / checkrefs (push) Has been cancelled
lint / cppcheck (push) Has been cancelled
lint / copyright (push) Has been cancelled
lint / jenkinsfiles (push) Has been cancelled
pre-commit / build (push) Has been cancelled
CSimulation2 used , CSimulation2Impl mixed /, and
CTurnManager used  for the same turn-count concept, forcing a
static_cast<int64_t> at the one place these types already met
(rejoin-test comparison).

Add  (alias for std::int32_t) in SimulationCommand.h and use
it consistently for every turn counter in CSimulation2/Impl,
CTurnManager and its subclasses (CLocalTurnManager,
CReplayTurnManager), CNetServerTurnManager/CNetClientTurnManager, and
the network wire format (m_Turn in CEndCommandBatchMessage,
CSimulationMessage, CSyncCheckMessage, CSyncErrorMessage, and
m_CurrentTurn in CLoadedGameMessage).

Turn *duration* fields (m_TurnLength, m_CommandDelay,
DEFAULT_TURN_LENGTH, COMMAND_DELAY_SP/MP, SetTurnLength,
GetSavedTurnLength return type) are left as u32 — they're milliseconds,
not a counter, and out of scope here.

Remaining turn_id_t vs size_t comparisons use std::cmp_equal or
explicit casts, matching the existing pattern in Simulation2.cpp.

Fixes #8718
2026-07-31 10:34:20 +02:00
Atrik
9ab89b01c2 Add Langbart to art credits
Some checks are pending
checkrefs / lfscheck (push) Waiting to run
checkrefs / checkrefs (push) Waiting to run
lint / cppcheck (push) Waiting to run
lint / copyright (push) Waiting to run
lint / jenkinsfiles (push) Waiting to run
pre-commit / build (push) Waiting to run
2026-07-30 20:54:19 +02:00
Atrik
dcce6666ae Add missing garrison flag to some houses
Art work by @Langbart

Fixes #6911
2026-07-30 20:54:19 +02:00
Atrik
202bdb133a Add Atrik to art credits 2026-07-30 20:54:19 +02:00
Atrik
46981d2b90 Add missing garrison flag to German houses 2026-07-30 20:54:19 +02:00
Ralph Sennhauser
49c887b7ca
Don't nest Paths in Common settings
Doesn't make sense to have the Paths StaticBox inside the Common
settings StaticBox so make it a top-level item as well.

While at it dissolve nested constructors to make the creation of the
widgets more readable.

Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
2026-07-30 18:43:24 +02:00
Atrik
91a37e32f6 Sort units spatially for freehand selection
Some checks are pending
checkrefs / lfscheck (push) Waiting to run
checkrefs / checkrefs (push) Waiting to run
lint / cppcheck (push) Waiting to run
lint / copyright (push) Waiting to run
lint / jenkinsfiles (push) Waiting to run
pre-commit / build (push) Waiting to run
2026-07-30 17:19:39 +02:00
Atrik
139b08fe6d Add tests for SelectionBoxTargeting 2026-07-30 17:19:39 +02:00
Atrik
dc6da217fd Enable using SelectionBox for targeting
Introduce a new  input, a command handler and adapt
UnitAI to handle arrays of targets in a single order
2026-07-30 17:19:39 +02:00
Atrik
e41dd2d3a4 Add Math.clamp utility function 2026-07-30 17:19:39 +02:00
phosit
c7953026dd
Use switchPage in autostart
Autostart used `OpenChildPage` to open the loading page. Because it's
started as child page the program doesn't end when closing it.
2026-07-30 12:36:51 +02:00
Ralph Sennhauser
0340cc9abd
Make NUSpline public members protected
Some checks are pending
checkrefs / lfscheck (push) Waiting to run
checkrefs / checkrefs (push) Waiting to run
lint / cppcheck (push) Waiting to run
lint / copyright (push) Waiting to run
lint / jenkinsfiles (push) Waiting to run
pre-commit / build (push) Waiting to run
Add a getter for MaxDistance and make members protected and inline
initialization.

Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
2026-07-29 18:44:03 +02:00
Ralph Sennhauser
aeeda05433
Prefix NUSpline member variables
To follow coding convention rename the vector Node to m_Nodes,
MaxDistance to m_MaxDistance and NodeCount to m_NodeCount.

Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
2026-07-29 18:44:03 +02:00
Atrik
0da1e0c398 Reveal Gaia wonders to players
Some checks are pending
checkrefs / lfscheck (push) Waiting to run
checkrefs / checkrefs (push) Waiting to run
lint / cppcheck (push) Waiting to run
lint / copyright (push) Waiting to run
lint / jenkinsfiles (push) Waiting to run
pre-commit / build (push) Waiting to run
2026-07-29 18:02:37 +02:00
Atrik
dc1e7179a3 Reveal and notify Wonders being constructed
Fixes #8683
2026-07-29 18:02:37 +02:00
Atrik
b6d9d87ba9 Add a test for ExploreCircle 2026-07-29 18:02:37 +02:00
Atrik
839226d542 Support revealing a small area of the map 2026-07-29 18:02:37 +02:00
Vladislav Belov
4125e5b645
Adds sRGB support to GLES 2026-07-29 01:29:11 +02:00
Vladislav Belov
f781181899
Adds sRGB support for proper lighting
Currently we're incorrectly using sRGB colors as raw inputs for lighting
instead of conversion to linear space. For proper PBR we need linear
values.
2026-07-29 01:29:09 +02:00
Vladislav Belov
e2f5b20922
Fixes GL internal format of textures in compute
In compute shaders load/store operations requires sized formats.
2026-07-28 23:32:23 +02:00
Vladislav Belov
b3165505b6
Fixes possible artifacts with GPU skinning on GL
std140 requires 16 bytes alignment and it makes alignment of uvec2 in
SkinData be equal to 16 bytes instead of 8 bytes.
2026-07-28 18:12:21 +02:00
Itms
15b9a39b00
Rebuild SPIRV shaders on all shader changes
Fixes #9096
2026-07-28 16:26:16 +02:00
152 changed files with 3115 additions and 487 deletions

View file

@ -11,7 +11,7 @@ jobs:
lfscheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Fetch the base branch
run: git fetch origin ${{ env.BASE_SHA }}
@ -24,27 +24,14 @@ jobs:
env:
GIT_LFS_SKIP_SMUDGE: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- name: Add remote fork origin for LFS
run: |
PR_REPO="${{ gitea.event.pull_request.head.repo.full_name || gitea.repository }}"
git remote add ${{ gitea.actor }} https://gitea.wildfiregames.com/${PR_REPO}.git
- name: Workaround for authentication problem with LFS
# https://gitea.com/gitea/act_runner/issues/164
run: |
git config --local \
http.${{ gitea.server_url }}/${{ gitea.repository }}.git/info/lfs/objects/.extraheader ''
PR_REPO="${{ gitea.event.pull_request.head.repo.full_name || gitea.repository }}"
EXTRAHEADER="$(git config --get --local http.${{ gitea.server_url }}/.extraheader)"
git config --local \
http.${{ gitea.server_url }}/${PR_REPO}.git/info/lfs/objects/batch.extraheader \
'${EXTRAHEADER}'
git config --local \
http.${{ gitea.server_url }}/${PR_REPO}.git/info/lfs/objects/.extraheader ''
- name: Download necessary LFS assets
shell: sh {0}
run: |

View file

@ -12,7 +12,7 @@ jobs:
cppcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
# cache only works for items in workspace, so configure apt to allow for caching.
- name: Setup apt cache locations
@ -26,7 +26,7 @@ jobs:
- name: Cache apt pkg db and and deb files
id: apt-cache
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
apt-cache
@ -49,11 +49,11 @@ jobs:
copyright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 100
- uses: actions/setup-python@v5
- uses: actions/setup-python@v7
with:
python-version: "3.11"
@ -66,10 +66,10 @@ jobs:
jenkinsfiles:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: '20'

View file

@ -7,17 +7,17 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- id: restore-pip-cache
uses: actions/cache/restore@v4
uses: actions/cache/restore@v6
with:
key: pip-cache-v1-${{ github.workflow }}
path: ~/.cache/pip
- uses: pre-commit/action@v3.0.1
- uses: actions/cache/save@v4
- uses: actions/cache/save@v6
if: steps.restore-pip-cache.outcome == 'success'
with:
key: pip-cache-v1-${{ github.workflow }}

View file

@ -34,7 +34,7 @@ repos:
\.patch$
)
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.9
rev: v0.16.0
hooks:
- id: ruff-check
args:
@ -56,7 +56,7 @@ repos:
files: ^binaries/
exclude: (^binaries/data/mods/(mod|public)/art/.*\.xml|\.dae$)
- repo: https://github.com/scop/pre-commit-shfmt
rev: v3.12.0-2
rev: v3.13.1-1
hooks:
- id: shfmt
args:
@ -67,7 +67,7 @@ repos:
hooks:
- id: shellcheck
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.45.0
rev: v0.47.0
hooks:
- id: markdownlint
language_version: 22.14.0
@ -77,13 +77,13 @@ repos:
^source/third_party/
)
- repo: https://github.com/adrienverge/yamllint
rev: v1.37.1
rev: v1.38.0
hooks:
- id: yamllint
args:
- --strict
- repo: https://github.com/eslint/eslint
rev: v9.39.2
rev: v10.8.0
hooks:
- id: eslint
language_version: 22.14.0

157
CMakeLists.txt Normal file
View file

@ -0,0 +1,157 @@
cmake_minimum_required(VERSION 3.25.1...4.0.0)
project(0ad VERSION 0.29.0)
# Available Options
option(android "Use non-working Android cross-compiling mode")
option(build-docs "Enable building the doxygen documentation(requires Network access)")
option(coverage "Enable code coverage data collection (GCC only)")
option(gles "Use non-working OpenGL ES 2.0 mode")
option(jenkins-tests "Configure CxxTest to use the XmlPrinter runner which produces Jenkins-compatible output")
option(minimal-flags "Only set compiler/linker flags that are really needed. Has no effect on Windows builds")
option(sanitize-address "Enable ASAN if available")
option(sanitize-thread "Enable TSAN if available")
option(sanitize-undefined-behaviour "Enable UBSAN if available")
option(with-system-cxxtest "Search standard paths for cxxtest, instead of using bundled copy")
option(with-lto "Enable Link Time Optimization (LTO)")
option(with-system-mozjs "Search standard paths for libmozjs115, instead of using bundled copy")
option(with-system-nvtt "Search standard paths for nvidia-texture-tools library, instead of using bundled copy")
option(with-valgrind "Enable Valgrind support (non-Windows only)")
option(without-audio "Disable use of OpenAL/Ogg/Vorbis APIs")
option(without-atlas "Disable Atlas scenario/map editor and ActorEditor")
option(without-dap-interface "Disable Dap interface project")
option(without-lobby "Disable the use of gloox and the multiplayer lobby")
option(without-miniupnpc "Disable use of miniupnpc for port forwarding")
option(without-nvtt "Disable use of NVTT")
option(without-pch "Disable generation and usage of precompiled headers")
option(without-tests "Disable generation of test projects")
# Windows specific options
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
option(fetch-prebuild-libs "Fetch prebuild libraries from SVN. Defaults to ON." ON)
endif()
# OS X specific options
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
option(macosx-version-min "Set minimum required version of the OS X API, the build will possibly fail if an older SDK is used, while newer API functions will be weakly linked (i.e. resolved at runtime)")
option(sysroot "Set compiler system root path, used for building against a non-system SDK. For example /usr/local becomes SYSROOT/user/local")
endif()
# Set the default build type if not specified (Only for single configuration generators)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
# Install options
set(bindir "" CACHE STRING "Directory for executables (typically '/usr/games'); default is to be relocatable")
set(datadir "" CACHE STRING "Directory for data files (typically '/usr/share/games/0ad'); default is ../data/ relative to executable")
set(libdir "" CACHE STRING "Directory for libraries (typically '/usr/lib/games/0ad'); default is ./ relative to executable")
# +++++++++++++++++++++ General Setup ++++++++++++++++++++
# Default Cache variables
# Append to the Modulepath. Allows to make custom cmake modules.
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# Set default Arch
set(ARCH "x86" CACHE STRING "CPU Architecture. Possible values are arm, aarch64, x86, amd64, e2k, ppc64, loong64, riscv64")
set(MACOS_ARCH "x86_64" CACHE STRING "Mac OS specific Architecture. Possible values are arm64 and x86_64")
option(link_execinfo "")
option(mozjs_is_debug_build "")
# Detect CPU architecture (simplistic). The user can target an architecture by setting '-DARCH=arch', but the game still selects some know value.
if(android)
set(ARCH "arm" CACHE STRING "CPU Architecture. Possible values are arm, aarch64, x86, amd64, e2k, ppc64, loong64, riscv64" FORCE)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
if(NOT CMAKE_VS_PLATFORM_NAME)
if(CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "AMD64" OR ENV{PROCESSOR_ARCHITEW6432} STREQUAL "AMD64")
set(ARCH "amd64" CACHE STRING "CPU Architecture. Possible values are arm, aarch64, x86, amd64, e2k, ppc64, loong64, riscv64" FORCE)
endif()
else()
if(CMAKE_VS_PLATFORM_NAME MATCHES "x64")
set(ARCH "amd64" CACHE STRING "CPU Architecture. Possible values are arm, aarch64, x86, amd64, e2k, ppc64, loong64, riscv64" FORCE)
endif()
endif()
else()
# could be parsed from the same command as premakes version
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "(x86_64|x64)")
set(MACOS_ARCH "x86_64" CACHE STRING "Mac OS specific Architecture. Possible values are arm64 and x86_64" FORCE)
elseif(CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "arm64")
set(MACOS_ARCH "arm64" CACHE STRING "Mac OS specific Architecture. Possible values are arm64 and x86_64" FORCE)
endif()
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "(x86_64|x64)")
set(ARCH "amd64" CACHE STRING "CPU Architecture. Possible values are arm, aarch64, x86, amd64, e2k, ppc64, loong64, riscv64" FORCE)
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "(aarch64|aarch64_be)")
set(ARCH "aarch64" CACHE STRING "CPU Architecture. Possible values are arm, aarch64, x86, amd64, e2k, ppc64, loong64, riscv64" FORCE)
else()
message(WARNING "Cannot determine architecture from GCC, assuming x86")
endif()
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
if(ARCH MATCHES "amd64")
set(0AD_EXT_LIBDIR ${CMAKE_SOURCE_DIR}/libraries/win64 CACHE STRING "Extern libraries directory of 0ad. Can be referenced in other CMakeLists.txt files.")
else()
set(0AD_EXT_LIBDIR ${CMAKE_SOURCE_DIR}/libraries/win32 CACHE STRING "Extern libraries directory of 0ad. Can be referenced in other CMakeLists.txt files.")
endif()
endif()
# On Windows check if wxWidgets is available, if not disable atlas and emit warning. This is because there are currently no prebuilt binaries provided.
if(NOT without-atlas AND CMAKE_SYSTEM_NAME STREQUAL "Windows")
if(NOT EXISTS "${0AD_EXT_LIBDIR}/wxwidgets/include/wx/wx.h")
message(STATUS "wxWidgets not found, disabling atlas")
set(without-atlas ON CACHE BOOL "Disable Atlas scenario/map editor and ActorEditor" FORCE)
endif()
endif()
# Test whether we need to link libexecinfo. This is mostly the case on musl systems, as well as on BSD systems : only glibc provides the
# backtrace symbols we require in the libc, for other libcs we use the libexecinfo library.
if(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
set(link_execinfo ON CACHE BOOL "" FORCE)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
include(CheckCSourceCompiles)
file(READ ${CMAKE_SOURCE_DIR}/build/premake/tests/execinfo.c content)
check_c_source_compiles([[${content}]] LINK_EXECINFO)
if(NOT LINK_EXECINFO)
set(link_execinfo ON CACHE BOOL "" FORCE)
else()
set(link_execinfo OFF CACHE BOOL "" FORCE)
endif()
unset(LINK_EXECINFO)
unset(content)
endif()
# Test whether system mozjs is built with --enable-debug. The pc file doesn't specify the required -DDEBUG needed in that case
# Currently only working on bash shell!
if(with-system-mozjs)
execute_process(
COMMAND bash "-c" "${CMAKE_C_COMPILER} $(pkg-config mozjs-128 --cflags) ${CMAKE_SOURCE_DIR}/build/premake/tests/mozdebug.c -o /dev/null"
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
RESULT_VARIABLE errorCode
OUTPUT_QUIET
ERROR_QUIET
)
if(NOT errorCode EQUAL 0)
set(mozjs_is_debug_build ON CACHE BOOL "" FORCE)
else()
set(mozjs_is_debug_build OFF CACHE BOOL "" FORCE)
endif()
endif()
# Include the BuildFlags target only once after all setup is finished
include(0ad-BuildFlags)
# +++++++++++++++++++++ Windows specific ++++++++++++++++++++
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
include(SetupWindowsLibs)
endif()
# Add subprojects
if(NOT without-atlas)
add_subdirectory(${CMAKE_SOURCE_DIR}/source/tools/atlas/)
endif()
# Add doxygen target
if(build-docs)
add_subdirectory(${CMAKE_SOURCE_DIR}/docs/doxygen)
endif()

View file

@ -78,6 +78,9 @@
<optional>
<attribute name="define"/>
</optional>
<optional>
<attribute name="srgb"/>
</optional>
</element>
</zeroOrMore>
</interleave>

View file

@ -12,10 +12,10 @@
layout(set = 2, binding = LOCATION, FORMAT) uniform image2D NAME
#if USE_DESCRIPTOR_INDEXING
#define STORAGE_BUFFER(LOCATION) \
layout(set = 2, binding = LOCATION)
layout(std430, set = 2, binding = LOCATION)
#else
#define STORAGE_BUFFER(LOCATION) \
layout(set = 1, binding = LOCATION)
layout(std430, set = 1, binding = LOCATION)
#endif
#else
// We use offset to the binding slot for OpenGL to avoid overlapping with other
@ -23,7 +23,7 @@
#define STORAGE_2D(LOCATION, FORMAT, NAME) \
layout(binding = LOCATION, FORMAT) uniform image2D NAME
#define STORAGE_BUFFER(LOCATION) \
layout(binding = LOCATION)
layout(std430, binding = LOCATION)
#endif
#endif // STAGE_COMPUTE

View file

@ -51,6 +51,18 @@
</textures>
</variant>
</group>
<group>
<variant name="ungarrisoned" frequency="1" />
<variant name="garrisoned">
<props>
<prop
actor="props/special/common/garrison_flag_achaemenids.xml"
attachpoint="garrisoned"
selectable="false"
/>
</props>
</variant>
</group>
<group>
<variant name="alive" frequency="1"/>
<variant file="structures/destruction_small.xml"/>

View file

@ -33,6 +33,18 @@
</textures>
</variant>
</group>
<group>
<variant frequency="1" name="ungarrisoned" />
<variant name="garrisoned">
<props>
<prop
actor="props/special/common/garrison_flag_celt.xml"
attachpoint="garrisoned"
selectable="false"
/>
</props>
</variant>
</group>
<group>
<variant name="alive" frequency="1"/>
<variant file="structures/destruction_small.xml"/>

View file

@ -53,6 +53,18 @@
</textures>
</variant>
</group>
<group>
<variant name="ungarrisoned" frequency="1" />
<variant name="garrisoned">
<props>
<prop
actor="props/special/common/garrison_flag_celt.xml"
attachpoint="garrisoned"
selectable="false"
/>
</props>
</variant>
</group>
<group>
<variant name="alive" frequency="1"/>
<variant file="structures/destruction_small.xml"/>

View file

@ -134,6 +134,20 @@
</props>
</variant>
</group>
<group>
<variant frequency="1" name="ungarrisoned" />
<variant name="garrisoned">
<props>
<prop
actor="props/special/common/garrison_flag_germ.xml"
attachpoint="garrisoned"
selectable="false"
/>
</props>
</variant>
</group>
<group>
<variant name="alive" frequency="1"/>
<variant file="structures/destruction_small.xml"/>

View file

@ -106,6 +106,18 @@
</props>
</variant>
</group>
<group>
<variant name="ungarrisoned" frequency="1" />
<variant name="garrisoned">
<props>
<prop
actor="props/special/common/garrison_flag_iber.xml"
attachpoint="garrisoned"
selectable="false"
/>
</props>
</variant>
</group>
<group>
<variant name="alive" frequency="1"/>
<variant file="structures/destruction_small.xml"/>

View file

@ -47,6 +47,18 @@
</textures>
</variant>
</group>
<group>
<variant name="ungarrisoned" frequency="1" />
<variant name="garrisoned">
<props>
<prop
actor="props/special/common/garrison_flag_rome.xml"
attachpoint="garrisoned"
selectable="false"
/>
</props>
</variant>
</group>
<group>
<variant name="alive" frequency="1"/>
<variant file="structures/destruction_small.xml"/>

View file

@ -43,6 +43,18 @@
</textures>
</variant>
</group>
<group>
<variant frequency="1" name="ungarrisoned" />
<variant name="garrisoned">
<props>
<prop
actor="props/special/common/garrison_flag_seleucids.xml"
attachpoint="garrisoned"
selectable="false"
/>
</props>
</variant>
</group>
<group>
<variant name="alive" frequency="1"/>
<variant file="structures/sele/light_damage.xml"/>

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:995da95e21261ae0a2718baee69d76ae5f6823d21eaefaa708a89651f2099bb0
size 586243
oid sha256:ed243a229225ec51ee090588a63307cbd2efedecca06f4f1a0922a4d51ee3967
size 586427

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d8e022a3ac132a32061583743d1322fc8792cdd6e7be57857547a8ed3cefbed6
size 525300
oid sha256:3ce649aa943771357d7c19f48294af719c50ca14178c32be60992f17845d685d
size 525483

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:21971f20f5567f820e0f3fffdd21015e2a5ffaee309be7b6efc38d3fc56aecd3
size 503936
oid sha256:d1d867c2e9f3db233847da547c466325ba9f0a9e7478175281f3538cbb694709
size 504112

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3c37b1b7f12a429cd07873d0fdb7cc8692f6578b3e5320a6be0e68420fb4688e
size 780868
oid sha256:cc9c95992889a4c2930a39bd20f7c86b73f7c7ddb3b36492650afae2ae424641
size 781051

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1230a92373c4e4684f791594efb35bb7d6c091377a11bd8f0daafd13816d04ec
size 56755
oid sha256:6779d6ca2067d6dd5594b3d2e5ef82d08de97ad058ce7084eb0bf8ae3290f4c2
size 56953

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:30a395c3b5a50214a689abe968ad15edb60212c3ea14041a2edc07295a4462c6
size 45317
oid sha256:9e6aca91d66df31b5820f39025c57937ce6a1593199b8a068722fdec5b0675f6
size 45523

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce554ad452cf75bd4dbb9da5de9830309a1dccf72f72f6a74475078af7f0e1c4
size 42851
oid sha256:07013561c7e46b020acbd03d4f371300c38d3e364c99a1e7bbcf7467aa8a34e6
size 43041

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:60f0614c1092395065c4ba8fe6bed64da873d38c579c9cbc400e9a9b8f30f3a8
size 272099
oid sha256:1416efa429cd64471d50e969c43ee723382c031c676414433f8fcd58176cfa77
size 272274

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c6e7d6264d7b3e03466bf1de58ab366a023120c6f78142aebc6570218a4803d9
size 274339
oid sha256:9f9e6debd5f2a9659e19e74b5826d3488167edb9a897ae1ed6e4db10742d94e8
size 274543

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0624fc07116aa70c14527457fced3afcf7b36df1535bbf10250455e6254aeed6
size 372821
oid sha256:091d68b7800d9141e87ba8aaf0fa93c5bc7d260dd459774075de610ca34855e2
size 372997

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ef199b28b9d9ef19e0817b189d9e6f1e2f9861ca211890b8d714f9fe05f5c61
size 420601
oid sha256:c1413de5371443f1249d87fa40a821c880c749513a0347d9128a99472bec7b33
size 420808

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:adf219c08e65bd7a149726d325b6f2b4fd67effc3dd1bc211c3163774ac8308e
size 201143
oid sha256:8aba3f3dc5a909ddadd9af5e7abfe9094f55ed6c97b39e7f97b09dc1cb951f4d
size 201315

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f8c688df60c4880dc92b6898728d4cef443c86831a2a2e87e61dd44661b58240
size 205000
oid sha256:0c3298efebf687a07d4693d9014c51e1e870e95fb9d9024c9126b40adfec75a5
size 205172

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9d86be00b7e911108e6666422a56eb061a996e6cf0bfe489d6c99f84be752444
size 216525
oid sha256:b97f6dff13f4639e746e1ce79c120169bccb2e09d4507dc900075cf85e788590
size 216697

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5e258d0b3a7b5c3fe2275c2816c636b7478ee1051df31a2cbe2f516465c692f9
size 239166
oid sha256:8d764a2d5a712f53f08560504f6841ed00b5268fea2a283d2d5254d57ce1df76
size 239339

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:82ff53473c25d6e17aeb97f795b543df3ec1c423e8ecb8398d0d014beac57f4e
size 101895
oid sha256:dccc1521e6e626e75ac2f05d2c8b2c2963ee751da08a9e819e4c28954b19f01a
size 102099

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2377bddedc6251acbe8f03bf1820d368b21cbf88fc2783695ff31036af8ed4e3
size 82812
oid sha256:ae1042461639bca3d3d73fb3e723ca404c8a897ee0e2ad417af1eebb5849d23f
size 83027

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5f1530adecb42af05b6b81719a5c4d00fa06a5eff33f50eda8312ce2e0fcd5f
size 77866
oid sha256:b71b8848a0a2e503adbff9c1e09c06c1e76e16537f38af5b35395a04f11b365a
size 78066

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c393e007e24473c1332c7d0c3a8093e0b542974da00b51b853ee413e45818ade
size 91109
oid sha256:256b3864edc754955814d5eec113e73fded49525d2e855a1171a4e4b495ffcc4
size 91304

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e8fb0f356f48284704d81b601c7947f32e91995757e8c2fc390ecaf772fbd950
size 84604
oid sha256:4ef0415cce15f4d1ff4e342c23a995ee5081d21b7f84eb6c19fb89c1d63f84a7
size 84789

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c2b0d0a461535e0802d2a07395afb4546515ef4d3ccc3eab18d55a0993bc165
size 184407
oid sha256:2ab540ad518de662ba9a4dbe4d0d0590b6ee0b771bc482073296c949ee33d571
size 184610

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:828e2e63b9686a601b3641be10f26e3b099ced5b8dc8d453ab94d41270f22b39
size 213385
oid sha256:39a6a575b8bbb0f82ed8cf4056e0df932a59fd6e3343005625b3b76644c6879c
size 213561

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4825c81a245ddae5637f3b2dee78f7749e27a48d2efa244bc95af075ddabcd1a
size 144598
oid sha256:93341f17ad185e28ce62a533f1db457aec4b69bb2f67c98e4b89e4bcf9503b6a
size 144784

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:427f9512af696afaae0721374a86ceb0e1432602013cb3dde818bfcd2791c163
size 212264
oid sha256:dc27dba302816891bb8f33bc65d9d68b2b330f946256aa966e5d684714af9fe2
size 212447

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:19ca2828e5e6b99065d3d901c1a90f1b73df065dc94b3a4b76b4e5804890c137
size 349847
oid sha256:270fe9095c3894fb448cb91668ad48d886088006c689f0efa148e8a78a3de043
size 350053

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57039fff36fbba71de79b89614fea988a98bdc85b2bdc5ef88ce3a632da95a55
size 340693
oid sha256:977b2ffb5dc9cf0364d125f0b3dd17e1fb0cd482b5c96fbf7c61b6e6a030db86
size 340906

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:221eeca4ee2e46023d346e80c7a3b4b3852699e0c193e31fa58e0619e858f7a2
size 272252
oid sha256:f177172ef923455050064af60377f2e1b171ff4ec72fa9aa5fdc5c4c28000550
size 272435

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:afc01d8e7e7efc988b2f78b58cfb6b48b5453b8786609e9ce5db38bc617a7984
size 279942
oid sha256:acdaaf09848dc5cb7cf207c7bb535865aa4199a5a50609ca3b632ce5d88d95a2
size 280139

View file

@ -330,3 +330,20 @@ Math.euclidDistance3D = function(x1, y1, z1, x2, y2, z2)
{
return Math.sqrt(Math.euclidDistance3DSquared(x1, y1, z1, x2, y2, z2));
};
/**
* Clamps a value between a minimum and maximum range.
*
* @param {number} value - The value to clamp
* @param {number} min - Minimum allowed value
* @param {number} max - Maximum allowed value
* @returns {number} The clamped value (min <= result <= max)
*/
Math.clamp = function(value, min, max)
{
if (value < min)
return min;
if (value > max)
return max;
return value;
};

View file

@ -442,3 +442,17 @@ Vector3D.div = function(v, f)
{
return new Vector3D(v.x / f, v.y / f, v.z / f);
};
/**
* Linear interpolation between this vector and another.
* @param {Vector2D} v - The target vector.
* @param {number} t - Interpolation factor (0 = this, 1 = v).
* @returns {Vector2D} A new vector at the interpolated position.
*/
Vector2D.prototype.lerp = function(v, t)
{
return new Vector2D(
this.x + (v.x - this.x) * t,
this.y + (v.y - this.y) * t
);
};

View file

@ -72,6 +72,7 @@
"List": [
{ "nick": "Alexandermb", "name": "Daniel Morgado" },
{ "nick": "Athos" },
{ "nick": "Atrik" },
{ "nick": "Basshunter", "name": "Sebastián Gómez" },
{ "nick": "BigTiger", "name": "Nathanael P. Moore" },
{ "nick": "Brightgalrs", "name": "Robert D. Schultz" },
@ -80,6 +81,7 @@
{ "nick": "Enrique", "name": "Enrique Keykens Espolio" },
{ "nick": "Gen.Kenobi", "name": "Daniel Schubert" },
{ "nick": "Historicity", "name": "Shane" },
{ "nick": "Langbart" },
{ "nick": "Leyto", "name": "Valentin Levalet" },
{ "nick": "LordGood", "name": "Johnathan B. Good" },
{ "nick": "m7600" },

View file

@ -507,6 +507,49 @@ var unitFilters = {
}
};
// Choose, inside a list of entities, which ones are targetable foes for boxtargeting.
// We also filter out entities that aren't visible, or low priority like livestocks
function getTargetableEntities(ents)
{
const player = g_ViewedPlayer;
const simState = GetSimState();
const isEnemy = simState.players[player].isEnemy;
// Filter valid target candidates
const candidates = ents.filter(entity =>
{
const entState = GetEntityState(entity);
return entState &&
unitFilters.isUnit(entity) &&
entState.visibility != "hidden";
});
// Enemy players
const enemyPlayers = candidates.filter(entity =>
{
const entState = GetEntityState(entity);
return isEnemy[entState.player] && entState.player !== 0 && !hasClass(entState, "Domestic");
});
if (enemyPlayers.length > 0)
return enemyPlayers;
// Gaia
const gaiaUnits = candidates.filter(entity =>
{
const entState = GetEntityState(entity);
return entState.player === 0;
});
if (gaiaUnits.length > 0)
return gaiaUnits;
// Domestic animals
return candidates.filter(entity =>
{
const entState = GetEntityState(entity);
return hasClass(entState, "Domestic");
});
}
// Choose, inside a list of entities, which ones will be selected.
// We may use several entity filters, until one returns at least one element.
function getPreferredEntities(ents)
@ -572,16 +615,19 @@ function handleInputBeforeGui(ev, hoveredObject)
case INPUT_BANDBOXING:
{
const bandbox = Engine.GetGUIObjectByName("bandbox");
const isAttackMode = Engine.HotkeyIsPressed("session.attack") && !g_IsObserver;
switch (ev.type)
{
case "mousemotion":
{
const rect = updateBandbox(bandbox, ev, false);
const ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer);
const preferredEntities = getPreferredEntities(ents);
g_Selection.setHighlightList(preferredEntities);
const player = isAttackMode ? -1 : g_ViewedPlayer;
const ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], player);
const highlightEntities = isAttackMode ? getTargetableEntities(ents) : getPreferredEntities(ents);
g_Selection.setHighlightList(highlightEntities);
return false;
}
@ -589,6 +635,30 @@ function handleInputBeforeGui(ev, hoveredObject)
if (ev.button == SDL_BUTTON_LEFT)
{
const rect = updateBandbox(bandbox, ev, true);
if (isAttackMode)
{
// Box attack behavior
const ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], -1);
// Filter to only enemy units
const enemyUnits = getTargetableEntities(ents);
// Get currently selected friendly units
const selectedUnits = g_Selection.toList();
if (selectedUnits.length && enemyUnits.length)
{
// Sort entities by position
const sortedCombattants = sortEntitiesForEngagement(selectedUnits, enemyUnits);
// Distribute attack orders
distributeAttackOrders(sortedCombattants.attackers, sortedCombattants.targets);
}
g_Selection.setHighlightList([]);
inputState = INPUT_NORMAL;
return true;
}
const ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer));
g_Selection.setHighlightList([]);
@ -1371,6 +1441,7 @@ function positionUnitsFreehandSelectionMouseUp(ev)
for (let i = 1; i < inputLine.length; ++i)
lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]);
// Filter units that can move
const selection = g_Selection.filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b);
// Checking the line for a minimum length to save performance.
@ -1380,39 +1451,43 @@ function positionUnitsFreehandSelectionMouseUp(ev)
return !!action && doAction(action, ev);
}
// Even distribution of the units on the line.
let p0 = inputLine[0];
let entityDistribution = [p0];
const distanceBetweenEnts = lengthOfLine / (selection.length - 1);
let freeDist = -distanceBetweenEnts;
// Sort units by their projection onto the line direction (parallel sorting)
const sortedSelection = sortEntitiesAlongLine(selection, inputLine[0], inputLine[inputLine.length - 1], true);
for (let i = 1; i < inputLine.length; ++i)
const selectionCount = sortedSelection.length;
const targetPositions = [];
// Distribute units evenly along the polyline
for (let i = 0; i < selectionCount; ++i)
{
const p1 = inputLine[i];
freeDist += inputLine[i - 1].distanceTo(p1);
const t = i / (selectionCount - 1);
const targetDistance = t * lengthOfLine;
while (freeDist >= 0)
let accumulated = 0;
let targetPoint = inputLine[0];
for (let j = 1; j < inputLine.length; ++j)
{
p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1);
entityDistribution.push(p0);
freeDist -= distanceBetweenEnts;
const segStart = inputLine[j - 1];
const segEnd = inputLine[j];
const segLength = segEnd.distanceTo(segStart);
if (accumulated + segLength >= targetDistance)
{
// Interpolate within this segment
const remaining = targetDistance - accumulated;
const frac = remaining / segLength;
targetPoint = segStart.lerp(segEnd, frac);
break;
}
accumulated += segLength;
}
targetPositions.push(targetPoint);
}
// Rounding errors can lead to missing or too many points.
entityDistribution = entityDistribution.slice(0, selection.length);
entityDistribution = entityDistribution.concat(new Array(selection.length - entityDistribution.length).fill(inputLine[inputLine.length - 1]));
if (Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[0]) +
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[selection.length - 1]) >
Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[selection.length - 1]) +
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[0]))
entityDistribution.reverse();
Engine.PostNetworkCommand({
"type": isAttackMovePressed() ? "attack-walk-custom" : "walk-custom",
"entities": selection,
"targetPositions": entityDistribution.map(pos => pos.toFixed(2)),
"entities": sortedSelection,
"targetPositions": targetPositions.map(pos => pos.toFixed(2)),
"targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] },
"queued": Engine.HotkeyIsPressed("session.queue"),
"pushFront": Engine.HotkeyIsPressed("session.pushorderfront"),
@ -1420,9 +1495,10 @@ function positionUnitsFreehandSelectionMouseUp(ev)
});
// Add target markers with a minimum distance of 5 to each other.
const entitiesBetweenMarker = Math.ceil(5 / distanceBetweenEnts);
for (let i = 0; i < entityDistribution.length; i += entitiesBetweenMarker)
DrawTargetMarker({ "x": entityDistribution[i].x, "z": entityDistribution[i].y });
const stepDistance = lengthOfLine / (selectionCount - 1);
const entitiesBetweenMarker = Math.max(1, Math.ceil(5 / stepDistance));
for (let i = 0; i < targetPositions.length; i += entitiesBetweenMarker)
DrawTargetMarker({ "x": targetPositions[i].x, "z": targetPositions[i].y });
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
@ -1929,3 +2005,108 @@ function clearSelection()
g_Selection.reset();
preSelectedAction = ACTION_NONE;
}
function distributeAttackOrders(attackers, targets)
{
if (targets.length < 1)
return;
// Play attack sounds from a sample of units in selection
const soundCount = Math.min(3, attackers.length);
for (let i = 0; i < soundCount; i++)
{
const t = soundCount === 1 ? 0 : i / (soundCount - 1);
const attackerIndex = Math.floor(t * (attackers.length - 1));
setTimeout(() =>
{
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_attack",
"entity": attackers[attackerIndex]
});
}, i * 180);
}
Engine.PostNetworkCommand({
"type": "attack-group",
"entities": attackers,
"targets": targets,
"queued": Engine.HotkeyIsPressed("session.queue"),
"allowCapture": false
});
}
/**
* Sorts attackers and targets for left-to-right engagement across the battle line.
*
* Entities are sorted by their perpendicular distance to the line connecting the
* two army centers, creating natural pairings across the battle front.
*
* @param {number[]} attackers - Array of attacking entity IDs.
* @param {number[]} targets - Array of target entity IDs.
* @returns {Object} { attackers: number[], targets: number[] } - Both arrays sorted
* for cross-line engagement (targets automatically reversed).
*/
function sortEntitiesForEngagement(attackers, targets)
{
const getPosition = (id) => GetEntityState(id).position;
const computeAveragePosition = (entities) =>
{
if (entities.length === 0) return new Vector2D(0, 0);
const vectors = entities.map(id => Vector2D.from3D(getPosition(id)));
return Vector2D.average(vectors);
};
// Calculate the axis of engagement - the line between the groups' average positions
const avgAttackers = computeAveragePosition(attackers);
const avgTargets = computeAveragePosition(targets);
// Sort each group by perpendicular distance to the engagement line
const sortedAttackers = sortEntitiesAlongLine(attackers, avgAttackers, avgTargets);
const sortedTargets = sortEntitiesAlongLine(targets, avgAttackers, avgTargets);
// Reverse targets so the leftmost attacker pairs with leftmost target
return {
"attackers": sortedAttackers,
"targets": sortedTargets.reverse()
};
}
/**
* Sorts entity IDs by their projection onto a line's direction or perpendicular axis.
*
* @param {number[]} entities - Array of entity IDs.
* @param {Vector2D} lineStart - Start point of the reference line.
* @param {Vector2D} lineEnd - End point of the reference line.
* @param {boolean} sortByDirection - If true, sort by parallel projection (along line);
* If false, sort by perpendicular distance (default).
* @returns {number[]} Sorted entity IDs.
*/
function sortEntitiesAlongLine(entities, lineStart, lineEnd, sortByDirection = false)
{
const dir = lineEnd.sub(lineStart);
const length = dir.length();
if (length === 0)
return entities.slice();
// Choose basis vector: either parallel or perpendicular to the line
let basis;
if (sortByDirection)
basis = dir.mult(1 / length); // Unit direction vector
else
basis = new Vector2D(-dir.y, dir.x).normalize(); // Perpendicular
const withCoord = entities.map(id =>
{
const pos = GetEntityState(id).position;
const point2D = Vector2D.from3D(pos);
const rel = point2D.sub(lineStart);
const coord = rel.dot(basis); // Project onto basis vector
return { id, coord };
});
// Sort by that coordinate
withCoord.sort((a, b) => a.coord - b.coord);
return withCoord.map(item => item.id);
}

View file

@ -603,4 +603,147 @@ TriggerHelper.SpawnAndTurretAtClasses = function(playerID, classes, templates, c
return results;
};
/**
* Makes an entity permanently visible to all players by:
* - Setting alwaysVisible flag
* - Deactivating fogging (no mirages)
* - Exploring the surrounding area
* - Forcing a visibility update
*
* @param {number} ent - Entity ID to make permanently visible
*/
TriggerHelper.MakeEntityPermanentlyVisible = function(ent)
{
const cmpVisibility = Engine.QueryInterface(ent, IID_Visibility);
if (cmpVisibility)
{
cmpVisibility.alwaysVisible = true;
cmpVisibility.SetActivated(true);
}
const cmpFogging = Engine.QueryInterface(ent, IID_Fogging);
if (cmpFogging)
cmpFogging.PermanentlyReveal("all");
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
const cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpRangeManager && cmpPosition && cmpPosition.IsInWorld())
{
const pos = cmpPosition.GetPosition2D();
const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
const exploreRadius = TriggerHelper.CalculateExplorationRadius(ent);
for (let i = 1; i < numPlayers; ++i)
cmpRangeManager.ExploreCircle(i, pos.x, pos.y, exploreRadius);
cmpRangeManager.RequestVisibilityUpdate(ent);
}
};
/**
* Calculate a reasonable exploration radius from an entity's footprint.
* Calculates the radius as the entity's full extent to reveal the surrounding area.
* This is equivalent to (extent/2) * 2, giving a 2x margin around the entity.
*/
TriggerHelper.CalculateExplorationRadius = function(ent)
{
const cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
if (!cmpFootprint)
return 50;
const shapeInfo = cmpFootprint.GetShape();
let halfExtent; // The radius of the entity (half its size)
if (shapeInfo && shapeInfo.type === "circle")
halfExtent = shapeInfo.radius || shapeInfo.size0;
else if (shapeInfo && shapeInfo.type === "square")
halfExtent = Math.max(shapeInfo.width || shapeInfo.size0, shapeInfo.depth || shapeInfo.size1) / 2;
else
return 50;
// Use 2x the entity's radius to reveal surrounding area
// This gives a comfortable margin around the entity
const radius = halfExtent * 2;
return radius;
};
/**
* Sends a notification to specific players.
*
* @param {string} message - The message to display (use markForTranslation)
* @param {number[]} players - Array of player IDs to send the notification to
* @param {number} duration - Duration in milliseconds
* @param {Object} parameters - Translation parameters (optional)
*/
TriggerHelper.SendNotification = function(message, players, duration = 30000, parameters = {}, translateParameters = [])
{
const cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
if (!cmpGuiInterface)
return;
// Ensure all parameter values are primitive types
const primitiveParameters = {};
for (const key in parameters)
{
const value = parameters[key];
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
primitiveParameters[key] = value;
else
primitiveParameters[key] = String(value);
}
cmpGuiInterface.AddTimeNotification(
{
"message": message,
"players": players,
"parameters": primitiveParameters,
"translateMessage": true,
"translateParameters": translateParameters
},
duration
);
};
/**
* Sends a notification to a specific player and a different notification to all other active players.
*
* @param {number} player - The player ID who gets the 'owner' message
* @param {string} ownerMessage - Message for the specified player (use markForTranslation)
* @param {string} othersMessage - Message for all other active players (use markForTranslation)
* @param {number} duration - Duration in milliseconds
* @param {Object} parameters - Translation parameters shared by both messages (optional)
* @param {Object} ownerParameters - Additional parameters for the owner message (optional)
* @param {Object} othersParameters - Additional parameters for the others message (optional)
*/
TriggerHelper.SendDualNotification = function(player, ownerMessage, othersMessage, duration = 30000, parameters = {}, ownerParameters = {}, othersParameters = {})
{
const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
const others = [-1]; // Include observers (player id -1)
for (let playerID = 1; playerID < numPlayers; ++playerID)
{
if (playerID == player)
continue;
const cmpPlayer = QueryPlayerIDInterface(playerID);
if (cmpPlayer && cmpPlayer.GetState() == "defeated")
continue;
others.push(playerID);
}
const mergedOwnerParams = Object.assign({}, parameters, ownerParameters);
const mergedOthersParams = Object.assign({}, parameters, othersParameters);
// Auto-detect translatable parameters (those starting with "_" or named "player")
const translateParameters = Object.keys(mergedOthersParams).filter(key => key.startsWith("_") || key == "player");
TriggerHelper.SendNotification(ownerMessage, [player], duration, mergedOwnerParams, translateParameters);
TriggerHelper.SendNotification(othersMessage, others, duration, mergedOthersParams, translateParameters);
};
TriggerHelper.SendWonderNotifications = function(owner, othersMessage, ownerMessage)
{
TriggerHelper.SendDualNotification(owner, ownerMessage, othersMessage, 30000, { "_player_": owner });
};
Engine.RegisterGlobal("TriggerHelper", TriggerHelper);

View file

@ -1,16 +1,42 @@
Trigger.prototype.WonderVictoryEntityRenamed = function(data)
{
if (this.wonderVictoryMessages[data.entity] && Engine.QueryInterface(data.newentity, IID_Wonder))
{
// When an entity is renamed, we first create a new entity,
// which in case it is a wonder will receive a timer.
// However on a rename we want to use the timer from the old entity,
// so we need to remove the timer of the new entity.
this.WonderVictoryDeleteTimer(data.newentity);
if (!Engine.QueryInterface(data.newentity, IID_Wonder))
return;
// Check if wonder victory is enabled
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
if (!cmpEndGameManager)
return;
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
if (!victoryConditions || !victoryConditions.includes("wonder"))
return;
// Handle timer transfer (only if old entity had the timer)
if (this.wonderVictoryMessages[data.entity])
{
this.WonderVictoryDeleteTimer(data.newentity);
this.wonderVictoryMessages[data.newentity] = this.wonderVictoryMessages[data.entity];
delete this.wonderVictoryMessages[data.entity];
}
// Make the completed wonder permanently visible
TriggerHelper.MakeEntityPermanentlyVisible(data.newentity);
// Send "wonder completed" notifications (only for non-Gaia wonders)
const cmpOwnership = Engine.QueryInterface(data.newentity, IID_Ownership);
if (cmpOwnership)
{
const owner = cmpOwnership.GetOwner();
if (owner > 0)
{
TriggerHelper.SendWonderNotifications(
owner,
markForTranslation("%(_player_)s has built a Wonder!"),
markForTranslation("You have built a Wonder!")
);
}
}
};
Trigger.prototype.WonderVictoryOwnershipChanged = function(data)
@ -18,6 +44,15 @@ Trigger.prototype.WonderVictoryOwnershipChanged = function(data)
if (!Engine.QueryInterface(data.entity, IID_Wonder))
return;
// Check if wonder victory is enabled
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
if (!cmpEndGameManager)
return;
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
if (!victoryConditions || !victoryConditions.includes("wonder"))
return;
this.WonderVictoryDeleteTimer(data.entity);
if (data.to > 0)
@ -146,11 +181,69 @@ Trigger.prototype.WonderVictorySetWinner = function(playerID)
n));
};
Trigger.prototype.WonderStartNotification = function(data)
{
// Check if wonder victory is enabled in game settings
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
if (!cmpEndGameManager)
return;
// Don't show the wonder foundation if it's not a wonder victory game
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
if (!victoryConditions || !victoryConditions.includes("wonder"))
return;
const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
if (!cmpTemplateManager)
return;
const template = cmpTemplateManager.GetTemplate(data.template);
if (!template || !("Wonder" in template))
return;
const owner = TriggerHelper.GetOwner(data.foundation);
if (owner <= 0)
return;
TriggerHelper.MakeEntityPermanentlyVisible(data.foundation);
TriggerHelper.SendWonderNotifications(
owner,
markForTranslation("%(_player_)s has started building a Wonder."),
markForTranslation("You have started building a Wonder.")
);
};
Trigger.prototype.RevealGaiaWonders = function()
{
Engine.ProfileStart("RevealGaiaWonders");
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
if (!cmpEndGameManager)
return;
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
if (!victoryConditions || !victoryConditions.includes("wonder"))
return;
// Find and reveal all Gaia wonders
const gaiaWonders = Engine.GetEntitiesWithInterface(IID_Wonder).filter(
ent => TriggerHelper.GetOwner(ent) === 0
);
for (const ent of gaiaWonders)
TriggerHelper.MakeEntityPermanentlyVisible(ent);
Engine.ProfileStop();
};
{
const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.RegisterTrigger("OnEntityRenamed", "WonderVictoryEntityRenamed", { "enabled": true });
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "WonderVictoryOwnershipChanged", { "enabled": true });
cmpTrigger.RegisterTrigger("OnDiplomacyChanged", "WonderVictoryDiplomacyChanged", { "enabled": true });
cmpTrigger.RegisterTrigger("OnPlayerWon", "WonderVictoryPlayerWon", { "enabled": true });
cmpTrigger.RegisterTrigger("OnConstructionStarted", "WonderStartNotification", { "enabled": true });
cmpTrigger.DoAfterDelay(0, "RevealGaiaWonders", {});
cmpTrigger.wonderVictoryMessages = {};
}

View file

@ -29,6 +29,7 @@ Fogging.prototype.Init = function()
this.mirages = [];
this.miraged = [];
this.seen = [];
this.revealed = [];
const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let player = 0; player < numPlayers; ++player)
@ -36,6 +37,7 @@ Fogging.prototype.Init = function()
this.mirages.push(INVALID_ENTITY);
this.miraged.push(false);
this.seen.push(false);
this.revealed.push(false);
}
};
@ -169,6 +171,45 @@ Fogging.prototype.WasSeen = function(player)
return this.seen[player];
};
/**
* Permanently reveal this entity to one or all players.
* Destroys existing mirages and optionally deactivates fogging entirely.
* @param {number|string} player - Player ID to reveal to, or "all" for all players.
*/
Fogging.prototype.PermanentlyReveal = function(player = "all")
{
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
// Validate single player case
if (player !== "all" && (player < 0 || player >= this.mirages.length))
return;
// Determine which players to reveal
const startPlayer = (player === "all") ? 0 : player;
const endPlayer = (player === "all") ? this.mirages.length : player + 1;
// Reveal each player in the range
for (let i = startPlayer; i < endPlayer; ++i)
{
if (this.mirages[i] != INVALID_ENTITY)
{
Engine.DestroyEntity(this.mirages[i]);
this.mirages[i] = INVALID_ENTITY;
this.miraged[i] = false;
}
this.revealed[i] = true;
this.seen[i] = true;
}
// Deactivate fogging if revealing to all players
if (player === "all")
this.activated = false;
// Request visibility update
if (cmpRangeManager)
cmpRangeManager.RequestVisibilityUpdate(this.entity);
};
Fogging.prototype.OnOwnershipChanged = function(msg)
{
// Always activate fogging for non-Gaia entities.
@ -209,7 +250,8 @@ Fogging.prototype.OnVisibilityChanged = function(msg)
this.seen[msg.player] = true;
}
if (msg.newVisibility == VIS_FOGGED && this.activated)
// Only create a mirage if the player hasnt been permanently revealed
if (msg.newVisibility == VIS_FOGGED && this.activated && !this.revealed[msg.player])
this.LoadMirage(msg.player);
};

View file

@ -424,10 +424,42 @@ UnitAI.prototype.UnitFsmSpec = {
"Order.Attack": function(msg)
{
const type = this.GetBestAttackAgainst(msg.data.target, msg.data.allowCapture);
if (!type)
return this.FinishOrder();
// Check if this is a group attack
const isGroupAttack = !!msg.data.targetArray;
// For group attacks with invalid initial target, find one from the array
if (isGroupAttack && (!msg.data.target || msg.data.target === INVALID_ENTITY))
{
if (!this.FindAndSetNextTarget(msg.data))
{
this.FinishOrder();
return ACCEPT_ORDER;
}
}
// Get the attack type for the current target
const type = this.GetBestAttackAgainst(msg.data.target, msg.data.allowCapture);
if (!type)
{
// Current target can't be attacked
if (isGroupAttack)
{
if (this.FindAndSetNextTarget(msg.data))
{
msg.data.attackType = this.GetBestAttackAgainst(msg.data.target, msg.data.allowCapture);
if (msg.data.attackType)
{
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
return ACCEPT_ORDER;
}
}
}
this.FinishOrder();
return ACCEPT_ORDER;
}
// Set attackType before range check
msg.data.attackType = type;
this.RememberTargetPosition();
@ -1366,6 +1398,12 @@ UnitAI.prototype.UnitFsmSpec = {
// Wait for individual members to finish
"enter": function(msg)
{
if (this.order.data.targetArray)
{
// For group attacks, we're done once we distributed targets
this.FinishOrder();
return true;
}
const target = this.order.data.target;
if (!this.CheckFormationTargetAttackRange(target))
{
@ -1602,6 +1640,26 @@ UnitAI.prototype.UnitFsmSpec = {
"INDIVIDUAL": {
"Attacked": function(msg)
{
// First, check if we have a group attack order and the attacker is in our target array
if (this.order?.type === "Attack" &&
this.order.data?.targetArray?.includes(msg.data.attacker))
{
// Switch to attack the entity that's attacking us
this.order.data.target = msg.data.attacker;
this.order.data.attackType = this.GetBestAttackAgainst(msg.data.attacker, this.order.data.allowCapture);
// Update the current target index
const currentIndex = this.order.data.targetArray.indexOf(msg.data.attacker);
if (currentIndex !== -1)
this.order.data.currentTargetIndex = currentIndex;
// Make sure we're in the right state to handle this
const currentState = this.GetCurrentState();
if (currentState === "INDIVIDUAL.IDLE" || currentState === "INDIVIDUAL.WALKING")
this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
return;
}
if (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force)
this.RespondToTargetedEntities([msg.data.attacker]);
},
@ -2150,6 +2208,35 @@ UnitAI.prototype.UnitFsmSpec = {
"Attacked": function(msg)
{
// Check if we're doing a group attack and the attacker is in our target array
if (!this.order.data.targetArray ||
!this.order.data.targetArray.includes(msg.data.attacker) ||
msg.data.attacker === this.order.data.target)
return;
// Switch to attack the entity that's attacking us
this.order.data.target = msg.data.attacker;
this.order.data.attackType = this.GetBestAttackAgainst(msg.data.attacker, this.order.data.allowCapture);
// Update the current target index
const currentIndex = this.order.data.targetArray.indexOf(msg.data.attacker);
if (currentIndex !== -1)
this.order.data.currentTargetIndex = currentIndex;
// Re-evaluate our approach to the new target
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
if (this.CanUnpack())
{
this.PushOrderFront("Unpack", { "force": true });
return;
}
this.SetNextState("ATTACKING");
}
// Continue approaching the new target
else if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
this.FinishOrder();
// If we're already in combat mode, ignore anyone else who's attacking us
// unless it's a melee attack since they may be blocking our way to the target
if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || !this.order.data.force))
@ -2209,7 +2296,23 @@ UnitAI.prototype.UnitFsmSpec = {
"MovementUpdate": function(msg)
{
if (msg.likelyFailure)
if (!msg.likelyFailure)
{
if (!this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
return;
if (this.CanUnpack())
{
this.PushOrderFront("Unpack", { "force": true });
return;
}
this.SetNextState("ATTACKING");
return;
}
// For group attacks, try next target instead of moving to last position
if (!this.order.data.targetArray || this.order.data.currentTargetIndex === undefined)
{
// This also handles hunting.
if (this.orderQueue.length > 1)
@ -2217,11 +2320,13 @@ UnitAI.prototype.UnitFsmSpec = {
this.FinishOrder();
return;
}
else if (!this.order.data.force || !this.order.data.lastPos)
if (!this.order.data.force || !this.order.data.lastPos)
{
this.SetNextState("COMBAT.FINDINGNEWTARGET");
return;
}
// If the order was forced, try moving to the target position,
// under the assumption that this is desirable if the target
// was somewhat far away - we'll likely end up closer to where
@ -2234,21 +2339,30 @@ UnitAI.prototype.UnitFsmSpec = {
return;
}
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
// Handle group attack case
// Remove current unreachable target from array
const currentIndex = this.order.data.currentTargetIndex;
if (currentIndex < this.order.data.targetArray.length)
this.order.data.targetArray.splice(currentIndex, 1);
// Try to find next valid target
if (this.order.data.targetArray.length === 0 || !this.FindAndSetNextTarget(this.order.data))
{
if (this.CanUnpack())
{
this.PushOrderFront("Unpack", { "force": true });
return;
}
this.SetNextState("ATTACKING");
this.FinishOrder();
return;
}
else if (msg.likelySuccess)
// Try moving again,
// attack range uses a height-related formula and our actual max range might have changed.
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
this.FinishOrder();
},
this.order.data.attackType = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
// Try approaching the new target
if (this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
return;
// If we can't approach the new target either, remove it and continue
this.order.data.targetArray.splice(this.order.data.currentTargetIndex, 1);
if (this.order.data.targetArray.length === 0)
this.FinishOrder();
}
},
"ATTACKING": {
@ -2322,6 +2436,18 @@ UnitAI.prototype.UnitFsmSpec = {
"OutOfRange": function()
{
// Check if the target is garrisoned or in a turret (turreted entities cannot be attacked by melee units)
const cmpTargetUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
const isTargetGarrisoned = cmpTargetUnitAI && cmpTargetUnitAI.isGarrisoned;
const isTargetInTurret = cmpTargetUnitAI && cmpTargetUnitAI.IsTurret();
// If target is garrisoned or in a turret (and we're not ranged), treat as invalid
if (isTargetGarrisoned || (isTargetInTurret && this.order.data.attackType !== "Ranged"))
{
this.ProcessMessage("TargetInvalidated");
return;
}
if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force))
{
if (this.CanPack())
@ -2332,11 +2458,53 @@ UnitAI.prototype.UnitFsmSpec = {
this.SetNextState("CHASING");
return;
}
// For group attacks, try next target instead of finding new ones
if (this.order.data.targetArray && this.order.data.currentTargetIndex !== undefined)
{
const currentIndex = this.order.data.currentTargetIndex;
// Remove current target since it's out of range and we can't chase
this.order.data.targetArray.splice(currentIndex, 1);
// Try to find next valid target in sequence
if (this.order.data.targetArray.length > 0 && this.FindAndSetNextTarget(this.order.data))
{
this.order.data.attackType = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
return;
}
}
this.SetNextState("FINDINGNEWTARGET");
},
"TargetInvalidated": function()
{
// For group attacks, remove current target and move to next in array
if (this.order.data.targetArray && this.order.data.currentTargetIndex !== undefined)
{
// Remove the current target from the array
const currentIndex = this.order.data.currentTargetIndex;
if (currentIndex < this.order.data.targetArray.length)
{
this.order.data.targetArray.splice(currentIndex, 1);
// If we have more targets, continue with the next one
if (this.order.data.targetArray.length > 0)
{
// Try to find next valid target in sequence
if (this.FindAndSetNextTarget(this.order.data))
{
this.order.data.attackType = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
return;
}
}
}
}
// No more targets or not a group attack - use original behavior
this.SetNextState("FINDINGNEWTARGET");
},
@ -2360,7 +2528,26 @@ UnitAI.prototype.UnitFsmSpec = {
"enter": function()
{
// Check if we are attacking a formation.
// First, check if we have pending targets in a group attack
if (this.order.data.targetArray && this.order.data.currentTargetIndex !== undefined)
{
// Try to find next valid target from the array
if (this.FindAndSetNextTarget(this.order.data))
{
this.order.data.attackType = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
this.SetNextState("COMBAT.ATTACKING");
return true;
}
// No valid targets in array - clean up and continue with normal behavior
this.order.data.targetArray = [];
this.order.data.currentTargetIndex = undefined;
}
if (!this.order.data.target)
return false;
// Try to find the formation the target was a part of.
let cmpFormation = Engine.QueryInterface(this.order.data.target, IID_Formation);
if (cmpFormation)
this.order.data.formationTarget = this.order.data.target;
@ -2468,6 +2655,12 @@ UnitAI.prototype.UnitFsmSpec = {
{
if (msg.likelyFailure)
{
// If this is a group attack with pending targets, try the next one
if (this.order.data.targetArray && this.order.data.targetArray.length > 0)
{
this.SetNextState("COMBAT.FINDINGNEWTARGET");
return;
}
// This also handles hunting.
if (this.orderQueue.length > 1)
{
@ -3887,6 +4080,9 @@ UnitAI.prototype.OnOwnershipChanged = function(msg)
UnitAI.prototype.OnDestroy = function()
{
// Clean up all group attack states before destruction
this.CleanupAllGroupAttackStates();
// Switch to an empty state to let states execute their leave handlers.
this.UnitFsm.SwitchToNextState(this, "");
@ -4093,6 +4289,9 @@ UnitAI.prototype.FsmStateNameChanged = function(state)
*/
UnitAI.prototype.FinishOrder = function()
{
// Clean up any group attack state before removing order
this.CleanupGroupAttackState(this.order);
if (!this.orderQueue.length)
{
const stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line
@ -4318,15 +4517,24 @@ UnitAI.prototype.ReplaceOrder = function(type, data)
const idx = this.orderQueue.findIndex(o => o.type == "LeaveFormation");
if (idx === -1)
{
// Clean up all queued orders before clearing them
this.CleanupAllGroupAttackStates();
this.orderQueue = [];
this.order = undefined;
}
else
{
// Clean up orders being discarded
for (let i = 0; i < idx; ++i)
this.CleanupGroupAttackState(this.orderQueue[i]);
this.orderQueue.splice(0, idx);
}
this.PushOrderFront(type, data);
}
else
{
// Clean up all queued orders before clearing them
this.CleanupAllGroupAttackStates();
this.orderQueue = [];
this.PushOrder(type, data);
}
@ -4405,6 +4613,7 @@ UnitAI.prototype.BackToWork = function()
if (this.IsTurret(cmpTurretable) && !cmpTurretable.LeaveTurret())
return false;
this.CleanupAllGroupAttackStates();
this.orderQueue = [];
this.AddOrders(this.workOrders);
@ -5060,6 +5269,9 @@ UnitAI.prototype.CheckTargetRange = function(target, iid, type)
*/
UnitAI.prototype.CheckTargetAttackRange = function(target, type)
{
if (!target || target === INVALID_ENTITY || !type)
return false;
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
{
@ -5793,6 +6005,75 @@ UnitAI.prototype.Attack = function(target, allowCapture = this.DEFAULT_CAPTURE,
this.AddOrder("Attack", order, queued, pushFront);
};
/**
* Adds a group attack order to the queue.
* Used when some unit(s) are ordered to attack multiple targets.
* Each unit receives the full target list (from Commands) but starts at a different position
* based on the startingIndex in the attacking group.
*
* @param {Array} targets - Array of target entity IDs to attack (pre-sorted by distance)
* @param {number} startingIndex - This unit's position in the attacking group (0-based)
* @param {number} unitCount - Total number of units in the attacking group
* @param {boolean} allowCapture - Whether capturing is allowed
* @param {boolean} queued - If true, add to end of queue instead of replacing current order
* @param {boolean} pushFront - If true, add to front of queue (interrupt current order)
*/
UnitAI.prototype.AttackGroup = function(targets, startingIndex, unitCount, allowCapture = this.DEFAULT_CAPTURE, queued = false, pushFront = false)
{
if (!targets?.length)
return;
// Check if adding these targets would exceed the limit
const currentTotal = this.CountTotalTargetsInQueue();
const newTargetCount = targets.length;
if (currentTotal + newTargetCount > this.MAX_TOTAL_TARGETS)
{
// Trim the targets to fit within the limit
const allowedNewTargets = Math.max(0, this.MAX_TOTAL_TARGETS - currentTotal);
if (allowedNewTargets === 0)
return;
// Trim the target array
targets = targets.slice(0, allowedNewTargets);
// If trimming results in no targets, just return
if (targets.length === 0)
return;
}
// If this unit is in a formation, move the formation controller out of the world
// This prevents the controller from staying at the coordinates where the units received the order
// That can potentially be far apart from where the fighting will occur.
// Result: The formation will reform into a more logical position once members are done (idle).
this.MoveFormationControllerOutOfWorld();
const stride = targets.length / unitCount;
const startIdx = Math.min(Math.round((startingIndex + 0.5) * stride), targets.length - 1);
const order = {
"force": true,
"allowCapture": allowCapture,
"targetArray": targets.slice(),
"target": INVALID_ENTITY,
"currentTargetIndex": startIdx,
"originalFirstTarget": targets[0],
};
this.RememberTargetPosition(order);
this.AddOrder("Attack", order, queued, pushFront);
};
UnitAI.prototype.MoveFormationControllerOutOfWorld = function()
{
if (!this.IsFormationMember() || !this.formationController)
return;
const cmpControllerPosition = Engine.QueryInterface(this.formationController, IID_Position);
if (cmpControllerPosition?.IsInWorld())
cmpControllerPosition.MoveOutOfWorld();
};
/**
* Adds garrison order to the queue, forced by the player.
*/
@ -6726,6 +7007,72 @@ UnitAI.prototype.AttackEntitiesByPreference = function(ents)
return this.RespondToTargetedEntities(entsWithoutPref);
};
UnitAI.prototype.FindAndSetNextTarget = function(data)
{
if (!this.EnsureValidTargetIndex(data))
return false;
const startIdx = data.currentTargetIndex;
const currentTarget = data.targetArray[startIdx];
// First check the starting target
if (this.CanAttack(currentTarget) && this.CheckTargetVisible(currentTarget))
return this.SetNextTarget(data, startIdx);
// Search forward using find
const forwardFound = data.targetArray
.slice(startIdx + 1)
.findIndex((target, i) =>
this.CanAttack(target) && this.CheckTargetVisible(target)
);
if (forwardFound !== -1)
return this.SetNextTarget(data, startIdx + 1 + forwardFound);
// Search backward using find
const backwardFound = data.targetArray
.slice(0, startIdx)
.reverse()
.findIndex((target, i) =>
this.CanAttack(target) && this.CheckTargetVisible(target)
);
if (backwardFound !== -1)
{
// Convert reverse index back to original index
const originalIndex = startIdx - 1 - backwardFound;
return this.SetNextTarget(data, originalIndex);
}
return false;
};
UnitAI.prototype.SetNextTarget = function(data, index)
{
const target = data.targetArray[index];
data.target = target;
data.attackType = this.GetBestAttackAgainst(target, data.allowCapture);
data.currentTargetIndex = index;
return true;
};
UnitAI.prototype.EnsureValidTargetIndex = function(data)
{
if (!data.targetArray || data.currentTargetIndex === undefined)
return false;
// If array is empty, clear the index
if (data.targetArray.length === 0)
{
data.currentTargetIndex = undefined;
return false;
}
data.currentTargetIndex = Math.clamp(data.currentTargetIndex, 0, data.targetArray.length - 1);
return true;
};
/**
* Call UnitAI.funcname(args) on all formation members.
* @param resetFinishedEntities - If true, call ResetFinishedEntities first.
@ -6837,6 +7184,50 @@ UnitAI.prototype.ResetObstructionMitigationFlag = function()
delete this.obstructionMitigationAttempted;
};
/**
* Counts the total number of target entities across all queued orders.
* This includes targetArray from group attacks and single targets.
* Each target reference is counted separately, even if the same entity
* appears in multiple orders or in both targetArray and target.
* @returns {number} - Total number of target entity references in the order queue
*/
UnitAI.prototype.CountTotalTargetsInQueue = function()
{
let total = 0;
for (const order of this.orderQueue)
{
if (order.data)
{
// Count group attack targets
if (order.data.targetArray && Array.isArray(order.data.targetArray))
total += order.data.targetArray.length;
// Add single target for consistency
if (order.data.target && order.data.target !== INVALID_ENTITY)
total++;
}
}
return total;
};
UnitAI.prototype.CleanupGroupAttackState = function(order)
{
if (order?.data?.targetArray)
{
delete order.data.targetArray;
delete order.data.currentTargetIndex;
}
};
UnitAI.prototype.CleanupAllGroupAttackStates = function()
{
for (const order of this.orderQueue)
this.CleanupGroupAttackState(order);
};
// Maximum number of target entities allowed across all queued orders
UnitAI.prototype.MAX_TOTAL_TARGETS = 1000;
UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec);
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);

View file

@ -138,6 +138,7 @@ function TestFormationExiting(mode)
"ResetActiveQuery": function(id) { if (mode == 0) return []; return [enemy]; },
"DisableActiveQuery": function(id) { },
"GetEntityFlagMask": function(identifier) { },
"GetLosVisibility": (ent, player) => "visible"
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
@ -319,8 +320,11 @@ function TestMoveIntoFormationWhileAttacking()
"ResetActiveQuery": function(id) { return [enemy]; },
"DisableActiveQuery": function(id) { },
"GetEntityFlagMask": function(identifier) { },
"GetLosVisibility": (target, player) => "visible",
"GetLosVisibilityPosition": (x, z, player) => "visible"
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; },
});
@ -406,6 +410,10 @@ function TestMoveIntoFormationWhileAttacking()
"GetHitpoints": function() { return 40; },
});
AddMock(enemy, IID_Ownership, {
"GetOwner": () => 2 // Different player ID (enemy)
});
const controllerFormation = ConstructComponent(controller, "Formation", {
"FormationShape": "square",
"ShiftRows": "false",
@ -421,6 +429,10 @@ function TestMoveIntoFormationWhileAttacking()
"DefaultStance": "aggressive"
});
AddMock(controller, IID_Ownership, {
"GetOwner": () => 1
});
AddMock(controller, IID_Position, {
"GetTurretParent": () => INVALID_ENTITY,
"JumpTo": function(x, z) { this.x = x; this.z = z; },
@ -458,7 +470,7 @@ function TestMoveIntoFormationWhileAttacking()
controllerFormation.SetMembers(units);
controllerAI.Attack(enemy, []);
controllerAI.Attack(enemy);
for (const ent of unitAIs)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
@ -889,4 +901,362 @@ function TestAttemptObstructionMitigation()
})();
}
TestAttemptObstructionMitigation();
TestAttemptObstructionMitigation();
function TestFindAndSetNextTargetBehavior()
{
ResetState();
const attacker = 10;
const targets = [20, 21, 22, 23, 24]; // Multiple targets for comprehensive testing
// Basic mocks needed for unitAI creation
AddMock(SYSTEM_ENTITY, IID_Timer, {
"SetInterval": () => {},
"SetTimeout": () => {}
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"CreateActiveQuery": () => 1,
"EnableActiveQuery": () => {},
"ResetActiveQuery": () => [],
"DisableActiveQuery": () => {},
"GetEntityFlagMask": () => 0
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": () => false
});
// Create the unitAI component
const unitAI = ConstructComponent(attacker, "UnitAI", {
"FormationController": "false",
"DefaultStance": "aggressive"
});
AddMock(attacker, IID_Attack, {
"GetBestAttackAgainst": () => "Melee"
});
unitAI.OnCreate();
// Mock the visibility and attackability checks
unitAI.CanAttack = (target) =>
{
// Target 21 is always unattackable (e.g., garrisoned, dead, etc.)
return target !== 21;
};
unitAI.CheckTargetVisible = (target) =>
{
// Target 22 is always not visible
return target !== 22;
};
// First target is valid
(function()
{
const data = {
"targetArray": [20, 21, 22, 23, 24],
"currentTargetIndex": 0, // Currently targeting 20
"target": 20,
"allowCapture": false
};
const found = unitAI.FindAndSetNextTarget(data);
TS_ASSERT(found);
TS_ASSERT_EQUALS(data.target, 20);
TS_ASSERT_EQUALS(data.currentTargetIndex, 0);
})();
// End of array behavior
(function()
{
const data = {
"targetArray": [18, 19, 20, 21, 22],
"currentTargetIndex": 2, // Currently targeting 22
"allowCapture": false
};
// 21 and 22 are invalid, should go backward and find 20
const found = unitAI.FindAndSetNextTarget(data);
TS_ASSERT(found);
TS_ASSERT_EQUALS(data.target, 20); // Should be assigned targetArray[initialTarget - 1]
})();
// No valid targets found
(function()
{
const data = {
"targetArray": [21, 22], // Both are invalid (21 unattackable, 22 not visible)
"currentTargetIndex": 0,
"target": 21,
"allowCapture": false
};
const found = unitAI.FindAndSetNextTarget(data);
TS_ASSERT(!found);
TS_ASSERT_EQUALS(data.target, 21); // Should remain unchanged
TS_ASSERT_EQUALS(data.currentTargetIndex, 0);
})();
// Empty target array
(function()
{
const data = {
"targetArray": [],
"currentTargetIndex": 0,
"target": 20,
"allowCapture": false
};
const found = unitAI.FindAndSetNextTarget(data);
TS_ASSERT(!found);
TS_ASSERT_EQUALS(data.target, 20); // Should remain unchanged
TS_ASSERT_EQUALS(data.currentTargetIndex, void 0); // Should nullify currentTargetIndex
})();
// Current target is last in array
(function()
{
const data = {
"targetArray": [20, 21, 22, 23, 24],
"currentTargetIndex": 4, // Currently targeting 24 (last element)
"allowCapture": false
};
const found = unitAI.FindAndSetNextTarget(data);
TS_ASSERT(found);
TS_ASSERT_EQUALS(data.target, 24); // Should remain unchanged
TS_ASSERT_EQUALS(data.currentTargetIndex, 4);
})();
// Single invalid target
(function()
{
const data = {
"targetArray": [21], // Only one target, which is invalid
"currentTargetIndex": 0,
"allowCapture": false
};
const found = unitAI.FindAndSetNextTarget(data);
TS_ASSERT(!found);
})();
// Verify if we don't do more calls then necessary
(function()
{
const searchOrder = [];
unitAI.CanAttack = (target) =>
{
searchOrder.push(target);
return target == 20;
};
unitAI.CheckTargetVisible = (target) => true; // All visible
const data = {
"targetArray": [20, 21, 22, 23],
"currentTargetIndex": 1, // Currently targeting 21
"allowCapture": false
};
const found = unitAI.FindAndSetNextTarget(data);
TS_ASSERT(found);
TS_ASSERT_EQUALS(searchOrder.length, 4); // Should find at 20 (4 calls)
})();
}
TestFindAndSetNextTargetBehavior();
function TestAttackGroupBehavior()
{
ResetState();
const attacker1 = 100;
const attacker2 = 101;
const targets = [200, 201, 202, 203, 204];
// Basic mocks needed for unitAI creation
AddMock(SYSTEM_ENTITY, IID_Timer, {
"SetInterval": () => {},
"SetTimeout": () => {}
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"CreateActiveQuery": () => 1,
"EnableActiveQuery": () => {},
"ResetActiveQuery": () => [],
"DisableActiveQuery": () => {},
"GetEntityFlagMask": () => 0
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": () => false
});
// Create two unitAI components
const unitAI1 = ConstructComponent(attacker1, "UnitAI", {
"FormationController": "false",
"DefaultStance": "aggressive"
});
const unitAI2 = ConstructComponent(attacker2, "UnitAI", {
"FormationController": "false",
"DefaultStance": "aggressive"
});
AddMock(attacker1, IID_Attack, {
"GetBestAttackAgainst": () => "Melee"
});
AddMock(attacker2, IID_Attack, {
"GetBestAttackAgainst": () => "Melee"
});
unitAI1.OnCreate();
unitAI2.OnCreate();
// Basic AttackGroup distribution
(function()
{
let orderAdded1 = null;
let orderAdded2 = null;
// Mock AddOrder to capture what orders are created
unitAI1.AddOrder = (type, data, queued, pushFront) =>
{
orderAdded1 = { type, data, queued, pushFront };
};
unitAI2.AddOrder = (type, data, queued, pushFront) =>
{
orderAdded2 = { type, data, queued, pushFront };
};
// Call AttackGroup for both units
unitAI1.AttackGroup(targets, 0, 2, false, false, false);
unitAI2.AttackGroup(targets, 1, 2, false, false, false);
// Verify unit1's order
TS_ASSERT(orderAdded1);
TS_ASSERT_EQUALS(orderAdded1.type, "Attack");
TS_ASSERT_EQUALS(orderAdded1.data.targetArray.length, 5);
TS_ASSERT_EQUALS(orderAdded1.data.target, INVALID_ENTITY);
// Unit 0 should start at position 1 ==> Math.round((0 + 0.5) * 2.5) = Math.round(1.25) = 1
TS_ASSERT_EQUALS(orderAdded1.data.currentTargetIndex, 1);
// Verify unit2's order
TS_ASSERT(orderAdded2);
TS_ASSERT_EQUALS(orderAdded2.type, "Attack");
TS_ASSERT_EQUALS(orderAdded2.data.targetArray.length, 5);
TS_ASSERT_EQUALS(orderAdded2.data.target, INVALID_ENTITY);
// Unit 1 should start at position 4 ==> Math.round((1 + 0.5) * 2.5) = Math.round(3.75) = 4
TS_ASSERT_EQUALS(orderAdded2.data.currentTargetIndex, 4);
})();
// More units than targets
(function()
{
let orderAdded1 = null;
let orderAdded2 = null;
let orderAdded3 = null;
const unitAI3 = ConstructComponent(102, "UnitAI", {
"FormationController": "false",
"DefaultStance": "aggressive"
});
AddMock(102, IID_Attack, { "GetBestAttackAgainst": () => "Melee" });
unitAI3.OnCreate();
unitAI1.AddOrder = (type, data, queued, pushFront) =>
{
orderAdded1 = { type, data, queued, pushFront };
};
unitAI2.AddOrder = (type, data, queued, pushFront) =>
{
orderAdded2 = { type, data, queued, pushFront };
};
unitAI3.AddOrder = (type, data, queued, pushFront) =>
{
orderAdded3 = { type, data, queued, pushFront };
};
const smallTargets = [300, 301];
unitAI1.AttackGroup(smallTargets, 0, 3, true, true, false);
unitAI2.AttackGroup(smallTargets, 1, 3, true, true, false);
unitAI3.AttackGroup(smallTargets, 2, 3, true, true, false);
// Verify distribution when more units than targets
TS_ASSERT(orderAdded1);
TS_ASSERT_EQUALS(orderAdded1.data.currentTargetIndex, 0); // 0 * 2 / 3 = 0
TS_ASSERT_EQUALS(orderAdded1.data.allowCapture, true);
TS_ASSERT_EQUALS(orderAdded1.queued, true);
TS_ASSERT(orderAdded2);
TS_ASSERT_EQUALS(orderAdded2.data.currentTargetIndex, 1); // 1 * 2 / 3 = 0.66, round = 1
TS_ASSERT(orderAdded2.data.currentTargetIndex < smallTargets.length);
TS_ASSERT(orderAdded3);
TS_ASSERT_EQUALS(orderAdded3.data.currentTargetIndex, 1); // 2 * 2 / 3 = 1.33, round = 1
TS_ASSERT(orderAdded3.data.currentTargetIndex < smallTargets.length);
})();
// Edge case - single target
(function()
{
let orderAdded = null;
unitAI1.AddOrder = (type, data, queued, pushFront) =>
{
orderAdded = { type, data, queued, pushFront };
};
const singleTarget = [400];
unitAI1.AttackGroup(singleTarget, 0, 1, false, false, true);
TS_ASSERT(orderAdded);
TS_ASSERT_EQUALS(orderAdded.data.targetArray.length, 1);
TS_ASSERT_EQUALS(orderAdded.data.currentTargetIndex, 0);
TS_ASSERT_EQUALS(orderAdded.pushFront, true);
})();
// Empty targets array should do nothing
(function()
{
let callCount = 0;
unitAI1.AddOrder = () => { callCount++; };
unitAI1.AttackGroup([], 0, 1);
TS_ASSERT_EQUALS(callCount, 0);
unitAI1.AttackGroup(null, 0, 1);
TS_ASSERT_EQUALS(callCount, 0);
unitAI1.AttackGroup(undefined, 0, 1);
TS_ASSERT_EQUALS(callCount, 0);
})();
// Verify RememberTargetPosition is called
(function()
{
let rememberedOrder = null;
unitAI1.RememberTargetPosition = (order) =>
{
rememberedOrder = order;
};
unitAI1.AddOrder = () => {};
const testTargets = [500, 501];
unitAI1.AttackGroup(testTargets, 0, 1);
TS_ASSERT(rememberedOrder);
TS_ASSERT_EQUALS(rememberedOrder.targetArray.length, 2);
})();
}
TestAttackGroupBehavior();

View file

@ -179,6 +179,28 @@ var g_Commands = {
});
},
"attack-group": function(player, cmd, data)
{
const unitAIs = data.entities.flatMap(ent =>
{
const cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
const cmpAttack = Engine.QueryInterface(ent, IID_Attack);
// Check if unit can move and has attack capability
const canAttack = cmpAttack && cmpAttack.GetAttackTypes().length > 0;
return (cmpUnitAI && cmpUnitAI.AbleToMove() && canAttack) ? [cmpUnitAI] : [];
});
if (!unitAIs.length || !cmd.targets?.length)
return;
// Pass the entire target array and unit count to each UnitAI
unitAIs.forEach((cmpUnitAI, index) =>
{
cmpUnitAI.AttackGroup(cmd.targets, index, unitAIs.length, cmd.allowCapture, cmd.queued, cmd.pushFront);
});
},
"patrol": function(player, cmd, data)
{
const ents = data.entities.length;

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -54,7 +54,7 @@ pipeline {
stage('Check for shader changes') {
when {
anyOf {
changeset 'binaries/data/mods/**/shaders/**/*.xml'
changeset 'binaries/data/mods/**/shaders/**/*'
changeset 'source/tools/spirv/compile.py'
}
}

260
cmake/0ad-BuildFlags.cmake Normal file
View file

@ -0,0 +1,260 @@
cmake_minimum_required(VERSION 3.25.1...4.0.0)
# Add an interface library with common set of build/linker flags and definitions.
set(BUILD_FLAGS_TARGET BuildFlags)
add_library(${BUILD_FLAGS_TARGET} INTERFACE)
# Enable and require C++20 standard.
target_compile_features(${BUILD_FLAGS_TARGET} INTERFACE cxx_std_20)
set_target_properties(${BUILD_FLAGS_TARGET}
PROPERTIES
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
)
# Settings for build types
if(with-lto)
include(CheckIPOSupported)
check_ipo_supported(RESULT lto_supported OUTPUT lto_supported_error)
if(lto_supported)
set_target_properties(${BUILD_FLAGS_TARGET}
PROPERTIES
INTERPROCEDURAL_OPTIMIZATION TRUE
)
else()
message(WARNING "IPO / LTO not supported: <${lto_supported_error}>")
endif()
unset(lto_supported)
unset(lto_supported_error)
endif()
target_compile_definitions(${BUILD_FLAGS_TARGET}
INTERFACE
$<$<CONFIG:Release>:NDEBUG>
$<$<CONFIG:Release>:CONFIG_FINAL=1>
$<$<CONFIG:Debug>:DEBUG>
# Game Configuration Defines
$<$<BOOL:${mozjs_is_debug_build}>:DEBUG>
$<$<BOOL:${gles}>:CONFIG2_DAP_INTERFACE=0>
$<$<BOOL:${without-audio}>:CONFIG2_GLES=1>
$<$<BOOL:${without-nvtt}>:CONFIG2_NVTT=0>
$<$<BOOL:${without-lobby}>:CONFIG2_LOBBY=0>
$<$<BOOL:${without-miniupnpc}>:CONFIG2_MINIUPNPC=0>
$<$<BOOL:${without-dap-interface}>:CONFIG2_DAP_INTERFACE=0>
)
# Address Sanitizer Settings
if(sanitize-address)
target_compile_options(${BUILD_FLAGS_TARGET} INTERFACE -fsanitize=address)
target_link_options(${BUILD_FLAGS_TARGET} INTERFACE -fsanitize=address)
endif()
if(sanitize-thread)
target_compile_options(${BUILD_FLAGS_TARGET} INTERFACE -fsanitize=thread)
target_link_options(${BUILD_FLAGS_TARGET} INTERFACE -fsanitize=thread)
endif()
if(sanitize-undefined-behaviour)
target_compile_options(${BUILD_FLAGS_TARGET} INTERFACE -fsanitize=undefined)
target_link_options(${BUILD_FLAGS_TARGET} INTERFACE -fsanitize=undefined)
endif()
# hide warnings caused by library includes (Nothing to do for gcc/clang)
target_compile_options(${BUILD_FLAGS_TARGET}
INTERFACE
$<$<CXX_COMPILER_ID:MSVC>:/external:W0>
)
# various platform-specific build flags
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_compile_options(${BUILD_FLAGS_TARGET}
INTERFACE
$<$<CXX_COMPILER_ID:MSVC>:/MP>
# Since KB4088875 Windows 7 has a soft requirement for SSE2.
# Windows 8+ and Firefox ESR52 make it hard requirement.
# Finally since VS2012 it's enabled implicitely when not set.
$<$<CXX_COMPILER_ID:MSVC>:/arch:SSE2>
# use native wchar_t type (not typedef to unsigned short)
$<$<CXX_COMPILER_ID:MSVC>:/Zc:wchar_t>
$<$<CXX_COMPILER_ID:MSVC>:/utf-8>
# SpiderMonkey only supports building with MSVC on a best-effort basis,
# and the traditional MSVC preprocessor is incompatible with some headers.
# Use the modern, standard-compliant MSVC preprocessor instead.
$<$<CXX_COMPILER_ID:MSVC>:/Zc:preprocessor>
# enable most of the standard warnings
$<$<CXX_COMPILER_ID:MSVC>:/W4>
# FIXME: conversion warnings, should add -Wconversion to gcc and clang flags as well
$<$<CXX_COMPILER_ID:MSVC>:/wd4267>
$<$<AND:$<CONFIG:Release>,$<CXX_COMPILER_ID:MSVC>>:/O2>
)
# disable LNK4221 warning, to avoid spending energy ordering projects in linker invocations
target_link_options(${BUILD_FLAGS_TARGET}
INTERFACE
"/ignore:4221"
)
# mozilla 115 linked list destructor in debug build
target_compile_definitions(${BUILD_FLAGS_TARGET} INTERFACE "__PRETTY_FUNCTION__=__FUNCSIG__")
# disable Windows debug heap, since it makes malloc/free hugely slower when running inside a debugger
set_target_properties(${BUILD_FLAGS_TARGET}
PROPERTIES
VS_DEBUGGER_ENVIRONMENT "_NO_DEBUG_HEAP=1"
)
elseif(UNIX)
if(NOT minimal-flags)
target_compile_options(${BUILD_FLAGS_TARGET}
INTERFACE
# most of the standard warnings
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-Wall>
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-Wextra>
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-Wpedantic>
# $<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-Wconversion> # FIXME: should seriously consider fixing so this warning can be enabled.
# add some other useful warnings that need to be enabled explicitly
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-Wunused-parameter> # (useful for finding some multiply-included header files)
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-Wredundant-decls>
# $<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-Wformat=2> # (useful sometimes, but a bit noisy, so skip it by default)
# $<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-Wcast-qual> # (useful for checking const-correctness, but a bit noisy, so skip it by default)
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-Wnon-virtual-dtor> # (sometimes noisy but finds real bugs)
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-Wundef> # (useful for finding macro name typos)
# disable some warnings that currently trigger
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-Wno-missing-field-initializers> # (this is common in external headers we can't fix)
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-Wno-reorder> # order of initialization list in constructors (lots of noise)
# enable security features (stack checking etc) that shouldn't have a significant effect on performance and can catch bugs
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-fstack-protector-strong>
# always enable strict aliasing (useful in debug builds because of the warnings)
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-fstrict-aliasing>
# don't omit frame pointers (for now), because performance will be impacted negatively by the way this breaks profilers more than it will be impacted positively by the optimisation
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-fno-omit-frame-pointer>
# FORTIFY_SOURCE needs optimizations to be enabled
$<$<AND:$<CONFIG:Release>,$<CXX_COMPILER_ID:Clang,AppleClang,GNU>>:-U_FORTIFY_SOURCE> # (avoid redefinition warning if already defined)
$<$<AND:$<CONFIG:Release>,$<CXX_COMPILER_ID:Clang,AppleClang,GNU>>:-D_FORTIFY_SOURCE=2>
)
target_link_options(${BUILD_FLAGS_TARGET}
INTERFACE
$<$<PLATFORM_ID:Darwin>:-multiply_defined>
$<$<PLATFORM_ID:Darwin>:suppress>
)
if(NOT without-pch)
# do something (?) so that ccache can handle compilation with PCH enabled (ccache 3.1+ also requires CCACHE_SLOPPINESS=time_macros for this to work)
target_compile_options(${BUILD_FLAGS_TARGET}
INTERFACE
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-fpch-preprocess>
)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_NAME MATCHES ".*BSD*")
target_compile_options(${BUILD_FLAGS_TARGET}
INTERFACE
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-fPIC>
)
target_link_options(${BUILD_FLAGS_TARGET}
INTERFACE
"-Wl,--no-undefined"
"-Wl,--as-needed"
"-Wl,-z,relro"
)
endif()
if(ARCH STREQUAL "x86")
# To support intrinsics like __sync_bool_compare_and_swap on x86 we need to set -march to something that supports them (i686).
# We use pentium3 to also enable other features like mmx and sse, while tuning for generic to have good performance on every supported CPU.
# Note that all these features are already supported on amd64.
target_compile_options(${BUILD_FLAGS_TARGET}
INTERFACE
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-march=pentium3>
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-mtune=generic>
# This allows x86 operating systems to handle the 2GB+ INTERFACE mod.
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-mtune=generic-D_FILE_OFFSET_BITS=64>
)
endif()
if(ARCH STREQUAL "arm")
target_compile_options(${BUILD_FLAGS_TARGET}
INTERFACE
# disable warnings about va_list ABI change and use compile-time flags for futher configuration.
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-Wno-psabi>
# Android uses softfp, so we should too.
$<$<AND:$<CXX_COMPILER_ID:Clang,AppleClang,GNU>,$<BOOL:${android}>>:-mfloat-abi=softfp>
)
endif()
endif()
if(coverage)
target_compile_options(${BUILD_FLAGS_TARGET}
INTERFACE
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-fprofile-arcs>
$<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-ftest-coverage>
)
target_link_libraries(${BUILD_FLAGS_TARGET} gcov)
endif()
# MacOS 10.12 only supports intel processors with SSE 4.1, so enable that.
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND ARCH STREQUAL "amd64")
target_compile_options(${BUILD_FLAGS_TARGET}
INTERFACE
"-msse4.1"
)
endif()
# Check if SDK path should be used
if(sysroot)
target_compile_options(${BUILD_FLAGS_TARGET}
INTERFACE
"-isysroot ${sysroot}"
)
target_link_options(${BUILD_FLAGS_TARGET}
INTERFACE
"-Wl,-syslibroot, ${sysroot}"
)
endif()
# On OS X, sometimes we need to specify the minimum API version to use
if(macosx-version-min)
# clang and llvm-gcc look at mmacosx-version-min to determine link target and CRT version, and use it to set the macosx_version_min linker flag
target_compile_options(${BUILD_FLAGS_TARGET}
INTERFACE
"-mmacosx-version-min=${macosx-version-min}"
)
target_link_options(${BUILD_FLAGS_TARGET}
INTERFACE
"-mmacosx-version-min=${macosx-version-min}"
)
endif()
# Only libc++ is supported on MacOS
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
target_compile_options(${BUILD_FLAGS_TARGET}
INTERFACE
-stdlib=libc++
)
target_link_options(${BUILD_FLAGS_TARGET}
INTERFACE
-stdlib=libc++
)
endif()
# Hide symbols in dynamic shared objects by default, for efficiency and for equivalence with Windows
# - they should be exported explicitly with __attribute__ ((visibility ("default")))
target_compile_options(${BUILD_FLAGS_TARGET}
INTERFACE
-fvisibility=hidden
)
target_compile_definitions(${BUILD_FLAGS_TARGET}
INTERFACE
$<$<BOOL:${bindir}>:INSTALLED_BINDIR=${bindir}>
$<$<BOOL:${datadir}>:INSTALLED_DATADIR=${bindir}>
$<$<BOOL:${libdir}>:INSTALLED_LIBDIR=${bindir}>
)
# RPATH Settings. Must be done through INSTALL_RPATH on target/global base.
if(CMAKE_SYSTEM_NAME MATCHES ".*BSD*")
# On FreeBSD we need to allow use of $ORIGIN
target_link_options(${BUILD_FLAGS_TARGET}
INTERFACE
-Wl,-z,origin
)
endif()
endif()

47
cmake/0ad-Functions.cmake Normal file
View file

@ -0,0 +1,47 @@
cmake_minimum_required(VERSION 3.25.1...4.0.0)
# 0AD Specific Functions
# Add Precompiled Headers. If no target is given, this macro will fail with a FATAL_ERROR.
# rationale: we need one PCH per static lib, since one global header would increase dependencies. To that end, we can either include them as
# "projectdir/precompiled.h", or add "source/PCH/projectdir" to the include path and put the PCH there. The latter is better because many
# projects contain several dirs and it's unclear where there the PCH should be stored. This way is also a bit easier to use in that
# source files always include "precompiled.h".
# Notes:
# * Visual Assist manages to use the project include path and can correctly open these files from the IDE.
# * precompiled.cpp (needed to "Create" the PCH) also goes in the abovementioned dir.
# * using CMakes precompiled Header is not possible, as it does not allow for a custom name like used here.
function(add_pch)
set(single_args TARGET PCH_DIR)
cmake_parse_arguments(args "" "${single_args}" "" ${ARGN})
if(NOT args_TARGET)
message(FATAL_ERROR "add_pch: Missing target!!")
endif()
include(PrecompiledHeader)
if(NOT args_PCH_DIR)
get_target_property(source_root ${args_TARGET} SOURCE_DIR)
set(args_PCH_DIR ${source_root}/pch/${args_TARGET})
endif()
# Put the project-specific PCH directory at the start of the include path, so '#include "precompiled.h"' will look in there first
target_include_directories(${args_TARGET}
BEFORE PRIVATE
${args_PCH_DIR}/
)
if (CMAKE_GENERATOR MATCHES "Visual Studio")
set(pch_header "precompiled.h")
elseif(CMAKE_GENERATOR MATCHES "Xcode")
set(pch_header "../${args_PCH_DIR}/precompiled.h")
else()
set(pch_header "${args_PCH_DIR}/precompiled.h")
endif()
precompile_header(${args_TARGET} ${pch_header} ${args_PCH_DIR}/precompiled.cpp)
target_sources(${args_TARGET}
PRIVATE
${args_PCH_DIR}/precompiled.cpp
${args_PCH_DIR}/precompiled.h
)
target_compile_definitions(${args_TARGET} PRIVATE CONFIG_ENABLE_PCH=1)
endfunction(add_pch)

View file

@ -0,0 +1,54 @@
function(find_prebuild_library _name)
include(FindPackageHandleStandardArgs)
set(multi_args COMPONENTS PATHS)
set(single_args INC_PATH VERSION)
cmake_parse_arguments(args "${multi_args}" "${single_args}" "" ${ARGN})
string(TOLOWER ${_name} libname)
string(TOUPPER ${_name} target_name)
# ++++++++ Handle COMPONENTS +++++++++++++++++++++
if(args_COMPONENTS)
endif()
# ++++++++ Find relevant elements ++++++++++++++++
find_library(Lib${_name} NAMES ${libname} ${libname}${args_VERSION})
if(NOT args_INC_PATH)
find_path(Lib${_name}_inc NAMES ${libname}.h PATHS ${0AD_EXT_LIBDIR}/${_name}/include/)
# recursive add all paths beneth included...
else()
set(Lib${_name}_inc ${args_INC_PATH})
endif()
if(NOT Lib${_name}_inc)
set(Lib${_name}_inc ${0AD_EXT_LIBDIR}/${_name}/include/)
endif()
find_package_handle_standard_args(Lib${_name} REQUIRED_VARS Lib${_name})
message(STATUS "${Lib${_name}} - ${Lib${_name}_inc} - ${Lib${_name}_FOUND} - ${target_name}::${target_name}")
if (Lib${_name}_FOUND)
mark_as_advanced(
Lib${_name}
Lib${_name}_inc
)
endif()
if(Lib${_name} MATCHES "LibBoost")
add_library(${target_name}::headers INTERFACE IMPORTED)
set_target_properties(${target_name}::headers PROPERTIES
IMPORTED_LOCATION ${Lib${_name}_inc}
INTERFACE_INCLUDE_DIRECTORIES ${Lib${_name}_inc}
)
elseif(Lib${_name} MATCHES "SDL2")
add_library(${target_name}::${target_name} UNKNOWN IMPORTED)
set_target_properties(${target_name}::${target_name} PROPERTIES
IMPORTED_LOCATION ${Lib${_name}}
INTERFACE_INCLUDE_DIRECTORIES ${Lib${_name}_inc}/SDL
)
elseif (Lib${_name} AND NOT TARGET ${target_name}::${target_name})
add_library(${target_name}::${target_name} UNKNOWN IMPORTED)
set_target_properties(${target_name}::${target_name} PROPERTIES
IMPORTED_LOCATION ${Lib${_name}}
INTERFACE_INCLUDE_DIRECTORIES ${Lib${_name}_inc}
)
endif()
endfunction()

View file

@ -0,0 +1,35 @@
# Adds a custom precompiled header. This header must be included in the code if not added with FORCE_INCLUDE
function(precompile_header _target _header _source)
set(single_args FORCE_INCLUDE)
cmake_parse_arguments(args "" "${single_args}" "" ${ARGN})
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" OR CMAKE_C_COMPILER_ID STREQUAL "MSVC")
# Solution based opon https://www.dominikgrabiec.com/posts/2022/10/18/precompiled_header_snippet.html
target_compile_options(${_target}
PRIVATE
"/Yu${_header}"
)
set_source_files_properties(${_source}
PROPERTIES
COMPILE_OPTIONS "/Yc${_header}"
)
if(${args_FORCE_INCLUDE})
set_source_files_properties(${_source}
PROPERTIES
COMPILE_OPTIONS /FI${_header}
)
endif()
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU|AppleClang" OR CMAKE_C_COMPILER_ID MATCHES "Clang|GNU|AppleClang")
# Solution is based on internal cmake code
set_source_files_properties(${_source}
PROPERTIES
COMPILE_OPTIONS -Winvalid-pch -x ${_header}.gch
)
if(${args_FORCE_INCLUDE})
set_source_files_properties(${_source}
PROPERTIES
COMPILE_OPTIONS include ${_header}.gch
)
endif()
endif()
endfunction()

View file

@ -0,0 +1,33 @@
# Checks out the SVN Revision of windows libraries. Additionally sets up the environment for cmake.
# To update the libraries change the 'SVN_REVISION'. Important: the '-r' in the Revision must be retained.
message(STATUS "Fetching Windows prebuild Libraries for ${ARCH}")
if(fetch-prebuild-libs)
include(FetchContent)
if(ARCH STREQUAL "amd64")
set(REPO_NAME windows-libs-amd64)
else()
set(REPO_NAME windows-libs)
endif()
FetchContent_Populate(
prebuild_libs
SVN_REPOSITORY https://svn.wildfiregames.com/public/${REPO_NAME}/trunk
SVN_REVISION -r28278
SOURCE_DIR ${0AD_EXT_LIBDIR}
)
endif()
message(STATUS "Copy dependencies' binaries to 'binaries/system/' and adding to 'CMAKE_PREFIX_PATH'")
set(DIR_LIST cpp-httplib enet fcollada freetype gloox iconv icu libcurl libpng libsodium libxml2 microsoft miniupnpc nvtt openal sdl2 spidermonkey vorbis zlib)
foreach(dir ${DIR_LIST})
file(COPY ${0AD_EXT_LIBDIR}/${dir}/bin DESTINATION ${CMAKE_SOURCE_DIR}/binaries/system)
list(APPEND CMAKE_PREFIX_PATH ${0AD_EXT_LIBDIR}/${dir})
endforeach()
# Add the whole libraries directory to CMAKE_PREFIX_PATH. May be redundand but needed for wxWidgets
list(APPEND CMAKE_PREFIX_PATH ${0AD_EXT_LIBDIR})
# Add libraries not set during binary copy
list(APPEND CMAKE_PREFIX_PATH ${0AD_EXT_LIBDIR}/fmt)
list(APPEND CMAKE_PREFIX_PATH ${0AD_EXT_LIBDIR}/libzip)
list(APPEND CMAKE_PREFIX_PATH ${0AD_EXT_LIBDIR}/cxxtest-4.4)
message(STATUS "Copy build tools to 'build/bin'")
file(COPY ${0AD_EXT_LIBDIR}/cxxtest-4.4/bin DESTINATION ${CMAKE_SOURCE_DIR}/build/bin)

View file

@ -6,7 +6,11 @@
## Building the Doxygen documentation
To generate the Doxygen documentation: run "cmake -S . -B output && cmake --build output".
To generate the Doxygen documentation:
```console
cmake -S 0ad/rootdir -B output && cmake --build output --target docs
```
If you build the documentation with cmake, the output is located in the folder html inside your
specified build directory.

View file

@ -1,64 +1,66 @@
cmake_minimum_required(VERSION 3.18.4...3.28.0)
project(Pyrogenesis DESCRIPTION "Pyrogenesis, a RTS Engine" LANGUAGES NONE)
cmake_minimum_required(VERSION 3.25.1...4.0.0)
# Check if Doxygen and graphviz are installed.
find_package(Doxygen 1.9.1 REQUIRED dot)
if(DOXYGEN_FOUND)
include(FetchContent)
include(FetchContent)
FetchContent_Declare(doxygen_awesome_css
GIT_REPOSITORY https://github.com/jothepro/doxygen-awesome-css
GIT_TAG v2.3.3
SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/styling
)
FetchContent_MakeAvailable(doxygen_awesome_css)
message(STATUS "Fetching doxygen_awesome_css")
FetchContent_Declare(doxygen_awesome_css
GIT_REPOSITORY https://github.com/jothepro/doxygen-awesome-css
GIT_TAG v2.4.2
SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/styling
)
FetchContent_MakeAvailable(doxygen_awesome_css)
# Get current Branch Name to set it as the Project Number.
find_package(Git)
if(Git_FOUND)
set(ENV{GIT_DISCOVERY_ACROSS_FILESYSTEM} 1)
execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse --is-inside-work-tree OUTPUT_VARIABLE IS_GIT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
if(IS_GIT)
execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse --abbrev-ref HEAD OUTPUT_VARIABLE CURRENT_BRANCH OUTPUT_STRIP_TRAILING_WHITESPACE)
endif()
endif()
# Get current Branch Name to set it as the Project Number.
find_package(Git)
if(Git_FOUND)
set(ENV{GIT_DISCOVERY_ACROSS_FILESYSTEM} 1)
execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse --is-inside-work-tree OUTPUT_VARIABLE IS_GIT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
if(IS_GIT)
execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse --abbrev-ref HEAD OUTPUT_VARIABLE CURRENT_BRANCH OUTPUT_STRIP_TRAILING_WHITESPACE)
endif()
endif()
# Doxygen Configuration.
if(CURRENT_BRANCH)
set(DOXYGEN_PROJECT_NUMBER ${CURRENT_BRANCH})
else()
set(DOXYGEN_PROJECT_NUMBER main)
endif()
set(DOXYGEN_PROJECT_LOGO ${CMAKE_CURRENT_SOURCE_DIR}/pyrogenesis.png)
set(DOXYGEN_TAB_SIZE 4)
set(DOXYGEN_EXCLUDE_PATTERNS */.svn* */tests/test_*)
set(DOXYGEN_INCLUDE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../source)
set(DOXYGEN_EXAMPLE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../source)
set(DOXYGEN_EXCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/../../source/tools ${CMAKE_CURRENT_SOURCE_DIR}/../../source/third_party)
set(DOXYGEN_GENERATE_TREEVIEW YES)
set(DOXYGEN_HTML_EXTRA_STYLESHEET ${doxygen_awesome_css_SOURCE_DIR}/doxygen-awesome.css ${CMAKE_CURRENT_SOURCE_DIR}/style.css)
set(DOXYGEN_JAVADOC_AUTOBRIEF YES)
set(DOXYGEN_EXTRACT_ALL YES)
set(DOXYGEN_EXTRACT_PRIVATE YES)
set(DOXYGEN_EXTRACT_STATIC YES)
set(DOXYGEN_EXTRACT_ANON_NSPACES YES)
set(DOXYGEN_SHOW_DIRECTORIES YES)
set(DOXYGEN_STRIP_CODE_COMMENTS NO)
set(DOXYGEN_MACRO_EXPANSION YES)
set(DOXYGEN_EXPAND_ONLY_PREDEF YES)
set(DOXYGEN_GENERATE_TODOLIST NO)
set(DOXYGEN_PREDEFINED "UNUSED(x)=x" "METHODDEF(x)=static x" "GLOBAL(x)=x")
set(DOXYGEN_EXPAND_AS_DEFINED DEFAULT_COMPONENT_ALLOCATOR DEFAULT_SCRIPT_WRAPPER DEFAULT_INTERFACE_WRAPPER DEFAULT_MESSAGE_IMPL MESSAGE INTERFACE COMPONENT GUISTDTYPE)
set(DOXYGEN_WARN_LOGFILE doxygen.log)
# Doxygen Configuration.
set(DOXYGEN_PROJECT_NAME "Pyrogenesis")
set(DOXYGEN_PROJECT_BRIEF "Pyrogenesis, a RTS Engine")
if(CURRENT_BRANCH)
set(DOXYGEN_PROJECT_NUMBER ${CURRENT_BRANCH})
else()
set(DOXYGEN_PROJECT_NUMBER main)
endif()
set(DOXYGEN_PROJECT_LOGO ${CMAKE_CURRENT_SOURCE_DIR}/pyrogenesis.png)
set(DOXYGEN_TAB_SIZE 4)
set(DOXYGEN_EXCLUDE_PATTERNS */.svn* */tests/test_*)
set(DOXYGEN_INCLUDE_PATH ${CMAKE_SOURCE_DIR}/source)
set(DOXYGEN_EXAMPLE_PATH ${CMAKE_SOURCE_DIR}/source)
set(DOXYGEN_EXCLUDE ${CMAKE_SOURCE_DIR}/source/tools ${CMAKE_SOURCE_DIR}/source/third_party)
set(DOXYGEN_GENERATE_TREEVIEW YES)
set(DOXYGEN_HTML_EXTRA_STYLESHEET ${doxygen_awesome_css_SOURCE_DIR}/doxygen-awesome.css ${CMAKE_CURRENT_SOURCE_DIR}/style.css)
set(DOXYGEN_JAVADOC_AUTOBRIEF YES)
set(DOXYGEN_EXTRACT_ALL YES)
set(DOXYGEN_EXTRACT_PRIVATE YES)
set(DOXYGEN_EXTRACT_STATIC YES)
set(DOXYGEN_EXTRACT_ANON_NSPACES YES)
set(DOXYGEN_SHOW_DIRECTORIES YES)
set(DOXYGEN_STRIP_CODE_COMMENTS NO)
set(DOXYGEN_MACRO_EXPANSION YES)
set(DOXYGEN_EXPAND_ONLY_PREDEF YES)
set(DOXYGEN_GENERATE_TODOLIST NO)
set(DOXYGEN_PREDEFINED "UNUSED(x)=x" "METHODDEF(x)=static x" "GLOBAL(x)=x")
set(DOXYGEN_EXPAND_AS_DEFINED DEFAULT_COMPONENT_ALLOCATOR DEFAULT_SCRIPT_WRAPPER DEFAULT_INTERFACE_WRAPPER DEFAULT_MESSAGE_IMPL MESSAGE INTERFACE COMPONENT GUISTDTYPE)
set(DOXYGEN_WARN_LOGFILE doxygen.log)
doxygen_add_docs(${CMAKE_PROJECT_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/../../source
${CMAKE_CURRENT_SOURCE_DIR}/mainpage.dox
${CMAKE_CURRENT_SOURCE_DIR}/../../LICENSE.md
ALL)
doxygen_add_docs(docs
${CMAKE_SOURCE_DIR}/source
${CMAKE_CURRENT_SOURCE_DIR}/mainpage.dox
${CMAKE_SOURCE_DIR}/LICENSE.md
COMMENT "Creating Doxygen for the engine."
)
else()
message(SEND_ERROR "Make sure Doxygen is installed and usable")
message(SEND_ERROR "Make sure Doxygen is installed and usable")
endif()

View file

@ -33,6 +33,8 @@ echo "Building cpp-httplib..."
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
echo "Skipping - already built (use --force-rebuild to override)"
exit
else
rm -f .already-built
fi
# fetch

View file

@ -33,6 +33,8 @@ done
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
echo "Skipping - already built (use --force-rebuild to override)"
exit
else
rm -f .already-built
fi
# fetch

View file

@ -49,6 +49,8 @@ done
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
echo "Skipping - already built (use --force-rebuild to override)"
exit
else
rm -f .already-built
fi
# fetch

View file

@ -49,6 +49,8 @@ done
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
echo "Skipping - already built (use --force-rebuild to override)"
exit
else
rm -f .already-built
fi
# fetch

View file

@ -36,6 +36,8 @@ done
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
echo "Skipping - already built (use --force-rebuild to override)"
exit
else
rm -f .already-built
fi
# fetch

View file

@ -56,6 +56,8 @@ echo "Building SpiderMonkey..."
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
echo "Skipping - already built (use --force-rebuild to override)"
exit
else
rm -f .already-built
fi
OS="${OS:=$(uname -s)}"

View file

@ -33,6 +33,8 @@ done
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
echo "Skipping - already built (use --force-rebuild to override)"
exit
else
rm -f .already-built
fi
# fetch

View file

@ -33,6 +33,7 @@ ignore = [
"ANN",
"C90",
"COM812",
"CPY001",
"D10",
"EM",
"FIX002",

View file

@ -92,12 +92,12 @@ void CCinemaManager::DrawSpline(Renderer::Backend::IDeviceCommandContext& device
if (spline.GetAllNodes().size() == 2)
smoothness = 2;
const float start = spline.MaxDistance.ToFloat() / smoothness;
const float start = spline.GetMaxDistance().ToFloat() / smoothness;
std::vector<CVector3D> line;
for (int i = 0; i <= smoothness; ++i)
{
const float time = start * i / spline.MaxDistance.ToFloat();
const float time = start * i / spline.GetMaxDistance().ToFloat();
line.emplace_back(spline.GetPosition(time));
}
@ -108,7 +108,7 @@ void CCinemaManager::DrawSpline(Renderer::Backend::IDeviceCommandContext& device
{
for (int i = 0; i <= smoothness; ++i)
{
const float time = start * i / spline.MaxDistance.ToFloat();
const float time = start * i / spline.GetMaxDistance().ToFloat();
const CVector3D tmp = spline.GetPosition(time);
const float groundY = g_Game->GetWorld()->GetTerrain().GetExactGroundLevel(tmp.X, tmp.Z);
g_Renderer.GetDebugRenderer().DrawLine(deviceCommandContext, tmp, CVector3D(tmp.X, groundY, tmp.Z), splineColor, 0.1f, false);

View file

@ -62,8 +62,7 @@ void CMaterial::AddRenderQuery(const char* key)
m_RenderQueries.Add(key);
}
void CMaterial::AddRequiredSampler(const CStr& samplerName)
void CMaterial::AddRequiredSampler(RequiredSampler sampler)
{
CStrIntern string(samplerName);
m_RequiredSamplers.push_back(string);
m_RequiredSamplers.emplace_back(std::move(sampler));
}

View file

@ -53,6 +53,12 @@ public:
CTexturePtr Sampler;
};
struct RequiredSampler
{
CStrIntern name;
bool sRGB;
};
typedef std::vector<TextureSampler> SamplersVector;
CMaterial();
@ -82,8 +88,8 @@ public:
void AddRenderQuery(const char* key);
const CShaderRenderQueries& GetRenderQueries() const { return m_RenderQueries; }
void AddRequiredSampler(const CStr& samplerName);
const std::vector<CStrIntern>& GetRequiredSampler() const { return m_RequiredSamplers; }
void AddRequiredSampler(RequiredSampler sampler);
const std::vector<RequiredSampler>& GetRequiredSamplers() const { return m_RequiredSamplers; }
private:
@ -92,7 +98,7 @@ private:
CTexturePtr m_DiffuseTexture;
SamplersVector m_Samplers;
std::vector<CStrIntern> m_RequiredSamplers;
std::vector<RequiredSampler> m_RequiredSamplers;
std::array<CStrIntern, static_cast<size_t>(Pass::COUNT)> m_ShaderEffects;
CShaderDefines m_ShaderDefines;

View file

@ -103,6 +103,7 @@ CMaterial CMaterialManager::LoadMaterial(const VfsPath& pathname)
AT(material);
AT(name);
AT(pass);
AT(srgb);
AT(value);
#undef AT
#undef EL
@ -161,7 +162,8 @@ CMaterial CMaterialManager::LoadMaterial(const VfsPath& pathname)
}
else if (token == el_required_texture)
{
material.AddRequiredSampler(attrs.GetNamedItem(at_name));
const bool sRGB{attrs.GetNamedItem(at_srgb) == "true"};
material.AddRequiredSampler({CStrIntern{attrs.GetNamedItem(at_name)}, sRGB});
if (!attrs.GetNamedItem(at_define).empty())
material.AddShaderDefine(CStrIntern(attrs.GetNamedItem(at_define)), str_1);
}

View file

@ -51,6 +51,22 @@
#include <sstream>
#include <utility>
namespace
{
bool ShouldInterpretTextureAsSRGB(const CObjectBase::Samp& sampler, const CMaterial& material)
{
const auto requiredSamplers{material.GetRequiredSamplers()};
auto it{std::find_if(requiredSamplers.begin(), requiredSamplers.end(),
[&](const CMaterial::RequiredSampler& requiredSampler)
{
return requiredSampler.name == sampler.m_SamplerName;
})};
return it != requiredSamplers.end() && it->sRGB;
}
}
CObjectEntry::CObjectEntry(const std::shared_ptr<CObjectBase>& base, const CSimulation2& simulation) :
m_Base(base), m_Color(1.0f, 1.0f, 1.0f, 1.0f), m_Simulation(simulation)
{
@ -155,6 +171,7 @@ bool CObjectEntry::BuildVariation(const std::vector<const std::set<CStr>*>& comp
{
CTextureProperties textureProps(samp.m_SamplerFile);
textureProps.SetAddressMode(Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE);
textureProps.SetSRGB(ShouldInterpretTextureAsSRGB(samp, material));
CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps);
// if we've loaded this model we're probably going to render it soon, so prefetch its texture.
// All textures are prefetched even in the fixed pipeline, including the normal maps etc.
@ -167,11 +184,14 @@ bool CObjectEntry::BuildVariation(const std::vector<const std::set<CStr>*>& comp
CModel* model = newModel.get();
m_Model = std::move(newModel);
for (const CStrIntern& requSampName : model->GetMaterial().GetRequiredSampler())
for (const auto& requiredSampler : model->GetMaterial().GetRequiredSamplers())
{
if (std::find_if(m_Samplers.begin(), m_Samplers.end(),
[&](const CObjectBase::Samp& sampler) { return sampler.m_SamplerName == requSampName; }) == m_Samplers.end())
LOGERROR("Actor %s: required texture sampler %s not found (material %s)", m_Base->GetIdentifier(), requSampName.string().c_str(), m_Base->m_Material.string8().c_str());
[&](const CObjectBase::Samp& sampler) { return sampler.m_SamplerName == requiredSampler.name; }) == m_Samplers.end())
{
LOGERROR("Actor %s: required texture sampler %s not found (material %s)",
m_Base->GetIdentifier(), requiredSampler.name.string().c_str(), m_Base->m_Material.string8().c_str());
}
}
// calculate initial object space bounds, based on vertex positions

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -60,7 +60,7 @@ namespace
{
Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded(
Renderer::Backend::IDevice* device, Tex& textureData, const bool hasS3TC)
Renderer::Backend::IDevice* device, Tex& textureData, const bool hasS3TC, const bool sRGB)
{
const bool alpha = (textureData.m_Flags & TEX_ALPHA) != 0;
const bool grey = (textureData.m_Flags & TEX_GREY) != 0;
@ -81,13 +81,13 @@ Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded(
switch (dxt)
{
case DXT1A:
return Renderer::Backend::Format::BC1_RGBA_UNORM;
return sRGB ? Renderer::Backend::Format::BC1_RGBA_SRGB : Renderer::Backend::Format::BC1_RGBA_UNORM;
case 1:
return Renderer::Backend::Format::BC1_RGB_UNORM;
return sRGB ? Renderer::Backend::Format::BC1_RGB_SRGB : Renderer::Backend::Format::BC1_RGB_UNORM;
case 3:
return Renderer::Backend::Format::BC2_UNORM;
return sRGB ? Renderer::Backend::Format::BC2_SRGB : Renderer::Backend::Format::BC2_UNORM;
case 5:
return Renderer::Backend::Format::BC3_UNORM;
return sRGB ? Renderer::Backend::Format::BC3_SRGB : Renderer::Backend::Format::BC3_UNORM;
default:
LOGERROR("Unknown DXT compression.");
return Renderer::Backend::Format::UNDEFINED;
@ -101,9 +101,12 @@ Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded(
{
case 8:
ENSURE(grey);
ENSURE(!sRGB);
return Renderer::Backend::Format::L8_UNORM;
case 24:
ENSURE(grey);
ENSURE(!alpha);
ENSURE(!sRGB);
if (device->IsTextureFormatSupported(Renderer::Backend::Format::R8G8B8_UNORM))
return Renderer::Backend::Format::R8G8B8_UNORM;
else
@ -114,7 +117,7 @@ Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded(
}
case 32:
ENSURE(alpha);
return Renderer::Backend::Format::R8G8B8A8_UNORM;
return sRGB ? Renderer::Backend::Format::R8G8B8A8_SRGB : Renderer::Backend::Format::R8G8B8A8_UNORM;
default:
LOGERROR("Unsupported BPP: %zu", textureData.m_Bpp);
}
@ -336,6 +339,7 @@ struct TPhash
hash_combine(seed, textureProperties.m_AnisotropicFilterEnabled);
hash_combine(seed, textureProperties.m_FormatOverride);
hash_combine(seed, textureProperties.m_IgnoreQuality);
hash_combine(seed, textureProperties.m_SRGB);
return seed;
}
@ -358,7 +362,8 @@ struct TPequal_to
lhs.m_AddressModeV == rhs.m_AddressModeV &&
lhs.m_AnisotropicFilterEnabled == rhs.m_AnisotropicFilterEnabled &&
lhs.m_FormatOverride == rhs.m_FormatOverride &&
lhs.m_IgnoreQuality == rhs.m_IgnoreQuality;
lhs.m_IgnoreQuality == rhs.m_IgnoreQuality &&
lhs.m_SRGB == rhs.m_SRGB;
}
bool operator()(const CTexturePtr& lhs, const CTexturePtr& rhs) const
@ -526,7 +531,8 @@ public:
}
else
{
format = ChooseFormatAndTransformTextureDataIfNeeded(m_Device, textureData, m_HasS3TC);
format = ChooseFormatAndTransformTextureDataIfNeeded(
m_Device, textureData, m_HasS3TC, texture->m_Properties.m_SRGB);
}
if (format == Renderer::Backend::Format::UNDEFINED)

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -245,6 +245,8 @@ public:
void SetIgnoreQuality(bool ignore) { m_IgnoreQuality = ignore; }
void SetSRGB(const bool sRGB) { m_SRGB = sRGB; }
private:
// Must update TPhash, TPequal_to when changing these fields
VfsPath m_Path;
@ -257,6 +259,7 @@ private:
Renderer::Backend::Format m_FormatOverride =
Renderer::Backend::Format::UNDEFINED;
bool m_IgnoreQuality = false;
bool m_SRGB{false};
};
/**

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -52,114 +52,111 @@ CVector3D GetPositionOnCubic(const CVector3D& startPos, const CVector3D& startVe
/*********************************** R N S **************************************************/
RNSpline::RNSpline()
: NodeCount(0)
{
}
RNSpline::RNSpline() = default;
RNSpline::~RNSpline() = default;
// adds node and updates segment length
void RNSpline::AddNode(const CFixedVector3D& pos)
{
if (NodeCount >= MAX_SPLINE_NODES)
if (m_NodeCount >= MAX_SPLINE_NODES)
return;
if (NodeCount == 0)
MaxDistance = fixed::Zero();
if (m_NodeCount == 0)
m_MaxDistance = fixed::Zero();
else
{
Node[NodeCount-1].Distance = (Node[NodeCount-1].Position - pos).Length();
MaxDistance += Node[NodeCount-1].Distance;
m_Nodes[m_NodeCount-1].Distance = (m_Nodes[m_NodeCount-1].Position - pos).Length();
m_MaxDistance += m_Nodes[m_NodeCount-1].Distance;
}
SplineData temp;
temp.Position = pos;
Node.push_back(temp);
++NodeCount;
m_Nodes.push_back(temp);
++m_NodeCount;
}
// called after all nodes added. This function calculates the node velocities
void RNSpline::BuildSpline()
{
if (NodeCount == 2)
if (m_NodeCount == 2)
{
Node[0].Velocity = GetStartVelocity(0);
Node[NodeCount-1].Velocity = GetEndVelocity(NodeCount-1);
m_Nodes[0].Velocity = GetStartVelocity(0);
m_Nodes[m_NodeCount-1].Velocity = GetEndVelocity(m_NodeCount-1);
return;
}
else if (NodeCount < 2)
else if (m_NodeCount < 2)
return;
for (int i = 1; i < NodeCount-1; ++i)
for (int i = 1; i < m_NodeCount-1; ++i)
{
CVector3D Next = Node[i+1].Position - Node[i].Position;
CVector3D Previous = Node[i-1].Position - Node[i].Position;
CVector3D Next = m_Nodes[i+1].Position - m_Nodes[i].Position;
CVector3D Previous = m_Nodes[i-1].Position - m_Nodes[i].Position;
Next.Normalize();
Previous.Normalize();
// split the angle (figure 4)
Node[i].Velocity = Next - Previous;
Node[i].Velocity.Normalize();
m_Nodes[i].Velocity = Next - Previous;
m_Nodes[i].Velocity.Normalize();
}
// calculate start and end velocities
Node[0].Velocity = GetStartVelocity(0);
Node[NodeCount-1].Velocity = GetEndVelocity(NodeCount-1);
m_Nodes[0].Velocity = GetStartVelocity(0);
m_Nodes[m_NodeCount-1].Velocity = GetEndVelocity(m_NodeCount-1);
}
// spline access function. time is 0 -> 1
CVector3D RNSpline::GetPosition(float time) const
{
if (NodeCount < 2)
if (m_NodeCount < 2)
return CVector3D(0.0f, 0.0f, 0.0f);
if (time < 0.0f)
time = 0.0f;
if (time > 1.0f)
time = 1.0f;
float Distance = time * MaxDistance.ToFloat();
float Distance = time * m_MaxDistance.ToFloat();
float CurrentDistance = 0.f;
int i = 0;
// Find which node we're on
while (CurrentDistance + Node[i].Distance.ToFloat() < Distance && i < NodeCount - 2)
while (CurrentDistance + m_Nodes[i].Distance.ToFloat() < Distance && i < m_NodeCount - 2)
{
CurrentDistance += Node[i].Distance.ToFloat();
CurrentDistance += m_Nodes[i].Distance.ToFloat();
++i;
}
ENSURE(i < NodeCount - 1);
ENSURE(i < m_NodeCount - 1);
float t = Distance - CurrentDistance;
// TODO: reimplement CVector3D comparator (float comparing is bad without EPS)
if (Node[i].Position == Node[i+1].Position || Node[i].Distance.ToFloat() < 1e-7) // distance too small or zero
if (m_Nodes[i].Position == m_Nodes[i+1].Position || m_Nodes[i].Distance.ToFloat() < 1e-7) // distance too small or zero
{
return Node[i+1].Position;
return m_Nodes[i+1].Position;
}
t /= Node[i].Distance.ToFloat(); // scale t in range 0 - 1
CVector3D startVel = Node[i].Velocity * Node[i].Distance.ToFloat();
CVector3D endVel = Node[i+1].Velocity * Node[i].Distance.ToFloat();
return GetPositionOnCubic(Node[i].Position, startVel,
Node[i+1].Position, endVel, t);
t /= m_Nodes[i].Distance.ToFloat(); // scale t in range 0 - 1
CVector3D startVel = m_Nodes[i].Velocity * m_Nodes[i].Distance.ToFloat();
CVector3D endVel = m_Nodes[i+1].Velocity * m_Nodes[i].Distance.ToFloat();
return GetPositionOnCubic(m_Nodes[i].Position, startVel,
m_Nodes[i+1].Position, endVel, t);
}
const std::vector<SplineData>& RNSpline::GetAllNodes() const
{
return Node;
return m_Nodes;
}
// internal. Based on Equation 14
CVector3D RNSpline::GetStartVelocity(int index)
{
if (index >= NodeCount - 1 || index < 0)
if (index >= m_NodeCount - 1 || index < 0)
return CVector3D(0.0f, 0.0f, 0.0f);
CVector3D temp = CVector3D(Node[index+1].Position - Node[index].Position) * 3.0f * (1.0f / Node[index].Distance.ToFloat());
return (temp - Node[index+1].Velocity)*0.5f;
CVector3D temp = CVector3D(m_Nodes[index+1].Position - m_Nodes[index].Position) * 3.0f * (1.0f / m_Nodes[index].Distance.ToFloat());
return (temp - m_Nodes[index+1].Velocity)*0.5f;
}
// internal. Based on Equation 15
CVector3D RNSpline::GetEndVelocity(int index)
{
if (index >= NodeCount || index < 1)
if (index >= m_NodeCount || index < 1)
return CVector3D(0.0f, 0.0f, 0.0f);
CVector3D temp = CVector3D(Node[index].Position - Node[index-1].Position) * 3.0f * (1.0f / Node[index-1].Distance.ToFloat());
return (temp - Node[index-1].Velocity) * 0.5f;
CVector3D temp = CVector3D(m_Nodes[index].Position - m_Nodes[index-1].Position) * 3.0f * (1.0f / m_Nodes[index-1].Distance.ToFloat());
return (temp - m_Nodes[index-1].Velocity) * 0.5f;
}
/*********************************** S N S **************************************************/
@ -176,21 +173,21 @@ void SNSpline::BuildSpline()
// smoothing filter.
void SNSpline::Smooth()
{
if (NodeCount < 3)
if (m_NodeCount < 3)
return;
CVector3D newVel;
CVector3D oldVel = GetStartVelocity(0);
for (int i = 1; i < NodeCount-1; ++i)
for (int i = 1; i < m_NodeCount-1; ++i)
{
// Equation 12
newVel = GetEndVelocity(i) * Node[i].Distance.ToFloat() + GetStartVelocity(i) * Node[i-1].Distance.ToFloat();
newVel = newVel * (1 / (Node[i-1].Distance + Node[i].Distance).ToFloat());
Node[i-1].Velocity = oldVel;
newVel = GetEndVelocity(i) * m_Nodes[i].Distance.ToFloat() + GetStartVelocity(i) * m_Nodes[i-1].Distance.ToFloat();
newVel = newVel * (1 / (m_Nodes[i-1].Distance + m_Nodes[i].Distance).ToFloat());
m_Nodes[i-1].Velocity = oldVel;
oldVel = newVel;
}
Node[NodeCount-1].Velocity = GetEndVelocity(NodeCount-1);
Node[NodeCount-2].Velocity = oldVel;
m_Nodes[m_NodeCount-1].Velocity = GetEndVelocity(m_NodeCount-1);
m_Nodes[m_NodeCount-2].Velocity = oldVel;
}
/*********************************** T N S **************************************************/
@ -201,15 +198,15 @@ TNSpline::~TNSpline() = default;
// ie time period is time from last node to this node
void TNSpline::AddNode(const CFixedVector3D& pos, const CFixedVector3D& rotation, fixed timePeriod)
{
if (NodeCount >= MAX_SPLINE_NODES)
if (m_NodeCount >= MAX_SPLINE_NODES)
return;
if (NodeCount == 0)
MaxDistance = fixed::Zero();
if (m_NodeCount == 0)
m_MaxDistance = fixed::Zero();
else
{
Node[NodeCount-1].Distance = timePeriod;
MaxDistance += Node[NodeCount-1].Distance;
m_Nodes[m_NodeCount-1].Distance = timePeriod;
m_MaxDistance += m_Nodes[m_NodeCount-1].Distance;
}
SplineData temp;
@ -219,56 +216,56 @@ void TNSpline::AddNode(const CFixedVector3D& pos, const CFixedVector3D& rotation
temp.Distance = fixed::Zero();
temp.Velocity = CVector3D(0.0f, 0.0f, 0.0f);
temp.Rotation = rotation;
Node.push_back(temp);
++NodeCount;
m_Nodes.push_back(temp);
++m_NodeCount;
}
//Inserts node before position
void TNSpline::InsertNode(const int index, const CFixedVector3D& pos, const CFixedVector3D& /*rotation*/,
fixed timePeriod)
{
if (NodeCount >= MAX_SPLINE_NODES || index < 0 || index > NodeCount)
if (m_NodeCount >= MAX_SPLINE_NODES || index < 0 || index > m_NodeCount)
return;
if (NodeCount == 0)
MaxDistance = fixed::Zero();
if (m_NodeCount == 0)
m_MaxDistance = fixed::Zero();
else
MaxDistance += timePeriod;
m_MaxDistance += timePeriod;
SplineData temp;
temp.Position = pos;
temp.Distance = timePeriod;
Node.insert(Node.begin() + index, temp);
m_Nodes.insert(m_Nodes.begin() + index, temp);
if (index > 0)
std::swap(Node[index].Distance, Node[index - 1].Distance);
++NodeCount;
std::swap(m_Nodes[index].Distance, m_Nodes[index - 1].Distance);
++m_NodeCount;
}
//Removes node at index
void TNSpline::RemoveNode(const int index)
{
if (NodeCount == 0 || index > NodeCount - 1)
if (m_NodeCount == 0 || index > m_NodeCount - 1)
return;
MaxDistance -= Node[index].Distance;
Node.erase(Node.begin() + index);
--NodeCount;
m_MaxDistance -= m_Nodes[index].Distance;
m_Nodes.erase(m_Nodes.begin() + index);
--m_NodeCount;
}
void TNSpline::UpdateNodeTime(const int index, fixed time)
{
if (NodeCount == 0 || index > NodeCount - 1)
if (m_NodeCount == 0 || index > m_NodeCount - 1)
return;
Node[index].Distance = time;
m_Nodes[index].Distance = time;
}
void TNSpline::UpdateNodePos(const int index, const CFixedVector3D& pos)
{
if (NodeCount == 0 || index > NodeCount - 1)
if (m_NodeCount == 0 || index > m_NodeCount - 1)
return;
Node[index].Position = pos;
m_Nodes[index].Position = pos;
}
void TNSpline::BuildSpline()
@ -289,15 +286,15 @@ void TNSpline::Smooth()
void TNSpline::Constrain()
{
if (NodeCount < 3)
if (m_NodeCount < 3)
return;
for (int i = 1; i < NodeCount-1; ++i)
for (int i = 1; i < m_NodeCount-1; ++i)
{
// Equation 13
float r0 = (Node[i].Position - Node[i - 1].Position).Length().ToFloat() / Node[i-1].Distance.ToFloat();
float r1 = (Node[i+1].Position - Node[i].Position).Length().ToFloat() / Node[i].Distance.ToFloat();
Node[i].Velocity *= 4.0f*r0*r1 / ((r0 + r1)*(r0 + r1));
float r0 = (m_Nodes[i].Position - m_Nodes[i - 1].Position).Length().ToFloat() / m_Nodes[i-1].Distance.ToFloat();
float r1 = (m_Nodes[i+1].Position - m_Nodes[i].Position).Length().ToFloat() / m_Nodes[i].Distance.ToFloat();
m_Nodes[i].Velocity *= 4.0f*r0*r1 / ((r0 + r1)*(r0 + r1));
}
}

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -63,12 +63,12 @@ public:
CVector3D GetRotation(float time) const;
const std::vector<SplineData>& GetAllNodes() const;
fixed MaxDistance;
int NodeCount;
fixed GetMaxDistance() const { return m_MaxDistance; }
protected:
std::vector<SplineData> Node;
fixed m_MaxDistance;
int m_NodeCount{0};
std::vector<SplineData> m_Nodes;
CVector3D GetStartVelocity(int index);
CVector3D GetEndVelocity(int index);
};

View file

@ -63,7 +63,7 @@ void CNetClientTurnManager::PostCommand(JS::HandleValue data)
// TODO: we should do this when the server stops sending our commands back to us
}
void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn)
void CNetClientTurnManager::NotifyFinishedOwnCommands(turn_id_t turn)
{
NETCLIENTTURN_LOG("NotifyFinishedOwnCommands(%d)\n", turn);
@ -79,7 +79,7 @@ void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn)
m_NetClient.SendMessage(&msg);
}
void CNetClientTurnManager::NotifyFinishedUpdate(u32 turn, const UpdateCallback&)
void CNetClientTurnManager::NotifyFinishedUpdate(turn_id_t turn, const UpdateCallback&)
{
bool quick = !TurnNeedsFullHash(turn);
std::string hash;
@ -110,10 +110,10 @@ void CNetClientTurnManager::OnDestroyConnection()
void CNetClientTurnManager::OnSimulationMessage(CSimulationMessage* msg)
{
// Command received from the server - store it for later execution
AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, msg->m_Turn);
AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, static_cast<turn_id_t>(msg->m_Turn));
}
void CNetClientTurnManager::OnSyncError(u32 turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames)
void CNetClientTurnManager::OnSyncError(turn_id_t turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames)
{
CStr expectedHashHex(Hexify(expectedHash));
NETCLIENTTURN_LOG("OnSyncError(%d, %hs)\n", turn, expectedHashHex.c_str());

View file

@ -49,12 +49,12 @@ public:
*/
void OnDestroyConnection();
void OnSyncError(u32 turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames);
void OnSyncError(turn_id_t turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames);
private:
void NotifyFinishedOwnCommands(u32 turn) override;
void NotifyFinishedOwnCommands(turn_id_t turn) override;
void NotifyFinishedUpdate(u32 turn, const UpdateCallback& sendEventToAll) override;
void NotifyFinishedUpdate(turn_id_t turn, const UpdateCallback& sendEventToAll) override;
CNetClient& m_NetClient;
};

View file

@ -139,7 +139,7 @@ public:
u32 m_Client;
i32 m_Player;
u32 m_Turn;
i32 m_Turn;
JS::PersistentRooted<JS::Value> m_Data;
private:
const Script::Interface& m_ScriptInterface;

View file

@ -150,7 +150,7 @@ u8* CSimulationMessage::Serialize(u8* pBuffer) const
CBufferBinarySerializer serializer(m_ScriptInterface, pos);
serializer.NumberU32_Unbounded("client", m_Client);
serializer.NumberI32_Unbounded("player", m_Player);
serializer.NumberU32_Unbounded("turn", m_Turn);
serializer.NumberI32_Unbounded("turn", m_Turn);
serializer.ScriptVal("command", const_cast<JS::PersistentRootedValue*>(&m_Data));
return serializer.GetBuffer();
@ -165,7 +165,7 @@ const u8* CSimulationMessage::Deserialize(const u8* pStart, const u8* pEnd)
CStdDeserializer deserializer(m_ScriptInterface, stream);
deserializer.NumberU32_Unbounded("client", m_Client);
deserializer.NumberI32_Unbounded("player", m_Player);
deserializer.NumberU32_Unbounded("turn", m_Turn);
deserializer.NumberI32_Unbounded("turn", m_Turn);
deserializer.ScriptVal("command", &m_Data);
return pEnd;
}
@ -177,7 +177,7 @@ size_t CSimulationMessage::GetSerializedLength() const
CLengthBinarySerializer serializer(m_ScriptInterface);
serializer.NumberU32_Unbounded("client", m_Client);
serializer.NumberI32_Unbounded("player", m_Player);
serializer.NumberU32_Unbounded("turn", m_Turn);
serializer.NumberI32_Unbounded("turn", m_Turn);
// TODO: The cast can probably be removed if and when ScriptVal can take a JS::HandleValue instead of
// a JS::MutableHandleValue (relies on JSAPI change). Also search for other casts like this one in that case.

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -27,7 +27,7 @@
#define PS_PROTOCOL_MAGIC 0x5073013f // 'P', 's', 0x01, '?'
#define PS_PROTOCOL_MAGIC_RESPONSE 0x50630121 // 'P', 'c', 0x01, '!'
#define PS_PROTOCOL_VERSION 0x01010019 // Arbitrary protocol
#define PS_PROTOCOL_VERSION 0x0101001A // Arbitrary protocol
#define PS_DEFAULT_PORT 0x5073 // 'P', 's'
// Set when lobby authentication is required. Used in the SrvHandshakeResponseMessage.
@ -231,7 +231,7 @@ START_NMT_CLASS_(ClientPaused, NMT_CLIENT_PAUSED)
END_NMT_CLASS()
START_NMT_CLASS_(LoadedGame, NMT_LOADED_GAME)
NMT_FIELD_INT(m_CurrentTurn, u32, 4)
NMT_FIELD_INT(m_CurrentTurn, i32, 4)
END_NMT_CLASS()
START_NMT_CLASS_(GameStart, NMT_GAME_START)
@ -243,17 +243,17 @@ START_NMT_CLASS_(GameSavedStart, NMT_SAVED_GAME_START)
END_NMT_CLASS()
START_NMT_CLASS_(EndCommandBatch, NMT_END_COMMAND_BATCH)
NMT_FIELD_INT(m_Turn, u32, 4)
NMT_FIELD_INT(m_Turn, i32, 4)
NMT_FIELD_INT(m_TurnLength, u32, 2)
END_NMT_CLASS()
START_NMT_CLASS_(SyncCheck, NMT_SYNC_CHECK)
NMT_FIELD_INT(m_Turn, u32, 4)
NMT_FIELD_INT(m_Turn, i32, 4)
NMT_FIELD(CStr, m_Hash)
END_NMT_CLASS()
START_NMT_CLASS_(SyncError, NMT_SYNC_ERROR)
NMT_FIELD_INT(m_Turn, u32, 4)
NMT_FIELD_INT(m_Turn, i32, 4)
NMT_FIELD(CStr, m_HashExpected)
NMT_START_ARRAY(m_PlayerNames)
NMT_FIELD(CStrW, m_Name)

View file

@ -49,6 +49,7 @@
#include "scriptinterface/Context.h"
#include "scriptinterface/Interface.h"
#include "scriptinterface/Request.h"
#include "simulation2/helpers/SimulationCommand.h"
#include "simulation2/system/TurnManager.h"
#include <algorithm>
@ -1172,9 +1173,9 @@ bool CNetServerWorker::OnSimulationCommand(CNetServerSession* session, CFsmEvent
server.Multicast(message, { NSS_INGAME });
// Save all the received commands
if (server.m_SavedCommands.size() < message->m_Turn + 1)
server.m_SavedCommands.resize(message->m_Turn + 1);
server.m_SavedCommands[message->m_Turn].push_back(*message);
if (server.m_SavedCommands.size() < static_cast<size_t>(message->m_Turn) + 1)
server.m_SavedCommands.resize(static_cast<size_t>(message->m_Turn) + 1);
server.m_SavedCommands[static_cast<size_t>(message->m_Turn)].push_back(*message);
// TODO: we shouldn't send the message back to the client that first sent it
return true;
@ -1396,14 +1397,14 @@ bool CNetServerWorker::OnJoinSyncingLoadedGame(CNetServerSession* session, CFsmE
CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
u32 turn = message->m_CurrentTurn;
u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
turn_id_t turn = message->m_CurrentTurn;
turn_id_t readyTurn = server.m_ServerTurnManager->GetReadyTurn();
// Send them all commands received since their saved state,
// and turn-ended messages for any turns that have already been processed
for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i)
for (turn_id_t i = turn + 1; i < std::max(readyTurn + 1, static_cast<turn_id_t>(server.m_SavedCommands.size())); ++i)
{
if (i < server.m_SavedCommands.size())
if (static_cast<size_t>(i) < server.m_SavedCommands.size())
for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
session->SendMessage(&server.m_SavedCommands[i][j]);

View file

@ -27,6 +27,7 @@
#include "network/NetServerSession.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "simulation2/helpers/SimulationCommand.h"
#include "simulation2/system/TurnManager.h"
#include <limits>
@ -50,7 +51,7 @@ CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server)
m_SavedTurnLengths.push_back(m_TurnLength);
}
void CNetServerTurnManager::NotifyFinishedClientCommands(CNetServerSession& session, u32 turn)
void CNetServerTurnManager::NotifyFinishedClientCommands(CNetServerSession& session, turn_id_t turn)
{
int client = session.GetHostID();
@ -105,11 +106,11 @@ void CNetServerTurnManager::CheckClientsReady()
msg.m_Turn = m_ReadyTurn;
m_NetServer.Multicast(&msg, { NSS_INGAME });
ENSURE(m_SavedTurnLengths.size() == m_ReadyTurn);
ENSURE(std::cmp_equal(m_SavedTurnLengths.size(), m_ReadyTurn));
m_SavedTurnLengths.push_back(m_TurnLength);
}
void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& session, u32 turn, const CStr& hash)
void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& session, turn_id_t turn, const CStr& hash)
{
int client = session.GetHostID();
@ -138,13 +139,13 @@ void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& sessio
m_ClientStateHashes[turn][client] = hash;
// Find the newest turn which we know all clients have simulated
u32 newest = std::numeric_limits<u32>::max();
turn_id_t newest = std::numeric_limits<turn_id_t>::max();
for (const std::pair<const int, Client>& clientData : m_ClientsData)
if (clientData.second.simulatedTurn < newest)
newest = clientData.second.simulatedTurn;
// For every set of state hashes that all clients have simulated, check for OOS
for (const std::pair<const u32, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
for (const std::pair<const turn_id_t, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
{
if (clientStateHash.first > newest)
break;
@ -187,7 +188,7 @@ void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& sessio
m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1));
}
void CNetServerTurnManager::InitialiseClient(int client, u32 turn, bool observer)
void CNetServerTurnManager::InitialiseClient(int client, turn_id_t turn, bool observer)
{
NETSERVERTURN_LOG("InitialiseClient(client=%d, turn=%d)\n", client, turn);
@ -206,7 +207,7 @@ void CNetServerTurnManager::UninitialiseClient(int client)
bool checkOOS = m_ClientsData[client].isOOS;
m_ClientsData.erase(client);
for (std::pair<const u32, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
for (std::pair<const turn_id_t, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
clientStateHash.second.erase(client);
// Check whether we're ready for the next turn now that we're not
@ -228,8 +229,8 @@ void CNetServerTurnManager::SetTurnLength(u32 msecs)
m_TurnLength = msecs;
}
u32 CNetServerTurnManager::GetSavedTurnLength(u32 turn)
u32 CNetServerTurnManager::GetSavedTurnLength(turn_id_t turn)
{
ENSURE(turn <= m_ReadyTurn);
return m_SavedTurnLengths.at(turn);
return m_SavedTurnLengths.at(static_cast<size_t>(turn));
}

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -21,6 +21,7 @@
#include "lib/code_annotation.h"
#include "lib/types.h"
#include "ps/CStr.h"
#include "simulation2/helpers/SimulationCommand.h"
#include <map>
#include <string>
@ -44,15 +45,15 @@ class CNetServerTurnManager
public:
CNetServerTurnManager(CNetServerWorker& server);
void NotifyFinishedClientCommands(CNetServerSession& session, u32 turn);
void NotifyFinishedClientCommands(CNetServerSession& session, turn_id_t turn);
void NotifyFinishedClientUpdate(CNetServerSession& session, u32 turn, const CStr& hash);
void NotifyFinishedClientUpdate(CNetServerSession& session, turn_id_t turn, const CStr& hash);
/**
* Inform the turn manager of a new client
* @param observer - whether this client is an observer.
*/
void InitialiseClient(int client, u32 turn, bool observer);
void InitialiseClient(int client, turn_id_t turn, bool observer);
/**
* Inform the turn manager that a previously-initialised client has left the game.
@ -65,13 +66,13 @@ public:
* Returns the latest turn for which all clients are ready;
* they will have already been told to execute this turn.
*/
u32 GetReadyTurn() { return m_ReadyTurn; }
turn_id_t GetReadyTurn() { return m_ReadyTurn; }
/**
* Returns the turn length that was used for the given turn.
* Requires turn <= GetReadyTurn().
*/
u32 GetSavedTurnLength(u32 turn);
u32 GetSavedTurnLength(turn_id_t turn);
private:
void CheckClientsReady();
@ -80,9 +81,9 @@ private:
{
CStrW playerName;
// Latest turn for which all commands have been received.
u32 readyTurn;
turn_id_t readyTurn;
// Last known simulated turn.
u32 simulatedTurn;
turn_id_t simulatedTurn;
bool isObserver;
bool isOOS = false;
};
@ -93,10 +94,10 @@ private:
bool m_HasSyncError = false;
// Map of turn -> {Client ID -> state hash}; old indexes <= min(m_ClientsSimulated) are deleted
std::map<u32, std::map<int, std::string>> m_ClientStateHashes;
std::map<turn_id_t, std::map<int, std::string>> m_ClientStateHashes;
/// The latest turn for which we have received all commands from all clients
u32 m_ReadyTurn;
turn_id_t m_ReadyTurn;
// Current turn length
u32 m_TurnLength;

View file

@ -90,7 +90,6 @@ CGame::CGame(bool replayLog, const SimulationDebugOptions debugOptions):
m_SimRate(1.0f),
m_PlayerID(-1),
m_ViewedPlayerID(-1),
m_IsSavedGame(false),
m_IsVisualReplay(false),
m_ReplayStream(NULL)
{
@ -224,7 +223,7 @@ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& saved
const Script::Interface& scriptInterface = m_Simulation2->GetScriptInterface();
Script::Request rq(scriptInterface);
m_IsSavedGame = !savedState.empty();
const bool isSavedGame{!savedState.empty()};
m_Simulation2->SetInitAttributes(attribs);
@ -285,7 +284,7 @@ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& saved
co_return g_Renderer.GetSceneRenderer().GetWaterManager().LoadWaterTextures();
}, L"LoadWaterTextures", 80);
if (m_IsSavedGame)
if (isSavedGame)
PS::Loader::Register(std::bind_front(
[](CGame* game, const std::string& state) -> PS::Loader::Task
{
@ -298,13 +297,32 @@ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& saved
co_return game->LoadVisualReplayData();
}, this), L"Loading visual replay data", 1000);
// Call the script function InitGame only for new games, not saved games
if (!isSavedGame)
{
// Perform some simulation initializations (replace skirmish entities, explore territories, etc.)
// that needs to be done before setting up the AI and shouldn't be done in Atlas
if (!g_AtlasGameLoop->running)
{
PS::Loader::Register(std::bind_front([](CGame* game) -> PS::Loader::Task
{
game->m_Simulation2->PreInitGame();
co_return 0;
}, this), L"PreInitGame", 5000);
}
PS::Loader::Register(std::bind_front([](CGame* game) -> PS::Loader::Task
{
game->m_Simulation2->InitGame();
co_return 0;
}, this), L"InitGame", 4000);
}
PS::Loader::EndRegistering();
}
int CGame::LoadInitialState(const std::string& savedState)
{
ENSURE(m_IsSavedGame);
std::stringstream stream(savedState);
bool ok = m_Simulation2->DeserializeState(stream);
@ -324,17 +342,6 @@ int CGame::LoadInitialState(const std::string& savedState)
**/
PSRETURN CGame::ReallyStartGame()
{
// Call the script function InitGame only for new games, not saved games
if (!m_IsSavedGame)
{
// Perform some simulation initializations (replace skirmish entities, explore territories, etc.)
// that needs to be done before setting up the AI and shouldn't be done in Atlas
if (!g_AtlasGameLoop->running)
m_Simulation2->PreInitGame();
m_Simulation2->InitGame();
}
// We need to do an initial Interpolate call to set up all the models etc,
// because Update might never interpolate (e.g. if the game starts paused)
// and we could end up rendering before having set up any models (so they'd

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -226,7 +226,6 @@ private:
std::vector<CColor> m_PlayerColors;
int LoadInitialState(const std::string& savedState);
bool m_IsSavedGame; // true if loading a saved game; false for a new game
bool m_CheatsEnabled;

View file

@ -826,7 +826,7 @@ bool Autostart(const CmdLineArgs& args)
Script::GetPropertyInt(rq, pageData, 1, &pageArgs);
Script::StructuredClone clonedpageArgs{Script::WriteStructuredClone(rq, pageArgs)};
g_GUI->OpenChildPage(pageName, std::move(clonedpageArgs));
g_GUI->SwitchPage(pageName, &scriptInterface, pageArgs);
return true;
}

View file

@ -215,7 +215,7 @@ void CPostprocManager::InitializePBR()
LOGWARNING("%s is unsupported", framebufferFormatName);
}
if (m_Device->GetCapabilities().computeShaders && m_Device->GetCapabilities().storage)
if (m_Device->GetCapabilities().computeShaders)
{
m_ResolvePBRComputeTech = g_Renderer.GetShaderManager().LoadEffect(str_compute_resolve_pbr);
if (m_ResolvePBRComputeTech)

View file

@ -32,8 +32,10 @@ enum class Format
R8G8_UINT,
R8G8B8_UNORM,
R8G8B8A8_UNORM,
R8G8B8A8_SRGB,
R8G8B8A8_UINT,
B8G8R8A8_UNORM,
B8G8R8A8_SRGB,
// TODO: we need to drop legacy A8 and L8 formats as soon as we have proper
// channel swizzling.
@ -65,9 +67,13 @@ enum class Format
D32_SFLOAT_S8_UINT,
BC1_RGB_UNORM,
BC1_RGB_SRGB,
BC1_RGBA_UNORM,
BC1_RGBA_SRGB,
BC2_UNORM,
BC3_UNORM
BC2_SRGB,
BC3_UNORM,
BC3_SRGB,
};
inline bool IsDepthFormat(const Format format)

View file

@ -352,11 +352,11 @@ std::unique_ptr<IDevice> CDevice::Create(SDL_Window* window)
#if CONFIG2_GLES
// Some GLES implementations have GL_EXT_texture_compression_dxt1
// but that only supports DXT1 so we can't use it.
capabilities.S3TC = GLAD_GL_EXT_texture_compression_s3tc;
capabilities.S3TC = GLAD_GL_EXT_texture_compression_s3tc && GLAD_GL_EXT_texture_compression_s3tc_srgb;
#else
// Note: we don't bother checking for GL_S3_s3tc - it is incompatible
// and irrelevant (was never widespread).
capabilities.S3TC = GLAD_GL_EXT_texture_compression_s3tc;
capabilities.S3TC = GLAD_GL_EXT_texture_compression_s3tc && GLAD_GL_EXT_texture_sRGB;
#endif
#if CONFIG2_GLES
capabilities.multisampling = false;
@ -738,6 +738,7 @@ bool CDevice::IsTextureFormatSupported(const Format format) const
case Format::R8G8B8_UNORM:
case Format::R8G8B8A8_UNORM:
case Format::R8G8B8A8_SRGB:
case Format::A8_UNORM:
case Format::L8_UNORM:
supported = true;
@ -763,9 +764,13 @@ bool CDevice::IsTextureFormatSupported(const Format format) const
break;
case Format::BC1_RGB_UNORM:
case Format::BC1_RGB_SRGB:
case Format::BC1_RGBA_UNORM:
case Format::BC1_RGBA_SRGB:
case Format::BC2_UNORM:
case Format::BC2_SRGB:
case Format::BC3_UNORM:
case Format::BC3_SRGB:
supported = m_Capabilities.S3TC;
break;

View file

@ -348,7 +348,8 @@ void CDeviceCommandContext::UploadTextureRegion(
GLenum pixelFormat = GL_RGBA;
switch (dataFormat)
{
case Format::R8G8B8A8_UNORM:
case Format::R8G8B8A8_UNORM: [[fallthrough]];
case Format::R8G8B8A8_SRGB:
break;
case Format::R8G8B8_UNORM:
pixelFormat = GL_RGB;
@ -381,9 +382,13 @@ void CDeviceCommandContext::UploadTextureRegion(
}
else if (
texture->GetFormat() == Format::BC1_RGB_UNORM ||
texture->GetFormat() == Format::BC1_RGB_SRGB ||
texture->GetFormat() == Format::BC1_RGBA_UNORM ||
texture->GetFormat() == Format::BC1_RGBA_SRGB ||
texture->GetFormat() == Format::BC2_UNORM ||
texture->GetFormat() == Format::BC3_UNORM)
texture->GetFormat() == Format::BC2_SRGB ||
texture->GetFormat() == Format::BC3_UNORM ||
texture->GetFormat() == Format::BC3_SRGB)
{
ENSURE(xOffset == 0 && yOffset == 0);
ENSURE(texture->GetFormat() == dataFormat);
@ -392,15 +397,27 @@ void CDeviceCommandContext::UploadTextureRegion(
GLenum internalFormat = GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
switch (texture->GetFormat())
{
case Format::BC1_RGB_SRGB:
internalFormat = GL_COMPRESSED_SRGB_S3TC_DXT1_EXT;
break;
case Format::BC1_RGBA_UNORM:
internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
break;
case Format::BC1_RGBA_SRGB:
internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;
break;
case Format::BC2_UNORM:
internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
break;
case Format::BC2_SRGB:
internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;
break;
case Format::BC3_UNORM:
internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
break;
case Format::BC3_SRGB:
internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;
break;
default:
break;
}

View file

@ -174,6 +174,12 @@ std::unique_ptr<CTexture> CTexture::Create(
// pixel format.
GLenum pixelFormat = GL_RGBA;
GLenum pixelType = GL_UNSIGNED_BYTE;
#if !CONFIG2_GLES
// load/store operation requires sized formats. See Table 8.25:
// https://registry.khronos.org/OpenGL/specs/gl/glspec43.core.pdf
if (usage & ITexture::Usage::STORAGE)
internalFormat = GL_RGBA8;
#endif
switch (format)
{
case Format::UNDEFINED:
@ -181,6 +187,9 @@ std::unique_ptr<CTexture> CTexture::Create(
break;
case Format::R8G8B8A8_UNORM:
break;
case Format::R8G8B8A8_SRGB:
internalFormat = GL_SRGB_ALPHA_EXT;
break;
case Format::R8G8B8_UNORM:
internalFormat = GL_RGB;
pixelFormat = GL_RGB;
@ -248,9 +257,13 @@ std::unique_ptr<CTexture> CTexture::Create(
break;
#endif
case Format::BC1_RGB_UNORM:
case Format::BC1_RGB_SRGB:
case Format::BC1_RGBA_UNORM:
case Format::BC1_RGBA_SRGB:
case Format::BC2_UNORM:
case Format::BC2_SRGB:
case Format::BC3_UNORM:
case Format::BC3_SRGB:
compressedFormat = true;
break;
default:

View file

@ -847,9 +847,13 @@ bool CDevice::IsTextureFormatSupported(const Format format) const
return false;
case Format::BC1_RGB_UNORM:
case Format::BC1_RGB_SRGB:
case Format::BC1_RGBA_UNORM:
case Format::BC1_RGBA_SRGB:
case Format::BC2_UNORM:
case Format::BC2_SRGB:
case Format::BC3_UNORM:
case Format::BC3_SRGB:
if (m_Capabilities.S3TC)
return true;
else

View file

@ -174,8 +174,10 @@ VkFormat FromFormat(const Format format)
CASE(R8G8_UNORM)
CASE(R8G8_UINT)
CASE(R8G8B8A8_UNORM)
CASE(R8G8B8A8_SRGB)
CASE(R8G8B8A8_UINT)
CASE(B8G8R8A8_UNORM)
CASE(B8G8R8A8_SRGB)
CASE(R16_UNORM)
CASE(R16_UINT)
@ -204,9 +206,13 @@ VkFormat FromFormat(const Format format)
CASE(D32_SFLOAT)
CASE2(BC1_RGB_UNORM, BC1_RGB_UNORM_BLOCK)
CASE2(BC1_RGB_SRGB, BC1_RGB_SRGB_BLOCK)
CASE2(BC1_RGBA_UNORM, BC1_RGBA_UNORM_BLOCK)
CASE2(BC1_RGBA_SRGB, BC1_RGBA_SRGB_BLOCK)
CASE2(BC2_UNORM, BC2_UNORM_BLOCK)
CASE2(BC2_SRGB, BC2_SRGB_BLOCK)
CASE2(BC3_UNORM, BC3_UNORM_BLOCK)
CASE2(BC3_SRGB, BC3_SRGB_BLOCK)
#undef CASE
#undef CASE2

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -179,13 +179,18 @@ void CRingCommandContext::ScheduleUpload(
const bool isCompressedFormat =
format == Format::BC1_RGB_UNORM ||
format == Format::BC1_RGB_SRGB ||
format == Format::BC1_RGBA_UNORM ||
format == Format::BC1_RGBA_SRGB ||
format == Format::BC2_UNORM ||
format == Format::BC3_UNORM;
format == Format::BC2_SRGB ||
format == Format::BC3_UNORM ||
format == Format::BC3_SRGB;
ENSURE(
format == Format::R8_UNORM ||
format == Format::R8G8_UNORM ||
format == Format::R8G8B8A8_UNORM ||
format == Format::R8G8B8A8_SRGB ||
format == Format::A8_UNORM ||
format == Format::L8_UNORM ||
isCompressedFormat);

View file

@ -92,13 +92,13 @@ public:
return serializationTestOption ? serializationTestOption->turn :
std::max(CConfigDB::GetIfInitialised("serializationtest", -1), -1);
}()},
m_RejoinTestTurn{[&]() -> std::optional<int>
m_RejoinTestTurn{[&]() -> std::optional<turn_id_t>
{
const auto* rejoinTestOption{
std::get_if<SimulationDebugOptions::RejoinTest>(&debugOptions.test)};
if (rejoinTestOption)
return rejoinTestOption->turn;
const int configVal{CConfigDB::GetIfInitialised("rejointest", -1)};
const turn_id_t configVal{CConfigDB::GetIfInitialised("rejointest", -1)};
if (configVal >= 0)
return configVal;
return std::nullopt;
@ -163,17 +163,17 @@ public:
std::set<VfsPath> m_LoadedScripts;
uint32_t m_TurnNumber;
turn_id_t m_TurnNumber;
bool m_EnableOOSLog{false};
OsPath m_OOSLogPath;
// Functions and data for the serialization test mode: (see Update() for relevant comments)
std::optional<int> m_SerializationTestTurn;
std::optional<turn_id_t> m_SerializationTestTurn;
bool m_TestingSerialization{false};
bool m_EnableSerializationTest{false};
std::optional<int> m_RejoinTestTurn;
std::optional<turn_id_t> m_RejoinTestTurn;
bool m_TestingRejoin{false};
// Secondary simulation (NB: order matters for destruction).
@ -383,7 +383,7 @@ void CSimulation2Impl::InitRNGSeedAI()
void CSimulation2Impl::Update(int turnLength, const std::vector<SimulationCommand>& commands)
{
PROFILE3("sim update");
PROFILE2_ATTR("turn %d", (int)m_TurnNumber);
PROFILE2_ATTR("turn %d", m_TurnNumber);
fixed turnLengthFixed = fixed::FromInt(turnLength) / 1000;
@ -408,11 +408,11 @@ void CSimulation2Impl::Update(int turnLength, const std::vector<SimulationComman
const Script::Interface& scriptInterface = m_ComponentManager.GetScriptInterface();
const bool startSerializationTest = m_SerializationTestTurn.has_value() &&
std::cmp_equal(m_SerializationTestTurn.value(), m_TurnNumber);
m_SerializationTestTurn.value() == m_TurnNumber;
if (startSerializationTest)
m_TestingSerialization = true;
const bool startRejoinTest = m_RejoinTestTurn.has_value() &&
static_cast<int64_t>(m_RejoinTestTurn.value()) == m_TurnNumber;
m_RejoinTestTurn.value() == m_TurnNumber;
if (startRejoinTest)
m_TestingRejoin = true;
@ -891,11 +891,11 @@ bool CSimulation2::DeserializeState(std::istream& stream)
return m->m_ComponentManager.DeserializeState(stream);
}
void CSimulation2::ActivateRejoinTest(int turn)
void CSimulation2::ActivateRejoinTest(turn_id_t turn)
{
if (m->m_RejoinTestTurn.has_value())
return;
LOGMESSAGERENDER("Rejoin test will activate in %i turns", turn - m->m_TurnNumber);
LOGMESSAGERENDER("Rejoin test will activate in %d turns", turn - m->m_TurnNumber);
m->m_RejoinTestTurn = turn;
}

View file

@ -22,6 +22,7 @@
#include "lib/file/vfs/vfs_path.h"
#include "lib/status.h"
#include "ps/Loader.h"
#include "simulation2/helpers/SimulationCommand.h"
#include "simulation2/system/DebugOptions.h"
#include "simulation2/system/Entity.h"
@ -240,7 +241,7 @@ public:
/**
* Activate the rejoin-test feature for turn @param turn.
*/
void ActivateRejoinTest(int turn);
void ActivateRejoinTest(turn_id_t turn);
std::string GenerateSchema();

View file

@ -277,6 +277,35 @@ struct EntityData
static_assert(sizeof(EntityData) == 24);
// Convert world-space circle to tile-space bounds
struct TileCircle
{
std::int32_t centerX, centerZ;
std::int32_t radius;
std::int32_t squaredRadius;
std::int32_t minX, maxX, minZ, maxZ;
};
// Apply an action to every tile within the circle
template<typename Func>
void ForEachTileInCircle(const TileCircle& circle, Func&& func)
{
for (std::int32_t tileZ = circle.minZ; tileZ <= circle.maxZ; ++tileZ)
{
const std::int32_t deltaZ{tileZ - circle.centerZ};
const std::int32_t squaredDeltaZ{deltaZ * deltaZ};
for (std::int32_t tileX = circle.minX; tileX <= circle.maxX; ++tileX)
{
const std::int32_t deltaX{tileX - circle.centerX};
if (deltaX * deltaX + squaredDeltaZ > circle.squaredRadius)
continue;
func(tileX, tileZ);
}
}
}
/**
* Functor for sorting entities by distance from a source point.
* It must only be passed entities that are in 'entities'
@ -2130,6 +2159,50 @@ public:
SeeExploredEntities(p);
}
TileCircle WorldToTileCircle(entity_pos_t worldX, entity_pos_t worldZ, fixed radius)
{
const std::int32_t centerTileX{(worldX / LOS_TILE_SIZE).ToInt_RoundToNearest()};
const std::int32_t centerTileZ{(worldZ / LOS_TILE_SIZE).ToInt_RoundToNearest()};
const std::int32_t tileRadius{(radius / LOS_TILE_SIZE).ToInt_RoundToInfinity()};
return TileCircle{
.centerX = centerTileX,
.centerZ = centerTileZ,
.radius = tileRadius,
.squaredRadius = tileRadius * tileRadius,
.minX = std::max(centerTileX - tileRadius, 0),
.maxX = std::min(centerTileX + tileRadius, m_LosVerticesPerSide - 1),
.minZ = std::max(centerTileZ - tileRadius, 0),
.maxZ = std::min(centerTileZ + tileRadius, m_LosVerticesPerSide - 1)
};
}
void ExploreCircle(player_id_t playerId, entity_pos_t worldX, entity_pos_t worldZ, fixed radius) override
{
if (playerId <= 0 || playerId > MAX_LOS_PLAYER_ID)
return;
const TileCircle circle{WorldToTileCircle(worldX, worldZ, radius)};
const u32 exploredBitMask{static_cast<u32>(LosState::EXPLORED) << (2 * (playerId - 1))};
u32& exploredCount = m_ExploredVertices.at(playerId);
ForEachTileInCircle(circle,
[&](std::int32_t tileX, std::int32_t tileZ)
{
if (LosIsOffWorld(tileX, tileZ))
return;
u32& losState = m_LosState.get(tileX, tileZ);
if (losState & exploredBitMask)
return;
losState |= exploredBitMask;
++exploredCount;
MarkVisibilityDirtyAroundTile(playerId, tileX, tileZ);
}
);
}
/**
* Force any entity in explored territory to appear for player p.
* This is useful for miraging entities inside the territory borders at the beginning of a game,

View file

@ -62,6 +62,7 @@ DEFINE_INTERFACE_METHOD("GetGaiaAndNonGaiaEntities", ICmpRangeManager, GetGaiaAn
DEFINE_INTERFACE_METHOD("SetDebugOverlay", ICmpRangeManager, SetDebugOverlay)
DEFINE_INTERFACE_METHOD("ExploreMap", ICmpRangeManager, ExploreMap)
DEFINE_INTERFACE_METHOD("ExploreTerritories", ICmpRangeManager, ExploreTerritories)
DEFINE_INTERFACE_METHOD("ExploreCircle", ICmpRangeManager, ExploreCircle)
DEFINE_INTERFACE_METHOD("SetLosRevealWholeMap", ICmpRangeManager, SetLosRevealWholeMap)
DEFINE_INTERFACE_METHOD("GetLosRevealWholeMap", ICmpRangeManager, GetLosRevealWholeMap)
DEFINE_INTERFACE_METHOD("SetLosRevealWholeMapForAll", ICmpRangeManager, SetLosRevealWholeMapForAll)

Some files were not shown because too many files have changed in this diff Show more