mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
Compare commits
34 commits
c62f5808ca
...
a58845d1c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a58845d1c2 | ||
|
|
853472586f | ||
|
|
c5dffb0dc9 | ||
|
|
887694d708 | ||
|
|
6ffc251114 | ||
|
|
0f939b8abf | ||
|
|
bf0c3af75a | ||
|
|
497efe5a99 | ||
|
|
609efa7e32 | ||
|
|
9adb3e7889 | ||
|
|
2c4f7d85b6 | ||
|
|
8918cfb0a7 | ||
|
|
1b07b799ec | ||
|
|
38b33b0484 | ||
|
|
ceca608848 | ||
|
|
f84b51212a | ||
|
|
fc3c0d7876 | ||
|
|
db23584fc3 | ||
|
|
f12975e9b9 | ||
|
|
2bc895bf0d | ||
|
|
3c215aff47 | ||
|
|
121d428ebb | ||
|
|
a8426f06a9 | ||
|
|
57a5740cce | ||
|
|
756b9b68c5 | ||
|
|
9f6a309434 | ||
|
|
1c2dbf2eb0 | ||
|
|
df72c1aad1 | ||
|
|
40ab70b804 | ||
|
|
f90804c78f | ||
|
|
d6bdf51d83 | ||
|
|
ae936e8177 | ||
|
|
06e199b12a | ||
|
|
84567e66c2 |
98 changed files with 2177 additions and 386 deletions
|
|
@ -3,22 +3,27 @@ name: pre-commit
|
|||
on:
|
||||
- push
|
||||
- pull_request
|
||||
env:
|
||||
PRE_COMMIT_VERSION: 4.6.0
|
||||
jobs:
|
||||
build:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- id: restore-pip-cache
|
||||
uses: actions/cache/restore@v6
|
||||
- uses: actions/cache@v6
|
||||
with:
|
||||
key: pip-cache-v1-${{ github.workflow }}
|
||||
key: pip-cache-v1-${{github.workflow}}-${{env.pythonLocation}}-${{env.PRE_COMMIT_VERSION}}
|
||||
path: ~/.cache/pip
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
- uses: actions/cache/save@v6
|
||||
if: steps.restore-pip-cache.outcome == 'success'
|
||||
- run: python -m pip install pre-commit=="${{ env.PRE_COMMIT_VERSION }}"
|
||||
shell: bash
|
||||
- run: python -m pip freeze --local
|
||||
shell: bash
|
||||
- uses: actions/cache@v6
|
||||
with:
|
||||
key: pip-cache-v1-${{ github.workflow }}
|
||||
path: ~/.cache/pip
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- run: pre-commit run --show-diff-on-failure --color=always --all-files
|
||||
shell: bash
|
||||
|
|
|
|||
157
CMakeLists.txt
Normal file
157
CMakeLists.txt
Normal 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()
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
var g_IncompatibleModsFile = "gui/incompatible_mods/incompatible_mods.txt";
|
||||
/* eslint-disable prefer-const -- Mods should be able to change it */
|
||||
let g_IncompatibleModsFile = "gui/incompatible_mods/incompatible_mods.txt";
|
||||
/* eslint-enable prefer-const */
|
||||
|
||||
function init(data)
|
||||
export function init(data)
|
||||
{
|
||||
Engine.GetGUIObjectByName("mainText").caption = Engine.TranslateLines(Engine.ReadFile(g_IncompatibleModsFile));
|
||||
return new Promise(closePageCallback =>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<objects>
|
||||
<script directory="gui/common/"/>
|
||||
<script directory="gui/incompatible_mods/"/>
|
||||
<script module="gui/incompatible_mods/incompatible_mods.js"/>
|
||||
|
||||
<!-- Add a translucent black background to fade out the menu page -->
|
||||
<object type="image" sprite="ModernFade"/>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
var g_ModsAvailableOnline = [];
|
||||
let g_ModsAvailableOnline = [];
|
||||
|
||||
/**
|
||||
* Indicates if we have encountered an error in one of the network-interaction attempts.
|
||||
|
|
@ -8,7 +8,7 @@ var g_ModsAvailableOnline = [];
|
|||
* Set to `true` by showErrorMessageBox
|
||||
* Set to `false` by init, updateModList, downloadFile, and cancelRequest
|
||||
*/
|
||||
var g_Failure;
|
||||
let g_Failure;
|
||||
|
||||
/**
|
||||
* Indicates if the user has cancelled a request.
|
||||
|
|
@ -19,11 +19,11 @@ var g_Failure;
|
|||
* Set to `true` by cancelRequest
|
||||
* Set to `false` by updateModList, and downloadFile
|
||||
*/
|
||||
var g_RequestCancelled;
|
||||
let g_RequestCancelled;
|
||||
|
||||
var g_RequestStartTime;
|
||||
let g_RequestStartTime;
|
||||
|
||||
var g_ModIOState = {
|
||||
const g_ModIOState = {
|
||||
/**
|
||||
* Finished status indicators
|
||||
*/
|
||||
|
|
@ -142,7 +142,7 @@ var g_ModIOState = {
|
|||
}
|
||||
};
|
||||
|
||||
function init(data)
|
||||
export function init(data)
|
||||
{
|
||||
const promise = progressDialog(
|
||||
translate("Initializing mod.io interface."),
|
||||
|
|
@ -153,6 +153,21 @@ function init(data)
|
|||
g_Failure = false;
|
||||
Engine.ModIoStartGetGameId();
|
||||
|
||||
Object.assign(Engine.GetGUIObjectByName("modFilter"), {
|
||||
"onPress": displayMods,
|
||||
"onTextEdit": displayMods
|
||||
});
|
||||
|
||||
Object.assign(Engine.GetGUIObjectByName("modsAvailableList"), {
|
||||
"onSelectionChange": showModDescription,
|
||||
"onSelectionColumnChange": displayMods,
|
||||
"onMouseLeftDoubleClickItem": downloadMod
|
||||
});
|
||||
|
||||
Engine.GetGUIObjectByName("compatibilityFilter").onPress = displayMods;
|
||||
Engine.GetGUIObjectByName("refreshButton").onPress = updateModList;
|
||||
Engine.GetGUIObjectByName("downloadButton").onPress = downloadMod;
|
||||
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise(closePageCallback =>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<objects>
|
||||
|
||||
<script directory="gui/common/"/>
|
||||
<script directory="gui/modio/"/>
|
||||
<script module="gui/modio/modio.js"/>
|
||||
|
||||
<object type="image" sprite="ModernFade"/>
|
||||
|
||||
|
|
@ -25,8 +25,6 @@
|
|||
style="ModernInput"
|
||||
size="16 0 200 24"
|
||||
>
|
||||
<action on="Press">displayMods();</action>
|
||||
<action on="TextEdit">displayMods();</action>
|
||||
<translatableAttribute id="placeholder_text" context="placeholder text for input field to filter mods">Filter</translatableAttribute>
|
||||
</object>
|
||||
|
||||
|
|
@ -39,10 +37,6 @@
|
|||
selected_column_order="1"
|
||||
font="sans-stroke-13"
|
||||
>
|
||||
<action on="SelectionChange">showModDescription();</action>
|
||||
<action on="SelectionColumnChange">displayMods();</action>
|
||||
<action on="MouseLeftDoubleClickItem">downloadMod();</action>
|
||||
|
||||
<!-- List headers -->
|
||||
<!-- Keep in sync with mod property names -->
|
||||
<column id="name_id" textcolor="255 255 255" width="20%">
|
||||
|
|
@ -69,9 +63,7 @@
|
|||
<!-- Right Panel: Compatibility Filter-->
|
||||
<object name="rightPanel" size="100%-250 20 100%-10 40" >
|
||||
<!-- Compatibility Filter Checkbox -->
|
||||
<object name="compatibilityFilter" type="checkbox" checked="true" style="ModernTickBox" size="0 4 20 100%">
|
||||
<action on="Press">displayMods();</action>
|
||||
</object>
|
||||
<object name="compatibilityFilter" type="checkbox" checked="true" style="ModernTickBox" size="0 4 20 100%" />
|
||||
<!-- Compatibility Filter Label -->
|
||||
<object type="text" size="20 2 100% 100%" text_align="left" textcolor="white">
|
||||
<translatableAttribute id="caption">Filter valid mods</translatableAttribute>
|
||||
|
|
@ -85,12 +77,10 @@
|
|||
|
||||
<object name="refreshButton" type="button" style="ModernButtonRed" size="100%-368 100%-44 100%-188 100%-16" enabled="false">
|
||||
<translatableAttribute id="caption">Refresh List</translatableAttribute>
|
||||
<action on="Press">updateModList();</action>
|
||||
</object>
|
||||
|
||||
<object name="downloadButton" type="button" style="ModernButtonRed" size="100%-184 100%-44 100%-16 100%-16" enabled="false">
|
||||
<translatableAttribute id="caption">Download</translatableAttribute>
|
||||
<action on="Press">downloadMod();</action>
|
||||
</object>
|
||||
|
||||
</object>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
function init(data)
|
||||
export function init(data)
|
||||
{
|
||||
Engine.GetGUIObjectByName("mainText").caption = Engine.TranslateLines(Engine.ReadFile("gui/modmod/help/help.txt"));
|
||||
return new Promise(closePageCallback =>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<objects>
|
||||
|
||||
<script directory="gui/common/"/>
|
||||
<script directory="gui/modmod/help/"/>
|
||||
<script module="gui/modmod/help/help.js"/>
|
||||
|
||||
<!-- Add a translucent black background to fade out the menu page -->
|
||||
<object type="image" sprite="ModernFade"/>
|
||||
|
|
|
|||
|
|
@ -19,33 +19,37 @@
|
|||
* This allows mods to express upwards and downwards compatibility.
|
||||
*/
|
||||
|
||||
import { downloadModsButton } from "gui/modmod/modmodio.js";
|
||||
import { regExpComparisonOperator, validateMod } from "gui/modmod/validatemod.js";
|
||||
|
||||
/**
|
||||
* Mod definitions loaded from the files, including invalid mods.
|
||||
*/
|
||||
var g_Mods = {};
|
||||
let g_Mods = {};
|
||||
|
||||
/**
|
||||
* Folder names of all mods that are or can be launched.
|
||||
*/
|
||||
var g_ModsEnabled = [];
|
||||
var g_ModsDisabled = [];
|
||||
let g_ModsEnabled = [];
|
||||
let g_ModsDisabled = [];
|
||||
|
||||
var g_ModsEnabledFiltered = [];
|
||||
var g_ModsDisabledFiltered = [];
|
||||
let g_ModsEnabledFiltered = [];
|
||||
let g_ModsDisabledFiltered = [];
|
||||
|
||||
/**
|
||||
* Cache mod compatibility recomputed when some mod is enbaled/disabled.
|
||||
*/
|
||||
var g_ModsCompatibility = [];
|
||||
const g_ModsCompatibility = [];
|
||||
|
||||
/**
|
||||
* Name of the mods installed by the ModInstaller.
|
||||
*/
|
||||
var g_InstalledMods;
|
||||
let g_InstalledMods;
|
||||
|
||||
var g_HasIncompatibleMods;
|
||||
let g_HasIncompatibleMods;
|
||||
|
||||
var g_FakeMod = {
|
||||
/* eslint-disable prefer-const -- Mods should be able to change them */
|
||||
let g_FakeMod = {
|
||||
"name": translate("This mod does not exist"),
|
||||
"version": "",
|
||||
"label": "",
|
||||
|
|
@ -54,12 +58,34 @@ var g_FakeMod = {
|
|||
"dependencies": []
|
||||
};
|
||||
|
||||
var g_ColorNoModSelected = "255 255 100";
|
||||
var g_ColorDependenciesMet = "100 255 100";
|
||||
var g_ColorDependenciesNotMet = "255 100 100";
|
||||
let g_ColorNoModSelected = "255 255 100";
|
||||
let g_ColorDependenciesMet = "100 255 100";
|
||||
let g_ColorDependenciesNotMet = "255 100 100";
|
||||
/* eslint-enable prefer-const */
|
||||
|
||||
function init(data, hotloadData)
|
||||
export function init(data, hotloadData)
|
||||
{
|
||||
Object.assign(Engine.GetGUIObjectByName("modsDisabledList"), {
|
||||
"onSelectionChange": selectedMod.bind(undefined, "modsDisabledList"),
|
||||
"onSelectionColumnChange": displayModLists,
|
||||
"onMouseLeftDoubleClickItem": enableMod
|
||||
});
|
||||
Object.assign(Engine.GetGUIObjectByName("modsEnabledList"), {
|
||||
"onSelectionChange": selectedMod.bind(undefined, "modsEnabledList"),
|
||||
"onMouseLeftDoubleClickItem": disableMod
|
||||
});
|
||||
|
||||
Engine.GetGUIObjectByName("enabledModUp").onPress =
|
||||
moveCurrItem.bind(undefined, "modsEnabledList", true);
|
||||
Engine.GetGUIObjectByName("enabledModDown").onPress =
|
||||
moveCurrItem.bind(undefined, "modsEnabledList", false);
|
||||
|
||||
Engine.GetGUIObjectByName("visitWebButton").onPress = visitModWebsite;
|
||||
Engine.GetGUIObjectByName("downloadButton").onPress = downloadModsButton.bind(undefined, initMods);
|
||||
Engine.GetGUIObjectByName("saveConfigurationButton").onPress = saveMods;
|
||||
Engine.GetGUIObjectByName("startButton").onPress = startMods;
|
||||
|
||||
|
||||
g_InstalledMods = data && data.installedMods || hotloadData && hotloadData.installedMods || [];
|
||||
g_HasIncompatibleMods = Engine.HasIncompatibleMods();
|
||||
|
||||
|
|
@ -354,7 +380,7 @@ function recomputeCompatibility(disabledAction = false)
|
|||
*/
|
||||
function isDependencyMet(dependency)
|
||||
{
|
||||
const operator = dependency.match(g_RegExpComparisonOperator);
|
||||
const operator = dependency.match(regExpComparisonOperator);
|
||||
const [name, version] = operator ? dependency.split(operator[0]) : [dependency, undefined];
|
||||
|
||||
return g_ModsEnabled.some(folder =>
|
||||
|
|
@ -403,7 +429,7 @@ function sortEnabledMods()
|
|||
{
|
||||
const dependencies = {};
|
||||
for (const folder of g_ModsEnabled)
|
||||
dependencies[folder] = getMod(folder).dependencies.map(d => d.split(g_RegExpComparisonOperator)[0]);
|
||||
dependencies[folder] = getMod(folder).dependencies.map(d => d.split(regExpComparisonOperator)[0]);
|
||||
|
||||
g_ModsEnabled.sort((folder1, folder2) =>
|
||||
dependencies[folder1].indexOf(getMod(folder2).name) != -1 ? 1 :
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<objects>
|
||||
|
||||
<script directory="gui/common/"/>
|
||||
<script directory="gui/modmod/"/>
|
||||
<script module="gui/modmod/modmod.js"/>
|
||||
|
||||
<object type="image" style="ModernWindow">
|
||||
|
||||
|
|
@ -80,10 +80,6 @@
|
|||
font="sans-stroke-13"
|
||||
auto_scroll="true"
|
||||
>
|
||||
<action on="SelectionChange">selectedMod(this.name);</action>
|
||||
<action on="SelectionColumnChange">displayModLists();</action>
|
||||
<action on="MouseLeftDoubleClickItem">enableMod();</action>
|
||||
|
||||
<!-- List headers -->
|
||||
<!-- Keep the column names in sync with the property names of mods -->
|
||||
<column id="name" textcolor="255 255 255" width="10%">
|
||||
|
|
@ -123,9 +119,6 @@
|
|||
tooltip_style="pgToolTip"
|
||||
auto_scroll="true"
|
||||
>
|
||||
<action on="SelectionChange">selectedMod(this.name);</action>
|
||||
<action on="MouseLeftDoubleClickItem">disableMod();</action>
|
||||
|
||||
<!-- List headers -->
|
||||
<column id="name" textcolor="255 255 255" width="10%">
|
||||
<translatableAttribute id="heading">Name</translatableAttribute>
|
||||
|
|
@ -159,7 +152,6 @@
|
|||
sprite_disabled="ModernArrowUpGrey"
|
||||
>
|
||||
<translatableAttribute id="tooltip">Change the order in which mods are launched. This should match the mods dependencies.</translatableAttribute>
|
||||
<action on="Press">moveCurrItem("modsEnabledList", true);</action>
|
||||
</object>
|
||||
<object
|
||||
name="enabledModDown"
|
||||
|
|
@ -173,7 +165,6 @@
|
|||
sprite_disabled="ModernArrowDownGrey"
|
||||
>
|
||||
<translatableAttribute id="tooltip">Change the order in which mods are launched. This should match the mods dependencies.</translatableAttribute>
|
||||
<action on="Press">moveCurrItem("modsEnabledList", false);</action>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
|
|
@ -181,7 +172,6 @@
|
|||
<object name="toggleModButton" type="button" style="ModernButtonRed" size="16 100%-80 196 100%-52" enabled="false"/>
|
||||
<object name="visitWebButton" type="button" style="ModernButtonRed" size="200 100%-80 380 100%-52" enabled="false">
|
||||
<translatableAttribute id="caption">Visit Website</translatableAttribute>
|
||||
<action on="Press">visitModWebsite();</action>
|
||||
</object>
|
||||
|
||||
<!-- Message -->
|
||||
|
|
@ -198,22 +188,19 @@
|
|||
|
||||
<object type="button" style="ModernButtonRed" size="100%-748 100%-44 100%-568 100%-16">
|
||||
<translatableAttribute id="caption">Help</translatableAttribute>
|
||||
<action on="Press">Engine.OpenChildPage("page_modhelp.xml");</action>
|
||||
<action on="Press">Engine.OpenChildPage("page_modhelp.xml");</action>
|
||||
</object>
|
||||
|
||||
<object type="button" style="ModernButtonRed" size="100%-564 100%-44 100%-384 100%-16">
|
||||
<object name="downloadButton" type="button" style="ModernButtonRed" size="100%-564 100%-44 100%-384 100%-16">
|
||||
<translatableAttribute id="caption">Download Mods</translatableAttribute>
|
||||
<action on="Press">downloadModsButton();</action>
|
||||
</object>
|
||||
|
||||
<object name="saveConfigurationButton" type="button" style="ModernButtonRed" size="100%-380 100%-44 100%-200 100%-16">
|
||||
<translatableAttribute id="caption">Save Configuration</translatableAttribute>
|
||||
<action on="Press">saveMods();</action>
|
||||
</object>
|
||||
|
||||
<object name="startButton" type="button" style="ModernButtonRed" size="100%-196 100%-44 100%-16 100%-16">
|
||||
<translatableAttribute id="caption">Save and Restart</translatableAttribute>
|
||||
<action on="Press">startMods();</action>
|
||||
</object>
|
||||
</object>
|
||||
</objects>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
function downloadModsButton()
|
||||
export function downloadModsButton(initMods)
|
||||
{
|
||||
initTerms({
|
||||
"Disclaimer": {
|
||||
|
|
@ -6,7 +6,7 @@ function downloadModsButton()
|
|||
"file": "gui/modio/Disclaimer.txt",
|
||||
"config": "modio.disclaimer",
|
||||
"accepted": false,
|
||||
"callback": openModIo,
|
||||
"callback": openModIo.bind(undefined, initMods),
|
||||
"urlButtons": [
|
||||
{
|
||||
"caption": translate("mod.io Terms"),
|
||||
|
|
@ -23,7 +23,7 @@ function downloadModsButton()
|
|||
openTerms("Disclaimer");
|
||||
}
|
||||
|
||||
async function openModIo(data)
|
||||
async function openModIo(initMods, data)
|
||||
{
|
||||
if (!data.accepted)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -48,12 +48,12 @@ const g_RegExpVersion= /[0-9]+(\.[0-9]+){0,2}/;
|
|||
/**
|
||||
* Version checks in mod dependencies can use these operators.
|
||||
*/
|
||||
const g_RegExpComparisonOperator = /(<=|>=|<|>|=)/;
|
||||
export const regExpComparisonOperator = /(<=|>=|<|>|=)/;
|
||||
|
||||
/**
|
||||
* Tests if a dependency compares a mod version against another, for instance "0ad<=0.0.16".
|
||||
*/
|
||||
const g_RegExpComparison = globalRegExp(new RegExp(g_RegExpName.source + g_RegExpComparisonOperator.source + g_RegExpVersion.source));
|
||||
const g_RegExpComparison = globalRegExp(new RegExp(g_RegExpName.source + regExpComparisonOperator.source + g_RegExpVersion.source));
|
||||
|
||||
/**
|
||||
* The label may not be empty.
|
||||
|
|
@ -69,7 +69,7 @@ function globalRegExp(regexp)
|
|||
* Returns whether the mod defines all required properties and whether all properties are valid.
|
||||
* Shows a notification if not.
|
||||
*/
|
||||
function validateMod(folder, modData, notify)
|
||||
export function validateMod(folder, modData, notify)
|
||||
{
|
||||
let valid = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Currently limited to at most 3 buttons per message box.
|
||||
* The convention is to have "cancel" appear first.
|
||||
*/
|
||||
function init(data)
|
||||
export function init(data)
|
||||
{
|
||||
// Set title
|
||||
Engine.GetGUIObjectByName("mbTitleBar").caption = data.title;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<objects>
|
||||
|
||||
<script directory="gui/common/"/>
|
||||
<script directory="gui/msgbox/"/>
|
||||
<script module="gui/msgbox/msgbox.js"/>
|
||||
|
||||
<!-- Fade out the background because it's non-interactable -->
|
||||
<object sprite="ModernFade" type="image"/>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
async function init()
|
||||
export async function init()
|
||||
{
|
||||
return { [Engine.openRequest]: {
|
||||
"page": "page_modmod.xml",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<objects>
|
||||
<script directory="gui/pregame/"/>
|
||||
<script module="gui/pregame/mainmenu.js"/>
|
||||
</objects>
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@
|
|||
* The user should be able to save and print the text of the terms.
|
||||
*/
|
||||
|
||||
var g_TermsPage;
|
||||
var g_TermsFile;
|
||||
var g_TermsSprintf;
|
||||
let g_TermsPage;
|
||||
let g_TermsFile;
|
||||
let g_TermsSprintf;
|
||||
|
||||
async function init(data)
|
||||
export async function init(data)
|
||||
{
|
||||
g_TermsPage = data.page;
|
||||
g_TermsFile = data.file;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<objects>
|
||||
|
||||
<script directory="gui/common/"/>
|
||||
<script directory="gui/termsdialog/"/>
|
||||
<script module="gui/termsdialog/termsdialog.js"/>
|
||||
|
||||
<object type="image" sprite="ModernFade"/>
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ class TimedConfirmation
|
|||
}
|
||||
}
|
||||
|
||||
function init(data)
|
||||
export function init(data)
|
||||
{
|
||||
return new TimedConfirmation().setup(data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<objects>
|
||||
|
||||
<script directory="gui/common/"/>
|
||||
<script directory="gui/timedconfirmation/"/>
|
||||
<script module="gui/timedconfirmation/timedconfirmation.js"/>
|
||||
|
||||
<!-- Fade out the background because it's non-interactable -->
|
||||
<object sprite="ModernFade" type="image"/>
|
||||
|
|
|
|||
|
|
@ -25,13 +25,17 @@ class CounterPopulation
|
|||
for (const resCode of g_ResourceData.GetCodes())
|
||||
total += playerState.resourceGatherers[resCode];
|
||||
|
||||
this.stats.caption = coloredText(total, total ? this.DefaultTotalGatherersColor : this.DefaultTotalGatherersColorZero);
|
||||
const colorizedTotal = coloredText(total,
|
||||
total ? this.DefaultTotalGatherersColor : this.DefaultTotalGatherersColorZero);
|
||||
this.stats.caption = colorizedTotal;
|
||||
|
||||
this.isTrainingBlocked = playerState.trainingBlocked;
|
||||
|
||||
this.panel.tooltip =
|
||||
setStringTags(translate(this.PopulationTooltip), CounterManager.ResourceTitleTags) +
|
||||
getAllyStatTooltip(this.getTooltipData.bind(this));
|
||||
setStringTags(translate(this.PopulationTooltipTitle), CounterManager.ResourceTitleTags) +
|
||||
"\n" + sprintf(this.PopulationTooltip, state) +
|
||||
getAllyStatTooltip(this.getTooltipData.bind(this)) + "\n" +
|
||||
sprintf(this.CurrentGatherersTooltip, { "currentGatherers": colorizedTotal });
|
||||
}
|
||||
|
||||
getTooltipData(playerState, playername)
|
||||
|
|
@ -62,10 +66,20 @@ class CounterPopulation
|
|||
// Translation: Do not insert spaces around the slash symbol for this exact string. Keep only one space between popLimit and popMax.
|
||||
CounterPopulation.prototype.CounterCaption = markForTranslation("%(popCount)s/%(popLimit)s (%(popMax)s)");
|
||||
|
||||
CounterPopulation.prototype.PopulationTooltip = markForTranslation("Population: current/limit (max)");
|
||||
CounterPopulation.prototype.PopulationTooltipTitle = markForTranslation("Population");
|
||||
CounterPopulation.prototype.PopulationTooltip =
|
||||
translate("Current population: %(popCount)s\nPopulation limit: %(popLimit)s\nMaximum population: %(popMax)s");
|
||||
|
||||
CounterPopulation.prototype.AllyPopulationTooltip = markForTranslation("%(popCount)s/%(popLimit)s (%(popMax)s)");
|
||||
|
||||
/**
|
||||
* Storing the translated and formatted gatherer string in the prototype.
|
||||
* Including the number might seem redundant but is required since the collor
|
||||
* isn't enough to associate it with the number in the top panel.
|
||||
*/
|
||||
CounterPopulation.prototype.CurrentGatherersTooltip =
|
||||
setStringTags(translate("Current gatherers: %(currentGatherers)s"), { "font": "sans-14" });
|
||||
|
||||
/**
|
||||
* Color to highlight the total number of gatherers at zero.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ class CounterResource
|
|||
this.count.caption = abbreviateLargeNumbers(Math.floor(playerState.resourceCounts[this.resCode]));
|
||||
|
||||
const gatherers = playerState.resourceGatherers[this.resCode];
|
||||
this.stats.caption = coloredText(gatherers, gatherers ? this.DefaultResourceGatherersColor : this.DefaultResourceGatherersColorZero);
|
||||
const colorizedGatherers = coloredText(gatherers,
|
||||
gatherers ? this.DefaultResourceGatherersColor : this.DefaultResourceGatherersColorZero);
|
||||
this.stats.caption = colorizedGatherers;
|
||||
|
||||
|
||||
// TODO: Set the tooltip only if hovered?
|
||||
|
|
@ -28,7 +30,9 @@ class CounterResource
|
|||
this.panel.tooltip =
|
||||
setStringTags(resourceNameFirstWord(this.resCode), CounterManager.ResourceTitleTags) +
|
||||
description +
|
||||
getAllyStatTooltip(this.getTooltipData.bind(this));
|
||||
getAllyStatTooltip(this.getTooltipData.bind(this)) + "\n" +
|
||||
sprintf(CounterPopulation.prototype.CurrentGatherersTooltip,
|
||||
{ "currentGatherers": colorizedGatherers });
|
||||
}
|
||||
|
||||
getTooltipData(playerState, playername)
|
||||
|
|
|
|||
|
|
@ -208,3 +208,13 @@
|
|||
“This is a woman's resolve; as for men, they may live and be slaves.” \n— Boudicca, rallying against Roman occupation (Tacitus's Annals, Book 14, Chapter 35)
|
||||
“There are two things, knowledge and opinion; one makes its possessor know, the other to be ignorant.” \n— (Hippocratic treatise known as “The Law”, part of the Hippocratic Corpus)
|
||||
“Let them hate me, so long as they fear me.” \n— Caligula (Suetonius, The Twelve Caesars, Book 30)
|
||||
“There is no instance of a nation benefiting from prolonged warfare.” \n— Sun Tzu (“The Art of War”, Chapter 2)
|
||||
“To subdue the enemy without fighting is the acme of skill.” \n— Sun Tzu (“The Art of War”, Chapter 3)
|
||||
“One defends when his strength is inadequate; he attacks when it is abundant.” \n— Sun Tzu (“The Art of War”, Chapter 4)
|
||||
“When torrential water tosses boulders, it is because of its momentum. When the strike of a hawk breaks the body of its prey, it is because of timing.” \n— Sun Tzu (“The Art of War”, Chapter 5)
|
||||
“Treat your men as you would your own beloved sons. And they will follow you into the deepest valley.” \n— Sun Tzu (“The Art of War”, Chapter 10)
|
||||
“Woe to the defeated!” \n— Brennus disputing the weight of gold ransom while sacking Rome c. 387 BC (Livy, “Ab Urbe Condita”, Book 5, Chapter 48)
|
||||
“\[Brutus] found the women fighting and perishing in company with the men with such bravery that they uttered no cry even in the midst of slaughter.” \n— Brutus in Lusitania during the battles with Viriathus (Appian of Alexandria, “The Spanish Wars”, 15)
|
||||
“What I desire for my own children, I desire for all men.” \n— Ashoka the Great (“The Kalinga Rock Edicts”)
|
||||
“If you Romans choose to lord it over the world, does it follow that the world is to accept slavery ?” \n— Caratacus pleading for pardon from Roman Emperor Claudius in Rome (Tacitus, “Annals”, 12.37)
|
||||
“And where am I to get so many soldiers ?” \n— Septimius Severus, frustrated with the challenge of controlling his own men and the harsh realities of frontier warfare against the northern tribes in Britannia. (Cassius Dio, “Roman History”, Vol IX, Book LXXVI, Section 11)
|
||||
|
|
|
|||
|
|
@ -286,17 +286,21 @@ Attack.prototype.CanAttack = function(target, wantedTypes)
|
|||
if (!cmpTargetPlayer || !cmpEntityPlayer)
|
||||
return false;
|
||||
|
||||
// Must be visible or miraged / with retainInFog flag, not completely hidden
|
||||
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
if (cmpRangeManager)
|
||||
{
|
||||
const visibility = cmpRangeManager.GetLosVisibility(target, cmpEntityPlayer.GetPlayerID());
|
||||
if (visibility == "hidden")
|
||||
return false;
|
||||
}
|
||||
|
||||
const types = this.GetAttackTypes(wantedTypes);
|
||||
const entityOwner = cmpEntityPlayer.GetPlayerID();
|
||||
const targetOwner = cmpTargetPlayer.GetPlayerID();
|
||||
const cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
|
||||
const cmpDiplomacy = QueryPlayerIDInterface(entityOwner, IID_Diplomacy);
|
||||
|
||||
// Check if the relative height difference is larger than the attack range
|
||||
// If the relative height is bigger, it means they will never be able to
|
||||
// reach each other, no matter how close they come.
|
||||
const heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
|
||||
|
||||
for (const type of types)
|
||||
{
|
||||
if (type != "Capture" && (!cmpDiplomacy?.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints()))
|
||||
|
|
@ -305,7 +309,8 @@ Attack.prototype.CanAttack = function(target, wantedTypes)
|
|||
if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner)))
|
||||
continue;
|
||||
|
||||
if (heightDiff > this.GetRange(type).max)
|
||||
// Check if the target is currently in range, or could ever be reached
|
||||
if (!this.IsTargetInRange(target, type) && !this.CanEverReachTarget(target, type))
|
||||
continue;
|
||||
|
||||
const restrictedClasses = this.GetRestrictedClasses(type);
|
||||
|
|
@ -319,6 +324,77 @@ Attack.prototype.CanAttack = function(target, wantedTypes)
|
|||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the target could potentially ever be reached with the given attack type,
|
||||
* as an optimistic estimate. This assumes the attacker can move to the closest
|
||||
* possible position to the target — ignoring obstructions and terrain features
|
||||
* (e.g., hills) that might help or hinder.
|
||||
*
|
||||
* This is a best-effort guess:
|
||||
* - It may return true even when the target is actually unreachable (e.g., turreted
|
||||
* units on walls with a height offset too large for the projectile to overcome).
|
||||
* - It may return false even when the target is reachable (e.g., a nearby hill could
|
||||
* provide enough elevation to hit a "too high" target, but we don't check for that).
|
||||
*
|
||||
* Currently these checks are mostly useful to determine if we can reach turreted units
|
||||
* (e.g. on a wall, outpost...).
|
||||
*
|
||||
* @param {number} targetId - The target entity ID.
|
||||
* @param {string} type - The attack type.
|
||||
* @return {boolean} - Whether the target is estimated to be reachable (see caveats above).
|
||||
*/
|
||||
Attack.prototype.CanEverReachTarget = function(targetId, type)
|
||||
{
|
||||
const cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
|
||||
const cmpTargetPosition = Engine.QueryInterface(targetId, IID_Position);
|
||||
|
||||
const thisHeightOffset = cmpThisPosition.GetHeightOffset();
|
||||
const targetHeightOffset = cmpTargetPosition.GetHeightOffset();
|
||||
|
||||
const range = this.GetRange(type);
|
||||
|
||||
// Find the closest horizontal distance we could ever get to the target.
|
||||
// We first determine the closest horizontal distance we could ever get to the target,
|
||||
// accounting for turreted units inside buildings:
|
||||
// - If the building blocks movement, we can only reach its exterior edge.
|
||||
// - If the building is passable, we can walk right up to the turret point.
|
||||
const cmpTurretable = Engine.QueryInterface(targetId, IID_Turretable);
|
||||
const holderId = cmpTurretable?.HolderID();
|
||||
let closestDistance = 0;
|
||||
if (holderId && holderId != INVALID_ENTITY)
|
||||
{
|
||||
const cmpTurretHolder = Engine.QueryInterface(holderId, IID_TurretHolder);
|
||||
if (cmpTurretHolder)
|
||||
{
|
||||
const turretPoint = cmpTurretHolder.GetOccupiedTurretPoint(targetId);
|
||||
closestDistance = cmpTurretHolder.GetClosestApproachDistanceToTurretPoint(turretPoint);
|
||||
}
|
||||
}
|
||||
|
||||
if (!range.parabolic)
|
||||
{
|
||||
// For non-parabolic attacks (e.g., "Melee" attack type), we check if the height offset
|
||||
// is within max range at the closest possible horizontal distance (simple 3D distance check).
|
||||
const heightDiff = Math.abs(targetHeightOffset - thisHeightOffset);
|
||||
return Math.sqrt(closestDistance * closestDistance + heightDiff * heightDiff) <= range.max;
|
||||
}
|
||||
|
||||
// For parabolic attacks (generally "Ranged" attack type), we use the parabolic formula
|
||||
// to determine if the height offset is surmountable at the closest possible distance.
|
||||
// Typical scenario: units on walls/towers may be unreachable if the attacker's
|
||||
// projectiles can't arc high enough, even at point-blank range.
|
||||
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
if (!cmpRangeManager)
|
||||
return true;
|
||||
|
||||
const yOrigin = this.GetAttackYOrigin(type);
|
||||
|
||||
const maxReachableHeightDiff = cmpRangeManager.GetMaxReachableParabolicHeight(
|
||||
range.max, yOrigin, closestDistance);
|
||||
|
||||
return targetHeightOffset - thisHeightOffset <= maxReachableHeightDiff;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns undefined if we have no preference or the lowest index of a preferred class.
|
||||
*/
|
||||
|
|
@ -353,12 +429,14 @@ Attack.prototype.GetPreference = function(target)
|
|||
*/
|
||||
Attack.prototype.GetFullAttackRange = function()
|
||||
{
|
||||
const ret = { "min": Infinity, "max": 0 };
|
||||
const ret = { "min": Infinity, "max": 0, "parabolic": false };
|
||||
for (const type of this.GetAttackTypes())
|
||||
{
|
||||
const range = this.GetRange(type);
|
||||
ret.min = Math.min(ret.min, range.min);
|
||||
ret.max = Math.max(ret.max, range.max);
|
||||
if (range.parabolic)
|
||||
ret.parabolic = true;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
|
@ -475,7 +553,39 @@ Attack.prototype.GetRange = function(type)
|
|||
let min = +(this.template[type].MinRange || 0);
|
||||
min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity);
|
||||
|
||||
return { "max": max, "min": min };
|
||||
return {
|
||||
"max": max,
|
||||
"min": min,
|
||||
"parabolic": type === "Ranged"
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the effective range for attacking a specific target, accounting
|
||||
* for elevation and projectile physics where applicable.
|
||||
* @param {number} target - The target entity ID.
|
||||
* @param {string} type - The attack type.
|
||||
* @return {{ min: number, max: number }} - The min and max effective range.
|
||||
*/
|
||||
Attack.prototype.GetEffectiveAttackRange = function(target, type)
|
||||
{
|
||||
const range = this.GetRange(type);
|
||||
|
||||
// Only Parabolic attacks get parabolic elevation adjustment
|
||||
if (!range.parabolic)
|
||||
return range;
|
||||
|
||||
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
if (!cmpRangeManager)
|
||||
return range;
|
||||
|
||||
const effectiveMax = cmpRangeManager.GetEffectiveParabolicRange(
|
||||
this.entity, target, range.max, this.GetAttackYOrigin(type));
|
||||
|
||||
if (effectiveMax < 0)
|
||||
return { "min": Infinity, "max": 0 }; // Out of range
|
||||
|
||||
return { "min": range.min, "max": effectiveMax };
|
||||
};
|
||||
|
||||
Attack.prototype.GetAttackYOrigin = function(type)
|
||||
|
|
@ -813,14 +923,9 @@ Attack.prototype.PerformAttack = function(type, target)
|
|||
*/
|
||||
Attack.prototype.IsTargetInRange = function(target, type)
|
||||
{
|
||||
const range = this.GetRange(type);
|
||||
return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).IsInTargetParabolicRange(
|
||||
this.entity,
|
||||
target,
|
||||
range.min,
|
||||
range.max,
|
||||
this.GetAttackYOrigin(type),
|
||||
false);
|
||||
const range = this.GetEffectiveAttackRange(target, type);
|
||||
return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).IsInTargetRange(
|
||||
this.entity, target, range.min, range.max, false);
|
||||
};
|
||||
|
||||
Attack.prototype.OnValueModification = function(msg)
|
||||
|
|
|
|||
|
|
@ -127,10 +127,20 @@ BuildingAI.prototype.SetupRangeQuery = function()
|
|||
|
||||
const range = cmpAttack.GetRange(attackType);
|
||||
const yOrigin = cmpAttack.GetAttackYOrigin(attackType);
|
||||
|
||||
// Get building's vision range
|
||||
const cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
|
||||
const visionRange = cmpVision ? cmpVision.GetRange() : 0;
|
||||
|
||||
// Base range
|
||||
const baseRange = Math.min(visionRange, range.max);
|
||||
|
||||
// This takes entity sizes into accounts, so no need to compensate for structure size.
|
||||
this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
|
||||
this.entity, range.min, range.max, yOrigin,
|
||||
enemies, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"));
|
||||
this.entity, range.min, range.max, baseRange, yOrigin,
|
||||
enemies, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"),
|
||||
true // Allow mirages for attack queries
|
||||
);
|
||||
|
||||
cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery);
|
||||
};
|
||||
|
|
@ -156,10 +166,17 @@ BuildingAI.prototype.SetupGaiaRangeQuery = function()
|
|||
const range = cmpAttack.GetRange(attackType);
|
||||
const yOrigin = cmpAttack.GetAttackYOrigin(attackType);
|
||||
|
||||
// Get building's vision range
|
||||
const cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
|
||||
const visionRange = cmpVision ? cmpVision.GetRange() : 0;
|
||||
|
||||
// Base range
|
||||
const baseRange = Math.min(visionRange, range.max);
|
||||
|
||||
// This query is only interested in Gaia entities that can attack.
|
||||
// This takes entity sizes into accounts, so no need to compensate for structure size.
|
||||
this.gaiaUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
|
||||
this.entity, range.min, range.max, yOrigin,
|
||||
this.entity, range.min, range.max, baseRange, yOrigin,
|
||||
[0], IID_Attack, cmpRangeManager.GetEntityFlagMask("normal"));
|
||||
|
||||
cmpRangeManager.EnableActiveQuery(this.gaiaUnitsQuery);
|
||||
|
|
@ -170,7 +187,6 @@ BuildingAI.prototype.SetupGaiaRangeQuery = function()
|
|||
*/
|
||||
BuildingAI.prototype.OnRangeUpdate = function(msg)
|
||||
{
|
||||
|
||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
if (!cmpAttack)
|
||||
return;
|
||||
|
|
@ -189,10 +205,10 @@ BuildingAI.prototype.OnRangeUpdate = function(msg)
|
|||
|
||||
// Add new targets.
|
||||
for (const entity of msg.added)
|
||||
if (cmpAttack.CanAttack(entity))
|
||||
if (!this.targetUnits.includes(entity))
|
||||
this.targetUnits.push(entity);
|
||||
|
||||
// Remove targets outside of vision-range.
|
||||
// Remove targets out of range.
|
||||
for (const entity of msg.removed)
|
||||
{
|
||||
const index = this.targetUnits.indexOf(entity);
|
||||
|
|
@ -375,13 +391,7 @@ BuildingAI.prototype.FireArrows = function()
|
|||
{
|
||||
|
||||
const selectedTarget = targets[targetIndex].entityId;
|
||||
if (this.CheckTargetVisible(selectedTarget) && cmpObstructionManager.IsInTargetParabolicRange(
|
||||
this.entity,
|
||||
selectedTarget,
|
||||
range.min,
|
||||
range.max,
|
||||
yOrigin,
|
||||
false))
|
||||
if (cmpAttack.CanAttack(selectedTarget, [attackType]))
|
||||
{
|
||||
cmpAttack.PerformAttack(attackType, selectedTarget);
|
||||
PlaySound("attack_" + attackType.toLowerCase(), this.entity);
|
||||
|
|
|
|||
|
|
@ -240,6 +240,39 @@ class TurretHolder
|
|||
return turret ? turret.name : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the closest horizontal distance an external entity could ever get
|
||||
* to the specified turret point. If the holder is passable, returns 0.
|
||||
* Otherwise returns the perpendicular distance from the turret point to the
|
||||
* nearest edge of the holder's obstruction.
|
||||
*
|
||||
* @param {string|Object} turretPoint - The turret point name or object.
|
||||
* @return {number} - The minimum possible horizontal distance.
|
||||
*/
|
||||
GetClosestApproachDistanceToTurretPoint(turretPoint)
|
||||
{
|
||||
if (typeof turretPoint === "string")
|
||||
turretPoint = this.TurretPointByName(turretPoint);
|
||||
if (!turretPoint)
|
||||
return 0;
|
||||
|
||||
const cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
|
||||
if (!cmpObstruction || !cmpObstruction.GetBlockMovementFlag(false))
|
||||
return 0;
|
||||
|
||||
const dxLocal = turretPoint.offset.x;
|
||||
const dzLocal = turretPoint.offset.z;
|
||||
|
||||
const halfSizes = cmpObstruction.GetObstructionHalfSizes();
|
||||
const hw = halfSizes.x;
|
||||
const hh = halfSizes.y;
|
||||
|
||||
if (hw == null || hh == null || hw < 0 || hh < 0)
|
||||
return 0;
|
||||
|
||||
return Math.max(0, Math.min(hw - Math.abs(dxLocal), hh - Math.abs(dzLocal)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number[]} - The turretted entityIDs.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4244,10 +4244,31 @@ UnitAI.prototype.SetupAttackRangeQuery = function(enable = true)
|
|||
return;
|
||||
|
||||
const range = this.GetQueryRange(IID_Attack);
|
||||
// Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
|
||||
this.losAttackRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
|
||||
range.min, range.max, players, IID_Resistance,
|
||||
cmpRangeManager.GetEntityFlagMask("normal"), false);
|
||||
if (range.parabolic)
|
||||
{
|
||||
const cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
const yOrigin = cmpAttack ? cmpAttack.GetAttackYOrigin("Ranged") : 0;
|
||||
|
||||
// Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
|
||||
this.losAttackRangeQuery = cmpRangeManager.CreateActiveParabolicQuery(
|
||||
this.entity,
|
||||
range.min,
|
||||
range.max,
|
||||
range.base,
|
||||
yOrigin,
|
||||
players,
|
||||
IID_Resistance,
|
||||
cmpRangeManager.GetEntityFlagMask("normal"),
|
||||
true // Allow mirages for attack queries
|
||||
);
|
||||
}
|
||||
else
|
||||
this.losAttackRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
|
||||
range.min, range.max, players, IID_Resistance,
|
||||
cmpRangeManager.GetEntityFlagMask("normal"),
|
||||
false,
|
||||
true // Allow mirages for attack queries
|
||||
);
|
||||
|
||||
if (enable)
|
||||
cmpRangeManager.EnableActiveQuery(this.losAttackRangeQuery);
|
||||
|
|
@ -5157,24 +5178,24 @@ UnitAI.prototype.MoveToTargetAttackRange = function(target, type)
|
|||
if (cmpFormation)
|
||||
target = cmpFormation.GetClosestMemberToEntity(this.entity);
|
||||
|
||||
if (type != "Ranged")
|
||||
return this.MoveToTargetRange(target, IID_Attack, type);
|
||||
|
||||
if (!this.CheckTargetVisible(target))
|
||||
return false;
|
||||
|
||||
const cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
if (!cmpAttack)
|
||||
return false;
|
||||
const range = cmpAttack.GetRange(type);
|
||||
|
||||
// In case the range returns negative, we are probably too high compared to the target. Hope we come close enough.
|
||||
const parabolicMaxRange = Math.max(0, Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEffectiveParabolicRange(this.entity, target, range.max, cmpAttack.GetAttackYOrigin(type)));
|
||||
const flatRange = cmpAttack.GetRange(type);
|
||||
const effectiveRange = cmpAttack.GetEffectiveAttackRange(target, type);
|
||||
if (effectiveRange.max < 0)
|
||||
return false;
|
||||
|
||||
// The parabole changes while walking so be cautious:
|
||||
const guessedMaxRange = parabolicMaxRange > range.max ? (range.max + parabolicMaxRange) / 2 : parabolicMaxRange;
|
||||
// The parabola changes while walking so be cautious:
|
||||
const guessedMaxRange = effectiveRange.max > flatRange.max ?
|
||||
(flatRange.max + effectiveRange.max) / 2 :
|
||||
effectiveRange.max;
|
||||
|
||||
return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange);
|
||||
return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, effectiveRange.min, guessedMaxRange);
|
||||
};
|
||||
|
||||
UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
|
||||
|
|
@ -5584,8 +5605,16 @@ UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
|
|||
if (!this.AbleToMove())
|
||||
return false;
|
||||
|
||||
// Check if we should chase based on stance
|
||||
if (this.GetStance().respondChase)
|
||||
return true;
|
||||
{
|
||||
// If we're allowed to chase beyond vision, always chase
|
||||
if (this.GetStance().respondChaseBeyondVision)
|
||||
return true;
|
||||
|
||||
// Otherwise, only chase if the target is within our personal vision
|
||||
return this.CheckTargetIsInVisionRange(target);
|
||||
}
|
||||
|
||||
// If we are guarding/escorting, chase at least as long as the guarded unit is in target range of the attacker
|
||||
if (this.isGuardOf)
|
||||
|
|
@ -6620,9 +6649,22 @@ UnitAI.prototype.FindWalkAndFightTargets = function()
|
|||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the detection range for the given interface, adjusted by stance.
|
||||
*
|
||||
* The query range depends on stance because it represents the distance at which
|
||||
* the unit should "notice" an enemy and potentially start moving toward it.
|
||||
*
|
||||
* @param {number} iid - IID_Vision, IID_Heal, or IID_Attack
|
||||
* @returns {{min: number, max: number, base: number, parabolic: boolean}}
|
||||
* 'parabolic' indicates that the caller
|
||||
* should use a parabolic range query (accounting for elevation) instead of a
|
||||
* flat 2D one. Generally used for projectile attacks.
|
||||
* 'base' is a non-parabolic 2D detection range that always counts as in-range.
|
||||
*/
|
||||
UnitAI.prototype.GetQueryRange = function(iid)
|
||||
{
|
||||
const ret = { "min": 0, "max": 0 };
|
||||
const ret = { "min": 0, "max": 0, "base": 0, "parabolic": false };
|
||||
|
||||
const cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
|
||||
if (!cmpVision)
|
||||
|
|
@ -6635,27 +6677,35 @@ UnitAI.prototype.GetQueryRange = function(iid)
|
|||
return ret;
|
||||
}
|
||||
|
||||
if (this.GetStance().respondStandGround)
|
||||
{
|
||||
const range = this.GetRange(iid);
|
||||
if (!range)
|
||||
return ret;
|
||||
ret.min = range.min;
|
||||
ret.max = Math.min(range.max, visionRange);
|
||||
}
|
||||
else if (this.GetStance().respondChase)
|
||||
ret.max = visionRange;
|
||||
const range = this.GetRange(iid);
|
||||
if (!range)
|
||||
return ret;
|
||||
|
||||
// The query range depends on stance because it represents the distance at which
|
||||
// the unit should "notice" an enemy and potentially start moving toward it.
|
||||
|
||||
// In all stances, always spot targets within effective attack/heal range.
|
||||
Object.assign(ret, range);
|
||||
|
||||
let nonParabolicMax = 0;
|
||||
if (this.GetStance().respondChase)
|
||||
// Chase: Always spot targets within vision range, so we can chase them.
|
||||
nonParabolicMax = visionRange;
|
||||
else if (this.GetStance().respondHoldGround)
|
||||
{
|
||||
const range = this.GetRange(iid);
|
||||
if (!range)
|
||||
return ret;
|
||||
ret.max = Math.min(range.max + visionRange / 2, visionRange);
|
||||
}
|
||||
// HoldGround: willing to move a bit, so spot targets within attack range + half vision.
|
||||
nonParabolicMax = Math.min(range.max + visionRange / 2, visionRange);
|
||||
|
||||
// StandGround: nonParabolicMax stays 0, using only parabolic range.
|
||||
|
||||
// We probably have stance 'passive' and we wouldn't have a range,
|
||||
// but as it is the default for healers we need to set it to something sane.
|
||||
else if (iid === IID_Heal)
|
||||
ret.max = visionRange;
|
||||
nonParabolicMax = visionRange;
|
||||
|
||||
if (ret.parabolic)
|
||||
ret.base = nonParabolicMax;
|
||||
else
|
||||
ret.max = nonParabolicMax;
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ Engine.LoadComponentScript("interfaces/Formation.js");
|
|||
Engine.LoadComponentScript("interfaces/Health.js");
|
||||
Engine.LoadComponentScript("interfaces/Resistance.js");
|
||||
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
||||
Engine.LoadComponentScript("interfaces/Turretable.js");
|
||||
Engine.LoadComponentScript("interfaces/TurretHolder.js");
|
||||
Engine.LoadComponentScript("Attack.js");
|
||||
|
||||
let entityID = 903;
|
||||
|
|
@ -52,6 +54,16 @@ function attackComponentTest(defenderClass, isEnemy, test_function)
|
|||
"IsEnemy": () => isEnemy
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
|
||||
"IsInTargetRange": () => true
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
||||
"GetEffectiveParabolicRange": () => 25,
|
||||
"GetMaxReachableParabolicHeight": () => 15,
|
||||
"GetLosVisibility": (target, owner) => "visible"
|
||||
});
|
||||
|
||||
const attacker = entityID;
|
||||
|
||||
AddMock(attacker, IID_Position, {
|
||||
|
|
@ -201,7 +213,7 @@ attackComponentTest(undefined, true, (attacker, cmpAttack, defender) =>
|
|||
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["Civilian"]);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80 });
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80, "parabolic": true });
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Capture"), { "Capture": 8 });
|
||||
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), {
|
||||
|
|
@ -416,3 +428,101 @@ function testAttackPreference()
|
|||
TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+4), undefined);
|
||||
}
|
||||
testAttackPreference();
|
||||
|
||||
function testCanEverReachTarget()
|
||||
{
|
||||
const attacker = ++entityID;
|
||||
|
||||
AddMock(attacker, IID_Position, {
|
||||
"IsInWorld": () => true,
|
||||
"GetHeightOffset": () => 0,
|
||||
"GetPosition2D": () => new Vector2D(1, 2)
|
||||
});
|
||||
|
||||
const cmpAttack = ConstructComponent(attacker, "Attack", {
|
||||
"Melee": {
|
||||
"Damage": { "Hack": 10, "Pierce": 0, "Crush": 0 },
|
||||
"MaxRange": 5
|
||||
},
|
||||
"Ranged": {
|
||||
"Damage": { "Hack": 0, "Pierce": 10, "Crush": 0 },
|
||||
"MaxRange": 30,
|
||||
"Projectile": { "Speed": 50, "Spread": 1, "Gravity": 1, "FriendlyFire": "false" }
|
||||
}
|
||||
});
|
||||
|
||||
// Melee target within 3D range
|
||||
{
|
||||
const defender = ++entityID;
|
||||
AddMock(defender, IID_Position, {
|
||||
"IsInWorld": () => true,
|
||||
"GetHeightOffset": () => 0
|
||||
});
|
||||
TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Melee"), true);
|
||||
}
|
||||
|
||||
// Melee target too high
|
||||
{
|
||||
const defender = ++entityID;
|
||||
AddMock(defender, IID_Position, {
|
||||
"IsInWorld": () => true,
|
||||
"GetHeightOffset": () => 10
|
||||
});
|
||||
TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Melee"), false);
|
||||
}
|
||||
|
||||
// Melee target at same height, within range (close distance)
|
||||
{
|
||||
const defender = ++entityID;
|
||||
AddMock(defender, IID_Position, {
|
||||
"IsInWorld": () => true,
|
||||
"GetHeightOffset": () => 4
|
||||
});
|
||||
// sqrt(0² + 4²) = 4 <= 5
|
||||
TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Melee"), true);
|
||||
}
|
||||
|
||||
// Ranged: target at same height — reachable from current position (check 1)
|
||||
{
|
||||
const defender = ++entityID;
|
||||
AddMock(defender, IID_Position, {
|
||||
"IsInWorld": () => true,
|
||||
"GetHeightOffset": () => 0,
|
||||
"GetPosition": () => new Vector3D(1, 0, 2)
|
||||
});
|
||||
// Need RangeManager mock for IsTargetInRange (check 1) to work
|
||||
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
||||
"GetEffectiveParabolicRange": () => 25,
|
||||
"GetMaxReachableParabolicHeight": () => 15
|
||||
});
|
||||
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
|
||||
"IsInTargetRange": () => true
|
||||
});
|
||||
TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Ranged"), true);
|
||||
}
|
||||
|
||||
// Ranged: target too high for parabolic arc even at closest approach (check 2)
|
||||
{
|
||||
const defender = ++entityID;
|
||||
AddMock(defender, IID_Position, {
|
||||
"IsInWorld": () => true,
|
||||
"GetHeightOffset": () => 20,
|
||||
"GetPosition": () => new Vector3D(1, 20, 2)
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
||||
"GetEffectiveParabolicRange": () => -1, // out of range
|
||||
"GetMaxReachableParabolicHeight": () => 10
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
|
||||
"IsInTargetRange": () => false
|
||||
});
|
||||
|
||||
// heightDiff = 20 - 0 = 20, maxReachableHeightDiff = 10 → unreachable
|
||||
TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Ranged"), false);
|
||||
}
|
||||
}
|
||||
testCanEverReachTarget();
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const enemyPlayer = 2;
|
|||
const alliedPlayer = 3;
|
||||
const turretHolderID = 9;
|
||||
const entitiesToTest = [10, 11, 12, 13];
|
||||
let entityID = 100;
|
||||
|
||||
AddMock(turretHolderID, IID_Ownership, {
|
||||
"GetOwner": () => player
|
||||
|
|
@ -244,3 +245,80 @@ cmpTurretHolder.OnOwnershipChanged({
|
|||
"from": INVALID_PLAYER
|
||||
});
|
||||
TS_ASSERT(cmpTurretHolder.OccupiesTurretPoint(spawned));
|
||||
|
||||
// Test GetClosestApproachDistanceToTurretPoint
|
||||
{
|
||||
const holder = ++entityID;
|
||||
|
||||
// Mock the holder's obstruction
|
||||
AddMock(holder, IID_Obstruction, {
|
||||
"GetBlockMovementFlag": () => true,
|
||||
"GetObstructionHalfSizes": () => ({ "x": 10, "y": 15 })
|
||||
});
|
||||
|
||||
const cmpHolder = ConstructComponent(holder, "TurretHolder", {
|
||||
"TurretPoints": {
|
||||
"center": {
|
||||
"X": "0",
|
||||
"Y": "5.0",
|
||||
"Z": "0"
|
||||
},
|
||||
"edge": {
|
||||
"X": "8.0",
|
||||
"Y": "5.0",
|
||||
"Z": "0"
|
||||
},
|
||||
"corner": {
|
||||
"X": "10.0",
|
||||
"Y": "5.0",
|
||||
"Z": "15.0"
|
||||
},
|
||||
"outside": {
|
||||
"X": "15.0",
|
||||
"Y": "5.0",
|
||||
"Z": "0"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Center point (0,0) in 20x30 building → min(10, 15) = 10
|
||||
TS_ASSERT_EQUALS(cmpHolder.GetClosestApproachDistanceToTurretPoint("center"), 10);
|
||||
|
||||
// Edge point (8,0) in 20x30 building → min(10-8, 15-0) = 2
|
||||
TS_ASSERT_EQUALS(cmpHolder.GetClosestApproachDistanceToTurretPoint("edge"), 2);
|
||||
|
||||
// Corner point (10,15) in 20x30 building → min(10-10, 15-15) = 0
|
||||
TS_ASSERT_EQUALS(cmpHolder.GetClosestApproachDistanceToTurretPoint("corner"), 0);
|
||||
|
||||
// Outside point (15,0) in 20x30 building → min(10-15, 15-0) = -5 → clamped to 0
|
||||
TS_ASSERT_EQUALS(cmpHolder.GetClosestApproachDistanceToTurretPoint("outside"), 0);
|
||||
|
||||
// Nonexistent turret point
|
||||
TS_ASSERT_EQUALS(cmpHolder.GetClosestApproachDistanceToTurretPoint("nonexistent"), 0);
|
||||
|
||||
// Pass object directly
|
||||
const turretPoint = cmpHolder.TurretPointByName("center");
|
||||
TS_ASSERT_EQUALS(cmpHolder.GetClosestApproachDistanceToTurretPoint(turretPoint), 10);
|
||||
|
||||
// Passable building (no obstruction or doesn't block movement)
|
||||
const passableHolder = ++entityID;
|
||||
AddMock(passableHolder, IID_Obstruction, {
|
||||
"GetBlockMovementFlag": () => false
|
||||
});
|
||||
const cmpHolderPassable = ConstructComponent(passableHolder, "TurretHolder", {
|
||||
"TurretPoints": {
|
||||
"center": { "X": "0", "Y": "5.0", "Z": "0" }
|
||||
}
|
||||
});
|
||||
TS_ASSERT_EQUALS(cmpHolderPassable.GetClosestApproachDistanceToTurretPoint("center"), 0);
|
||||
|
||||
// No obstruction component at all
|
||||
++entityID;
|
||||
const cmpHolderNoObst = ConstructComponent(entityID, "TurretHolder", {
|
||||
"TurretPoints": {
|
||||
"center": { "X": "0", "Y": "5.0", "Z": "0" }
|
||||
}
|
||||
});
|
||||
TS_ASSERT_EQUALS(cmpHolderNoObst.GetClosestApproachDistanceToTurretPoint("center"), 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@
|
|||
{ "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.9 }
|
||||
],
|
||||
"auraName": "Economic Fortune",
|
||||
"auraDescription": "Solon brought in a new system of weights and measures, fathers were encouraged to find trades for their sons.\nEconomic technologies −10% resource costs."
|
||||
"auraDescription": "Solon brought in a new system of weights and measures, and fathers were encouraged to find trades for their sons.\nEconomic technologies −10% resource costs."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@
|
|||
{ "value": "TerritoryInfluence/Radius", "multiply": 1.2 }
|
||||
],
|
||||
"auraName": "Territorial Expansion",
|
||||
"auraDescription": "At its height, the Empire's borders spanned from the Fergana Valley in the west, to Korea in the east, and to northern Vietnam in the south.\nTerritory influence bonus +20%."
|
||||
"auraDescription": "At its height, the Empire's borders spanned from the Fergana Valley in the west to Korea in the east and to northern Vietnam in the south.\nTerritory influence bonus +20%."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@
|
|||
{ "value": "Resistance/Entity/Damage/Crush", "add": 1 }
|
||||
],
|
||||
"auraName": "Founder and Defender of the Republic",
|
||||
"auraDescription": "Brutus was one of the key figures in the overthrow of the monarchy and the founding of the Roman Republic. Later, as consul he led a Roman army to victory against the Etruscan King Tarquinius who sought to retake the throne.\nHumans and Siege Engines +1 crush, hack, pierce resistance."
|
||||
"auraDescription": "Brutus was one of the key figures in the overthrow of the monarchy and the founding of the Roman Republic. Later, as consul, he led a Roman army to victory against the Etruscan King Tarquinius who sought to retake the throne.\nHumans and Siege Engines +1 crush, hack, and pierce resistance."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ function ChangeEntityTemplate(oldEnt, newTemplate)
|
|||
if (cmpVisual && cmpNewVisual)
|
||||
cmpNewVisual.SetActorSeed(cmpVisual.GetActorSeed());
|
||||
|
||||
// Set ownership so turret checks work properly
|
||||
const cmpOwnership = Engine.QueryInterface(oldEnt, IID_Ownership);
|
||||
const cmpNewOwnership = Engine.QueryInterface(newEnt, IID_Ownership);
|
||||
if (cmpOwnership && cmpNewOwnership)
|
||||
cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
|
||||
|
||||
const cmpOldTurretable = Engine.QueryInterface(oldEnt, IID_Turretable);
|
||||
|
||||
// If the old entity is turreted, we need to handle it before copying position
|
||||
|
|
@ -39,9 +45,15 @@ function ChangeEntityTemplate(oldEnt, newTemplate)
|
|||
// Check if it's allowed to occupy the turret point
|
||||
const cmpTurretHolderOfOldEnt = Engine.QueryInterface(cmpOldTurretable.HolderID(), IID_TurretHolder);
|
||||
|
||||
if (cmpTurretHolderOfNewEnt &&
|
||||
!cmpTurretHolderOfOldEnt.AllowedToOccupyTurretPoint(newEnt, cmpOldTurretable.GetTurretPointName(), true))
|
||||
cmpOldTurretable.LeaveTurret(true);
|
||||
if (cmpTurretHolderOfOldEnt)
|
||||
{
|
||||
// Find the actual turret point object using the old entity
|
||||
const turretPoint = cmpTurretHolderOfOldEnt.GetOccupiedTurretPoint(oldEnt);
|
||||
|
||||
if (!turretPoint || !cmpTurretHolderOfOldEnt.AllowedToOccupyTurretPoint(newEnt, turretPoint, true))
|
||||
cmpOldTurretable.LeaveTurret(true);
|
||||
// If allowed, don't leave the turret - OnEntityRenamed will handle the swap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,24 +98,6 @@ function ChangeEntityTemplate(oldEnt, newTemplate)
|
|||
for (const entity of cmpTurretHolder.GetEntities())
|
||||
cmpNewTurretHolder.SetReservedTurretPoint(cmpTurretHolder.GetOccupiedTurretPointName(entity));
|
||||
|
||||
let owner;
|
||||
const cmpTerritoryDecay = Engine.QueryInterface(newEnt, IID_TerritoryDecay);
|
||||
if (cmpTerritoryDecay && cmpTerritoryDecay.HasTerritoryOwnership() && cmpNewPosition)
|
||||
{
|
||||
const pos = cmpNewPosition.GetPosition2D();
|
||||
const cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager);
|
||||
owner = cmpTerritoryManager.GetOwner(pos.x, pos.y);
|
||||
}
|
||||
else
|
||||
{
|
||||
const cmpOwnership = Engine.QueryInterface(oldEnt, IID_Ownership);
|
||||
if (cmpOwnership)
|
||||
owner = cmpOwnership.GetOwner();
|
||||
}
|
||||
const cmpNewOwnership = Engine.QueryInterface(newEnt, IID_Ownership);
|
||||
if (cmpNewOwnership)
|
||||
cmpNewOwnership.SetOwner(owner);
|
||||
|
||||
CopyControlGroups(oldEnt, newEnt);
|
||||
|
||||
// Rescale capture points
|
||||
|
|
|
|||
260
cmake/0ad-BuildFlags.cmake
Normal file
260
cmake/0ad-BuildFlags.cmake
Normal 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
47
cmake/0ad-Functions.cmake
Normal 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)
|
||||
54
cmake/FindPrebuildLibrary.cmake
Normal file
54
cmake/FindPrebuildLibrary.cmake
Normal 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()
|
||||
35
cmake/PrecompiledHeader.cmake
Normal file
35
cmake/PrecompiledHeader.cmake
Normal 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()
|
||||
33
cmake/SetupWindowsLibs.cmake
Normal file
33
cmake/SetupWindowsLibs.cmake
Normal 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)
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -69,7 +69,7 @@ CObjectBase::CObjectBase(CObjectManager& objectManager, CActorDef& actorDef, u8
|
|||
m_Properties.m_FloatOnWater = false;
|
||||
|
||||
// Remove leading art/actors/ & include quality level.
|
||||
m_Identifier = m_ActorDef.m_Pathname.string8().substr(11) + CStr::FromInt(m_QualityLevel);
|
||||
m_Identifier = fmt::format("{}{}", m_ActorDef.m_Pathname.string8().substr(11), m_QualityLevel);
|
||||
}
|
||||
|
||||
std::unique_ptr<CObjectBase> CObjectBase::CopyWithQuality(u8 newQualityLevel) const
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -156,7 +156,8 @@ bool ResolveIncludesImpl(
|
|||
it = includeCache.emplace(path, std::move(includeContent)).first;
|
||||
}
|
||||
// We need to insert #line directives to have correct line numbers in errors.
|
||||
chunks.emplace_back(lineDirective + "1\n" + it->second + "\n" + lineDirective + CStr::FromUInt(line + 1) + "\n");
|
||||
chunks.emplace_back(fmt::format("{}1\n{}\n{}{}\n", lineDirective, it->second, lineDirective,
|
||||
line + 1));
|
||||
processedParts.emplace_back(currentPart.substr(0, lineStart));
|
||||
if (!ResolveIncludesImpl(chunks.back(), includeCache, includeCallback, chunks, processedParts))
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -755,7 +755,7 @@ void CGUI::Xeromyces_ReadObject(const XMBData& xmb, XMBElement element, IGUIObje
|
|||
// Check if name isn't set, generate an internal name in that case.
|
||||
if (!NameSet)
|
||||
{
|
||||
object->SetName("__internal(" + CStr::FromInt(m_InternalNameNumber) + ")");
|
||||
object->SetName(fmt::format("__internal({})", m_InternalNameNumber));
|
||||
++m_InternalNameNumber;
|
||||
}
|
||||
|
||||
|
|
@ -977,7 +977,7 @@ void CGUI::Xeromyces_ReadRepeat(const XMBData& xmb, XMBElement element, IGUIObje
|
|||
|
||||
for (int n = 0; n < count; ++n)
|
||||
{
|
||||
NameSubst.emplace_back(var, "[" + CStr::FromInt(n) + "]");
|
||||
NameSubst.emplace_back(var, fmt::format("[{}]", n));
|
||||
|
||||
XERO_ITER_EL(element, child)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ CStr CNetMessage::ToString() const
|
|||
if (GetType() == NMT_INVALID)
|
||||
return "MESSAGE_TYPE_NONE { Undefined Message }";
|
||||
else
|
||||
return "Unknown Message " + CStr::FromInt(GetType());
|
||||
return fmt::format("Unknown Message {}", static_cast<int>(GetType()));
|
||||
}
|
||||
|
||||
CNetMessage* CNetMessageFactory::CreateMessage(const void* pData,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@
|
|||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <fmt/format.h>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
|
|
@ -70,6 +71,10 @@
|
|||
#include <miniupnpc/upnperrors.h>
|
||||
#endif
|
||||
|
||||
#if FMT_VERSION >= 80000
|
||||
#include <fmt/xchar.h>
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Number of peers to allocate for the enet host.
|
||||
* Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
|
||||
|
|
@ -1641,7 +1646,7 @@ CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
|
|||
if (unique)
|
||||
return name;
|
||||
|
||||
name = original + L" (" + CStrW::FromUInt(id++) + L")";
|
||||
name = fmt::format(L"{}({})", original, id++);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
#include "NetStats.h"
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <string>
|
||||
|
||||
enum
|
||||
|
|
@ -77,7 +78,7 @@ const std::vector<ProfileColumn>& CNetStatsTable::GetColumns()
|
|||
std::lock_guard<std::mutex> lock(m_Mutex);
|
||||
|
||||
for (size_t i = 0; i < m_LatchedData.size(); ++i)
|
||||
m_ColumnDescriptions.push_back(ProfileColumn("Peer "+CStr::FromUInt(i), 80));
|
||||
m_ColumnDescriptions.push_back(ProfileColumn(fmt::format("Peer {}", i), 80));
|
||||
}
|
||||
|
||||
return m_ColumnDescriptions;
|
||||
|
|
@ -95,7 +96,7 @@ CStr CNetStatsTable::GetCellText(size_t row, size_t col)
|
|||
#define ROW(id, title, member) \
|
||||
case id: \
|
||||
if (col == 0) return title; \
|
||||
if (m_Peer) return CStr::FromUInt(m_Peer->member); \
|
||||
if (m_Peer) return std::to_string(m_Peer->member); \
|
||||
return "???"
|
||||
|
||||
switch(row)
|
||||
|
|
@ -129,7 +130,7 @@ void CNetStatsTable::LatchHostState(const ENetHost& host)
|
|||
std::lock_guard<std::mutex> lock(m_Mutex);
|
||||
|
||||
#define ROW(id, title, member) \
|
||||
m_LatchedData[i].push_back(CStr::FromUInt(host.peers[i].member));
|
||||
m_LatchedData[i].push_back(std::to_string(host.peers[i].member));
|
||||
|
||||
m_LatchedData.clear();
|
||||
m_LatchedData.resize(host.peerCount);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
static inline CStr NetMessageStringConvert(u32 arg)
|
||||
{
|
||||
return CStr::FromUInt(arg);
|
||||
return std::to_string(arg);
|
||||
}
|
||||
|
||||
static inline CStr NetMessageStringConvert(const CStr8& arg)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2021 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
|
||||
|
|
@ -202,27 +202,6 @@ CStr CStr::Repeat(const CStr& str, size_t reps)
|
|||
|
||||
// Construction from numbers:
|
||||
|
||||
CStr CStr::FromInt(int n)
|
||||
{
|
||||
tstringstream<StrBase> ss;
|
||||
ss << n;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
CStr CStr::FromUInt(unsigned int n)
|
||||
{
|
||||
tstringstream<StrBase> ss;
|
||||
ss << n;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
CStr CStr::FromInt64(i64 n)
|
||||
{
|
||||
tstringstream<StrBase> ss;
|
||||
ss << n;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
CStr CStr::FromDouble(double n)
|
||||
{
|
||||
tstringstream<StrBase> ss;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -95,9 +95,6 @@ public:
|
|||
|
||||
// Conversions:
|
||||
|
||||
static CStr FromInt(int n);
|
||||
static CStr FromUInt(unsigned int n);
|
||||
static CStr FromInt64(i64 n);
|
||||
static CStr FromDouble(double n);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,6 +27,7 @@
|
|||
#include <SDL_keycode.h>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <fmt/format.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
|
@ -122,7 +123,7 @@ CStr FindScancodeName(SDL_Scancode scancode)
|
|||
const char* name = SDL_GetScancodeName(scancode);
|
||||
// Some scancodes have no name, but we must have something to save/load/recognize it, so parse it as SYM_XX
|
||||
if (strlen(name) == 0)
|
||||
return CStr("SYM_") + CStr::FromInt(scancode);
|
||||
return fmt::format("SYM_{}", static_cast<int>(scancode));
|
||||
return name;
|
||||
}
|
||||
|
||||
|
|
@ -221,7 +222,7 @@ CStr FindKeyName(SDL_Scancode scancode)
|
|||
return name;
|
||||
|
||||
// Else, show something regardless, so the player knows it's at least recognized.
|
||||
return CStr("SYM_") + CStr::FromInt(scancode);
|
||||
return fmt::format("SYM_{}", static_cast<int>(scancode));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -517,7 +517,7 @@ void rewriteBuffer(u8* buffer, u32& bufferSize)
|
|||
std::string basic = attrib;
|
||||
std::map<std::string, double>::iterator time_attrib = time_per_attribute.find(attrib);
|
||||
if (time_attrib != time_per_attribute.end())
|
||||
basic += " " + CStr::FromInt(1000000*time_attrib->second) + "us";
|
||||
basic = fmt::format("{} {}us", basic, 1000000 * time_attrib->second);
|
||||
|
||||
u32 length = static_cast<u32>(basic.size());
|
||||
memcpy(buffer + writePos, &length, sizeof(length));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -44,6 +44,7 @@
|
|||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <deque>
|
||||
#include <fmt/format.h>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
|
@ -337,7 +338,7 @@ private:
|
|||
{
|
||||
long code = -1;
|
||||
curl_easy_getinfo(m_Curl, CURLINFO_RESPONSE_CODE, &code);
|
||||
SetStatus("completed:" + CStr::FromInt(code));
|
||||
SetStatus(fmt::format("completed:{}", code));
|
||||
|
||||
// Check for success code
|
||||
if (code == 200)
|
||||
|
|
@ -360,7 +361,7 @@ private:
|
|||
if (errorString.empty())
|
||||
errorString = curl_easy_strerror(err);
|
||||
|
||||
SetStatus("failed:" + CStr::FromInt(err) + ":" + errorString);
|
||||
SetStatus(fmt::format("failed:{}:{}", static_cast<int>(err), errorString));
|
||||
}
|
||||
|
||||
// We got an unhandled return code or a connection failure;
|
||||
|
|
@ -385,12 +386,12 @@ private:
|
|||
r += "user_id=";
|
||||
AppendEscaped(r, m_UserID);
|
||||
|
||||
r += "&time=" + CStr::FromInt64(report.m_Time);
|
||||
r = fmt::format("{}&time={}", std::move(r), report.m_Time);
|
||||
|
||||
r += "&type=";
|
||||
AppendEscaped(r, report.m_Type);
|
||||
|
||||
r += "&version=" + CStr::FromInt(report.m_Version);
|
||||
r = fmt::format("{}&version={}", std::move(r), report.m_Version);
|
||||
|
||||
r += "&data=";
|
||||
AppendEscaped(r, report.m_Data);
|
||||
|
|
@ -529,7 +530,7 @@ bool CUserReporter::IsReportingEnabled()
|
|||
|
||||
void CUserReporter::SetReportingEnabled(bool enabled)
|
||||
{
|
||||
CStr val = CStr::FromInt(enabled ? REPORTER_VERSION : 0);
|
||||
const std::string val{std::to_string(enabled ? REPORTER_VERSION : 0)};
|
||||
g_ConfigDB.SetValueString(CFG_USER, "userreport.enabledversion", val);
|
||||
g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.enabledversion", val);
|
||||
|
||||
|
|
|
|||
|
|
@ -90,21 +90,21 @@ CStr CScriptStatsTable::GetCellText(size_t row, size_t col)
|
|||
if (col == 0)
|
||||
return "max nominal heap bytes";
|
||||
uint32_t n = JS_GetGCParameter(m_ScriptInterfaces.at(col-1).first->GetGeneralJSContext(), JSGC_MAX_BYTES);
|
||||
return CStr::FromUInt(n);
|
||||
return std::to_string(n);
|
||||
}
|
||||
case Row_Bytes:
|
||||
{
|
||||
if (col == 0)
|
||||
return "allocated bytes";
|
||||
uint32_t n = JS_GetGCParameter(m_ScriptInterfaces.at(col-1).first->GetGeneralJSContext(), JSGC_BYTES);
|
||||
return CStr::FromUInt(n);
|
||||
return std::to_string(n);
|
||||
}
|
||||
case Row_NumberGC:
|
||||
{
|
||||
if (col == 0)
|
||||
return "number of GCs";
|
||||
uint32_t n = JS_GetGCParameter(m_ScriptInterfaces.at(col-1).first->GetGeneralJSContext(), JSGC_NUMBER);
|
||||
return CStr::FromUInt(n);
|
||||
return std::to_string(n);
|
||||
}
|
||||
default:
|
||||
return "???";
|
||||
|
|
@ -116,4 +116,4 @@ AbstractProfileTable* CScriptStatsTable::GetChild(size_t /*row*/)
|
|||
return 0;
|
||||
}
|
||||
|
||||
} // namespace Script
|
||||
} // namespace Script
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ struct Query
|
|||
CEntityHandle source; // TODO: this could crash if an entity is destroyed while a Query is still referencing it
|
||||
entity_pos_t minRange;
|
||||
entity_pos_t maxRange;
|
||||
entity_pos_t baseRange; // Non-parabolic detection range
|
||||
entity_pos_t yOrigin; // Used for parabolas only.
|
||||
u32 ownersMask;
|
||||
i32 interface;
|
||||
|
|
@ -195,6 +196,7 @@ struct Query
|
|||
bool enabled;
|
||||
bool parabolic;
|
||||
bool accountForSize; // If true, the query accounts for unit sizes, otherwise it treats all entities as points.
|
||||
bool preferMirages; // If true, include mirages and filter HIDDEN entities. Otherwise exclude mirages and no visibility filter.
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -349,6 +351,7 @@ struct SerializeHelper<Query>
|
|||
{
|
||||
serialize.NumberFixed_Unbounded("min range", value.minRange);
|
||||
serialize.NumberFixed_Unbounded("max range", value.maxRange);
|
||||
serialize.NumberFixed_Unbounded("baseRange", value.baseRange);
|
||||
serialize.NumberFixed_Unbounded("yOrigin", value.yOrigin);
|
||||
serialize.NumberU32_Unbounded("owners mask", value.ownersMask);
|
||||
serialize.NumberI32_Unbounded("interface", value.interface);
|
||||
|
|
@ -357,6 +360,7 @@ struct SerializeHelper<Query>
|
|||
serialize.Bool("enabled", value.enabled);
|
||||
serialize.Bool("parabolic",value.parabolic);
|
||||
serialize.Bool("account for size",value.accountForSize);
|
||||
serialize.Bool("preferMirages", value.preferMirages);
|
||||
}
|
||||
|
||||
void operator()(ISerializer& serialize, const char* name, Query& value, const CSimContext&)
|
||||
|
|
@ -992,21 +996,20 @@ public:
|
|||
|
||||
tag_t CreateActiveQuery(entity_id_t source,
|
||||
entity_pos_t minRange, entity_pos_t maxRange,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags, bool accountForSize) override
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags,
|
||||
bool accountForSize, bool preferMirages) override
|
||||
{
|
||||
tag_t id = m_QueryNext++;
|
||||
m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flags, accountForSize);
|
||||
|
||||
m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flags, accountForSize, preferMirages);
|
||||
return id;
|
||||
}
|
||||
|
||||
tag_t CreateActiveParabolicQuery(entity_id_t source,
|
||||
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags) override
|
||||
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t baseRange, entity_pos_t yOrigin,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags, bool preferMirages = false) override
|
||||
{
|
||||
tag_t id = m_QueryNext++;
|
||||
m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, yOrigin, owners, requiredInterface, flags, true);
|
||||
|
||||
m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, baseRange, yOrigin, owners, requiredInterface, flags, true, preferMirages);
|
||||
return id;
|
||||
}
|
||||
|
||||
|
|
@ -1297,10 +1300,35 @@ public:
|
|||
if (id == q.source.GetId())
|
||||
return false;
|
||||
|
||||
// Ignore if it's missing the required interface
|
||||
if (q.interface && !GetSimContext().GetComponentManager().QueryInterface(id, q.interface))
|
||||
// Check if this is a mirage entity
|
||||
CmpPtr<ICmpMirage> cmpMirage(GetSimContext(), id);
|
||||
bool isMirage = !!cmpMirage;
|
||||
|
||||
// If it's a mirage and we're not including mirages, skip it
|
||||
if (isMirage && !q.preferMirages)
|
||||
return false;
|
||||
|
||||
// If it's not a mirage, check interface normally
|
||||
if (!isMirage && q.interface && !GetSimContext().GetComponentManager().QueryInterface(id, q.interface))
|
||||
return false;
|
||||
|
||||
// Filter hidden entities when we want mirages (i.e., we care about visibility)
|
||||
if (q.preferMirages && q.source.GetId() != INVALID_ENTITY)
|
||||
{
|
||||
// Look up the source's current owner
|
||||
EntityMap<EntityData>::const_iterator itSource = m_EntityData.find(q.source.GetId());
|
||||
if (itSource != m_EntityData.end())
|
||||
{
|
||||
player_id_t sourceOwner = itSource->second.owner;
|
||||
if (sourceOwner != INVALID_PLAYER)
|
||||
{
|
||||
LosVisibility vis = GetPlayerVisibility(entity.visibilities, sourceOwner);
|
||||
if (vis == LosVisibility::HIDDEN)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1324,13 +1352,18 @@ public:
|
|||
// Not the entire world, so check a parabolic range, or a regular range.
|
||||
else if (q.parabolic)
|
||||
{
|
||||
// The yOrigin is part of the 3D position, as the source is really that much heigher.
|
||||
// The yOrigin is part of the 3D position, as the source is really that much higher.
|
||||
CmpPtr<ICmpPosition> cmpSourcePosition(q.source);
|
||||
CFixedVector3D pos3d = cmpSourcePosition->GetPosition()+
|
||||
CFixedVector3D(entity_pos_t::Zero(), q.yOrigin, entity_pos_t::Zero()) ;
|
||||
// Get a quick list of entities that are potentially in range, with a cutoff of 2*maxRange.
|
||||
CFixedVector3D pos3d = cmpSourcePosition->GetPosition() +
|
||||
CFixedVector3D(entity_pos_t::Zero(), q.yOrigin, entity_pos_t::Zero());
|
||||
// Get a quick list of entities that are potentially in range.
|
||||
// For parabolic queries, the search radius must cover:
|
||||
// 1. The baseRange circle (non-parabolic detection)
|
||||
// 2. The maximum possible horizontal extent of the parabolic range
|
||||
// Multiplying maxRange by 2 provides a safe upper bound for all possible height differences.
|
||||
entity_pos_t subdivisionRange = std::max(q.baseRange, q.maxRange * 2);
|
||||
subdivisionResultsBuffer.clear();
|
||||
m_Subdivision.GetNear(subdivisionResultsBuffer, pos, q.maxRange * 2);
|
||||
m_Subdivision.GetNear(subdivisionResultsBuffer, pos, subdivisionRange);
|
||||
|
||||
for (size_t i = 0; i < subdivisionResultsBuffer.size(); ++i)
|
||||
{
|
||||
|
|
@ -1340,6 +1373,20 @@ public:
|
|||
if (!TestEntityQuery(q, it->first, it->second))
|
||||
continue;
|
||||
|
||||
CFixedVector2D delta2D = CFixedVector2D(it->second.x, it->second.z) - pos;
|
||||
|
||||
// Check base range first
|
||||
bool inBaseRange = !q.baseRange.IsZero() && delta2D.CompareLength(q.baseRange) <= 0;
|
||||
|
||||
if (inBaseRange)
|
||||
{
|
||||
// In base range - no need for parabolic check
|
||||
if (q.minRange.IsZero() || delta2D.CompareLength(q.minRange) >= 0)
|
||||
r.push_back(it->first);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parabolic check for entities outside base range
|
||||
CmpPtr<ICmpPosition> cmpSecondPosition(GetSimContext(), subdivisionResultsBuffer[i]);
|
||||
if (!cmpSecondPosition || !cmpSecondPosition->IsInWorld())
|
||||
continue;
|
||||
|
|
@ -1357,7 +1404,7 @@ public:
|
|||
continue;
|
||||
|
||||
if (!q.minRange.IsZero())
|
||||
if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange) < 0)
|
||||
if (delta2D.CompareLength(q.minRange) < 0)
|
||||
continue;
|
||||
|
||||
r.push_back(it->first);
|
||||
|
|
@ -1394,6 +1441,22 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute effective horizontal range given a reference range and height difference.
|
||||
*/
|
||||
static entity_pos_t ComputeParabolicRange(entity_pos_t range, entity_pos_t heightDiff)
|
||||
{
|
||||
if (heightDiff < -range / 2)
|
||||
return NEVER_IN_RANGE;
|
||||
|
||||
entity_pos_t effectiveRange;
|
||||
effectiveRange.SetInternalValue(static_cast<i32>(isqrt64(
|
||||
SQUARE_U64_FIXED(range) +
|
||||
static_cast<i64>(heightDiff.GetInternalValue()) * static_cast<i64>(range.GetInternalValue()) * 2
|
||||
)));
|
||||
return effectiveRange;
|
||||
}
|
||||
|
||||
entity_pos_t GetEffectiveParabolicRange(entity_id_t source, entity_id_t target, entity_pos_t range, entity_pos_t yOrigin) const override
|
||||
{
|
||||
// For non-positive ranges, just return the range.
|
||||
|
|
@ -1408,13 +1471,32 @@ public:
|
|||
if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld())
|
||||
return NEVER_IN_RANGE;
|
||||
|
||||
entity_pos_t heightDifference = cmpSourcePosition->GetHeightOffset() - cmpTargetPosition->GetHeightOffset() + yOrigin;
|
||||
if (heightDifference < -range / 2)
|
||||
return NEVER_IN_RANGE;
|
||||
// GetPosition() returns the world height (terrain + water + offset)
|
||||
CFixedVector3D sourcePos = cmpSourcePosition->GetPosition();
|
||||
CFixedVector3D targetPos = cmpTargetPosition->GetPosition();
|
||||
|
||||
entity_pos_t effectiveRange;
|
||||
effectiveRange.SetInternalValue(static_cast<i32>(isqrt64(SQUARE_U64_FIXED(range) + static_cast<i64>(heightDifference.GetInternalValue()) * static_cast<i64>(range.GetInternalValue()) * 2)));
|
||||
return effectiveRange;
|
||||
entity_pos_t heightDiff = sourcePos.Y - targetPos.Y + yOrigin;
|
||||
return ComputeParabolicRange(range, heightDiff);
|
||||
}
|
||||
|
||||
entity_pos_t GetMaxReachableParabolicHeight(entity_pos_t range, entity_pos_t yOrigin, entity_pos_t horizDistance) const override
|
||||
{
|
||||
// EffectiveRange² = range² + 2 * range * heightDiff
|
||||
// Solve for heightDiff when effectiveRange = horizDistance:
|
||||
// heightDiff = (horizDistance² - range²) / (2 * range)
|
||||
// Max target height above source = yOrigin - heightDiff
|
||||
// = yOrigin + (range² - horizDistance²) / (2 * range)
|
||||
//
|
||||
// If horizDistance > range, the result is less than yOrigin (can be negative),
|
||||
// meaning the source must be above the target to compensate for the extra horizontal distance.
|
||||
// The caller can decide if that's acceptable.
|
||||
i64 rangeSq = SQUARE_U64_FIXED(range);
|
||||
i64 distSq = SQUARE_U64_FIXED(horizDistance);
|
||||
i64 numerator = rangeSq - distSq;
|
||||
|
||||
entity_pos_t result;
|
||||
result.SetInternalValue(static_cast<i32>(numerator / static_cast<i64>(range.GetInternalValue() * 2)));
|
||||
return yOrigin + result;
|
||||
}
|
||||
|
||||
entity_pos_t GetElevationAdaptedRange(const CFixedVector3D& pos1, const CFixedVector3D& rot, entity_pos_t range, entity_pos_t yOrigin, entity_pos_t angle) const override
|
||||
|
|
@ -1519,7 +1601,8 @@ public:
|
|||
|
||||
Query ConstructQuery(entity_id_t source,
|
||||
entity_pos_t minRange, entity_pos_t maxRange,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flagsMask, bool accountForSize) const
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flagsMask,
|
||||
bool accountForSize, bool preferMirages = false) const
|
||||
{
|
||||
// Min range must be non-negative.
|
||||
if (minRange < entity_pos_t::Zero())
|
||||
|
|
@ -1530,6 +1613,8 @@ public:
|
|||
if (maxRange < entity_pos_t::Zero() && maxRange != ALWAYS_IN_RANGE)
|
||||
LOGWARNING("CCmpRangeManager: Invalid max range %f in query for entity %u", maxRange.ToDouble(), source);
|
||||
|
||||
CmpPtr<ICmpOwnership> cmpOwnership(GetSimContext(), source);
|
||||
|
||||
Query q;
|
||||
q.enabled = false;
|
||||
q.parabolic = false;
|
||||
|
|
@ -1538,6 +1623,7 @@ public:
|
|||
q.maxRange = maxRange;
|
||||
q.yOrigin = entity_pos_t::Zero();
|
||||
q.accountForSize = accountForSize;
|
||||
q.preferMirages = preferMirages;
|
||||
|
||||
if (q.accountForSize && q.source.GetId() != INVALID_ENTITY && q.maxRange != ALWAYS_IN_RANGE)
|
||||
{
|
||||
|
|
@ -1574,12 +1660,14 @@ public:
|
|||
}
|
||||
|
||||
Query ConstructParabolicQuery(entity_id_t source,
|
||||
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flagsMask, bool accountForSize) const
|
||||
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t baseRange, entity_pos_t yOrigin,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flagsMask,
|
||||
bool accountForSize, bool preferMirages = false) const
|
||||
{
|
||||
Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flagsMask, accountForSize);
|
||||
Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flagsMask, accountForSize, preferMirages);
|
||||
q.parabolic = true;
|
||||
q.yOrigin = yOrigin;
|
||||
q.baseRange = baseRange;
|
||||
return q;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -45,6 +45,14 @@ std::string ICmpObstruction::CheckFoundation_wrapper(const std::string& classNam
|
|||
}
|
||||
}
|
||||
|
||||
CFixedVector2D ICmpObstruction::GetObstructionHalfSizes_wrapper() const
|
||||
{
|
||||
ICmpObstructionManager::ObstructionSquare square;
|
||||
if (!GetObstructionSquare(square))
|
||||
return CFixedVector2D(entity_pos_t::FromInt(-1), entity_pos_t::FromInt(-1));
|
||||
return CFixedVector2D(square.hw, square.hh);
|
||||
}
|
||||
|
||||
BEGIN_INTERFACE_WRAPPER(Obstruction)
|
||||
DEFINE_INTERFACE_METHOD("GetSize", ICmpObstruction, GetSize)
|
||||
DEFINE_INTERFACE_METHOD("CheckShorePlacement", ICmpObstruction, CheckShorePlacement)
|
||||
|
|
@ -55,6 +63,7 @@ DEFINE_INTERFACE_METHOD("GetEntitiesBlockingConstruction", ICmpObstruction, GetE
|
|||
DEFINE_INTERFACE_METHOD("GetEntitiesDeletedUponConstruction", ICmpObstruction, GetEntitiesDeletedUponConstruction)
|
||||
DEFINE_INTERFACE_METHOD("SetActive", ICmpObstruction, SetActive)
|
||||
DEFINE_INTERFACE_METHOD("SetDisableBlockMovementPathfinding", ICmpObstruction, SetDisableBlockMovementPathfinding)
|
||||
DEFINE_INTERFACE_METHOD("GetObstructionHalfSizes", ICmpObstruction, GetObstructionHalfSizes_wrapper)
|
||||
DEFINE_INTERFACE_METHOD("GetBlockMovementFlag", ICmpObstruction, GetBlockMovementFlag)
|
||||
DEFINE_INTERFACE_METHOD("SetControlGroup", ICmpObstruction, SetControlGroup)
|
||||
DEFINE_INTERFACE_METHOD("GetControlGroup", ICmpObstruction, GetControlGroup)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -104,6 +104,12 @@ public:
|
|||
*/
|
||||
virtual std::string CheckFoundation_wrapper(const std::string& className, bool onlyCenterPoint) const;
|
||||
|
||||
/**
|
||||
* GetObstructionSquare wrapper for script calls.
|
||||
* @return [hw, hh] half-sizes of the obstruction square, or empty array on failure.
|
||||
*/
|
||||
virtual CFixedVector2D GetObstructionHalfSizes_wrapper() const;
|
||||
|
||||
/**
|
||||
* Test whether this entity is colliding with any obstructions that share its
|
||||
* control groups and block the creation of foundations.
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ DEFINE_INTERFACE_METHOD("GetLosRevealWholeMap", ICmpRangeManager, GetLosRevealWh
|
|||
DEFINE_INTERFACE_METHOD("SetLosRevealWholeMapForAll", ICmpRangeManager, SetLosRevealWholeMapForAll)
|
||||
DEFINE_INTERFACE_METHOD("GetLosRevealWholeMapForAll", ICmpRangeManager, GetLosRevealWholeMapForAll)
|
||||
DEFINE_INTERFACE_METHOD("GetEffectiveParabolicRange", ICmpRangeManager, GetEffectiveParabolicRange)
|
||||
DEFINE_INTERFACE_METHOD("GetMaxReachableParabolicHeight", ICmpRangeManager, GetMaxReachableParabolicHeight)
|
||||
DEFINE_INTERFACE_METHOD("GetElevationAdaptedRange", ICmpRangeManager, GetElevationAdaptedRange)
|
||||
DEFINE_INTERFACE_METHOD("ActivateScriptedVisibility", ICmpRangeManager, ActivateScriptedVisibility)
|
||||
DEFINE_INTERFACE_METHOD("GetLosVisibility", ICmpRangeManager, GetLosVisibility_wrapper)
|
||||
|
|
|
|||
|
|
@ -161,29 +161,42 @@ public:
|
|||
* @param requiredInterface if non-zero, an interface ID that matching entities must implement.
|
||||
* @param flags if a entity in range has one of the flags set it will show up.
|
||||
* @param accountForSize if true, compensate for source/target entity sizes.
|
||||
* @param preferMirages if true, mirage entities are included (bypassing interface checks)
|
||||
* and HIDDEN entities are filtered out (needed for targeting fogged enemies).
|
||||
* When false (default), mirages are excluded entirely and real entities
|
||||
* are returned regardless of visibility (even if hidden).
|
||||
* @return unique non-zero identifier of query.
|
||||
*/
|
||||
virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags, bool accountForSize) = 0;
|
||||
virtual tag_t CreateActiveQuery(entity_id_t source,
|
||||
entity_pos_t minRange, entity_pos_t maxRange,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags,
|
||||
bool accountForSize, bool preferMirages = false) = 0;
|
||||
|
||||
/**
|
||||
* Construct an active query of a paraboloic form around the unit.
|
||||
/**
|
||||
* Construct an active query of a parabolic form around the unit.
|
||||
* The query will be disabled by default.
|
||||
* @param source the entity around which the range will be computed.
|
||||
* @param minRange non-negative minimum horizontal distance in metres (inclusive). MinRange doesn't do parabolic checks.
|
||||
* @param maxRange non-negative maximum distance in metres (inclusive) for units on the same elevation;
|
||||
* or -1.0 to ignore distance.
|
||||
* For units on a different height positions, a physical correct paraboloid with height=maxRange/2 above the unit is used to query them
|
||||
* @param baseRange non-negative base detection range in metres (inclusive) for simple 2D circle checks.
|
||||
* Units within this horizontal distance are always considered in range regardless of height.
|
||||
* Set to 0 to disable (original parabolic-only behavior).
|
||||
* @param yOrigin extra bonus so the source can be placed higher and shoot further
|
||||
* @param owners list of player IDs that matching entities may have; -1 matches entities with no owner.
|
||||
* @param requiredInterface if non-zero, an interface ID that matching entities must implement.
|
||||
* @param flags if a entity in range has one of the flags set it will show up.
|
||||
* @param preferMirages if true, mirage entities are included (bypassing interface checks)
|
||||
* and HIDDEN entities are filtered out (needed for targeting fogged enemies).
|
||||
* When false (default), mirages are excluded entirely and real entities
|
||||
* are returned regardless of visibility (even if hidden).
|
||||
* NB: this one has no accountForSize parameter (assumed true), because we currently can only have 7 arguments for JS functions.
|
||||
* @return unique non-zero identifier of query.
|
||||
*/
|
||||
virtual tag_t CreateActiveParabolicQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags) = 0;
|
||||
|
||||
virtual tag_t CreateActiveParabolicQuery(entity_id_t source,
|
||||
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t baseRange, entity_pos_t yOrigin,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags, bool preferMirages = false) = 0;
|
||||
|
||||
/**
|
||||
* Get the effective range in a parablic range query.
|
||||
|
|
@ -195,6 +208,18 @@ public:
|
|||
*/
|
||||
virtual entity_pos_t GetEffectiveParabolicRange(entity_id_t source, entity_id_t target, entity_pos_t range, entity_pos_t yOrigin) const = 0;
|
||||
|
||||
/**
|
||||
* Get the max height (relative to the source) a parabolic projectile can reach
|
||||
* at a given horizontal distance.
|
||||
* @param source the entity at the origin.
|
||||
* @param range the maximum parabolic range on flat terrain.
|
||||
* @param yOrigin height bonus for the source.
|
||||
* @param horizDistance the horizontal distance to check.
|
||||
* @return the maximum reachable height difference (target height - source height),
|
||||
* or a very negative value if the horizontal distance exceeds the range.
|
||||
*/
|
||||
virtual entity_pos_t GetMaxReachableParabolicHeight(entity_pos_t range, entity_pos_t yOrigin, entity_pos_t horizDistance) const = 0;
|
||||
|
||||
/**
|
||||
* Get the average elevation over 8 points on distance range around the entity
|
||||
* @param id the entity id to look around
|
||||
|
|
|
|||
|
|
@ -63,13 +63,13 @@ public:
|
|||
entity_id_t GetTurretParent() const override {return INVALID_ENTITY;}
|
||||
void UpdateTurretPosition() override {}
|
||||
std::set<entity_id_t>* GetTurrets() override { return nullptr; }
|
||||
bool IsInWorld() const override { return true; }
|
||||
void MoveOutOfWorld() override { }
|
||||
bool IsInWorld() const override { return m_InWorld; }
|
||||
void MoveOutOfWorld() override { m_InWorld = false; }
|
||||
void MoveTo(entity_pos_t /*x*/, entity_pos_t /*z*/) override { }
|
||||
void MoveAndTurnTo(entity_pos_t /*x*/, entity_pos_t /*z*/, entity_angle_t /*a*/) override { }
|
||||
void JumpTo(entity_pos_t /*x*/, entity_pos_t /*z*/) override { }
|
||||
void SetHeightOffset(entity_pos_t /*dy*/) override { }
|
||||
entity_pos_t GetHeightOffset() const override { return entity_pos_t::Zero(); }
|
||||
void SetHeightOffset(entity_pos_t dy) override { m_HeightOffset = dy; }
|
||||
entity_pos_t GetHeightOffset() const override { return m_HeightOffset; }
|
||||
void SetHeightFixed(entity_pos_t /*y*/) override { }
|
||||
entity_pos_t GetHeightFixed() const override { return entity_pos_t::Zero(); }
|
||||
entity_pos_t GetHeightAtFixed(entity_pos_t, entity_pos_t) const override { return entity_pos_t::Zero(); }
|
||||
|
|
@ -94,6 +94,8 @@ public:
|
|||
CMatrix3D GetInterpolatedTransform(float /*frameOffset*/) const override { return CMatrix3D(); }
|
||||
|
||||
CFixedVector3D m_Pos;
|
||||
entity_pos_t m_HeightOffset = entity_pos_t::Zero();
|
||||
bool m_InWorld = true;
|
||||
};
|
||||
|
||||
class MockObstructionRgm : public ICmpObstruction
|
||||
|
|
@ -154,7 +156,7 @@ public:
|
|||
{
|
||||
ComponentTestHelper test(*g_ScriptContext);
|
||||
|
||||
ICmpRangeManager* cmp = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
||||
ICmpRangeManager* rangeManager = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
||||
|
||||
MockVisionRgm vision;
|
||||
test.AddMock(100, IID_Vision, vision);
|
||||
|
|
@ -165,41 +167,41 @@ public:
|
|||
// This tests that the incremental computation produces the correct result
|
||||
// in various edge cases
|
||||
|
||||
cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512));
|
||||
cmp->Verify();
|
||||
{ CMessageCreate msg(100); cmp->HandleMessage(msg, false); }
|
||||
cmp->Verify();
|
||||
{ CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); }
|
||||
cmp->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromDouble(257.95), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
||||
cmp->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromInt(253), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
||||
cmp->Verify();
|
||||
rangeManager->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512));
|
||||
rangeManager->Verify();
|
||||
{ CMessageCreate msg(100); rangeManager->HandleMessage(msg, false); }
|
||||
rangeManager->Verify();
|
||||
{ CMessageOwnershipChanged msg(100, -1, 1); rangeManager->HandleMessage(msg, false); }
|
||||
rangeManager->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromDouble(257.95), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||
rangeManager->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromInt(253), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||
rangeManager->Verify();
|
||||
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
||||
cmp->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||
rangeManager->Verify();
|
||||
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256)+entity_pos_t::Epsilon(), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
||||
cmp->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256)-entity_pos_t::Epsilon(), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
||||
cmp->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256)+entity_pos_t::Epsilon(), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
||||
cmp->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256)-entity_pos_t::Epsilon(), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
||||
cmp->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256)+entity_pos_t::Epsilon(), entity_pos_t::FromInt(256), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||
rangeManager->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256)-entity_pos_t::Epsilon(), entity_pos_t::FromInt(256), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||
rangeManager->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256)+entity_pos_t::Epsilon(), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||
rangeManager->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256)-entity_pos_t::Epsilon(), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||
rangeManager->Verify();
|
||||
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(383), entity_pos_t::FromInt(84), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
||||
cmp->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(348), entity_pos_t::FromInt(83), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
||||
cmp->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(383), entity_pos_t::FromInt(84), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||
rangeManager->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(348), entity_pos_t::FromInt(83), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||
rangeManager->Verify();
|
||||
|
||||
std::mt19937 rng;
|
||||
for (size_t i = 0; i < 1024; ++i)
|
||||
{
|
||||
double x = std::uniform_real_distribution<double>(0.0, 512.0)(rng);
|
||||
double z = std::uniform_real_distribution<double>(0.0, 512.0)(rng);
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromDouble(x), entity_pos_t::FromDouble(z), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
||||
cmp->Verify();
|
||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromDouble(x), entity_pos_t::FromDouble(z), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||
rangeManager->Verify();
|
||||
}
|
||||
|
||||
// Test OwnershipChange, GetEntitiesByPlayer, GetNonGaiaEntities
|
||||
|
|
@ -208,22 +210,22 @@ public:
|
|||
for (player_id_t newOwner = 0; newOwner < 8; ++newOwner)
|
||||
{
|
||||
CMessageOwnershipChanged msg(100, previousOwner, newOwner);
|
||||
cmp->HandleMessage(msg, false);
|
||||
rangeManager->HandleMessage(msg, false);
|
||||
|
||||
for (player_id_t i = 0; i < 8; ++i)
|
||||
TS_ASSERT_EQUALS(cmp->GetEntitiesByPlayer(i).size(), i == newOwner ? 1 : 0);
|
||||
TS_ASSERT_EQUALS(rangeManager->GetEntitiesByPlayer(i).size(), i == newOwner ? 1 : 0);
|
||||
|
||||
TS_ASSERT_EQUALS(cmp->GetNonGaiaEntities().size(), newOwner > 0 ? 1 : 0);
|
||||
TS_ASSERT_EQUALS(rangeManager->GetNonGaiaEntities().size(), newOwner > 0 ? 1 : 0);
|
||||
previousOwner = newOwner;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void test_queries()
|
||||
void test_range_queries_distance_only()
|
||||
{
|
||||
ComponentTestHelper test(*g_ScriptContext);
|
||||
|
||||
ICmpRangeManager* cmp = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
||||
ICmpRangeManager* rangeManager = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
||||
|
||||
MockVisionRgm vision, vision2;
|
||||
MockPositionRgm position, position2;
|
||||
|
|
@ -236,101 +238,204 @@ public:
|
|||
test.AddMock(101, IID_Position, position2);
|
||||
test.AddMock(101, IID_Obstruction, obs2);
|
||||
|
||||
cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512));
|
||||
cmp->Verify();
|
||||
{ CMessageCreate msg(100); cmp->HandleMessage(msg, false); }
|
||||
{ CMessageCreate msg(101); cmp->HandleMessage(msg, false); }
|
||||
rangeManager->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512));
|
||||
rangeManager->Verify();
|
||||
{ CMessageCreate msg(100); rangeManager->HandleMessage(msg, false); }
|
||||
{ CMessageCreate msg(101); rangeManager->HandleMessage(msg, false); }
|
||||
|
||||
{ CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); }
|
||||
{ CMessageOwnershipChanged msg(101, -1, 1); cmp->HandleMessage(msg, false); }
|
||||
// Don't set ownership for either entity - leave both as INVALID_PLAYER.
|
||||
// This bypasses the visibility check in TestEntityQuery, allowing us to test
|
||||
// the core distance calculation logic independently of the LOS system.
|
||||
|
||||
auto move = [&cmp](entity_id_t ent, MockPositionRgm& pos, fixed x, fixed z) {
|
||||
auto move = [&rangeManager](entity_id_t ent, MockPositionRgm& pos, fixed x, fixed z) {
|
||||
pos.m_Pos = CFixedVector3D(x, fixed::Zero(), z);
|
||||
{ CMessagePositionChanged msg(ent, true, x, z, entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
||||
{ CMessagePositionChanged msg(ent, true, x, z, entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||
};
|
||||
|
||||
move(100, position, fixed::FromInt(10), fixed::FromInt(10));
|
||||
move(101, position2, fixed::FromInt(10), fixed::FromInt(20));
|
||||
|
||||
std::vector<entity_id_t> nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
||||
// Query for owner -1 (INVALID_PLAYER) since both entities have no owner
|
||||
std::vector<entity_id_t> nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
||||
nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||
|
||||
move(101, position2, fixed::FromInt(10), fixed::FromInt(10));
|
||||
nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||
nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
||||
|
||||
move(101, position2, fixed::FromInt(10), fixed::FromInt(13));
|
||||
nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||
nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
||||
|
||||
move(101, position2, fixed::FromInt(10), fixed::FromInt(15));
|
||||
// In range thanks to self obstruction size.
|
||||
nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||
// In range thanks to target obstruction size.
|
||||
nearby = cmp->ExecuteQuery(101, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(101, fixed::FromInt(0), fixed::FromInt(4), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{100});
|
||||
|
||||
// Trickier: min-range is closest-to-closest, but rotation may change the real distance.
|
||||
nearby = cmp->ExecuteQuery(100, fixed::FromInt(2), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(2), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||
nearby = cmp->ExecuteQuery(100, fixed::FromInt(5), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(5), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||
nearby = cmp->ExecuteQuery(100, fixed::FromInt(6), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(6), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
||||
nearby = cmp->ExecuteQuery(101, fixed::FromInt(5), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(101, fixed::FromInt(5), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{100});
|
||||
nearby = cmp->ExecuteQuery(101, fixed::FromInt(6), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(101, fixed::FromInt(6), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
||||
|
||||
}
|
||||
|
||||
void test_IsInTargetParabolicRange()
|
||||
void test_range_queries_visibility_filtering()
|
||||
{
|
||||
ComponentTestHelper test(*g_ScriptContext);
|
||||
ICmpRangeManager* cmp = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
||||
|
||||
ICmpRangeManager* rangeManager = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
||||
|
||||
MockVisionRgm vision, vision2;
|
||||
MockPositionRgm position, position2;
|
||||
MockObstructionRgm obs(fixed::FromInt(2)), obs2(fixed::Zero());
|
||||
test.AddMock(100, IID_Vision, vision);
|
||||
test.AddMock(100, IID_Position, position);
|
||||
test.AddMock(100, IID_Obstruction, obs);
|
||||
|
||||
test.AddMock(101, IID_Vision, vision2);
|
||||
test.AddMock(101, IID_Position, position2);
|
||||
test.AddMock(101, IID_Obstruction, obs2);
|
||||
|
||||
rangeManager->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512));
|
||||
rangeManager->Verify();
|
||||
{ CMessageCreate msg(100); rangeManager->HandleMessage(msg, false); }
|
||||
{ CMessageCreate msg(101); rangeManager->HandleMessage(msg, false); }
|
||||
|
||||
// Set ownership for both entities so they have proper owners
|
||||
{ CMessageOwnershipChanged msg(100, -1, 1); rangeManager->HandleMessage(msg, false); }
|
||||
{ CMessageOwnershipChanged msg(101, -1, 1); rangeManager->HandleMessage(msg, false); }
|
||||
|
||||
auto move = [&rangeManager](entity_id_t ent, MockPositionRgm& pos, fixed x, fixed z) {
|
||||
pos.m_Pos = CFixedVector3D(x, fixed::Zero(), z);
|
||||
{ CMessagePositionChanged msg(ent, true, x, z, entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||
};
|
||||
|
||||
move(100, position, fixed::FromInt(10), fixed::FromInt(10));
|
||||
move(101, position2, fixed::FromInt(10), fixed::FromInt(15));
|
||||
|
||||
std::vector<int> owners;
|
||||
owners.push_back(1);
|
||||
|
||||
// Test 1: With preferMirages = true, we should get the mirage entity when visible
|
||||
ICmpRangeManager::tag_t query = rangeManager->CreateActiveQuery(
|
||||
100, // source
|
||||
fixed::FromInt(0), // minRange
|
||||
fixed::FromInt(50), // maxRange
|
||||
owners, // owners
|
||||
0, // requiredInterface
|
||||
rangeManager->GetEntityFlagMask("normal"),
|
||||
true, // accountForSize
|
||||
true // preferMirages = true
|
||||
);
|
||||
rangeManager->EnableActiveQuery(query);
|
||||
|
||||
// With reveal map enabled, entity should be visible
|
||||
rangeManager->SetLosRevealWholeMap(1, true);
|
||||
{ CMessageUpdate msg(fixed::FromInt(1)); rangeManager->HandleMessage(msg, false); }
|
||||
|
||||
std::vector<entity_id_t> nearby = rangeManager->ResetActiveQuery(query);
|
||||
// Should return either the real entity (101) or a mirage.
|
||||
// For this test, we just verify something is returned.
|
||||
TS_ASSERT_EQUALS(nearby.size(), 1);
|
||||
|
||||
// Disable reveal map - entity should become hidden
|
||||
rangeManager->SetLosRevealWholeMap(1, false);
|
||||
{ CMessageUpdate msg(fixed::FromInt(1)); rangeManager->HandleMessage(msg, false); }
|
||||
|
||||
nearby = rangeManager->ResetActiveQuery(query);
|
||||
// Should return empty (no visible entities)
|
||||
TS_ASSERT_EQUALS(nearby.size(), 0);
|
||||
|
||||
// Re-enable reveal map
|
||||
rangeManager->SetLosRevealWholeMap(1, true);
|
||||
{ CMessageUpdate msg(fixed::FromInt(1)); rangeManager->HandleMessage(msg, false); }
|
||||
|
||||
nearby = rangeManager->ResetActiveQuery(query);
|
||||
TS_ASSERT_EQUALS(nearby.size(), 1);
|
||||
|
||||
// Test 2: With preferMirages = false (default), visibility filtering is disabled
|
||||
ICmpRangeManager::tag_t query2 = rangeManager->CreateActiveQuery(
|
||||
100, // source
|
||||
fixed::FromInt(0), // minRange
|
||||
fixed::FromInt(50), // maxRange
|
||||
owners, // owners
|
||||
0, // requiredInterface
|
||||
rangeManager->GetEntityFlagMask("normal"),
|
||||
true, // accountForSize
|
||||
false // preferMirages = false
|
||||
);
|
||||
rangeManager->EnableActiveQuery(query2);
|
||||
|
||||
// With preferMirages = false, entity should be returned even when hidden
|
||||
rangeManager->SetLosRevealWholeMap(1, false);
|
||||
{ CMessageUpdate msg(fixed::FromInt(1)); rangeManager->HandleMessage(msg, false); }
|
||||
|
||||
nearby = rangeManager->ResetActiveQuery(query2);
|
||||
// Should return the real entity (101) even though hidden
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||
|
||||
// Clean up
|
||||
rangeManager->DestroyActiveQuery(query);
|
||||
rangeManager->DestroyActiveQuery(query2);
|
||||
}
|
||||
|
||||
void test_ParabolicRangeBasic()
|
||||
{
|
||||
ComponentTestHelper test(*g_ScriptContext);
|
||||
ICmpRangeManager* rangeManager = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
||||
|
||||
const entity_id_t source = 200;
|
||||
const entity_id_t target = 201;
|
||||
entity_pos_t range = fixed::FromInt(-3);
|
||||
entity_pos_t yOrigin = fixed::FromInt(-20);
|
||||
entity_pos_t range{fixed::FromInt(-3)};
|
||||
entity_pos_t yOrigin{fixed::FromInt(-20)};
|
||||
|
||||
// Invalid range.
|
||||
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), range);
|
||||
TS_ASSERT_EQUALS(rangeManager->GetEffectiveParabolicRange(source, target, range, yOrigin), range);
|
||||
|
||||
// No source ICmpPosition.
|
||||
range = fixed::FromInt(10);
|
||||
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE);
|
||||
TS_ASSERT_EQUALS(rangeManager->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE);
|
||||
|
||||
// No target ICmpPosition.
|
||||
MockPositionRgm cmpSourcePosition;
|
||||
test.AddMock(source, IID_Position, cmpSourcePosition);
|
||||
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE);
|
||||
TS_ASSERT_EQUALS(rangeManager->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE);
|
||||
|
||||
// Too much height difference.
|
||||
MockPositionRgm cmpTargetPosition;
|
||||
test.AddMock(target, IID_Position, cmpTargetPosition);
|
||||
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE);
|
||||
TS_ASSERT_EQUALS(rangeManager->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE);
|
||||
|
||||
// If no offset we get the range.
|
||||
range = fixed::FromInt(20);
|
||||
yOrigin = fixed::Zero();
|
||||
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), range);
|
||||
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, fixed::Zero(), yOrigin), fixed::Zero());
|
||||
TS_ASSERT_EQUALS(rangeManager->GetEffectiveParabolicRange(source, target, range, yOrigin), range);
|
||||
TS_ASSERT_EQUALS(rangeManager->GetEffectiveParabolicRange(source, target, fixed::Zero(), yOrigin), fixed::Zero());
|
||||
|
||||
// Normal case.
|
||||
// Normal case with yOrigin only (no terrain difference)
|
||||
yOrigin = fixed::FromInt(5);
|
||||
range = fixed::FromInt(10);
|
||||
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), fixed::FromFloat(14.142136f));
|
||||
TS_ASSERT_EQUALS(rangeManager->GetEffectiveParabolicRange(source, target, range, yOrigin), fixed::FromFloat(14.142136f));
|
||||
|
||||
// Big range.
|
||||
range = fixed::FromInt(260);
|
||||
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), fixed::FromFloat(264.952820f));
|
||||
TS_ASSERT_EQUALS(rangeManager->GetEffectiveParabolicRange(source, target, range, yOrigin), fixed::FromFloat(264.952820f));
|
||||
}
|
||||
|
||||
void test_ExploreCircle()
|
||||
|
|
@ -367,4 +472,72 @@ public:
|
|||
|
||||
cmp->Verify();
|
||||
}
|
||||
|
||||
void test_ParabolicRangeWithTerrain()
|
||||
{
|
||||
ComponentTestHelper test(*g_ScriptContext);
|
||||
ICmpRangeManager* rangeManager = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
||||
|
||||
const entity_id_t source{200};
|
||||
const entity_id_t target{201};
|
||||
|
||||
MockPositionRgm sourcePos;
|
||||
MockPositionRgm targetPos;
|
||||
test.AddMock(source, IID_Position, sourcePos);
|
||||
test.AddMock(target, IID_Position, targetPos);
|
||||
|
||||
const entity_pos_t range{fixed::FromInt(100)};
|
||||
const entity_pos_t yOrigin{fixed::Zero()};
|
||||
|
||||
// Source on high ground (Y=10), target on low ground (Y=0)
|
||||
sourcePos.m_Pos = CFixedVector3D(fixed::Zero(), fixed::FromInt(10), fixed::Zero());
|
||||
targetPos.m_Pos = CFixedVector3D(fixed::Zero(), fixed::Zero(), fixed::FromInt(50));
|
||||
entity_pos_t effective = rangeManager->GetEffectiveParabolicRange(source, target, range, yOrigin);
|
||||
TS_ASSERT_DELTA(effective.ToFloat(), 109.5445f, 0.01f); // ~109.54
|
||||
|
||||
// Source on low ground (Y=0), target on high ground (Y=10)
|
||||
sourcePos.m_Pos = CFixedVector3D(fixed::Zero(), fixed::Zero(), fixed::Zero());
|
||||
targetPos.m_Pos = CFixedVector3D(fixed::Zero(), fixed::FromInt(10), fixed::FromInt(50));
|
||||
effective = rangeManager->GetEffectiveParabolicRange(source, target, range, yOrigin);
|
||||
TS_ASSERT_DELTA(effective.ToFloat(), 89.4427f, 0.01f); // ~89.44
|
||||
|
||||
// Source with height offset (Y=15), target on flat ground (Y=0), with yOrigin
|
||||
sourcePos.m_Pos = CFixedVector3D(fixed::Zero(), fixed::FromInt(15), fixed::Zero());
|
||||
targetPos.m_Pos = CFixedVector3D(fixed::Zero(), fixed::Zero(), fixed::FromInt(50));
|
||||
const entity_pos_t yOrigin2{fixed::FromInt(2)};
|
||||
effective = rangeManager->GetEffectiveParabolicRange(source, target, range, yOrigin2);
|
||||
TS_ASSERT_DELTA(effective.ToFloat(), 115.7583f, 0.01f); // ~115.76
|
||||
}
|
||||
|
||||
void test_ParabolicRangeTargetTooHigh()
|
||||
{
|
||||
ComponentTestHelper test(*g_ScriptContext);
|
||||
ICmpRangeManager* rangeManager = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
||||
|
||||
const entity_id_t source{200};
|
||||
const entity_id_t target{201};
|
||||
|
||||
MockPositionRgm sourcePos;
|
||||
MockPositionRgm targetPos;
|
||||
test.AddMock(source, IID_Position, sourcePos);
|
||||
test.AddMock(target, IID_Position, targetPos);
|
||||
|
||||
// Source on flat ground (height=0), target very high (height=30)
|
||||
sourcePos.m_Pos = CFixedVector3D(fixed::Zero(), fixed::Zero(), fixed::Zero());
|
||||
targetPos.m_Pos = CFixedVector3D(fixed::Zero(), fixed::FromInt(30), fixed::Zero());
|
||||
|
||||
const entity_pos_t range{fixed::FromInt(50)};
|
||||
const entity_pos_t yOrigin{fixed::Zero()};
|
||||
|
||||
// heightDifference = 0 - 30 = -30, range/2 = 25
|
||||
// -30 < -25 → NEVER_IN_RANGE
|
||||
TS_ASSERT_EQUALS(rangeManager->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE);
|
||||
|
||||
// Target at borderline height (25)
|
||||
targetPos.m_Pos = CFixedVector3D(fixed::Zero(), fixed::FromInt(25), fixed::Zero());
|
||||
|
||||
const entity_pos_t effective = rangeManager->GetEffectiveParabolicRange(source, target, range, yOrigin);
|
||||
TS_ASSERT_DIFFERS(effective, NEVER_IN_RANGE);
|
||||
TS_ASSERT_EQUALS(effective, fixed::Zero());
|
||||
}
|
||||
};
|
||||
|
|
|
|||
5
source/tools/atlas/AtlasFrontends/CMakeLists.txt
Normal file
5
source/tools/atlas/AtlasFrontends/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(ActorEditor
|
||||
PRIVATE
|
||||
$<$<PLATFORM_ID:Windows>: ActorEditor.rc> # Caution, the blank before the File is needed
|
||||
ActorEditor.cpp
|
||||
)
|
||||
15
source/tools/atlas/AtlasObject/CMakeLists.txt
Normal file
15
source/tools/atlas/AtlasObject/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
target_sources(AtlasObject
|
||||
PRIVATE
|
||||
AtlasObject.h
|
||||
AtlasObjectImpl.cpp
|
||||
AtlasObjectImpl.h
|
||||
AtlasObjectJS.cpp
|
||||
AtlasObjectText.cpp
|
||||
AtlasObjectText.h
|
||||
AtlasObjectXML.cpp
|
||||
JSONSpiritInclude.h
|
||||
)
|
||||
|
||||
if(NOT without-tests)
|
||||
add_subdirectory(tests/)
|
||||
endif()
|
||||
13
source/tools/atlas/AtlasObject/tests/CMakeLists.txt
Normal file
13
source/tools/atlas/AtlasObject/tests/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
cxxtest_add_test(test_atlas_object_xml testAtlasObjectXML.cpp ${CMAKE_CURRENT_LIST_DIR}/test_AtlasObjectXML.h)
|
||||
|
||||
target_include_directories(test_atlas_object_xml
|
||||
PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/source
|
||||
${CXXTEST_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_link_libraries(test_atlas_object_xml
|
||||
PRIVATE
|
||||
${BUILD_FLAGS_TARGET}
|
||||
AtlasObject
|
||||
)
|
||||
13
source/tools/atlas/AtlasUI/ActorEditor/CMakeLists.txt
Normal file
13
source/tools/atlas/AtlasUI/ActorEditor/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
ActorEditor.cpp
|
||||
ActorEditor.h
|
||||
ActorEditorListCtrl.cpp
|
||||
ActorEditorListCtrl.h
|
||||
AnimListEditor.cpp
|
||||
AnimListEditor.h
|
||||
PropListEditor.cpp
|
||||
PropListEditor.h
|
||||
TexListEditor.cpp
|
||||
TexListEditor.h
|
||||
)
|
||||
5
source/tools/atlas/AtlasUI/CMakeLists.txt
Normal file
5
source/tools/atlas/AtlasUI/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
add_subdirectory(ActorEditor/)
|
||||
add_subdirectory(CustomControls/)
|
||||
add_subdirectory(General/)
|
||||
add_subdirectory(Misc/)
|
||||
add_subdirectory(ScenarioEditor/)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
ToolButton.cpp
|
||||
ToolButton.h
|
||||
)
|
||||
12
source/tools/atlas/AtlasUI/CustomControls/CMakeLists.txt
Normal file
12
source/tools/atlas/AtlasUI/CustomControls/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
add_subdirectory(Buttons/)
|
||||
add_subdirectory(Canvas/)
|
||||
add_subdirectory(ColorDialog/)
|
||||
add_subdirectory(DraggableListCtrl/)
|
||||
add_subdirectory(EditableListCtrl/)
|
||||
add_subdirectory(FileHistory/)
|
||||
add_subdirectory(HighResTimer/)
|
||||
add_subdirectory(MapDialog/)
|
||||
add_subdirectory(MapResizeDialog/)
|
||||
add_subdirectory(SnapSplitterWindow/)
|
||||
add_subdirectory(VirtualDirTreeCtrl/)
|
||||
add_subdirectory(Windows/)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
Canvas.cpp
|
||||
Canvas.h
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
ColorDialog.cpp
|
||||
ColorDialog.h
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
DraggableListCtrl.cpp
|
||||
DraggableListCtrl.h
|
||||
DraggableListCtrlCommands.cpp
|
||||
DraggableListCtrlCommands.h
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
EditableListCtrl.cpp
|
||||
EditableListCtrl.h
|
||||
EditableListCtrlCommands.cpp
|
||||
EditableListCtrlCommands.h
|
||||
FieldEditCtrl.cpp
|
||||
FieldEditCtrl.h
|
||||
ListCtrlValidator.cpp
|
||||
ListCtrlValidator.h
|
||||
QuickComboBox.cpp
|
||||
QuickComboBox.h
|
||||
QuickFileCtrl.cpp
|
||||
QuickFileCtrl.h
|
||||
QuickTextCtrl.cpp
|
||||
QuickTextCtrl.h
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
FileHistory.cpp
|
||||
FileHistory.h
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
HighResTimer.cpp
|
||||
HighResTimer.h
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
MapDialog.cpp
|
||||
MapDialog.h
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
MapResizeDialog.cpp
|
||||
MapResizeDialog.h
|
||||
PseudoMiniMapPanel.cpp
|
||||
PseudoMiniMapPanel.h
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
SnapSplitterWindow.cpp
|
||||
SnapSplitterWindow.h
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
virtualdirtreectrl.cpp
|
||||
virtualdirtreectrl.h
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
AtlasDialog.cpp
|
||||
AtlasDialog.h
|
||||
AtlasWindow.cpp
|
||||
AtlasWindow.h
|
||||
)
|
||||
16
source/tools/atlas/AtlasUI/General/CMakeLists.txt
Normal file
16
source/tools/atlas/AtlasUI/General/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
AtlasClipboard.cpp
|
||||
AtlasClipboard.h
|
||||
AtlasEventLoop.cpp
|
||||
AtlasEventLoop.h
|
||||
AtlasWindowCommand.cpp
|
||||
AtlasWindowCommand.h
|
||||
AtlasWindowCommandProc.cpp
|
||||
AtlasWindowCommandProc.h
|
||||
Datafile.cpp
|
||||
Datafile.h
|
||||
IAtlasSerialiser.h
|
||||
Observable.cpp
|
||||
Observable.h
|
||||
)
|
||||
13
source/tools/atlas/AtlasUI/Misc/CMakeLists.txt
Normal file
13
source/tools/atlas/AtlasUI/Misc/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
DLLInterface.cpp
|
||||
DLLInterface.h
|
||||
KeyMap.cpp
|
||||
KeyMap.h
|
||||
actored.h
|
||||
precompiled.cpp
|
||||
precompiled.h
|
||||
$<$<PLATFORM_ID:Windows>: atlas.rc> # Caution, the blank before the File is needed
|
||||
)
|
||||
|
||||
add_subdirectory(Graphics/)
|
||||
12
source/tools/atlas/AtlasUI/Misc/Graphics/CMakeLists.txt
Normal file
12
source/tools/atlas/AtlasUI/Misc/Graphics/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
if(CMAKE_SYSTEM_NAME MATCHES "Linux")
|
||||
include(GNUInstallDirs)
|
||||
install(
|
||||
FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ActorEditor.ico
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ArchiveViewer.ico
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FileConverter.ico
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ScenarioEditor.ico
|
||||
DESTINATION
|
||||
${CMAKE_INSTALL_DATADIR}/0ad/tools/ActorEditor/icons
|
||||
)
|
||||
endif()
|
||||
10
source/tools/atlas/AtlasUI/ScenarioEditor/CMakeLists.txt
Normal file
10
source/tools/atlas/AtlasUI/ScenarioEditor/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
ScenarioEditor.cpp
|
||||
ScenarioEditor.h
|
||||
SectionLayout.cpp
|
||||
SectionLayout.h
|
||||
)
|
||||
|
||||
add_subdirectory(Sections/)
|
||||
add_subdirectory(Tools/)
|
||||
|
|
@ -273,8 +273,16 @@ private:
|
|||
|
||||
if (evt.GetWheelRotation())
|
||||
{
|
||||
float speed = 16.f * ScenarioEditor::GetSpeedModifier();
|
||||
POST_MESSAGE(SmoothZoom, (eRenderView::GAME, evt.GetWheelRotation() * speed / evt.GetWheelDelta()));
|
||||
if (evt.GetWheelAxis() == wxMOUSE_WHEEL_VERTICAL)
|
||||
{
|
||||
float speed = 16.f * ScenarioEditor::GetSpeedModifier();
|
||||
POST_MESSAGE(SmoothZoom, (eRenderView::GAME, evt.GetWheelRotation() * speed / evt.GetWheelDelta()));
|
||||
}
|
||||
else
|
||||
{
|
||||
float speed = ScenarioEditor::GetSpeedModifier();
|
||||
POST_MESSAGE(RotateY, (evt.GetWheelRotation() * speed / evt.GetWheelDelta()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
add_subdirectory(Cinema/)
|
||||
add_subdirectory(Common/)
|
||||
add_subdirectory(Environment/)
|
||||
add_subdirectory(Map/)
|
||||
add_subdirectory(Object/)
|
||||
add_subdirectory(Player/)
|
||||
add_subdirectory(Terrain/)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
Cinema.cpp
|
||||
Cinema.h
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
Sidebar.cpp
|
||||
Sidebar.h
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
Environment.cpp
|
||||
Environment.h
|
||||
LightControl.cpp
|
||||
LightControl.h
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
Map.cpp
|
||||
Map.h
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
Object.cpp
|
||||
Object.h
|
||||
VariationControl.cpp
|
||||
VariationControl.h
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
Player.cpp
|
||||
Player.h
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
Terrain.cpp
|
||||
Terrain.h
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
ActorViewerTool.cpp
|
||||
AlterElevation.cpp
|
||||
FillTerrain.cpp
|
||||
FlattenElevation.cpp
|
||||
PaintTerrain.cpp
|
||||
PickWaterHeight.cpp
|
||||
PikeElevation.cpp
|
||||
PlaceObject.cpp
|
||||
ReplaceTerrain.cpp
|
||||
SmoothElevation.cpp
|
||||
TransformObject.cpp
|
||||
TransformPath.cpp
|
||||
)
|
||||
|
||||
add_subdirectory(Common/)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
target_sources(AtlasUI
|
||||
PRIVATE
|
||||
Brushes.cpp
|
||||
Brushes.h
|
||||
MiscState.cpp
|
||||
MiscState.h
|
||||
ObjectSettings.cpp
|
||||
ObjectSettings.h
|
||||
Tools.cpp
|
||||
Tools.h
|
||||
)
|
||||
162
source/tools/atlas/CMakeLists.txt
Normal file
162
source/tools/atlas/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
cmake_minimum_required(VERSION 3.25.1...4.0.0)
|
||||
|
||||
project(AtlasMisc LANGUAGES CXX)
|
||||
|
||||
include(0ad-Functions)
|
||||
|
||||
# +++++++++++++++++++++ Project Macros ++++++++++++++++++++
|
||||
macro(set_atlas_build_flags _target)
|
||||
target_link_options(${_target}
|
||||
PRIVATE
|
||||
$<$<PLATFORM_ID:Linux,FreeBSD>:-fPIC>
|
||||
$<$<PLATFORM_ID:Linux>:-rdynamic>
|
||||
)
|
||||
target_compile_options(${_target}
|
||||
PRIVATE
|
||||
$<$<PLATFORM_ID:Linux,FreeBSD>:-fPIC>
|
||||
$<$<PLATFORM_ID:Linux,Darwin>:-Wno-unused-local-typedefs>
|
||||
)
|
||||
target_link_libraries(${_target}
|
||||
PRIVATE
|
||||
$<$<PLATFORM_ID:Windows>:winmm delayimp>
|
||||
)
|
||||
endmacro()
|
||||
|
||||
# +++++++++++++++++++++ Required Packages ++++++++++++++++++
|
||||
find_package(wxWidgets 3.0.4 REQUIRED COMPONENTS gl xml)
|
||||
find_package(Iconv 1.0 REQUIRED)
|
||||
find_package(ZLIB 1.2 REQUIRED)
|
||||
find_package(LibXml2 2.9 REQUIRED)
|
||||
find_package(CxxTest 4.4 REQUIRED)
|
||||
if(UNIX)
|
||||
find_package(Boost 1.69.0 REQUIRED CONFIG)
|
||||
find_package(SDL2 2.0.2 REQUIRED)
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
include(FindPrebuildLibrary)
|
||||
find_prebuild_library(Sdl2)
|
||||
find_prebuild_library(Boost INC_PATH ${0AD_EXT_LIBDIR}/boost/include/)
|
||||
endif()
|
||||
|
||||
# +++++++++++++++++++++ Project Targets ++++++++++++++++++++
|
||||
add_library(AtlasObject STATIC)
|
||||
add_library(AtlasUI SHARED)
|
||||
add_executable(ActorEditor)
|
||||
|
||||
# Set Macos Bundle and Windows Window Application
|
||||
set_target_properties(ActorEditor
|
||||
PROPERTIES
|
||||
WIN32_EXECUTABLE $<$<PLATFORM_ID:Windows>:TRUE>
|
||||
MACOSX_BUNDLE $<$<PLATFORM_ID:Darwin>:TRUE>
|
||||
)
|
||||
|
||||
add_subdirectory(AtlasFrontends/)
|
||||
add_subdirectory(AtlasObject/)
|
||||
add_subdirectory(AtlasUI/)
|
||||
|
||||
# +++++++++++++++++++++ Set Output Paths +++++++++++++++++++
|
||||
set_target_properties(AtlasObject
|
||||
PROPERTIES
|
||||
ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/binaries/system
|
||||
)
|
||||
set_target_properties(AtlasUI
|
||||
PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/binaries/system
|
||||
)
|
||||
set_target_properties(ActorEditor
|
||||
PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/binaries/system
|
||||
)
|
||||
|
||||
# +++++++++++++++++++++ AtlasObject ++++++++++++++++++++++++
|
||||
set_atlas_build_flags(AtlasObject)
|
||||
target_include_directories(AtlasObject PRIVATE ${CMAKE_SOURCE_DIR}/source)
|
||||
target_link_libraries(AtlasObject
|
||||
PRIVATE
|
||||
${BUILD_FLAGS_TARGET}
|
||||
$<$<PLATFORM_ID:Linux>:Boost::headers>
|
||||
$<$<PLATFORM_ID:Windows>:BOOST::headers>
|
||||
Iconv::Iconv
|
||||
LibXml2::LibXml2
|
||||
SDL2::SDL2
|
||||
)
|
||||
|
||||
# +++++++++++++++++++++ AtlasUI ++++++++++++++++++++++++++++
|
||||
if(NOT without-pch)
|
||||
add_pch(TARGET AtlasUI PCH_DIR ${PROJECT_SOURCE_DIR}/AtlasUI/Misc)
|
||||
else()
|
||||
target_compile_definitions(AtlasUI PRIVATE CONFIG_ENABLE_PCH=0)
|
||||
set_target_properties(AtlasUI PROPERTIES
|
||||
DISABLE_PRECOMPILE_HEADERS TRUE
|
||||
)
|
||||
target_include_directories(AtlasUI
|
||||
BEFORE PRIVATE
|
||||
${PROJECT_SOURCE_DIR}/AtlasUI/Misc
|
||||
)
|
||||
endif()
|
||||
|
||||
set_atlas_build_flags(AtlasUI)
|
||||
|
||||
target_include_directories(AtlasUI PRIVATE ${CMAKE_SOURCE_DIR}/source/)
|
||||
target_link_libraries(AtlasUI
|
||||
PRIVATE
|
||||
${BUILD_FLAGS_TARGET}
|
||||
LibXml2::LibXml2
|
||||
Iconv::Iconv
|
||||
$<$<PLATFORM_ID:Linux>:Boost::headers>
|
||||
$<$<PLATFORM_ID:Windows>:BOOST::headers>
|
||||
SDL2::SDL2
|
||||
ZLIB::ZLIB
|
||||
AtlasObject
|
||||
wxWidgets::wxWidgets
|
||||
)
|
||||
target_compile_definitions(AtlasUI
|
||||
PRIVATE
|
||||
$<$<VERSION_GREATER_EQUAL:${wxWidgets_VERSION},3.3.0>:wxNO_REQUIRE_LITERAL_MSGIDS>
|
||||
)
|
||||
|
||||
# +++++++++++++++++++++ ActorEditor ++++++++++++++++++++++++
|
||||
target_include_directories(ActorEditor
|
||||
PRIVATE
|
||||
${PROJECT_SOURCE_DIR}/
|
||||
)
|
||||
target_link_libraries(ActorEditor
|
||||
PRIVATE
|
||||
${BUILD_FLAGS_TARGET}
|
||||
$<$<NOT:$<PLATFORM_ID:Windows>>:AtlasObject>
|
||||
AtlasUI
|
||||
)
|
||||
target_link_options(ActorEditor
|
||||
PRIVATE
|
||||
$<$<AND:$<PLATFORM_ID:Windows>,$<STREQUAL:${ARCH},amd64>>:/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df'>
|
||||
$<$<AND:$<PLATFORM_ID:Windows>,$<STREQUAL:${ARCH},x86>>:/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='X86' publicKeyToken='6595b64144ccf1df'>
|
||||
$<$<PLATFORM_ID:Darwin>:-arch ${MACOS_ARCH}>
|
||||
)
|
||||
target_compile_options(ActorEditor
|
||||
PRIVATE
|
||||
$<$<PLATFORM_ID:Darwin>:-arch ${MACOS_ARCH}>
|
||||
)
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
set_target_properties(ActorEditor PROPERTIES
|
||||
XCODE_ATTRIBUTE_ARCHS ${MACOS_ARCH}
|
||||
XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET $<$<BOOL:${macosx-version-min}>:${macosx-version-min}>
|
||||
)
|
||||
endif()
|
||||
|
||||
# get_target_property(win32 ActorEditor WIN32_EXECUTABLE)
|
||||
# get_target_property(macos ActorEditor MACOSX_BUNDLE)
|
||||
# file(GENERATE OUTPUT debugexpr.txt CONTENT "${win32} ${macos}" TARGET ActorEditor)
|
||||
# include(CMakePrintHelpers)
|
||||
# cmake_print_properties(TARGETS AtlasUI
|
||||
# PROPERTIES
|
||||
# # INCLUDE_DIRECTORIES
|
||||
# # LINK_LIBRARIES
|
||||
# # COMPILE_OPTIONS
|
||||
# # COMPILE_DEFINITIONS
|
||||
# # WIN32_EXECUTABLE
|
||||
# # MACOSX_BUNDLE
|
||||
# # IMPORTED_LOCATION
|
||||
# # INTERFACE_INCLUDE_DIRECTORIES
|
||||
# # CXX_EXTENSIONS
|
||||
# # CXX_STANDARD_REQUIRED
|
||||
# )
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2012 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
|
||||
|
|
@ -41,7 +41,8 @@ struct GameLoopState
|
|||
struct Input
|
||||
{
|
||||
float scrollSpeed[6]; // [fwd, bwd, left, right, cw-rotation, ccw-rotation]. 0.0f for disabled.
|
||||
float zoomDelta;
|
||||
float zoomDelta{0.f};
|
||||
float rotateDelta{0.f};
|
||||
} input;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,14 @@ MESSAGEHANDLER(RotateAround)
|
|||
}
|
||||
}
|
||||
|
||||
MESSAGEHANDLER(RotateY)
|
||||
{
|
||||
if (!g_Game || g_Game->GetView()->GetCinema()->IsPlaying())
|
||||
return;
|
||||
|
||||
g_AtlasGameLoop->input.rotateDelta = msg->angle;
|
||||
}
|
||||
|
||||
MESSAGEHANDLER(LookAt)
|
||||
{
|
||||
// TODO: different camera depending on msg->view
|
||||
|
|
|
|||
|
|
@ -117,6 +117,35 @@ bool InputProcessor::ProcessInput(GameLoopState* state)
|
|||
moved = true;
|
||||
}
|
||||
|
||||
if (state->input.rotateDelta != 0.f)
|
||||
{
|
||||
float angle{4 * state->realFrameLength};
|
||||
if (input.rotateDelta > 0 )
|
||||
{
|
||||
if (angle > input.rotateDelta)
|
||||
{
|
||||
angle = input.rotateDelta;
|
||||
input.rotateDelta = 0.f;
|
||||
}
|
||||
else
|
||||
input.rotateDelta -= angle;
|
||||
}
|
||||
else
|
||||
{
|
||||
angle *= -1;
|
||||
if (angle < input.rotateDelta)
|
||||
{
|
||||
angle = input.rotateDelta;
|
||||
input.rotateDelta = 0.f;
|
||||
}
|
||||
else
|
||||
input.rotateDelta -= angle;
|
||||
}
|
||||
Rotate(camera, angle);
|
||||
|
||||
moved = true;
|
||||
}
|
||||
|
||||
if (moved)
|
||||
{
|
||||
camera.UpdateFrustum();
|
||||
|
|
|
|||
|
|
@ -456,6 +456,10 @@ MESSAGE(RotateAround,
|
|||
((Position, pos))
|
||||
);
|
||||
|
||||
MESSAGE(RotateY,
|
||||
((float, angle))
|
||||
);
|
||||
|
||||
MESSAGE(LookAt,
|
||||
((int, view)) // eRenderView
|
||||
((Position, pos))
|
||||
|
|
|
|||
1
source/tools/dist/build-osx-bundle.py
vendored
1
source/tools/dist/build-osx-bundle.py
vendored
|
|
@ -137,6 +137,7 @@ with open(BUNDLE_CONTENTS + "/Info.plist", "wb") as f:
|
|||
"CFBundleDevelopmentRegion": "English",
|
||||
"CFBundleInfoDictionaryVersion": "6.0",
|
||||
"CFBundleIconFile": "0ad",
|
||||
"LSApplicationCategoryType": "public.app-category.strategy-games",
|
||||
"LSHasLocalizedDisplayName": True,
|
||||
"LSMinimumSystemVersion": BUNDLE_MIN_OSX_VERSION,
|
||||
"NSHumanReadableCopyright": f"Copyright © {datetime.now(tz=UTC).year} Wildfire Games",
|
||||
|
|
|
|||
Loading…
Reference in a new issue