mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
Compare commits
49 commits
1f7aad79f3
...
99d5431c1f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99d5431c1f | ||
|
|
a55f3ae0dc | ||
|
|
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 | ||
|
|
c4e7364802 | ||
|
|
867bdf318b | ||
|
|
e64a138f92 | ||
|
|
4a7b7e7df1 | ||
|
|
fa90ddc8b6 | ||
|
|
764a420797 | ||
|
|
8a2bb10827 | ||
|
|
af9d102126 | ||
|
|
822092fcb4 | ||
|
|
6e79dc2f70 | ||
|
|
6fdcd68172 | ||
|
|
e4f4030a5e | ||
|
|
2fb6347125 | ||
|
|
7aa756f6a4 | ||
|
|
e712601602 | ||
|
|
865ca4cd55 | ||
|
|
dcb7cef308 |
104 changed files with 1844 additions and 891 deletions
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
lfscheck:
|
lfscheck:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Fetch the base branch
|
- name: Fetch the base branch
|
||||||
run: git fetch origin ${{ env.BASE_SHA }}
|
run: git fetch origin ${{ env.BASE_SHA }}
|
||||||
|
|
@ -24,27 +24,14 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GIT_LFS_SKIP_SMUDGE: "1"
|
GIT_LFS_SKIP_SMUDGE: "1"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
- name: Add remote fork origin for LFS
|
- name: Add remote fork origin for LFS
|
||||||
run: |
|
run: |
|
||||||
PR_REPO="${{ gitea.event.pull_request.head.repo.full_name || gitea.repository }}"
|
PR_REPO="${{ gitea.event.pull_request.head.repo.full_name || gitea.repository }}"
|
||||||
git remote add ${{ gitea.actor }} https://gitea.wildfiregames.com/${PR_REPO}.git
|
git remote add ${{ gitea.actor }} https://gitea.wildfiregames.com/${PR_REPO}.git
|
||||||
- name: Workaround for authentication problem with LFS
|
|
||||||
# https://gitea.com/gitea/act_runner/issues/164
|
|
||||||
run: |
|
|
||||||
git config --local \
|
|
||||||
http.${{ gitea.server_url }}/${{ gitea.repository }}.git/info/lfs/objects/.extraheader ''
|
|
||||||
|
|
||||||
PR_REPO="${{ gitea.event.pull_request.head.repo.full_name || gitea.repository }}"
|
|
||||||
EXTRAHEADER="$(git config --get --local http.${{ gitea.server_url }}/.extraheader)"
|
|
||||||
git config --local \
|
|
||||||
http.${{ gitea.server_url }}/${PR_REPO}.git/info/lfs/objects/batch.extraheader \
|
|
||||||
'${EXTRAHEADER}'
|
|
||||||
git config --local \
|
|
||||||
http.${{ gitea.server_url }}/${PR_REPO}.git/info/lfs/objects/.extraheader ''
|
|
||||||
- name: Download necessary LFS assets
|
- name: Download necessary LFS assets
|
||||||
shell: sh {0}
|
shell: sh {0}
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ jobs:
|
||||||
cppcheck:
|
cppcheck:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
# cache only works for items in workspace, so configure apt to allow for caching.
|
# cache only works for items in workspace, so configure apt to allow for caching.
|
||||||
- name: Setup apt cache locations
|
- name: Setup apt cache locations
|
||||||
|
|
@ -26,7 +26,7 @@ jobs:
|
||||||
|
|
||||||
- name: Cache apt pkg db and and deb files
|
- name: Cache apt pkg db and and deb files
|
||||||
id: apt-cache
|
id: apt-cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
apt-cache
|
apt-cache
|
||||||
|
|
@ -49,11 +49,11 @@ jobs:
|
||||||
copyright:
|
copyright:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 100
|
fetch-depth: 100
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
|
|
@ -66,10 +66,10 @@ jobs:
|
||||||
jenkinsfiles:
|
jenkinsfiles:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,27 @@ name: pre-commit
|
||||||
on:
|
on:
|
||||||
- push
|
- push
|
||||||
- pull_request
|
- pull_request
|
||||||
|
env:
|
||||||
|
PRE_COMMIT_VERSION: 4.6.0
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
pre-commit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
- id: restore-pip-cache
|
- uses: actions/cache@v6
|
||||||
uses: actions/cache/restore@v4
|
|
||||||
with:
|
with:
|
||||||
key: pip-cache-v1-${{ github.workflow }}
|
key: pip-cache-v1-${{github.workflow}}-${{env.pythonLocation}}-${{env.PRE_COMMIT_VERSION}}
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
- uses: pre-commit/action@v3.0.1
|
- run: python -m pip install pre-commit=="${{ env.PRE_COMMIT_VERSION }}"
|
||||||
- uses: actions/cache/save@v4
|
shell: bash
|
||||||
if: steps.restore-pip-cache.outcome == 'success'
|
- run: python -m pip freeze --local
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
key: pip-cache-v1-${{ github.workflow }}
|
path: ~/.cache/pre-commit
|
||||||
path: ~/.cache/pip
|
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
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ repos:
|
||||||
\.patch$
|
\.patch$
|
||||||
)
|
)
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.12.9
|
rev: v0.16.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
args:
|
args:
|
||||||
|
|
@ -56,7 +56,7 @@ repos:
|
||||||
files: ^binaries/
|
files: ^binaries/
|
||||||
exclude: (^binaries/data/mods/(mod|public)/art/.*\.xml|\.dae$)
|
exclude: (^binaries/data/mods/(mod|public)/art/.*\.xml|\.dae$)
|
||||||
- repo: https://github.com/scop/pre-commit-shfmt
|
- repo: https://github.com/scop/pre-commit-shfmt
|
||||||
rev: v3.12.0-2
|
rev: v3.13.1-1
|
||||||
hooks:
|
hooks:
|
||||||
- id: shfmt
|
- id: shfmt
|
||||||
args:
|
args:
|
||||||
|
|
@ -67,7 +67,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: shellcheck
|
- id: shellcheck
|
||||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||||
rev: v0.45.0
|
rev: v0.47.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: markdownlint
|
- id: markdownlint
|
||||||
language_version: 22.14.0
|
language_version: 22.14.0
|
||||||
|
|
@ -77,13 +77,13 @@ repos:
|
||||||
^source/third_party/
|
^source/third_party/
|
||||||
)
|
)
|
||||||
- repo: https://github.com/adrienverge/yamllint
|
- repo: https://github.com/adrienverge/yamllint
|
||||||
rev: v1.37.1
|
rev: v1.38.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: yamllint
|
- id: yamllint
|
||||||
args:
|
args:
|
||||||
- --strict
|
- --strict
|
||||||
- repo: https://github.com/eslint/eslint
|
- repo: https://github.com/eslint/eslint
|
||||||
rev: v9.39.2
|
rev: v10.8.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: eslint
|
- id: eslint
|
||||||
language_version: 22.14.0
|
language_version: 22.14.0
|
||||||
|
|
|
||||||
|
|
@ -527,6 +527,11 @@ icons.sizescale = 1.0
|
||||||
entityradiusscale = 1.0
|
entityradiusscale = 1.0
|
||||||
blinkduration = 1.7 ; The blink duration while pinging
|
blinkduration = 1.7 ; The blink duration while pinging
|
||||||
pingduration = 50.0 ; The duration for which an entity will be pinged after an attack notification
|
pingduration = 50.0 ; The duration for which an entity will be pinged after an attack notification
|
||||||
|
position = panel-left ; Set minimap position (panel-left, screen-left)
|
||||||
|
size = 1 ; Size of the minimap. 1 (small) to 5 (huge). Each value is 15% bigger
|
||||||
|
shape = square ; Shape of the minimap
|
||||||
|
expand.toggle = "Alt+C" ; Expand minimap
|
||||||
|
|
||||||
|
|
||||||
[gui.session.notifications]
|
[gui.session.notifications]
|
||||||
attack = true ; Show a chat notification if you are attacked by another player
|
attack = true ; Show a chat notification if you are attacked by another player
|
||||||
|
|
|
||||||
|
|
@ -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));
|
Engine.GetGUIObjectByName("mainText").caption = Engine.TranslateLines(Engine.ReadFile(g_IncompatibleModsFile));
|
||||||
return new Promise(closePageCallback =>
|
return new Promise(closePageCallback =>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<objects>
|
<objects>
|
||||||
<script directory="gui/common/"/>
|
<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 -->
|
<!-- Add a translucent black background to fade out the menu page -->
|
||||||
<object type="image" sprite="ModernFade"/>
|
<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.
|
* 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 `true` by showErrorMessageBox
|
||||||
* Set to `false` by init, updateModList, downloadFile, and cancelRequest
|
* Set to `false` by init, updateModList, downloadFile, and cancelRequest
|
||||||
*/
|
*/
|
||||||
var g_Failure;
|
let g_Failure;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if the user has cancelled a request.
|
* Indicates if the user has cancelled a request.
|
||||||
|
|
@ -19,11 +19,11 @@ var g_Failure;
|
||||||
* Set to `true` by cancelRequest
|
* Set to `true` by cancelRequest
|
||||||
* Set to `false` by updateModList, and downloadFile
|
* 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
|
* Finished status indicators
|
||||||
*/
|
*/
|
||||||
|
|
@ -142,7 +142,7 @@ var g_ModIOState = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function init(data)
|
export function init(data)
|
||||||
{
|
{
|
||||||
const promise = progressDialog(
|
const promise = progressDialog(
|
||||||
translate("Initializing mod.io interface."),
|
translate("Initializing mod.io interface."),
|
||||||
|
|
@ -153,6 +153,21 @@ function init(data)
|
||||||
g_Failure = false;
|
g_Failure = false;
|
||||||
Engine.ModIoStartGetGameId();
|
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([
|
return Promise.race([
|
||||||
promise,
|
promise,
|
||||||
new Promise(closePageCallback =>
|
new Promise(closePageCallback =>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<objects>
|
<objects>
|
||||||
|
|
||||||
<script directory="gui/common/"/>
|
<script directory="gui/common/"/>
|
||||||
<script directory="gui/modio/"/>
|
<script module="gui/modio/modio.js"/>
|
||||||
|
|
||||||
<object type="image" sprite="ModernFade"/>
|
<object type="image" sprite="ModernFade"/>
|
||||||
|
|
||||||
|
|
@ -25,8 +25,6 @@
|
||||||
style="ModernInput"
|
style="ModernInput"
|
||||||
size="16 0 200 24"
|
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>
|
<translatableAttribute id="placeholder_text" context="placeholder text for input field to filter mods">Filter</translatableAttribute>
|
||||||
</object>
|
</object>
|
||||||
|
|
||||||
|
|
@ -39,10 +37,6 @@
|
||||||
selected_column_order="1"
|
selected_column_order="1"
|
||||||
font="sans-stroke-13"
|
font="sans-stroke-13"
|
||||||
>
|
>
|
||||||
<action on="SelectionChange">showModDescription();</action>
|
|
||||||
<action on="SelectionColumnChange">displayMods();</action>
|
|
||||||
<action on="MouseLeftDoubleClickItem">downloadMod();</action>
|
|
||||||
|
|
||||||
<!-- List headers -->
|
<!-- List headers -->
|
||||||
<!-- Keep in sync with mod property names -->
|
<!-- Keep in sync with mod property names -->
|
||||||
<column id="name_id" textcolor="255 255 255" width="20%">
|
<column id="name_id" textcolor="255 255 255" width="20%">
|
||||||
|
|
@ -69,9 +63,7 @@
|
||||||
<!-- Right Panel: Compatibility Filter-->
|
<!-- Right Panel: Compatibility Filter-->
|
||||||
<object name="rightPanel" size="100%-250 20 100%-10 40" >
|
<object name="rightPanel" size="100%-250 20 100%-10 40" >
|
||||||
<!-- Compatibility Filter Checkbox -->
|
<!-- Compatibility Filter Checkbox -->
|
||||||
<object name="compatibilityFilter" type="checkbox" checked="true" style="ModernTickBox" size="0 4 20 100%">
|
<object name="compatibilityFilter" type="checkbox" checked="true" style="ModernTickBox" size="0 4 20 100%" />
|
||||||
<action on="Press">displayMods();</action>
|
|
||||||
</object>
|
|
||||||
<!-- Compatibility Filter Label -->
|
<!-- Compatibility Filter Label -->
|
||||||
<object type="text" size="20 2 100% 100%" text_align="left" textcolor="white">
|
<object type="text" size="20 2 100% 100%" text_align="left" textcolor="white">
|
||||||
<translatableAttribute id="caption">Filter valid mods</translatableAttribute>
|
<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">
|
<object name="refreshButton" type="button" style="ModernButtonRed" size="100%-368 100%-44 100%-188 100%-16" enabled="false">
|
||||||
<translatableAttribute id="caption">Refresh List</translatableAttribute>
|
<translatableAttribute id="caption">Refresh List</translatableAttribute>
|
||||||
<action on="Press">updateModList();</action>
|
|
||||||
</object>
|
</object>
|
||||||
|
|
||||||
<object name="downloadButton" type="button" style="ModernButtonRed" size="100%-184 100%-44 100%-16 100%-16" enabled="false">
|
<object name="downloadButton" type="button" style="ModernButtonRed" size="100%-184 100%-44 100%-16 100%-16" enabled="false">
|
||||||
<translatableAttribute id="caption">Download</translatableAttribute>
|
<translatableAttribute id="caption">Download</translatableAttribute>
|
||||||
<action on="Press">downloadMod();</action>
|
|
||||||
</object>
|
</object>
|
||||||
|
|
||||||
</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"));
|
Engine.GetGUIObjectByName("mainText").caption = Engine.TranslateLines(Engine.ReadFile("gui/modmod/help/help.txt"));
|
||||||
return new Promise(closePageCallback =>
|
return new Promise(closePageCallback =>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<objects>
|
<objects>
|
||||||
|
|
||||||
<script directory="gui/common/"/>
|
<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 -->
|
<!-- Add a translucent black background to fade out the menu page -->
|
||||||
<object type="image" sprite="ModernFade"/>
|
<object type="image" sprite="ModernFade"/>
|
||||||
|
|
|
||||||
|
|
@ -19,33 +19,37 @@
|
||||||
* This allows mods to express upwards and downwards compatibility.
|
* 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.
|
* 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.
|
* Folder names of all mods that are or can be launched.
|
||||||
*/
|
*/
|
||||||
var g_ModsEnabled = [];
|
let g_ModsEnabled = [];
|
||||||
var g_ModsDisabled = [];
|
let g_ModsDisabled = [];
|
||||||
|
|
||||||
var g_ModsEnabledFiltered = [];
|
let g_ModsEnabledFiltered = [];
|
||||||
var g_ModsDisabledFiltered = [];
|
let g_ModsDisabledFiltered = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache mod compatibility recomputed when some mod is enbaled/disabled.
|
* Cache mod compatibility recomputed when some mod is enbaled/disabled.
|
||||||
*/
|
*/
|
||||||
var g_ModsCompatibility = [];
|
const g_ModsCompatibility = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the mods installed by the ModInstaller.
|
* 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"),
|
"name": translate("This mod does not exist"),
|
||||||
"version": "",
|
"version": "",
|
||||||
"label": "",
|
"label": "",
|
||||||
|
|
@ -54,12 +58,34 @@ var g_FakeMod = {
|
||||||
"dependencies": []
|
"dependencies": []
|
||||||
};
|
};
|
||||||
|
|
||||||
var g_ColorNoModSelected = "255 255 100";
|
let g_ColorNoModSelected = "255 255 100";
|
||||||
var g_ColorDependenciesMet = "100 255 100";
|
let g_ColorDependenciesMet = "100 255 100";
|
||||||
var g_ColorDependenciesNotMet = "255 100 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_InstalledMods = data && data.installedMods || hotloadData && hotloadData.installedMods || [];
|
||||||
g_HasIncompatibleMods = Engine.HasIncompatibleMods();
|
g_HasIncompatibleMods = Engine.HasIncompatibleMods();
|
||||||
|
|
||||||
|
|
@ -354,7 +380,7 @@ function recomputeCompatibility(disabledAction = false)
|
||||||
*/
|
*/
|
||||||
function isDependencyMet(dependency)
|
function isDependencyMet(dependency)
|
||||||
{
|
{
|
||||||
const operator = dependency.match(g_RegExpComparisonOperator);
|
const operator = dependency.match(regExpComparisonOperator);
|
||||||
const [name, version] = operator ? dependency.split(operator[0]) : [dependency, undefined];
|
const [name, version] = operator ? dependency.split(operator[0]) : [dependency, undefined];
|
||||||
|
|
||||||
return g_ModsEnabled.some(folder =>
|
return g_ModsEnabled.some(folder =>
|
||||||
|
|
@ -403,7 +429,7 @@ function sortEnabledMods()
|
||||||
{
|
{
|
||||||
const dependencies = {};
|
const dependencies = {};
|
||||||
for (const folder of g_ModsEnabled)
|
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) =>
|
g_ModsEnabled.sort((folder1, folder2) =>
|
||||||
dependencies[folder1].indexOf(getMod(folder2).name) != -1 ? 1 :
|
dependencies[folder1].indexOf(getMod(folder2).name) != -1 ? 1 :
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<objects>
|
<objects>
|
||||||
|
|
||||||
<script directory="gui/common/"/>
|
<script directory="gui/common/"/>
|
||||||
<script directory="gui/modmod/"/>
|
<script module="gui/modmod/modmod.js"/>
|
||||||
|
|
||||||
<object type="image" style="ModernWindow">
|
<object type="image" style="ModernWindow">
|
||||||
|
|
||||||
|
|
@ -80,10 +80,6 @@
|
||||||
font="sans-stroke-13"
|
font="sans-stroke-13"
|
||||||
auto_scroll="true"
|
auto_scroll="true"
|
||||||
>
|
>
|
||||||
<action on="SelectionChange">selectedMod(this.name);</action>
|
|
||||||
<action on="SelectionColumnChange">displayModLists();</action>
|
|
||||||
<action on="MouseLeftDoubleClickItem">enableMod();</action>
|
|
||||||
|
|
||||||
<!-- List headers -->
|
<!-- List headers -->
|
||||||
<!-- Keep the column names in sync with the property names of mods -->
|
<!-- Keep the column names in sync with the property names of mods -->
|
||||||
<column id="name" textcolor="255 255 255" width="10%">
|
<column id="name" textcolor="255 255 255" width="10%">
|
||||||
|
|
@ -123,9 +119,6 @@
|
||||||
tooltip_style="pgToolTip"
|
tooltip_style="pgToolTip"
|
||||||
auto_scroll="true"
|
auto_scroll="true"
|
||||||
>
|
>
|
||||||
<action on="SelectionChange">selectedMod(this.name);</action>
|
|
||||||
<action on="MouseLeftDoubleClickItem">disableMod();</action>
|
|
||||||
|
|
||||||
<!-- List headers -->
|
<!-- List headers -->
|
||||||
<column id="name" textcolor="255 255 255" width="10%">
|
<column id="name" textcolor="255 255 255" width="10%">
|
||||||
<translatableAttribute id="heading">Name</translatableAttribute>
|
<translatableAttribute id="heading">Name</translatableAttribute>
|
||||||
|
|
@ -159,7 +152,6 @@
|
||||||
sprite_disabled="ModernArrowUpGrey"
|
sprite_disabled="ModernArrowUpGrey"
|
||||||
>
|
>
|
||||||
<translatableAttribute id="tooltip">Change the order in which mods are launched. This should match the mods dependencies.</translatableAttribute>
|
<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>
|
||||||
<object
|
<object
|
||||||
name="enabledModDown"
|
name="enabledModDown"
|
||||||
|
|
@ -173,7 +165,6 @@
|
||||||
sprite_disabled="ModernArrowDownGrey"
|
sprite_disabled="ModernArrowDownGrey"
|
||||||
>
|
>
|
||||||
<translatableAttribute id="tooltip">Change the order in which mods are launched. This should match the mods dependencies.</translatableAttribute>
|
<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>
|
||||||
</object>
|
</object>
|
||||||
|
|
||||||
|
|
@ -181,7 +172,6 @@
|
||||||
<object name="toggleModButton" type="button" style="ModernButtonRed" size="16 100%-80 196 100%-52" enabled="false"/>
|
<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">
|
<object name="visitWebButton" type="button" style="ModernButtonRed" size="200 100%-80 380 100%-52" enabled="false">
|
||||||
<translatableAttribute id="caption">Visit Website</translatableAttribute>
|
<translatableAttribute id="caption">Visit Website</translatableAttribute>
|
||||||
<action on="Press">visitModWebsite();</action>
|
|
||||||
</object>
|
</object>
|
||||||
|
|
||||||
<!-- Message -->
|
<!-- Message -->
|
||||||
|
|
@ -198,22 +188,19 @@
|
||||||
|
|
||||||
<object type="button" style="ModernButtonRed" size="100%-748 100%-44 100%-568 100%-16">
|
<object type="button" style="ModernButtonRed" size="100%-748 100%-44 100%-568 100%-16">
|
||||||
<translatableAttribute id="caption">Help</translatableAttribute>
|
<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>
|
||||||
|
|
||||||
<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>
|
<translatableAttribute id="caption">Download Mods</translatableAttribute>
|
||||||
<action on="Press">downloadModsButton();</action>
|
|
||||||
</object>
|
</object>
|
||||||
|
|
||||||
<object name="saveConfigurationButton" type="button" style="ModernButtonRed" size="100%-380 100%-44 100%-200 100%-16">
|
<object name="saveConfigurationButton" type="button" style="ModernButtonRed" size="100%-380 100%-44 100%-200 100%-16">
|
||||||
<translatableAttribute id="caption">Save Configuration</translatableAttribute>
|
<translatableAttribute id="caption">Save Configuration</translatableAttribute>
|
||||||
<action on="Press">saveMods();</action>
|
|
||||||
</object>
|
</object>
|
||||||
|
|
||||||
<object name="startButton" type="button" style="ModernButtonRed" size="100%-196 100%-44 100%-16 100%-16">
|
<object name="startButton" type="button" style="ModernButtonRed" size="100%-196 100%-44 100%-16 100%-16">
|
||||||
<translatableAttribute id="caption">Save and Restart</translatableAttribute>
|
<translatableAttribute id="caption">Save and Restart</translatableAttribute>
|
||||||
<action on="Press">startMods();</action>
|
|
||||||
</object>
|
</object>
|
||||||
</object>
|
</object>
|
||||||
</objects>
|
</objects>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
function downloadModsButton()
|
export function downloadModsButton(initMods)
|
||||||
{
|
{
|
||||||
initTerms({
|
initTerms({
|
||||||
"Disclaimer": {
|
"Disclaimer": {
|
||||||
|
|
@ -6,7 +6,7 @@ function downloadModsButton()
|
||||||
"file": "gui/modio/Disclaimer.txt",
|
"file": "gui/modio/Disclaimer.txt",
|
||||||
"config": "modio.disclaimer",
|
"config": "modio.disclaimer",
|
||||||
"accepted": false,
|
"accepted": false,
|
||||||
"callback": openModIo,
|
"callback": openModIo.bind(undefined, initMods),
|
||||||
"urlButtons": [
|
"urlButtons": [
|
||||||
{
|
{
|
||||||
"caption": translate("mod.io Terms"),
|
"caption": translate("mod.io Terms"),
|
||||||
|
|
@ -23,7 +23,7 @@ function downloadModsButton()
|
||||||
openTerms("Disclaimer");
|
openTerms("Disclaimer");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openModIo(data)
|
async function openModIo(initMods, data)
|
||||||
{
|
{
|
||||||
if (!data.accepted)
|
if (!data.accepted)
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -48,12 +48,12 @@ const g_RegExpVersion= /[0-9]+(\.[0-9]+){0,2}/;
|
||||||
/**
|
/**
|
||||||
* Version checks in mod dependencies can use these operators.
|
* 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".
|
* 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.
|
* 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.
|
* Returns whether the mod defines all required properties and whether all properties are valid.
|
||||||
* Shows a notification if not.
|
* Shows a notification if not.
|
||||||
*/
|
*/
|
||||||
function validateMod(folder, modData, notify)
|
export function validateMod(folder, modData, notify)
|
||||||
{
|
{
|
||||||
let valid = true;
|
let valid = true;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* Currently limited to at most 3 buttons per message box.
|
* Currently limited to at most 3 buttons per message box.
|
||||||
* The convention is to have "cancel" appear first.
|
* The convention is to have "cancel" appear first.
|
||||||
*/
|
*/
|
||||||
function init(data)
|
export function init(data)
|
||||||
{
|
{
|
||||||
// Set title
|
// Set title
|
||||||
Engine.GetGUIObjectByName("mbTitleBar").caption = data.title;
|
Engine.GetGUIObjectByName("mbTitleBar").caption = data.title;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<objects>
|
<objects>
|
||||||
|
|
||||||
<script directory="gui/common/"/>
|
<script directory="gui/common/"/>
|
||||||
<script directory="gui/msgbox/"/>
|
<script module="gui/msgbox/msgbox.js"/>
|
||||||
|
|
||||||
<!-- Fade out the background because it's non-interactable -->
|
<!-- Fade out the background because it's non-interactable -->
|
||||||
<object sprite="ModernFade" type="image"/>
|
<object sprite="ModernFade" type="image"/>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
async function init()
|
export async function init()
|
||||||
{
|
{
|
||||||
return { [Engine.openRequest]: {
|
return { [Engine.openRequest]: {
|
||||||
"page": "page_modmod.xml",
|
"page": "page_modmod.xml",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<objects>
|
<objects>
|
||||||
<script directory="gui/pregame/"/>
|
<script module="gui/pregame/mainmenu.js"/>
|
||||||
</objects>
|
</objects>
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@
|
||||||
* The user should be able to save and print the text of the terms.
|
* The user should be able to save and print the text of the terms.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var g_TermsPage;
|
let g_TermsPage;
|
||||||
var g_TermsFile;
|
let g_TermsFile;
|
||||||
var g_TermsSprintf;
|
let g_TermsSprintf;
|
||||||
|
|
||||||
async function init(data)
|
export async function init(data)
|
||||||
{
|
{
|
||||||
g_TermsPage = data.page;
|
g_TermsPage = data.page;
|
||||||
g_TermsFile = data.file;
|
g_TermsFile = data.file;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<objects>
|
<objects>
|
||||||
|
|
||||||
<script directory="gui/common/"/>
|
<script directory="gui/common/"/>
|
||||||
<script directory="gui/termsdialog/"/>
|
<script module="gui/termsdialog/termsdialog.js"/>
|
||||||
|
|
||||||
<object type="image" sprite="ModernFade"/>
|
<object type="image" sprite="ModernFade"/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ class TimedConfirmation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function init(data)
|
export function init(data)
|
||||||
{
|
{
|
||||||
return new TimedConfirmation().setup(data);
|
return new TimedConfirmation().setup(data);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<objects>
|
<objects>
|
||||||
|
|
||||||
<script directory="gui/common/"/>
|
<script directory="gui/common/"/>
|
||||||
<script directory="gui/timedconfirmation/"/>
|
<script module="gui/timedconfirmation/timedconfirmation.js"/>
|
||||||
|
|
||||||
<!-- Fade out the background because it's non-interactable -->
|
<!-- Fade out the background because it's non-interactable -->
|
||||||
<object sprite="ModernFade" type="image"/>
|
<object sprite="ModernFade" type="image"/>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5460d4fe38fc4f4d0ab8532cfb63e4b53c574c2e14e08d572f101a843c72a398
|
||||||
|
size 6709
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:90d1cba3ebcf5683baf022d1a21be361840c5a7c7203d622583770ae16fe1914
|
||||||
|
size 6270
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7f58a4a6e3a45eaa8ad7056640a22ab46fd2a820cc55f59435910a7048e38f0c
|
||||||
|
size 6677
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2ef0fc7b741ae84795a64b95bdc85e2d2cda4f3a3cd2ef9e3c0341fa1aa3f964
|
||||||
|
size 6298
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8e5aa1c6e722029cb1115e3d7b35dd5e51af56c20d2470f3b7a2c9d8c42a2d85
|
||||||
|
size 4364
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b3aaf6a3a4a3debe1a41e9a1b829161a0f0011a42efc2454046d8928c687a1cb
|
||||||
|
size 6038
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:08f6bb87c4da438a0d9f0ee3940c95a042e8171adfdaa57fdadcb05dd617a023
|
||||||
|
size 5563
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:682c4075d5bf1fafa96b0a22a5ec56781fbdbc0e6ed8231ed1ab6d110cc08c3d
|
||||||
|
size 4252
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8b8a44793a609b25c9d2f8e295d58e7cda7662fcb402e196bc3f64ff130b4fa8
|
||||||
|
size 6305
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:679fbf447256c7046b29bffe6c5ea140b9bcaa0e43369fe393a6dcc12151697b
|
||||||
|
size 6144
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:436878aaa7be80c0846a133337af7a0c86371edaf3d4acd5397a8fa20ff0853e
|
||||||
|
size 18266
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b9b2bbdec7c2586c7ac2fc86769f305e43656dacd76985253ad2326496180eeb
|
||||||
|
size 16981
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:582b6b10793ae3bcf286d1db8ee46192d79cb475c0847916994921f8ecbc200a
|
||||||
|
size 51092
|
||||||
|
|
@ -179,6 +179,10 @@
|
||||||
"name": "Flare (toggle)",
|
"name": "Flare (toggle)",
|
||||||
"desc": "Set the cursor to Flare. The hotkey can be released."
|
"desc": "Set the cursor to Flare. The hotkey can be released."
|
||||||
},
|
},
|
||||||
|
"session.minimap.expand.toggle": {
|
||||||
|
"name": "Expand minimap",
|
||||||
|
"desc": "Toggle minimap expand."
|
||||||
|
},
|
||||||
"session.calltoarms": {
|
"session.calltoarms": {
|
||||||
"name": "Call to arms",
|
"name": "Call to arms",
|
||||||
"desc": "Send the selected units on attack move to the specified location after dropping resources."
|
"desc": "Send the selected units on attack move to the specified location after dropping resources."
|
||||||
|
|
|
||||||
|
|
@ -729,6 +729,50 @@
|
||||||
"tooltip": "Show special icons for some entities on the minimap.",
|
"tooltip": "Show special icons for some entities on the minimap.",
|
||||||
"config": "gui.session.minimap.icons.enabled"
|
"config": "gui.session.minimap.icons.enabled"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "dropdown",
|
||||||
|
"label": "Minimap position",
|
||||||
|
"tooltip": "Choose where the minimap is displayed.",
|
||||||
|
"config": "gui.session.minimap.position",
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"value": "panel-left",
|
||||||
|
"label": "Panel Left",
|
||||||
|
"tooltip": "Display the minimap attached to the bottom panel."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "screen-left",
|
||||||
|
"label": "Screen Left",
|
||||||
|
"tooltip": "Display the minimap in the bottom-left corner of the screen."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dropdown",
|
||||||
|
"label": "Minimap layout",
|
||||||
|
"tooltip": "Choose between square and round layout",
|
||||||
|
"config": "gui.session.minimap.shape",
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"value": "square",
|
||||||
|
"label": "Square",
|
||||||
|
"tooltip": "Original 0.A.D minimap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "round",
|
||||||
|
"label": "Round",
|
||||||
|
"tooltip": "Round version"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "slider",
|
||||||
|
"label": "Minimap size",
|
||||||
|
"tooltip": "Choose the size of the minimap.",
|
||||||
|
"config": "gui.session.minimap.size",
|
||||||
|
"min": 1,
|
||||||
|
"max": 2
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Rank icon above status bar",
|
"label": "Rank icon above status bar",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<object name="minimapPanel"
|
<object name="minimapPanel"
|
||||||
size="0 0 100% 100%"
|
size="0 2 100% 100%"
|
||||||
sprite="mapPanel"
|
sprite="mapPanel"
|
||||||
type="image"
|
type="image"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- Civ background image -->
|
<!-- Civ background image -->
|
||||||
<object
|
<object
|
||||||
name="minimapBackgroundTexture"
|
name="minimapBackgroundTexture"
|
||||||
|
|
@ -15,8 +14,9 @@
|
||||||
|
|
||||||
<!-- Background circle -->
|
<!-- Background circle -->
|
||||||
<object
|
<object
|
||||||
|
name="minimapCircle"
|
||||||
type="image"
|
type="image"
|
||||||
size="4 6 100%-4 100%-6"
|
size="4 4 100%-4 100%-4"
|
||||||
sprite="stretched:session/minimap_circle_modern.png"
|
sprite="stretched:session/minimap_circle_modern.png"
|
||||||
ghost="true"
|
ghost="true"
|
||||||
/>
|
/>
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
<!-- Idle Worker Button -->
|
<!-- Idle Worker Button -->
|
||||||
<object name="idleWorkerButton"
|
<object name="idleWorkerButton"
|
||||||
type="button"
|
type="button"
|
||||||
size="100%-119 100%-121 100%-4 100%-6"
|
size="100%-119 100%-121 100%-4 100%-4"
|
||||||
tooltip_style="sessionToolTipBottom"
|
tooltip_style="sessionToolTipBottom"
|
||||||
hotkey="selection.idleworker"
|
hotkey="selection.idleworker"
|
||||||
sprite="stretched:session/minimap-idle.png"
|
sprite="stretched:session/minimap-idle.png"
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
<!-- Total number of idle workers -->
|
<!-- Total number of idle workers -->
|
||||||
<object name="totalNumberIdleWorkers"
|
<object name="totalNumberIdleWorkers"
|
||||||
type="text"
|
type="text"
|
||||||
size="85% 89% 97% 98%"
|
size="100%-119 100%-121 100%-3 100%-2"
|
||||||
style="totalNumberIdleWorkersCount"
|
style="totalNumberIdleWorkersCount"
|
||||||
text_align="right"
|
text_align="right"
|
||||||
text_valign="bottom"
|
text_valign="bottom"
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
<!-- Diplomacy Colors Button -->
|
<!-- Diplomacy Colors Button -->
|
||||||
<object name="diplomacyColorsButton"
|
<object name="diplomacyColorsButton"
|
||||||
type="button"
|
type="button"
|
||||||
size="3 100%-121 118 100%-6"
|
size="4 100%-121 118 100%-4"
|
||||||
tooltip_style="sessionToolTipBottom"
|
tooltip_style="sessionToolTipBottom"
|
||||||
hotkey="session.diplomacycolors"
|
hotkey="session.diplomacycolors"
|
||||||
/>
|
/>
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
<object
|
<object
|
||||||
name="flareButton"
|
name="flareButton"
|
||||||
type="button"
|
type="button"
|
||||||
size="2 4 117 119"
|
size="4 4 113 119"
|
||||||
tooltip_style="sessionToolTip"
|
tooltip_style="sessionToolTip"
|
||||||
hotkey="session.flareactivate"
|
hotkey="session.flareactivate"
|
||||||
sprite_disabled="stretched:session/minimap-flare-disabled.png"
|
sprite_disabled="stretched:session/minimap-flare-disabled.png"
|
||||||
|
|
@ -64,7 +64,7 @@
|
||||||
<!-- MiniMap -->
|
<!-- MiniMap -->
|
||||||
<object
|
<object
|
||||||
name="minimap"
|
name="minimap"
|
||||||
size="8 10 100%-8 100%-10"
|
size="8 8 100%-8 100%-8"
|
||||||
type="minimap"
|
type="minimap"
|
||||||
mask="true"
|
mask="true"
|
||||||
flare_texture_count="16"
|
flare_texture_count="16"
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,15 @@
|
||||||
*/
|
*/
|
||||||
class MiniMapDiplomacyColorsButton
|
class MiniMapDiplomacyColorsButton
|
||||||
{
|
{
|
||||||
constructor(diplomacyColors)
|
constructor(diplomacyColors, theme)
|
||||||
{
|
{
|
||||||
|
this.theme = theme;
|
||||||
|
this.diplomacyColors = diplomacyColors;
|
||||||
this.diplomacyColorsButton = Engine.GetGUIObjectByName("diplomacyColorsButton");
|
this.diplomacyColorsButton = Engine.GetGUIObjectByName("diplomacyColorsButton");
|
||||||
this.diplomacyColorsButton.onPress = diplomacyColors.toggle.bind(diplomacyColors);
|
this.diplomacyColorsButton.onPress = diplomacyColors.toggle.bind(diplomacyColors);
|
||||||
|
this.diplomacyColorsButton.mouse_event_mask = this.theme.mouse_event_mask;
|
||||||
this.diplomacyColorsButton.mouse_event_mask = this.SpriteMask;
|
const initialState = this.diplomacyColors.isEnabled?.() ?? false;
|
||||||
|
this.onDiplomacyColorsChange(initialState);
|
||||||
diplomacyColors.registerDiplomacyColorsChangeHandler(this.onDiplomacyColorsChange.bind(this));
|
diplomacyColors.registerDiplomacyColorsChangeHandler(this.onDiplomacyColorsChange.bind(this));
|
||||||
registerHotkeyChangeHandler(this.onHotkeyChange.bind(this));
|
registerHotkeyChangeHandler(this.onHotkeyChange.bind(this));
|
||||||
}
|
}
|
||||||
|
|
@ -24,21 +26,11 @@ class MiniMapDiplomacyColorsButton
|
||||||
onDiplomacyColorsChange(enabled)
|
onDiplomacyColorsChange(enabled)
|
||||||
{
|
{
|
||||||
this.diplomacyColorsButton.sprite =
|
this.diplomacyColorsButton.sprite =
|
||||||
"stretched:" +
|
enabled ? this.theme.sprite_on : this.theme.sprite_off;
|
||||||
(enabled ? this.SpriteEnabled : this.SpriteDisabled);
|
|
||||||
|
|
||||||
this.diplomacyColorsButton.sprite_over =
|
this.diplomacyColorsButton.sprite_over =
|
||||||
"stretched:" +
|
enabled ? this.theme.sprite_on_over : this.theme.sprite_off_over;
|
||||||
(enabled ? this.SpriteEnabledOver : this.SpriteDisabledOver);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MiniMapDiplomacyColorsButton.prototype.Tooltip = markForTranslation("Toggle Diplomacy Colors");
|
MiniMapDiplomacyColorsButton.prototype.Tooltip = markForTranslation("Toggle Diplomacy Colors");
|
||||||
|
|
||||||
MiniMapDiplomacyColorsButton.prototype.SpriteEnabled = "session/minimap-diplomacy-on.png";
|
|
||||||
MiniMapDiplomacyColorsButton.prototype.SpriteDisabled = "session/minimap-diplomacy-off.png";
|
|
||||||
|
|
||||||
MiniMapDiplomacyColorsButton.prototype.SpriteEnabledOver = "session/minimap-diplomacy-on-highlight.png";
|
|
||||||
MiniMapDiplomacyColorsButton.prototype.SpriteDisabledOver = "session/minimap-diplomacy-off-highlight.png";
|
|
||||||
|
|
||||||
MiniMapDiplomacyColorsButton.prototype.SpriteMask = "texture:session/minimap-diplomacy-on.png";
|
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,6 @@ class MiniMapFlareButton
|
||||||
|
|
||||||
rebuild()
|
rebuild()
|
||||||
{
|
{
|
||||||
if (g_IsObserver)
|
|
||||||
{
|
|
||||||
this.flareButton.sprite = "stretched:session/minimap-observer-flare.png";
|
|
||||||
this.flareButton.sprite_over = "stretched:session/minimap-observer-flare-highlight.png";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.flareButton.sprite = "stretched:session/minimap-player-flare.png";
|
|
||||||
this.flareButton.sprite_over = "stretched:session/minimap-player-flare-highlight.png";
|
|
||||||
}
|
|
||||||
this.updateTooltip();
|
this.updateTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,309 @@ class MiniMapPanel
|
||||||
{
|
{
|
||||||
constructor(playerViewControl, diplomacyColors, idleWorkerClasses)
|
constructor(playerViewControl, diplomacyColors, idleWorkerClasses)
|
||||||
{
|
{
|
||||||
this.diplomacyColorsButton = new MiniMapDiplomacyColorsButton(diplomacyColors);
|
this.loadConfig();
|
||||||
|
|
||||||
|
this.themes = {
|
||||||
|
"square": {
|
||||||
|
"panelSprite": "mapPanel",
|
||||||
|
"circleSprite": "stretched:session/minimap_circle_modern.png",
|
||||||
|
"flare": {
|
||||||
|
"baseSize": "4 4 113 119",
|
||||||
|
"anchors": ["left", "top"],
|
||||||
|
"sprite": "stretched:session/minimap-player-flare.png",
|
||||||
|
"sprite_over": "stretched:session/minimap-player-flare-highlight.png",
|
||||||
|
"sprite_disabled": "stretched:session/minimap-player-flare-disabled.png",
|
||||||
|
"mouse_event_mask": "texture:session/minimap-player-flare.png",
|
||||||
|
"observer_sprite": "stretched:session/minimap-observer-flare.png",
|
||||||
|
"observer_sprite_over": "stretched:session/minimap-observer-flare-highlight.png"
|
||||||
|
},
|
||||||
|
"diplomacy": {
|
||||||
|
"baseSize": "4 100%-121 119 100%-4",
|
||||||
|
"anchors": ["left", "bottom"],
|
||||||
|
"sprite_on": "stretched:session/minimap-diplomacy-on.png",
|
||||||
|
"sprite_off": "stretched:session/minimap-diplomacy-off.png",
|
||||||
|
"sprite_on_over": "stretched:session/minimap-diplomacy-on-highlight.png",
|
||||||
|
"sprite_off_over": "stretched:session/minimap-diplomacy-off-highlight.png",
|
||||||
|
"mouse_event_mask": "texture:session/minimap-diplomacy-on.png"
|
||||||
|
},
|
||||||
|
"idle": {
|
||||||
|
"baseSize": "100%-119 100%-121 100%-4 100%-4",
|
||||||
|
"anchors": ["right", "bottom"],
|
||||||
|
"sprite": "stretched:session/minimap-idle.png",
|
||||||
|
"sprite_over": "stretched:session/minimap-idle-highlight.png",
|
||||||
|
"sprite_disabled": "stretched:session/minimap-idle-disabled.png",
|
||||||
|
"mouse_event_mask": "texture:session/minimap-idle.png"
|
||||||
|
},
|
||||||
|
"idleText": {
|
||||||
|
"baseSize": "100%-119 100%-121 100%-3 100%-2",
|
||||||
|
"anchors": ["right", "bottom"],
|
||||||
|
"text_align": "right",
|
||||||
|
"text_valign": "bottom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"round": {
|
||||||
|
"panelSprite": "",
|
||||||
|
"mapOffset": -10,
|
||||||
|
"circleSprite": "stretched:session/minimap_expanded_circle.png",
|
||||||
|
"flare": {
|
||||||
|
"baseSize": "100%-48 100%-28 100%-24 100%-4",
|
||||||
|
"sprite": "stretched:session/minimap-round-flare.png",
|
||||||
|
"sprite_over": "stretched:session/minimap-round-flare-highlight.png",
|
||||||
|
"sprite_disabled": "stretched:session/minimap-round-flare-disabled.png",
|
||||||
|
"mouse_event_mask": "texture:session/minimap-round-flare.png",
|
||||||
|
"observer_sprite": "stretched:session/minimap-round-observer-flare.png",
|
||||||
|
"observer_sprite_over": "stretched:session/minimap-round-observer-flare-highlight.png"
|
||||||
|
},
|
||||||
|
"diplomacy": {
|
||||||
|
"baseSize": "22 100%-28 46 100%-4",
|
||||||
|
"sprite_on": "stretched:session/minimap-round-diplomacy-on.png",
|
||||||
|
"sprite_off": "stretched:session/minimap-round-diplomacy-off.png",
|
||||||
|
"sprite_on_over": "stretched:session/minimap-round-diplomacy-on-highlight.png",
|
||||||
|
"sprite_off_over": "stretched:session/minimap-round-diplomacy-off-highlight.png",
|
||||||
|
"mouse_event_mask": "texture:session/minimap-round-diplomacy-off.png"
|
||||||
|
},
|
||||||
|
"idle": {
|
||||||
|
"baseSize": "100%-28 100%-46 100%-4 100%-22",
|
||||||
|
"sprite": "stretched:session/minimap-round-idle.png",
|
||||||
|
"sprite_over": "stretched:session/minimap-round-idle-highlight.png",
|
||||||
|
"sprite_disabled": "stretched:session/minimap-round-idle-disabled.png",
|
||||||
|
"mouse_event_mask": "texture:session/minimap-round-idle.png"
|
||||||
|
},
|
||||||
|
"idleText": {
|
||||||
|
"baseSize": "100%-28 100%-46 100%-4 100%-22",
|
||||||
|
"text_align": "center",
|
||||||
|
"text_valign": "center"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.expandedState = {
|
||||||
|
"panelSprite": "",
|
||||||
|
"hideButtons": true,
|
||||||
|
"circleSprite": "stretched:session/minimap_expanded_circle.png",
|
||||||
|
"computeLayout": this.computeExpandedLayout.bind(this)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.panel = Engine.GetGUIObjectByName("minimapPanel");
|
||||||
|
this.minimapCircle = Engine.GetGUIObjectByName("minimapCircle");
|
||||||
|
this.minimapMap = Engine.GetGUIObjectByName("minimap");
|
||||||
|
this.minimapBackgroundTexture = Engine.GetGUIObjectByName("minimapBackgroundTexture");
|
||||||
|
this.session = Engine.GetGUIObjectByName("session");
|
||||||
|
this.supplementalPanel = Engine.GetGUIObjectByName("supplementalSelectionDetails");
|
||||||
this.idleWorkerButton = new MiniMapIdleWorkerButton(playerViewControl, idleWorkerClasses);
|
this.idleWorkerButton = new MiniMapIdleWorkerButton(playerViewControl, idleWorkerClasses);
|
||||||
|
this.totalNumberIdleWorkers = Engine.GetGUIObjectByName("totalNumberIdleWorkers");
|
||||||
this.flareButton = new MiniMapFlareButton(playerViewControl);
|
this.flareButton = new MiniMapFlareButton(playerViewControl);
|
||||||
this.miniMap = new MiniMap();
|
this.miniMap = new MiniMap();
|
||||||
|
this.diplomacyColors = diplomacyColors;
|
||||||
|
this.diplomacyColorsButton = new MiniMapDiplomacyColorsButton(diplomacyColors, this.themes[this.shape].diplomacy);
|
||||||
|
const panelRect = this.panel.getComputedSize();
|
||||||
|
this.defaultPanelWidth = panelRect.right - panelRect.left;
|
||||||
|
this.defaultPanelHeight = panelRect.bottom - panelRect.top;
|
||||||
playerViewControl.registerViewedPlayerChangeHandler(this.rebuild.bind(this));
|
playerViewControl.registerViewedPlayerChangeHandler(this.rebuild.bind(this));
|
||||||
registerHotkeyChangeHandler(this.rebuild.bind(this));
|
registerHotkeyChangeHandler(this.rebuild.bind(this));
|
||||||
|
registerConfigChangeHandler(this.onConfigChange.bind(this));
|
||||||
|
Engine.SetGlobalHotkey("session.minimap.expand.toggle", "Press", this.toggleExpanded.bind(this));
|
||||||
|
this.expanded = false;
|
||||||
|
this.applyLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadConfig()
|
||||||
|
{
|
||||||
|
this.position = Engine.ConfigDB_GetValue("user", "gui.session.minimap.position");
|
||||||
|
this.sizeScale = Engine.ConfigDB_GetValue("user", "gui.session.minimap.size");
|
||||||
|
this.shape = Engine.ConfigDB_GetValue("user", "gui.session.minimap.shape") || "square";
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentTheme()
|
||||||
|
{
|
||||||
|
if (this.expanded)
|
||||||
|
return this.expandedState;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...this.themes[this.shape],
|
||||||
|
"hideButtons": false,
|
||||||
|
"computeLayout": this.computeNormalLayout.bind(this)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isObserver()
|
||||||
|
{
|
||||||
|
return g_IsObserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleExpanded()
|
||||||
|
{
|
||||||
|
this.expanded = !this.expanded;
|
||||||
|
this.applyLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfigChange(changes)
|
||||||
|
{
|
||||||
|
if (![...changes].some(change =>
|
||||||
|
change == "gui.session.minimap.position" ||
|
||||||
|
change == "gui.session.minimap.size" ||
|
||||||
|
change == "gui.session.minimap.shape"))
|
||||||
|
return;
|
||||||
|
this.loadConfig();
|
||||||
|
this.applyLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyLayout()
|
||||||
|
{
|
||||||
|
const theme = this.currentTheme;
|
||||||
|
|
||||||
|
this.panel.size = theme.computeLayout();
|
||||||
|
this.panel.sprite = theme.panelSprite;
|
||||||
|
this.minimapCircle.sprite = theme.circleSprite;
|
||||||
|
this.minimapCircle.hidden = false;
|
||||||
|
const offset = theme.mapOffset ?? 0;
|
||||||
|
this.minimapBackgroundTexture.size = `4 ${4 + offset} 100%-4 100%-${4 - offset}`;
|
||||||
|
this.minimapCircle.size = `4 ${4 + offset} 100%-4 100%-${4 - offset}`;
|
||||||
|
this.minimapMap.size = `8 ${8 + offset} 100%-8 100%-${8 - offset}`;
|
||||||
|
this.refreshButtons(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshButtons(theme)
|
||||||
|
{
|
||||||
|
const hidden = theme.hideButtons;
|
||||||
|
this.flareButton.flareButton.hidden = hidden;
|
||||||
|
this.idleWorkerButton.idleWorkerButton.hidden = hidden;
|
||||||
|
this.idleWorkerButton.totalNumberIdleWorkers.hidden = hidden;
|
||||||
|
this.diplomacyColorsButton.diplomacyColorsButton.hidden = hidden;
|
||||||
|
if (hidden)
|
||||||
|
return;
|
||||||
|
const scale = parseFloat(this.sizeScale) || 1.0;
|
||||||
|
const flareConfig = this.isObserver() && theme.flare?.observer_sprite ?
|
||||||
|
{ ...theme.flare, "sprite": theme.flare.observer_sprite, "sprite_over": theme.flare.observer_sprite_over } :
|
||||||
|
theme.flare;
|
||||||
|
this.applyButtonSprites(this.flareButton.flareButton, flareConfig);
|
||||||
|
this.applyScaledSize(this.flareButton.flareButton, theme.flare, scale);
|
||||||
|
this.applyButtonSprites(this.idleWorkerButton.idleWorkerButton, theme.idle);
|
||||||
|
this.applyScaledSize(this.idleWorkerButton.idleWorkerButton, theme.idle, scale);
|
||||||
|
this.applyScaledSize(this.totalNumberIdleWorkers, theme.idleText, scale);
|
||||||
|
if (theme.idleText.text_align !== undefined)
|
||||||
|
this.totalNumberIdleWorkers.text_align = theme.idleText.text_align;
|
||||||
|
if (theme.idleText.text_valign !== undefined)
|
||||||
|
this.totalNumberIdleWorkers.text_valign = theme.idleText.text_valign;
|
||||||
|
this.diplomacyColorsButton.theme = theme.diplomacy;
|
||||||
|
this.applyButtonSprites(this.diplomacyColorsButton.diplomacyColorsButton, theme.diplomacy);
|
||||||
|
this.diplomacyColorsButton.onDiplomacyColorsChange(this.diplomacyColors.isEnabled?.() ?? false);
|
||||||
|
this.applyScaledSize(this.diplomacyColorsButton.diplomacyColorsButton, theme.diplomacy, scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyButtonSprites(button, config)
|
||||||
|
{
|
||||||
|
if (!config)
|
||||||
|
return;
|
||||||
|
if (config.mouse_event_mask !== undefined)
|
||||||
|
button.mouse_event_mask = config.mouse_event_mask;
|
||||||
|
if (!config.sprite)
|
||||||
|
return;
|
||||||
|
button.sprite = config.sprite;
|
||||||
|
if (config.sprite_over !== undefined)
|
||||||
|
button.sprite_over = config.sprite_over;
|
||||||
|
if (config.sprite_disabled !== undefined)
|
||||||
|
button.sprite_disabled = config.sprite_disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyScaledSize(element, cfg, scale)
|
||||||
|
{
|
||||||
|
if (!cfg)
|
||||||
|
return;
|
||||||
|
const original = cfg.baseSize.split(/\s+/);
|
||||||
|
const parts = original.map(part =>
|
||||||
|
{
|
||||||
|
const match = part.match(/^100%-(\d+)$/);
|
||||||
|
if (match)
|
||||||
|
return `100%-${Math.round(+match[1] * scale)}`;
|
||||||
|
if (/^-?\d+$/.test(part))
|
||||||
|
return `${Math.round(+part * scale)}`;
|
||||||
|
return part;
|
||||||
|
});
|
||||||
|
if (!cfg.anchors)
|
||||||
|
{
|
||||||
|
element.size = parts.join(" ");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scaled = { "left": parts[0], "top": parts[1], "right": parts[2], "bottom": parts[3] };
|
||||||
|
|
||||||
|
for (const anchor of cfg.anchors)
|
||||||
|
{
|
||||||
|
const idx = ["left", "top", "right", "bottom"].indexOf(anchor);
|
||||||
|
scaled[anchor] = original[idx];
|
||||||
|
}
|
||||||
|
element.size = [scaled.left, scaled.top, scaled.right, scaled.bottom].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
computeNormalLayout()
|
||||||
|
{
|
||||||
|
return this.position === "panel-left" ?
|
||||||
|
this.computePanelLeftLayout() :
|
||||||
|
this.computeScreenLeftLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
getScaledPanelSize()
|
||||||
|
{
|
||||||
|
const scale = parseFloat(this.sizeScale) || 1.0;
|
||||||
|
return {
|
||||||
|
"width": Math.round(this.defaultPanelWidth * scale),
|
||||||
|
"height": Math.round(this.defaultPanelHeight * scale)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
computePanelLeftLayout()
|
||||||
|
{
|
||||||
|
const { width, height } = this.getScaledPanelSize();
|
||||||
|
const parentRect = this.panel.parent.getComputedSize();
|
||||||
|
const parentHeight = parentRect.bottom - parentRect.top;
|
||||||
|
return {
|
||||||
|
"left": this.defaultPanelWidth - width,
|
||||||
|
"top": parentHeight - height,
|
||||||
|
"right": this.defaultPanelWidth,
|
||||||
|
"bottom": parentHeight
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
computeScreenLeftLayout()
|
||||||
|
{
|
||||||
|
const { width, height } = this.getScaledPanelSize();
|
||||||
|
const windowSize = this.session.getComputedSize();
|
||||||
|
const parentRect = this.panel.parent.getComputedSize();
|
||||||
|
const screenTop = (windowSize.bottom - windowSize.top) - height;
|
||||||
|
const maxRight = this.supplementalPanel.getComputedSize().left;
|
||||||
|
let left = 0;
|
||||||
|
let right = width;
|
||||||
|
if (right > maxRight)
|
||||||
|
{
|
||||||
|
right = maxRight;
|
||||||
|
left = right - width;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"left": left - parentRect.left,
|
||||||
|
"top": screenTop - parentRect.top,
|
||||||
|
"right": right - parentRect.left,
|
||||||
|
"bottom": screenTop + height - parentRect.top
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
computeExpandedLayout()
|
||||||
|
{
|
||||||
|
const windowSize = this.session.getComputedSize();
|
||||||
|
const scale = 0.55;
|
||||||
|
const marginLeft = 20;
|
||||||
|
const verticalOffset = -40;
|
||||||
|
const screenHeight = windowSize.bottom - windowSize.top;
|
||||||
|
const size = Math.floor(screenHeight * scale);
|
||||||
|
const screenTop = Math.floor((screenHeight - size) / 2) + verticalOffset;
|
||||||
|
const parentRect = this.panel.parent.getComputedSize();
|
||||||
|
const top = screenTop - parentRect.top;
|
||||||
|
const left = marginLeft - parentRect.left;
|
||||||
|
return {
|
||||||
|
"left": left,
|
||||||
|
"top": top,
|
||||||
|
"right": left + size,
|
||||||
|
"bottom": top + size
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
flare(target, playerID)
|
flare(target, playerID)
|
||||||
|
|
@ -25,13 +322,13 @@ class MiniMapPanel
|
||||||
|
|
||||||
rebuild()
|
rebuild()
|
||||||
{
|
{
|
||||||
|
this.applyLayout();
|
||||||
this.setCivBackgroundTexture();
|
this.setCivBackgroundTexture();
|
||||||
}
|
}
|
||||||
|
|
||||||
setCivBackgroundTexture()
|
setCivBackgroundTexture()
|
||||||
{
|
{
|
||||||
const playerCiv = g_ViewedPlayer > 0 ? g_Players[g_ViewedPlayer].civ : "gaia";
|
const playerCiv = g_ViewedPlayer > 0 ? g_Players[g_ViewedPlayer].civ : "gaia";
|
||||||
const backgroundObject = Engine.GetGUIObjectByName("minimapBackgroundTexture");
|
this.minimapBackgroundTexture.sprite = `stretched:session/icons/bkg/background_circle_${playerCiv}.png`;
|
||||||
backgroundObject.sprite = `stretched:session/icons/bkg/background_circle_${playerCiv}.png`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,17 @@ class CounterPopulation
|
||||||
for (const resCode of g_ResourceData.GetCodes())
|
for (const resCode of g_ResourceData.GetCodes())
|
||||||
total += playerState.resourceGatherers[resCode];
|
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.isTrainingBlocked = playerState.trainingBlocked;
|
||||||
|
|
||||||
this.panel.tooltip =
|
this.panel.tooltip =
|
||||||
setStringTags(translate(this.PopulationTooltip), CounterManager.ResourceTitleTags) +
|
setStringTags(translate(this.PopulationTooltipTitle), CounterManager.ResourceTitleTags) +
|
||||||
getAllyStatTooltip(this.getTooltipData.bind(this));
|
"\n" + sprintf(this.PopulationTooltip, state) +
|
||||||
|
getAllyStatTooltip(this.getTooltipData.bind(this)) + "\n" +
|
||||||
|
sprintf(this.CurrentGatherersTooltip, { "currentGatherers": colorizedTotal });
|
||||||
}
|
}
|
||||||
|
|
||||||
getTooltipData(playerState, playername)
|
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.
|
// 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.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)");
|
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.
|
* 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]));
|
this.count.caption = abbreviateLargeNumbers(Math.floor(playerState.resourceCounts[this.resCode]));
|
||||||
|
|
||||||
const gatherers = playerState.resourceGatherers[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?
|
// TODO: Set the tooltip only if hovered?
|
||||||
|
|
@ -28,7 +30,9 @@ class CounterResource
|
||||||
this.panel.tooltip =
|
this.panel.tooltip =
|
||||||
setStringTags(resourceNameFirstWord(this.resCode), CounterManager.ResourceTitleTags) +
|
setStringTags(resourceNameFirstWord(this.resCode), CounterManager.ResourceTitleTags) +
|
||||||
description +
|
description +
|
||||||
getAllyStatTooltip(this.getTooltipData.bind(this));
|
getAllyStatTooltip(this.getTooltipData.bind(this)) + "\n" +
|
||||||
|
sprintf(CounterPopulation.prototype.CurrentGatherersTooltip,
|
||||||
|
{ "currentGatherers": colorizedGatherers });
|
||||||
}
|
}
|
||||||
|
|
||||||
getTooltipData(playerState, playername)
|
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)
|
“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)
|
“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)
|
“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)
|
if (!cmpTargetPlayer || !cmpEntityPlayer)
|
||||||
return false;
|
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 types = this.GetAttackTypes(wantedTypes);
|
||||||
const entityOwner = cmpEntityPlayer.GetPlayerID();
|
const entityOwner = cmpEntityPlayer.GetPlayerID();
|
||||||
const targetOwner = cmpTargetPlayer.GetPlayerID();
|
const targetOwner = cmpTargetPlayer.GetPlayerID();
|
||||||
const cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
|
const cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
|
||||||
const cmpDiplomacy = QueryPlayerIDInterface(entityOwner, IID_Diplomacy);
|
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)
|
for (const type of types)
|
||||||
{
|
{
|
||||||
if (type != "Capture" && (!cmpDiplomacy?.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints()))
|
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)))
|
if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner)))
|
||||||
continue;
|
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;
|
continue;
|
||||||
|
|
||||||
const restrictedClasses = this.GetRestrictedClasses(type);
|
const restrictedClasses = this.GetRestrictedClasses(type);
|
||||||
|
|
@ -319,6 +324,77 @@ Attack.prototype.CanAttack = function(target, wantedTypes)
|
||||||
return false;
|
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.
|
* 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()
|
Attack.prototype.GetFullAttackRange = function()
|
||||||
{
|
{
|
||||||
const ret = { "min": Infinity, "max": 0 };
|
const ret = { "min": Infinity, "max": 0, "parabolic": false };
|
||||||
for (const type of this.GetAttackTypes())
|
for (const type of this.GetAttackTypes())
|
||||||
{
|
{
|
||||||
const range = this.GetRange(type);
|
const range = this.GetRange(type);
|
||||||
ret.min = Math.min(ret.min, range.min);
|
ret.min = Math.min(ret.min, range.min);
|
||||||
ret.max = Math.max(ret.max, range.max);
|
ret.max = Math.max(ret.max, range.max);
|
||||||
|
if (range.parabolic)
|
||||||
|
ret.parabolic = true;
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
@ -475,7 +553,39 @@ Attack.prototype.GetRange = function(type)
|
||||||
let min = +(this.template[type].MinRange || 0);
|
let min = +(this.template[type].MinRange || 0);
|
||||||
min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity);
|
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)
|
Attack.prototype.GetAttackYOrigin = function(type)
|
||||||
|
|
@ -813,14 +923,9 @@ Attack.prototype.PerformAttack = function(type, target)
|
||||||
*/
|
*/
|
||||||
Attack.prototype.IsTargetInRange = function(target, type)
|
Attack.prototype.IsTargetInRange = function(target, type)
|
||||||
{
|
{
|
||||||
const range = this.GetRange(type);
|
const range = this.GetEffectiveAttackRange(target, type);
|
||||||
return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).IsInTargetParabolicRange(
|
return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).IsInTargetRange(
|
||||||
this.entity,
|
this.entity, target, range.min, range.max, false);
|
||||||
target,
|
|
||||||
range.min,
|
|
||||||
range.max,
|
|
||||||
this.GetAttackYOrigin(type),
|
|
||||||
false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Attack.prototype.OnValueModification = function(msg)
|
Attack.prototype.OnValueModification = function(msg)
|
||||||
|
|
|
||||||
|
|
@ -127,10 +127,20 @@ BuildingAI.prototype.SetupRangeQuery = function()
|
||||||
|
|
||||||
const range = cmpAttack.GetRange(attackType);
|
const range = cmpAttack.GetRange(attackType);
|
||||||
const yOrigin = cmpAttack.GetAttackYOrigin(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 takes entity sizes into accounts, so no need to compensate for structure size.
|
||||||
this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
|
this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
|
||||||
this.entity, range.min, range.max, yOrigin,
|
this.entity, range.min, range.max, baseRange, yOrigin,
|
||||||
enemies, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"));
|
enemies, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"),
|
||||||
|
true // Allow mirages for attack queries
|
||||||
|
);
|
||||||
|
|
||||||
cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery);
|
cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery);
|
||||||
};
|
};
|
||||||
|
|
@ -156,10 +166,17 @@ BuildingAI.prototype.SetupGaiaRangeQuery = function()
|
||||||
const range = cmpAttack.GetRange(attackType);
|
const range = cmpAttack.GetRange(attackType);
|
||||||
const yOrigin = cmpAttack.GetAttackYOrigin(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 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 takes entity sizes into accounts, so no need to compensate for structure size.
|
||||||
this.gaiaUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
|
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"));
|
[0], IID_Attack, cmpRangeManager.GetEntityFlagMask("normal"));
|
||||||
|
|
||||||
cmpRangeManager.EnableActiveQuery(this.gaiaUnitsQuery);
|
cmpRangeManager.EnableActiveQuery(this.gaiaUnitsQuery);
|
||||||
|
|
@ -170,7 +187,6 @@ BuildingAI.prototype.SetupGaiaRangeQuery = function()
|
||||||
*/
|
*/
|
||||||
BuildingAI.prototype.OnRangeUpdate = function(msg)
|
BuildingAI.prototype.OnRangeUpdate = function(msg)
|
||||||
{
|
{
|
||||||
|
|
||||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||||
if (!cmpAttack)
|
if (!cmpAttack)
|
||||||
return;
|
return;
|
||||||
|
|
@ -189,10 +205,10 @@ BuildingAI.prototype.OnRangeUpdate = function(msg)
|
||||||
|
|
||||||
// Add new targets.
|
// Add new targets.
|
||||||
for (const entity of msg.added)
|
for (const entity of msg.added)
|
||||||
if (cmpAttack.CanAttack(entity))
|
if (!this.targetUnits.includes(entity))
|
||||||
this.targetUnits.push(entity);
|
this.targetUnits.push(entity);
|
||||||
|
|
||||||
// Remove targets outside of vision-range.
|
// Remove targets out of range.
|
||||||
for (const entity of msg.removed)
|
for (const entity of msg.removed)
|
||||||
{
|
{
|
||||||
const index = this.targetUnits.indexOf(entity);
|
const index = this.targetUnits.indexOf(entity);
|
||||||
|
|
@ -375,13 +391,7 @@ BuildingAI.prototype.FireArrows = function()
|
||||||
{
|
{
|
||||||
|
|
||||||
const selectedTarget = targets[targetIndex].entityId;
|
const selectedTarget = targets[targetIndex].entityId;
|
||||||
if (this.CheckTargetVisible(selectedTarget) && cmpObstructionManager.IsInTargetParabolicRange(
|
if (cmpAttack.CanAttack(selectedTarget, [attackType]))
|
||||||
this.entity,
|
|
||||||
selectedTarget,
|
|
||||||
range.min,
|
|
||||||
range.max,
|
|
||||||
yOrigin,
|
|
||||||
false))
|
|
||||||
{
|
{
|
||||||
cmpAttack.PerformAttack(attackType, selectedTarget);
|
cmpAttack.PerformAttack(attackType, selectedTarget);
|
||||||
PlaySound("attack_" + attackType.toLowerCase(), this.entity);
|
PlaySound("attack_" + attackType.toLowerCase(), this.entity);
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,39 @@ class TurretHolder
|
||||||
return turret ? turret.name : "";
|
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.
|
* @return {number[]} - The turretted entityIDs.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -4244,10 +4244,31 @@ UnitAI.prototype.SetupAttackRangeQuery = function(enable = true)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const range = this.GetQueryRange(IID_Attack);
|
const range = this.GetQueryRange(IID_Attack);
|
||||||
// Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
|
if (range.parabolic)
|
||||||
this.losAttackRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
|
{
|
||||||
range.min, range.max, players, IID_Resistance,
|
const cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||||
cmpRangeManager.GetEntityFlagMask("normal"), false);
|
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)
|
if (enable)
|
||||||
cmpRangeManager.EnableActiveQuery(this.losAttackRangeQuery);
|
cmpRangeManager.EnableActiveQuery(this.losAttackRangeQuery);
|
||||||
|
|
@ -5157,24 +5178,24 @@ UnitAI.prototype.MoveToTargetAttackRange = function(target, type)
|
||||||
if (cmpFormation)
|
if (cmpFormation)
|
||||||
target = cmpFormation.GetClosestMemberToEntity(this.entity);
|
target = cmpFormation.GetClosestMemberToEntity(this.entity);
|
||||||
|
|
||||||
if (type != "Ranged")
|
|
||||||
return this.MoveToTargetRange(target, IID_Attack, type);
|
|
||||||
|
|
||||||
if (!this.CheckTargetVisible(target))
|
if (!this.CheckTargetVisible(target))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
const cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||||
if (!cmpAttack)
|
if (!cmpAttack)
|
||||||
return false;
|
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 flatRange = cmpAttack.GetRange(type);
|
||||||
const parabolicMaxRange = Math.max(0, Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEffectiveParabolicRange(this.entity, target, range.max, cmpAttack.GetAttackYOrigin(type)));
|
const effectiveRange = cmpAttack.GetEffectiveAttackRange(target, type);
|
||||||
|
if (effectiveRange.max < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
// The parabole changes while walking so be cautious:
|
// The parabola changes while walking so be cautious:
|
||||||
const guessedMaxRange = parabolicMaxRange > range.max ? (range.max + parabolicMaxRange) / 2 : parabolicMaxRange;
|
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)
|
UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
|
||||||
|
|
@ -5584,8 +5605,16 @@ UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
|
||||||
if (!this.AbleToMove())
|
if (!this.AbleToMove())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Check if we should chase based on stance
|
||||||
if (this.GetStance().respondChase)
|
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 we are guarding/escorting, chase at least as long as the guarded unit is in target range of the attacker
|
||||||
if (this.isGuardOf)
|
if (this.isGuardOf)
|
||||||
|
|
@ -6620,9 +6649,22 @@ UnitAI.prototype.FindWalkAndFightTargets = function()
|
||||||
return false;
|
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)
|
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);
|
const cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
|
||||||
if (!cmpVision)
|
if (!cmpVision)
|
||||||
|
|
@ -6635,27 +6677,35 @@ UnitAI.prototype.GetQueryRange = function(iid)
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.GetStance().respondStandGround)
|
const range = this.GetRange(iid);
|
||||||
{
|
if (!range)
|
||||||
const range = this.GetRange(iid);
|
return ret;
|
||||||
if (!range)
|
|
||||||
return ret;
|
// The query range depends on stance because it represents the distance at which
|
||||||
ret.min = range.min;
|
// the unit should "notice" an enemy and potentially start moving toward it.
|
||||||
ret.max = Math.min(range.max, visionRange);
|
|
||||||
}
|
// In all stances, always spot targets within effective attack/heal range.
|
||||||
else if (this.GetStance().respondChase)
|
Object.assign(ret, range);
|
||||||
ret.max = visionRange;
|
|
||||||
|
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)
|
else if (this.GetStance().respondHoldGround)
|
||||||
{
|
// HoldGround: willing to move a bit, so spot targets within attack range + half vision.
|
||||||
const range = this.GetRange(iid);
|
nonParabolicMax = Math.min(range.max + visionRange / 2, visionRange);
|
||||||
if (!range)
|
|
||||||
return ret;
|
// StandGround: nonParabolicMax stays 0, using only parabolic range.
|
||||||
ret.max = Math.min(range.max + visionRange / 2, visionRange);
|
|
||||||
}
|
|
||||||
// We probably have stance 'passive' and we wouldn't have a 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.
|
// but as it is the default for healers we need to set it to something sane.
|
||||||
else if (iid === IID_Heal)
|
else if (iid === IID_Heal)
|
||||||
ret.max = visionRange;
|
nonParabolicMax = visionRange;
|
||||||
|
|
||||||
|
if (ret.parabolic)
|
||||||
|
ret.base = nonParabolicMax;
|
||||||
|
else
|
||||||
|
ret.max = nonParabolicMax;
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ Engine.LoadComponentScript("interfaces/Formation.js");
|
||||||
Engine.LoadComponentScript("interfaces/Health.js");
|
Engine.LoadComponentScript("interfaces/Health.js");
|
||||||
Engine.LoadComponentScript("interfaces/Resistance.js");
|
Engine.LoadComponentScript("interfaces/Resistance.js");
|
||||||
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
||||||
|
Engine.LoadComponentScript("interfaces/Turretable.js");
|
||||||
|
Engine.LoadComponentScript("interfaces/TurretHolder.js");
|
||||||
Engine.LoadComponentScript("Attack.js");
|
Engine.LoadComponentScript("Attack.js");
|
||||||
|
|
||||||
let entityID = 903;
|
let entityID = 903;
|
||||||
|
|
@ -52,6 +54,16 @@ function attackComponentTest(defenderClass, isEnemy, test_function)
|
||||||
"IsEnemy": () => isEnemy
|
"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;
|
const attacker = entityID;
|
||||||
|
|
||||||
AddMock(attacker, IID_Position, {
|
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.GetPreferredClasses("Melee"), ["Civilian"]);
|
||||||
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]);
|
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("Capture"), { "Capture": 8 });
|
||||||
|
|
||||||
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), {
|
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), {
|
||||||
|
|
@ -416,3 +428,101 @@ function testAttackPreference()
|
||||||
TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+4), undefined);
|
TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+4), undefined);
|
||||||
}
|
}
|
||||||
testAttackPreference();
|
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 alliedPlayer = 3;
|
||||||
const turretHolderID = 9;
|
const turretHolderID = 9;
|
||||||
const entitiesToTest = [10, 11, 12, 13];
|
const entitiesToTest = [10, 11, 12, 13];
|
||||||
|
let entityID = 100;
|
||||||
|
|
||||||
AddMock(turretHolderID, IID_Ownership, {
|
AddMock(turretHolderID, IID_Ownership, {
|
||||||
"GetOwner": () => player
|
"GetOwner": () => player
|
||||||
|
|
@ -244,3 +245,80 @@ cmpTurretHolder.OnOwnershipChanged({
|
||||||
"from": INVALID_PLAYER
|
"from": INVALID_PLAYER
|
||||||
});
|
});
|
||||||
TS_ASSERT(cmpTurretHolder.OccupiesTurretPoint(spawned));
|
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 }
|
{ "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.9 }
|
||||||
],
|
],
|
||||||
"auraName": "Economic Fortune",
|
"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 }
|
{ "value": "TerritoryInfluence/Radius", "multiply": 1.2 }
|
||||||
],
|
],
|
||||||
"auraName": "Territorial Expansion",
|
"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 }
|
{ "value": "Resistance/Entity/Damage/Crush", "add": 1 }
|
||||||
],
|
],
|
||||||
"auraName": "Founder and Defender of the Republic",
|
"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)
|
if (cmpVisual && cmpNewVisual)
|
||||||
cmpNewVisual.SetActorSeed(cmpVisual.GetActorSeed());
|
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);
|
const cmpOldTurretable = Engine.QueryInterface(oldEnt, IID_Turretable);
|
||||||
|
|
||||||
// If the old entity is turreted, we need to handle it before copying position
|
// 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
|
// Check if it's allowed to occupy the turret point
|
||||||
const cmpTurretHolderOfOldEnt = Engine.QueryInterface(cmpOldTurretable.HolderID(), IID_TurretHolder);
|
const cmpTurretHolderOfOldEnt = Engine.QueryInterface(cmpOldTurretable.HolderID(), IID_TurretHolder);
|
||||||
|
|
||||||
if (cmpTurretHolderOfNewEnt &&
|
if (cmpTurretHolderOfOldEnt)
|
||||||
!cmpTurretHolderOfOldEnt.AllowedToOccupyTurretPoint(newEnt, cmpOldTurretable.GetTurretPointName(), true))
|
{
|
||||||
cmpOldTurretable.LeaveTurret(true);
|
// 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())
|
for (const entity of cmpTurretHolder.GetEntities())
|
||||||
cmpNewTurretHolder.SetReservedTurretPoint(cmpTurretHolder.GetOccupiedTurretPointName(entity));
|
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);
|
CopyControlGroups(oldEnt, newEnt);
|
||||||
|
|
||||||
// Rescale capture points
|
// Rescale capture points
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ echo "Building cpp-httplib..."
|
||||||
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
||||||
echo "Skipping - already built (use --force-rebuild to override)"
|
echo "Skipping - already built (use --force-rebuild to override)"
|
||||||
exit
|
exit
|
||||||
|
else
|
||||||
|
rm -f .already-built
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# fetch
|
# fetch
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ done
|
||||||
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
||||||
echo "Skipping - already built (use --force-rebuild to override)"
|
echo "Skipping - already built (use --force-rebuild to override)"
|
||||||
exit
|
exit
|
||||||
|
else
|
||||||
|
rm -f .already-built
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# fetch
|
# fetch
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ done
|
||||||
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
||||||
echo "Skipping - already built (use --force-rebuild to override)"
|
echo "Skipping - already built (use --force-rebuild to override)"
|
||||||
exit
|
exit
|
||||||
|
else
|
||||||
|
rm -f .already-built
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# fetch
|
# fetch
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ done
|
||||||
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
||||||
echo "Skipping - already built (use --force-rebuild to override)"
|
echo "Skipping - already built (use --force-rebuild to override)"
|
||||||
exit
|
exit
|
||||||
|
else
|
||||||
|
rm -f .already-built
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# fetch
|
# fetch
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ done
|
||||||
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
||||||
echo "Skipping - already built (use --force-rebuild to override)"
|
echo "Skipping - already built (use --force-rebuild to override)"
|
||||||
exit
|
exit
|
||||||
|
else
|
||||||
|
rm -f .already-built
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# fetch
|
# fetch
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ echo "Building SpiderMonkey..."
|
||||||
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
||||||
echo "Skipping - already built (use --force-rebuild to override)"
|
echo "Skipping - already built (use --force-rebuild to override)"
|
||||||
exit
|
exit
|
||||||
|
else
|
||||||
|
rm -f .already-built
|
||||||
fi
|
fi
|
||||||
|
|
||||||
OS="${OS:=$(uname -s)}"
|
OS="${OS:=$(uname -s)}"
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ done
|
||||||
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
||||||
echo "Skipping - already built (use --force-rebuild to override)"
|
echo "Skipping - already built (use --force-rebuild to override)"
|
||||||
exit
|
exit
|
||||||
|
else
|
||||||
|
rm -f .already-built
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# fetch
|
# fetch
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ ignore = [
|
||||||
"ANN",
|
"ANN",
|
||||||
"C90",
|
"C90",
|
||||||
"COM812",
|
"COM812",
|
||||||
|
"CPY001",
|
||||||
"D10",
|
"D10",
|
||||||
"EM",
|
"EM",
|
||||||
"FIX002",
|
"FIX002",
|
||||||
|
|
|
||||||
|
|
@ -53,25 +53,6 @@ public:
|
||||||
void SetElevation(float f);
|
void SetElevation(float f);
|
||||||
void SetRotation(float f);
|
void SetRotation(float f);
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate brightness of a point of a unit with the given normal vector,
|
|
||||||
* for rendering with CPU lighting.
|
|
||||||
* The resulting color contains both ambient and diffuse light.
|
|
||||||
* To cope with sun overbrightness, the color is scaled by 0.5.
|
|
||||||
*
|
|
||||||
* @param normal normal vector (must have length 1)
|
|
||||||
*/
|
|
||||||
RGBColor EvaluateUnitScaled(const CVector3D& normal) const
|
|
||||||
{
|
|
||||||
float dot = -normal.Dot(m_SunDir);
|
|
||||||
|
|
||||||
RGBColor color = m_AmbientColor;
|
|
||||||
if (dot > 0)
|
|
||||||
color += m_SunColor * dot;
|
|
||||||
|
|
||||||
return color * 0.5f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comparison operators
|
// Comparison operators
|
||||||
bool operator==(const CLightEnv& o) const
|
bool operator==(const CLightEnv& o) const
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright (C) 2025 Wildfire Games.
|
/* Copyright (C) 2026 Wildfire Games.
|
||||||
* This file is part of 0 A.D.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 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;
|
m_Properties.m_FloatOnWater = false;
|
||||||
|
|
||||||
// Remove leading art/actors/ & include quality level.
|
// 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
|
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.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 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;
|
it = includeCache.emplace(path, std::move(includeContent)).first;
|
||||||
}
|
}
|
||||||
// We need to insert #line directives to have correct line numbers in errors.
|
// 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));
|
processedParts.emplace_back(currentPart.substr(0, lineStart));
|
||||||
if (!ResolveIncludesImpl(chunks.back(), includeCache, includeCallback, chunks, processedParts))
|
if (!ResolveIncludesImpl(chunks.back(), includeCache, includeCallback, chunks, processedParts))
|
||||||
return false;
|
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.
|
// Check if name isn't set, generate an internal name in that case.
|
||||||
if (!NameSet)
|
if (!NameSet)
|
||||||
{
|
{
|
||||||
object->SetName("__internal(" + CStr::FromInt(m_InternalNameNumber) + ")");
|
object->SetName(fmt::format("__internal({})", m_InternalNameNumber));
|
||||||
++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)
|
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)
|
XERO_ITER_EL(element, child)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ CStr CNetMessage::ToString() const
|
||||||
if (GetType() == NMT_INVALID)
|
if (GetType() == NMT_INVALID)
|
||||||
return "MESSAGE_TYPE_NONE { Undefined Message }";
|
return "MESSAGE_TYPE_NONE { Undefined Message }";
|
||||||
else
|
else
|
||||||
return "Unknown Message " + CStr::FromInt(GetType());
|
return fmt::format("Unknown Message {}", static_cast<int>(GetType()));
|
||||||
}
|
}
|
||||||
|
|
||||||
CNetMessage* CNetMessageFactory::CreateMessage(const void* pData,
|
CNetMessage* CNetMessageFactory::CreateMessage(const void* pData,
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <fmt/format.h>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
@ -70,6 +71,10 @@
|
||||||
#include <miniupnpc/upnperrors.h>
|
#include <miniupnpc/upnperrors.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if FMT_VERSION >= 80000
|
||||||
|
#include <fmt/xchar.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of peers to allocate for the enet host.
|
* Number of peers to allocate for the enet host.
|
||||||
* Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
|
* Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
|
||||||
|
|
@ -1641,7 +1646,7 @@ CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
|
||||||
if (unique)
|
if (unique)
|
||||||
return name;
|
return name;
|
||||||
|
|
||||||
name = original + L" (" + CStrW::FromUInt(id++) + L")";
|
name = fmt::format(L"{}({})", original, id++);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
#include "NetStats.h"
|
#include "NetStats.h"
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
enum
|
enum
|
||||||
|
|
@ -77,7 +78,7 @@ const std::vector<ProfileColumn>& CNetStatsTable::GetColumns()
|
||||||
std::lock_guard<std::mutex> lock(m_Mutex);
|
std::lock_guard<std::mutex> lock(m_Mutex);
|
||||||
|
|
||||||
for (size_t i = 0; i < m_LatchedData.size(); ++i)
|
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;
|
return m_ColumnDescriptions;
|
||||||
|
|
@ -95,7 +96,7 @@ CStr CNetStatsTable::GetCellText(size_t row, size_t col)
|
||||||
#define ROW(id, title, member) \
|
#define ROW(id, title, member) \
|
||||||
case id: \
|
case id: \
|
||||||
if (col == 0) return title; \
|
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 "???"
|
return "???"
|
||||||
|
|
||||||
switch(row)
|
switch(row)
|
||||||
|
|
@ -129,7 +130,7 @@ void CNetStatsTable::LatchHostState(const ENetHost& host)
|
||||||
std::lock_guard<std::mutex> lock(m_Mutex);
|
std::lock_guard<std::mutex> lock(m_Mutex);
|
||||||
|
|
||||||
#define ROW(id, title, member) \
|
#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.clear();
|
||||||
m_LatchedData.resize(host.peerCount);
|
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.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
static inline CStr NetMessageStringConvert(u32 arg)
|
static inline CStr NetMessageStringConvert(u32 arg)
|
||||||
{
|
{
|
||||||
return CStr::FromUInt(arg);
|
return std::to_string(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline CStr NetMessageStringConvert(const CStr8& 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.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 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:
|
// 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)
|
CStr CStr::FromDouble(double n)
|
||||||
{
|
{
|
||||||
tstringstream<StrBase> ss;
|
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.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -95,9 +95,6 @@ public:
|
||||||
|
|
||||||
// Conversions:
|
// Conversions:
|
||||||
|
|
||||||
static CStr FromInt(int n);
|
|
||||||
static CStr FromUInt(unsigned int n);
|
|
||||||
static CStr FromInt64(i64 n);
|
|
||||||
static CStr FromDouble(double n);
|
static CStr FromDouble(double n);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,6 @@ CGame::CGame(bool replayLog, const SimulationDebugOptions debugOptions):
|
||||||
m_SimRate(1.0f),
|
m_SimRate(1.0f),
|
||||||
m_PlayerID(-1),
|
m_PlayerID(-1),
|
||||||
m_ViewedPlayerID(-1),
|
m_ViewedPlayerID(-1),
|
||||||
m_IsSavedGame(false),
|
|
||||||
m_IsVisualReplay(false),
|
m_IsVisualReplay(false),
|
||||||
m_ReplayStream(NULL)
|
m_ReplayStream(NULL)
|
||||||
{
|
{
|
||||||
|
|
@ -224,7 +223,7 @@ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& saved
|
||||||
const Script::Interface& scriptInterface = m_Simulation2->GetScriptInterface();
|
const Script::Interface& scriptInterface = m_Simulation2->GetScriptInterface();
|
||||||
Script::Request rq(scriptInterface);
|
Script::Request rq(scriptInterface);
|
||||||
|
|
||||||
m_IsSavedGame = !savedState.empty();
|
const bool isSavedGame{!savedState.empty()};
|
||||||
|
|
||||||
m_Simulation2->SetInitAttributes(attribs);
|
m_Simulation2->SetInitAttributes(attribs);
|
||||||
|
|
||||||
|
|
@ -285,7 +284,7 @@ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& saved
|
||||||
co_return g_Renderer.GetSceneRenderer().GetWaterManager().LoadWaterTextures();
|
co_return g_Renderer.GetSceneRenderer().GetWaterManager().LoadWaterTextures();
|
||||||
}, L"LoadWaterTextures", 80);
|
}, L"LoadWaterTextures", 80);
|
||||||
|
|
||||||
if (m_IsSavedGame)
|
if (isSavedGame)
|
||||||
PS::Loader::Register(std::bind_front(
|
PS::Loader::Register(std::bind_front(
|
||||||
[](CGame* game, const std::string& state) -> PS::Loader::Task
|
[](CGame* game, const std::string& state) -> PS::Loader::Task
|
||||||
{
|
{
|
||||||
|
|
@ -298,13 +297,32 @@ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& saved
|
||||||
co_return game->LoadVisualReplayData();
|
co_return game->LoadVisualReplayData();
|
||||||
}, this), L"Loading visual replay data", 1000);
|
}, this), L"Loading visual replay data", 1000);
|
||||||
|
|
||||||
|
// Call the script function InitGame only for new games, not saved games
|
||||||
|
if (!isSavedGame)
|
||||||
|
{
|
||||||
|
// Perform some simulation initializations (replace skirmish entities, explore territories, etc.)
|
||||||
|
// that needs to be done before setting up the AI and shouldn't be done in Atlas
|
||||||
|
if (!g_AtlasGameLoop->running)
|
||||||
|
{
|
||||||
|
PS::Loader::Register(std::bind_front([](CGame* game) -> PS::Loader::Task
|
||||||
|
{
|
||||||
|
game->m_Simulation2->PreInitGame();
|
||||||
|
co_return 0;
|
||||||
|
}, this), L"PreInitGame", 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
PS::Loader::Register(std::bind_front([](CGame* game) -> PS::Loader::Task
|
||||||
|
{
|
||||||
|
game->m_Simulation2->InitGame();
|
||||||
|
co_return 0;
|
||||||
|
}, this), L"InitGame", 4000);
|
||||||
|
}
|
||||||
|
|
||||||
PS::Loader::EndRegistering();
|
PS::Loader::EndRegistering();
|
||||||
}
|
}
|
||||||
|
|
||||||
int CGame::LoadInitialState(const std::string& savedState)
|
int CGame::LoadInitialState(const std::string& savedState)
|
||||||
{
|
{
|
||||||
ENSURE(m_IsSavedGame);
|
|
||||||
|
|
||||||
std::stringstream stream(savedState);
|
std::stringstream stream(savedState);
|
||||||
|
|
||||||
bool ok = m_Simulation2->DeserializeState(stream);
|
bool ok = m_Simulation2->DeserializeState(stream);
|
||||||
|
|
@ -324,17 +342,6 @@ int CGame::LoadInitialState(const std::string& savedState)
|
||||||
**/
|
**/
|
||||||
PSRETURN CGame::ReallyStartGame()
|
PSRETURN CGame::ReallyStartGame()
|
||||||
{
|
{
|
||||||
// Call the script function InitGame only for new games, not saved games
|
|
||||||
if (!m_IsSavedGame)
|
|
||||||
{
|
|
||||||
// Perform some simulation initializations (replace skirmish entities, explore territories, etc.)
|
|
||||||
// that needs to be done before setting up the AI and shouldn't be done in Atlas
|
|
||||||
if (!g_AtlasGameLoop->running)
|
|
||||||
m_Simulation2->PreInitGame();
|
|
||||||
|
|
||||||
m_Simulation2->InitGame();
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to do an initial Interpolate call to set up all the models etc,
|
// We need to do an initial Interpolate call to set up all the models etc,
|
||||||
// because Update might never interpolate (e.g. if the game starts paused)
|
// because Update might never interpolate (e.g. if the game starts paused)
|
||||||
// and we could end up rendering before having set up any models (so they'd
|
// and we could end up rendering before having set up any models (so they'd
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright (C) 2025 Wildfire Games.
|
/* Copyright (C) 2026 Wildfire Games.
|
||||||
* This file is part of 0 A.D.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -226,7 +226,6 @@ private:
|
||||||
std::vector<CColor> m_PlayerColors;
|
std::vector<CColor> m_PlayerColors;
|
||||||
|
|
||||||
int LoadInitialState(const std::string& savedState);
|
int LoadInitialState(const std::string& savedState);
|
||||||
bool m_IsSavedGame; // true if loading a saved game; false for a new game
|
|
||||||
|
|
||||||
bool m_CheatsEnabled;
|
bool m_CheatsEnabled;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright (C) 2025 Wildfire Games.
|
/* Copyright (C) 2026 Wildfire Games.
|
||||||
* This file is part of 0 A.D.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
#include <SDL_keycode.h>
|
#include <SDL_keycode.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <fmt/format.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -122,7 +123,7 @@ CStr FindScancodeName(SDL_Scancode scancode)
|
||||||
const char* name = SDL_GetScancodeName(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
|
// 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)
|
if (strlen(name) == 0)
|
||||||
return CStr("SYM_") + CStr::FromInt(scancode);
|
return fmt::format("SYM_{}", static_cast<int>(scancode));
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,7 +222,7 @@ CStr FindKeyName(SDL_Scancode scancode)
|
||||||
return name;
|
return name;
|
||||||
|
|
||||||
// Else, show something regardless, so the player knows it's at least recognized.
|
// 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::string basic = attrib;
|
||||||
std::map<std::string, double>::iterator time_attrib = time_per_attribute.find(attrib);
|
std::map<std::string, double>::iterator time_attrib = time_per_attribute.find(attrib);
|
||||||
if (time_attrib != time_per_attribute.end())
|
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());
|
u32 length = static_cast<u32>(basic.size());
|
||||||
memcpy(buffer + writePos, &length, sizeof(length));
|
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.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -44,6 +44,7 @@
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <fmt/format.h>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
@ -337,7 +338,7 @@ private:
|
||||||
{
|
{
|
||||||
long code = -1;
|
long code = -1;
|
||||||
curl_easy_getinfo(m_Curl, CURLINFO_RESPONSE_CODE, &code);
|
curl_easy_getinfo(m_Curl, CURLINFO_RESPONSE_CODE, &code);
|
||||||
SetStatus("completed:" + CStr::FromInt(code));
|
SetStatus(fmt::format("completed:{}", code));
|
||||||
|
|
||||||
// Check for success code
|
// Check for success code
|
||||||
if (code == 200)
|
if (code == 200)
|
||||||
|
|
@ -360,7 +361,7 @@ private:
|
||||||
if (errorString.empty())
|
if (errorString.empty())
|
||||||
errorString = curl_easy_strerror(err);
|
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;
|
// We got an unhandled return code or a connection failure;
|
||||||
|
|
@ -385,12 +386,12 @@ private:
|
||||||
r += "user_id=";
|
r += "user_id=";
|
||||||
AppendEscaped(r, m_UserID);
|
AppendEscaped(r, m_UserID);
|
||||||
|
|
||||||
r += "&time=" + CStr::FromInt64(report.m_Time);
|
r = fmt::format("{}&time={}", std::move(r), report.m_Time);
|
||||||
|
|
||||||
r += "&type=";
|
r += "&type=";
|
||||||
AppendEscaped(r, report.m_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=";
|
r += "&data=";
|
||||||
AppendEscaped(r, report.m_Data);
|
AppendEscaped(r, report.m_Data);
|
||||||
|
|
@ -529,7 +530,7 @@ bool CUserReporter::IsReportingEnabled()
|
||||||
|
|
||||||
void CUserReporter::SetReportingEnabled(bool enabled)
|
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.SetValueString(CFG_USER, "userreport.enabledversion", val);
|
||||||
g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.enabledversion", val);
|
g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.enabledversion", val);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright (C) 2025 Wildfire Games.
|
/* Copyright (C) 2026 Wildfire Games.
|
||||||
* This file is part of 0 A.D.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -33,7 +33,7 @@ namespace Renderer::Backend { class IDeviceCommandContext; }
|
||||||
* This computes and binds per-vertex data; the modifier is responsible
|
* This computes and binds per-vertex data; the modifier is responsible
|
||||||
* for setting any shader uniforms etc.
|
* for setting any shader uniforms etc.
|
||||||
*/
|
*/
|
||||||
class CPUSkinnedModelVertexRenderer : public ModelVertexRenderer
|
class CPUSkinnedModelVertexRenderer final : public ModelVertexRenderer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
CPUSkinnedModelVertexRenderer();
|
CPUSkinnedModelVertexRenderer();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright (C) 2025 Wildfire Games.
|
/* Copyright (C) 2026 Wildfire Games.
|
||||||
* This file is part of 0 A.D.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -34,7 +34,7 @@ namespace Renderer::Backend { class IShaderProgram; }
|
||||||
* This computes and binds per-vertex data; the modifier is responsible
|
* This computes and binds per-vertex data; the modifier is responsible
|
||||||
* for setting any shader uniforms etc.
|
* for setting any shader uniforms etc.
|
||||||
*/
|
*/
|
||||||
class GPUSkinnedModelModelRenderer : public ModelVertexRenderer
|
class GPUSkinnedModelModelRenderer final : public ModelVertexRenderer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
GPUSkinnedModelModelRenderer();
|
GPUSkinnedModelModelRenderer();
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ struct InstancingModelRendererInternals;
|
||||||
* This computes and binds per-vertex data; the modifier is responsible
|
* This computes and binds per-vertex data; the modifier is responsible
|
||||||
* for setting any shader uniforms etc (including the instancing transform).
|
* for setting any shader uniforms etc (including the instancing transform).
|
||||||
*/
|
*/
|
||||||
class InstancingModelRenderer : public ModelVertexRenderer
|
class InstancingModelRenderer final : public ModelVertexRenderer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
InstancingModelRenderer();
|
InstancingModelRenderer();
|
||||||
|
|
|
||||||
|
|
@ -102,15 +102,11 @@ CMaterial::Pass GetMaterialPassFromCullGroup(const int cullGroup, const ERenderM
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModelRenderer::Init()
|
// static
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to copy object-space position and normal vectors into arrays.
|
|
||||||
void ModelRenderer::CopyPositionAndNormals(
|
void ModelRenderer::CopyPositionAndNormals(
|
||||||
const CModelDefPtr& mdef,
|
const CModelDefPtr& mdef,
|
||||||
const VertexArrayIterator<CVector3D>& Position,
|
const VertexArrayIterator<CVector3D>& Position,
|
||||||
const VertexArrayIterator<CVector3D>& Normal)
|
const VertexArrayIterator<CVector3D>& Normal)
|
||||||
{
|
{
|
||||||
size_t numVertices = mdef->GetNumVertices();
|
size_t numVertices = mdef->GetNumVertices();
|
||||||
SModelVertex* vertices = mdef->GetVertices();
|
SModelVertex* vertices = mdef->GetVertices();
|
||||||
|
|
@ -122,11 +118,11 @@ void ModelRenderer::CopyPositionAndNormals(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to transform position and normal vectors into world-space.
|
// static
|
||||||
void ModelRenderer::BuildPositionAndNormals(
|
void ModelRenderer::BuildPositionAndNormals(
|
||||||
CModel* model,
|
CModel* model,
|
||||||
const VertexArrayIterator<CVector3D>& Position,
|
const VertexArrayIterator<CVector3D>& Position,
|
||||||
const VertexArrayIterator<CVector3D>& Normal)
|
const VertexArrayIterator<CVector3D>& Normal)
|
||||||
{
|
{
|
||||||
CModelDefPtr mdef = model->GetModelDef();
|
CModelDefPtr mdef = model->GetModelDef();
|
||||||
size_t numVertices = mdef->GetNumVertices();
|
size_t numVertices = mdef->GetNumVertices();
|
||||||
|
|
@ -160,43 +156,16 @@ void ModelRenderer::BuildPositionAndNormals(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
// Helper function for lighting
|
|
||||||
void ModelRenderer::BuildColor4ub(
|
|
||||||
CModel* model,
|
|
||||||
const VertexArrayIterator<CVector3D>& Normal,
|
|
||||||
const VertexArrayIterator<SColor4ub>& Color)
|
|
||||||
{
|
|
||||||
PROFILE("lighting vertices");
|
|
||||||
|
|
||||||
CModelDefPtr mdef = model->GetModelDef();
|
|
||||||
size_t numVertices = mdef->GetNumVertices();
|
|
||||||
const CLightEnv& lightEnv = g_Renderer.GetSceneRenderer().GetLightEnv();
|
|
||||||
CColor shadingColor = model->GetShadingColor();
|
|
||||||
|
|
||||||
for (size_t j = 0; j < numVertices; ++j)
|
|
||||||
{
|
|
||||||
RGBColor tempcolor = lightEnv.EvaluateUnitScaled(Normal[j]);
|
|
||||||
tempcolor.X *= shadingColor.r;
|
|
||||||
tempcolor.Y *= shadingColor.g;
|
|
||||||
tempcolor.Z *= shadingColor.b;
|
|
||||||
Color[j] = ConvertRGBColorTo4ub(tempcolor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void ModelRenderer::GenTangents(const CModelDefPtr& mdef, std::vector<float>& newVertices, bool gpuSkinning)
|
void ModelRenderer::GenTangents(const CModelDefPtr& mdef, std::vector<float>& newVertices, bool gpuSkinning)
|
||||||
{
|
{
|
||||||
MikkTSpace ms(mdef, newVertices, gpuSkinning);
|
MikkTSpace ms(mdef, newVertices, gpuSkinning);
|
||||||
ms.Generate();
|
ms.Generate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
// Copy UV coordinates
|
|
||||||
void ModelRenderer::BuildUV(
|
void ModelRenderer::BuildUV(
|
||||||
const CModelDefPtr& mdef,
|
const CModelDefPtr& mdef, const VertexArrayIterator<float[2]>& UV, int UVset)
|
||||||
const VertexArrayIterator<float[2]>& UV,
|
|
||||||
int UVset)
|
|
||||||
{
|
{
|
||||||
const size_t numVertices = mdef->GetNumVertices();
|
const size_t numVertices = mdef->GetNumVertices();
|
||||||
const size_t numberOfUVPerVertex = mdef->GetNumUVsPerVertex();
|
const size_t numberOfUVPerVertex = mdef->GetNumUVsPerVertex();
|
||||||
|
|
@ -209,11 +178,9 @@ void ModelRenderer::BuildUV(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
// Build default indices array.
|
|
||||||
void ModelRenderer::BuildIndices(
|
void ModelRenderer::BuildIndices(
|
||||||
const CModelDefPtr& mdef,
|
const CModelDefPtr& mdef, const VertexArrayIterator<u16>& Indices)
|
||||||
const VertexArrayIterator<u16>& Indices)
|
|
||||||
{
|
{
|
||||||
size_t idxidx = 0;
|
size_t idxidx = 0;
|
||||||
SModelFace* faces = mdef->GetFaces();
|
SModelFace* faces = mdef->GetFaces();
|
||||||
|
|
@ -227,105 +194,6 @@ void ModelRenderer::BuildIndices(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// ShaderModelRenderer implementation
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal data of the ShaderModelRenderer.
|
|
||||||
*
|
|
||||||
* Separated into the source file to increase implementation hiding (and to
|
|
||||||
* avoid some causes of recompiles).
|
|
||||||
*/
|
|
||||||
struct ShaderModelRenderer::ShaderModelRendererInternals
|
|
||||||
{
|
|
||||||
ShaderModelRendererInternals(ShaderModelRenderer* r) : m_Renderer(r) { }
|
|
||||||
|
|
||||||
/// Back-link to "our" renderer
|
|
||||||
ShaderModelRenderer* m_Renderer;
|
|
||||||
|
|
||||||
/// ModelVertexRenderer used for vertex transformations
|
|
||||||
ModelVertexRendererPtr vertexRenderer;
|
|
||||||
|
|
||||||
/// List of submitted models for rendering in this frame
|
|
||||||
std::vector<CModel*> submissions[CSceneRenderer::CULL_MAX];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Construction/Destruction
|
|
||||||
ShaderModelRenderer::ShaderModelRenderer(ModelVertexRendererPtr vertexrenderer)
|
|
||||||
{
|
|
||||||
m = new ShaderModelRendererInternals(this);
|
|
||||||
m->vertexRenderer = vertexrenderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderModelRenderer::~ShaderModelRenderer()
|
|
||||||
{
|
|
||||||
delete m;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit one model.
|
|
||||||
void ShaderModelRenderer::Submit(int cullGroup, CModel* model)
|
|
||||||
{
|
|
||||||
CModelRData* rdata = (CModelRData*)model->GetRenderData();
|
|
||||||
|
|
||||||
// Ensure model data is valid
|
|
||||||
const void* key = m->vertexRenderer.get();
|
|
||||||
if (!rdata || rdata->GetKey() != key)
|
|
||||||
{
|
|
||||||
model->InvalidatePosition();
|
|
||||||
rdata = m->vertexRenderer->CreateModelData(key, model);
|
|
||||||
model->SetRenderData(rdata);
|
|
||||||
model->SetDirty(~0u);
|
|
||||||
}
|
|
||||||
|
|
||||||
m->submissions[cullGroup].push_back(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Call update for all submitted models and enter the rendering phase
|
|
||||||
void ShaderModelRenderer::PrepareModels(
|
|
||||||
Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
|
|
||||||
{
|
|
||||||
for (int cullGroup = 0; cullGroup < CSceneRenderer::CULL_MAX; ++cullGroup)
|
|
||||||
{
|
|
||||||
for (CModel* model : m->submissions[cullGroup])
|
|
||||||
{
|
|
||||||
model->ValidatePosition();
|
|
||||||
|
|
||||||
CModelRData* rdata = static_cast<CModelRData*>(model->GetRenderData());
|
|
||||||
ENSURE(rdata->GetKey() == m->vertexRenderer.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
m->vertexRenderer->UpdateModelsData(deviceCommandContext, m->submissions[cullGroup]);
|
|
||||||
|
|
||||||
for (CModel* model : m->submissions[cullGroup])
|
|
||||||
{
|
|
||||||
CModelRData* rdata = static_cast<CModelRData*>(model->GetRenderData());
|
|
||||||
rdata->m_UpdateFlags = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderModelRenderer::UploadModels(
|
|
||||||
Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
|
|
||||||
{
|
|
||||||
for (int cullGroup = 0; cullGroup < CSceneRenderer::CULL_MAX; ++cullGroup)
|
|
||||||
{
|
|
||||||
m->vertexRenderer->UploadModelsData(deviceCommandContext, m->submissions[cullGroup]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the submissions list
|
|
||||||
void ShaderModelRenderer::EndFrame()
|
|
||||||
{
|
|
||||||
for (int cullGroup = 0; cullGroup < CSceneRenderer::CULL_MAX; ++cullGroup)
|
|
||||||
m->submissions[cullGroup].clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Helper structs for ShaderModelRenderer::Render():
|
// Helper structs for ShaderModelRenderer::Render():
|
||||||
|
|
||||||
struct SMRSortByDistItem
|
struct SMRSortByDistItem
|
||||||
|
|
@ -413,12 +281,12 @@ struct SMRCompareTechBucket
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void ShaderModelRenderer::Render(
|
void ModelRenderer::Render(
|
||||||
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
|
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
|
||||||
const RenderModifierPtr& modifier, const CShaderDefines& context,
|
ModelVertexRenderer& modelVertexRenderer, RenderModifier& modifier, const CShaderDefines& context,
|
||||||
int cullGroup, int flags, const ERenderMode renderMode)
|
int cullGroup, int flags, const ERenderMode renderMode, std::span<CModel*> submissions)
|
||||||
{
|
{
|
||||||
if (m->submissions[cullGroup].empty())
|
if (submissions.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
CMatrix3D worldToCam;
|
CMatrix3D worldToCam;
|
||||||
|
|
@ -429,7 +297,7 @@ void ShaderModelRenderer::Render(
|
||||||
/*
|
/*
|
||||||
* Rendering approach:
|
* Rendering approach:
|
||||||
*
|
*
|
||||||
* m->submissions contains the list of CModels to render.
|
* submissions contains the list of CModels to render.
|
||||||
*
|
*
|
||||||
* The data we need to render a model is:
|
* The data we need to render a model is:
|
||||||
* - CShaderTechnique
|
* - CShaderTechnique
|
||||||
|
|
@ -497,10 +365,9 @@ void ShaderModelRenderer::Render(
|
||||||
{
|
{
|
||||||
PROFILE3("bucketing by material");
|
PROFILE3("bucketing by material");
|
||||||
|
|
||||||
for (size_t i = 0; i < m->submissions[cullGroup].size(); ++i)
|
for (CModel* model : submissions)
|
||||||
{
|
{
|
||||||
CModel* model = m->submissions[cullGroup][i];
|
const CMaterial& material{model->GetMaterial()};
|
||||||
const CMaterial material{model->GetMaterial()};
|
|
||||||
const CShaderDefines& defines{material.GetShaderDefines()};
|
const CShaderDefines& defines{material.GetShaderDefines()};
|
||||||
const CStrIntern shaderEffect{material.GetShaderEffect(materialPass)};
|
const CStrIntern shaderEffect{material.GetShaderEffect(materialPass)};
|
||||||
SMRMaterialBucketKey key(shaderEffect, defines);
|
SMRMaterialBucketKey key(shaderEffect, defines);
|
||||||
|
|
@ -683,7 +550,7 @@ void ShaderModelRenderer::Render(
|
||||||
|
|
||||||
Renderer::Backend::IShaderProgram* shader = currentTech->GetShader(pass);
|
Renderer::Backend::IShaderProgram* shader = currentTech->GetShader(pass);
|
||||||
|
|
||||||
modifier->BeginPass(deviceCommandContext, shader);
|
modifier.BeginPass(deviceCommandContext, shader);
|
||||||
|
|
||||||
// TODO: Use a more generic approach to handle bound queries.
|
// TODO: Use a more generic approach to handle bound queries.
|
||||||
bool boundTime = false;
|
bool boundTime = false;
|
||||||
|
|
@ -753,7 +620,7 @@ void ShaderModelRenderer::Render(
|
||||||
if (newModeldef != currentModeldef)
|
if (newModeldef != currentModeldef)
|
||||||
{
|
{
|
||||||
currentModeldef = newModeldef;
|
currentModeldef = newModeldef;
|
||||||
m->vertexRenderer->PrepareModelDef(deviceCommandContext, *currentModeldef);
|
modelVertexRenderer.PrepareModelDef(deviceCommandContext, *currentModeldef);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind all uniforms when any change
|
// Bind all uniforms when any change
|
||||||
|
|
@ -813,12 +680,12 @@ void ShaderModelRenderer::Render(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
modifier->PrepareModel(deviceCommandContext, model);
|
modifier.PrepareModel(deviceCommandContext, model);
|
||||||
|
|
||||||
CModelRData* rdata = static_cast<CModelRData*>(model->GetRenderData());
|
CModelRData* rdata = static_cast<CModelRData*>(model->GetRenderData());
|
||||||
ENSURE(rdata->GetKey() == m->vertexRenderer.get());
|
ENSURE(rdata->GetKey() == &modelVertexRenderer);
|
||||||
|
|
||||||
m->vertexRenderer->RenderModel(deviceCommandContext, shader, model, rdata);
|
modelVertexRenderer.RenderModel(deviceCommandContext, shader, model, rdata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
#include "lib/types.h"
|
#include "lib/types.h"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <span>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
class CModel;
|
class CModel;
|
||||||
|
|
@ -39,17 +40,8 @@ namespace Renderer::Backend { class IDeviceCommandContext; }
|
||||||
struct SColor4ub;
|
struct SColor4ub;
|
||||||
template <typename T> class VertexArrayIterator;
|
template <typename T> class VertexArrayIterator;
|
||||||
|
|
||||||
class RenderModifier;
|
|
||||||
typedef std::shared_ptr<RenderModifier> RenderModifierPtr;
|
|
||||||
|
|
||||||
class LitRenderModifier;
|
|
||||||
typedef std::shared_ptr<LitRenderModifier> LitRenderModifierPtr;
|
|
||||||
|
|
||||||
class ModelVertexRenderer;
|
class ModelVertexRenderer;
|
||||||
typedef std::shared_ptr<ModelVertexRenderer> ModelVertexRendererPtr;
|
class RenderModifier;
|
||||||
|
|
||||||
class ModelRenderer;
|
|
||||||
typedef std::shared_ptr<ModelRenderer> ModelRendererPtr;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class CModelRData: Render data that is maintained per CModel.
|
* Class CModelRData: Render data that is maintained per CModel.
|
||||||
|
|
@ -83,25 +75,12 @@ private:
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class ModelRenderer: Abstract base class for all model renders.
|
* ModelRenderer renders a per-frame list of models. It loads the appropriate
|
||||||
|
* shaders for rendering each model, and that batches by shader technique (and
|
||||||
|
* by mesh and texture).
|
||||||
*
|
*
|
||||||
* A ModelRenderer manages a per-frame list of models.
|
* ModelRenderer delegates vertex transformation/setup to a
|
||||||
*
|
* ModelVertexRenderer. It delegates fragment stage setup to a RenderModifier.
|
||||||
* It is supposed to be derived in order to create new ways in which
|
|
||||||
* the per-frame list of models can be managed (for batching, for
|
|
||||||
* transparent rendering, etc.) or potentially for rarely used special
|
|
||||||
* effects.
|
|
||||||
*
|
|
||||||
* A typical ModelRenderer will delegate vertex transformation/setup
|
|
||||||
* to a ModelVertexRenderer.
|
|
||||||
* It will delegate fragment stage setup to a RenderModifier.
|
|
||||||
*
|
|
||||||
* For most purposes, you should use a BatchModelRenderer with
|
|
||||||
* specialized ModelVertexRenderer and RenderModifier implementations.
|
|
||||||
*
|
|
||||||
* It is suggested that a derived class implement the provided generic
|
|
||||||
* Render function, however in some cases it may be necessary to supply
|
|
||||||
* a Render function with a different prototype.
|
|
||||||
*
|
*
|
||||||
* ModelRenderer also contains a number of static helper functions
|
* ModelRenderer also contains a number of static helper functions
|
||||||
* for building vertex arrays.
|
* for building vertex arrays.
|
||||||
|
|
@ -109,61 +88,10 @@ private:
|
||||||
class ModelRenderer
|
class ModelRenderer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ModelRenderer() { }
|
|
||||||
virtual ~ModelRenderer() { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialise global settings.
|
|
||||||
* Should be called before using the class.
|
|
||||||
*/
|
|
||||||
static void Init();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit: Submit a model for rendering this frame.
|
|
||||||
*
|
|
||||||
* preconditions : The model must not have been submitted to any
|
|
||||||
* ModelRenderer in this frame. Submit may only be called
|
|
||||||
* after EndFrame and before PrepareModels.
|
|
||||||
*
|
|
||||||
* @param model The model that will be added to the list of models
|
|
||||||
* submitted this frame.
|
|
||||||
*/
|
|
||||||
virtual void Submit(int cullGroup, CModel* model) = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PrepareModels: Calculate renderer data for all previously
|
|
||||||
* submitted models.
|
|
||||||
*
|
|
||||||
* Must be called before any rendering calls and after all models
|
|
||||||
* for this frame have been submitted.
|
|
||||||
*/
|
|
||||||
virtual void PrepareModels(
|
|
||||||
Renderer::Backend::IDeviceCommandContext* deviceCommandContext) = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload renderer data for all previously submitted models to backend.
|
|
||||||
*
|
|
||||||
* Must be called before any rendering calls and after all models
|
|
||||||
* for this frame have been prepared.
|
|
||||||
*/
|
|
||||||
virtual void UploadModels(
|
|
||||||
Renderer::Backend::IDeviceCommandContext* deviceCommandContext) = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EndFrame: Remove all models from the list of submitted
|
|
||||||
* models.
|
|
||||||
*/
|
|
||||||
virtual void EndFrame() = 0;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render: Render submitted models, using the given RenderModifier to setup
|
* Render: Render submitted models, using the given RenderModifier to setup
|
||||||
* the fragment stage.
|
* the fragment stage.
|
||||||
*
|
*
|
||||||
* @note It is suggested that derived model renderers implement and use
|
|
||||||
* this Render functions. However, a highly specialized model renderer
|
|
||||||
* may need to "disable" this function and provide its own Render function
|
|
||||||
* with a different prototype.
|
|
||||||
*
|
|
||||||
* preconditions : PrepareModels must be called after all models have been
|
* preconditions : PrepareModels must be called after all models have been
|
||||||
* submitted and before calling Render.
|
* submitted and before calling Render.
|
||||||
*
|
*
|
||||||
|
|
@ -172,10 +100,10 @@ public:
|
||||||
* If flags is non-zero, only models that contain flags in their
|
* If flags is non-zero, only models that contain flags in their
|
||||||
* CModel::GetFlags() are rendered.
|
* CModel::GetFlags() are rendered.
|
||||||
*/
|
*/
|
||||||
virtual void Render(
|
void Render(
|
||||||
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
|
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
|
||||||
const RenderModifierPtr& modifier, const CShaderDefines& context,
|
ModelVertexRenderer& modelVertexRenderer, RenderModifier& modifier, const CShaderDefines& context,
|
||||||
int cullGroup, int flags, const ERenderMode renderMode) = 0;
|
int cullGroup, int flags, const ERenderMode renderMode, std::span<CModel*> submissions);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CopyPositionAndNormals: Copy unanimated object-space vertices and
|
* CopyPositionAndNormals: Copy unanimated object-space vertices and
|
||||||
|
|
@ -190,9 +118,9 @@ public:
|
||||||
* The array behind the iterator must be as large as the Position array.
|
* The array behind the iterator must be as large as the Position array.
|
||||||
*/
|
*/
|
||||||
static void CopyPositionAndNormals(
|
static void CopyPositionAndNormals(
|
||||||
const CModelDefPtr& mdef,
|
const CModelDefPtr& mdef,
|
||||||
const VertexArrayIterator<CVector3D>& Position,
|
const VertexArrayIterator<CVector3D>& Position,
|
||||||
const VertexArrayIterator<CVector3D>& Normal);
|
const VertexArrayIterator<CVector3D>& Normal);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BuildPositionAndNormals: Build animated vertices and normals,
|
* BuildPositionAndNormals: Build animated vertices and normals,
|
||||||
|
|
@ -209,25 +137,9 @@ public:
|
||||||
* the Position array.
|
* the Position array.
|
||||||
*/
|
*/
|
||||||
static void BuildPositionAndNormals(
|
static void BuildPositionAndNormals(
|
||||||
CModel* model,
|
CModel* model,
|
||||||
const VertexArrayIterator<CVector3D>& Position,
|
const VertexArrayIterator<CVector3D>& Position,
|
||||||
const VertexArrayIterator<CVector3D>& Normal);
|
const VertexArrayIterator<CVector3D>& Normal);
|
||||||
|
|
||||||
/**
|
|
||||||
* BuildColor4ub: Build lighting colors for the given model,
|
|
||||||
* based on previously calculated world space normals.
|
|
||||||
*
|
|
||||||
* @param model The model that is to be lit.
|
|
||||||
* @param Normal Array of the model's normal vectors, animated and
|
|
||||||
* transformed into world space.
|
|
||||||
* @param Color Points to the array that will receive the lit vertex color.
|
|
||||||
* The array behind the iterator must large enough to hold
|
|
||||||
* model->GetModelDef()->GetNumVertices() vertices.
|
|
||||||
*/
|
|
||||||
static void BuildColor4ub(
|
|
||||||
CModel* model,
|
|
||||||
const VertexArrayIterator<CVector3D>& Normal,
|
|
||||||
const VertexArrayIterator<SColor4ub>& Color);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BuildUV: Copy UV coordinates into the given vertex array.
|
* BuildUV: Copy UV coordinates into the given vertex array.
|
||||||
|
|
@ -238,9 +150,9 @@ public:
|
||||||
* mdef->GetNumVertices() vertices.
|
* mdef->GetNumVertices() vertices.
|
||||||
*/
|
*/
|
||||||
static void BuildUV(
|
static void BuildUV(
|
||||||
const CModelDefPtr& mdef,
|
const CModelDefPtr& mdef,
|
||||||
const VertexArrayIterator<float[2]>& UV,
|
const VertexArrayIterator<float[2]>& UV,
|
||||||
int UVset);
|
int UVset);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BuildIndices: Create the indices array for the given CModelDef.
|
* BuildIndices: Create the indices array for the given CModelDef.
|
||||||
|
|
@ -250,8 +162,7 @@ public:
|
||||||
* mdef->GetNumFaces()*3 elements.
|
* mdef->GetNumFaces()*3 elements.
|
||||||
*/
|
*/
|
||||||
static void BuildIndices(
|
static void BuildIndices(
|
||||||
const CModelDefPtr& mdef,
|
const CModelDefPtr& mdef, const VertexArrayIterator<u16>& Indices);
|
||||||
const VertexArrayIterator<u16>& Indices);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GenTangents: Generate tangents for the given CModelDef.
|
* GenTangents: Generate tangents for the given CModelDef.
|
||||||
|
|
@ -263,33 +174,4 @@ public:
|
||||||
static void GenTangents(const CModelDefPtr& mdef, std::vector<float>& newVertices, bool gpuSkinning);
|
static void GenTangents(const CModelDefPtr& mdef, std::vector<float>& newVertices, bool gpuSkinning);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of ModelRenderer that loads the appropriate shaders for
|
|
||||||
* rendering each model, and that batches by shader technique (and by mesh and texture).
|
|
||||||
*/
|
|
||||||
class ShaderModelRenderer : public ModelRenderer
|
|
||||||
{
|
|
||||||
friend struct ShaderModelRendererInternals;
|
|
||||||
|
|
||||||
public:
|
|
||||||
ShaderModelRenderer(ModelVertexRendererPtr vertexrender);
|
|
||||||
~ShaderModelRenderer() override;
|
|
||||||
|
|
||||||
// Batching implementations
|
|
||||||
void Submit(int cullGroup, CModel* model) override;
|
|
||||||
void PrepareModels(
|
|
||||||
Renderer::Backend::IDeviceCommandContext* deviceCommandContext) override;
|
|
||||||
void UploadModels(
|
|
||||||
Renderer::Backend::IDeviceCommandContext* deviceCommandContext) override;
|
|
||||||
void EndFrame() override;
|
|
||||||
void Render(
|
|
||||||
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
|
|
||||||
const RenderModifierPtr& modifier, const CShaderDefines& context,
|
|
||||||
int cullGroup, int flags, const ERenderMode renderMode) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct ShaderModelRendererInternals;
|
|
||||||
ShaderModelRendererInternals* m;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // INCLUDED_MODELRENDERER
|
#endif // INCLUDED_MODELRENDERER
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@
|
||||||
#include "ps/VideoMode.h"
|
#include "ps/VideoMode.h"
|
||||||
#include "ps/World.h"
|
#include "ps/World.h"
|
||||||
#include "renderer/DebugRenderer.h"
|
#include "renderer/DebugRenderer.h"
|
||||||
#include "renderer/ModelRenderer.h"
|
|
||||||
#include "renderer/PostprocManager.h"
|
#include "renderer/PostprocManager.h"
|
||||||
#include "renderer/RenderingOptions.h"
|
#include "renderer/RenderingOptions.h"
|
||||||
#include "renderer/SceneRenderer.h"
|
#include "renderer/SceneRenderer.h"
|
||||||
|
|
@ -456,7 +455,6 @@ CRenderer::CRenderer(Renderer::Backend::IDevice* device)
|
||||||
|
|
||||||
ModelDefActivateFastImpl();
|
ModelDefActivateFastImpl();
|
||||||
ColorActivateFastImpl();
|
ColorActivateFastImpl();
|
||||||
ModelRenderer::Init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CRenderer::~CRenderer()
|
CRenderer::~CRenderer()
|
||||||
|
|
|
||||||
|
|
@ -212,17 +212,17 @@ void CRenderingOptions::ReadConfigAndSetupHooks()
|
||||||
m_ConfigHooks->Setup("silhouettes", m_Silhouettes);
|
m_ConfigHooks->Setup("silhouettes", m_Silhouettes);
|
||||||
|
|
||||||
m_ConfigHooks->Setup("gpuskinning", [this]() {
|
m_ConfigHooks->Setup("gpuskinning", [this]() {
|
||||||
const Renderer::Backend::IDevice::Capabilities& capabilities{
|
if (g_ConfigDB.Get("gpuskinning", false))
|
||||||
g_VideoMode.GetBackendDevice()->GetCapabilities()};
|
|
||||||
if (!g_ConfigDB.Get("gpuskinning", false))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (capabilities.computeShaders && capabilities.storage)
|
|
||||||
m_GPUSkinning = true;
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
m_GPUSkinning = false;
|
const Renderer::Backend::IDevice::Capabilities& capabilities{
|
||||||
LOGMESSAGE("GPU skinning isn't supported on the current hardware.");
|
g_VideoMode.GetBackendDevice()->GetCapabilities()};
|
||||||
|
if (capabilities.computeShaders && capabilities.storage)
|
||||||
|
m_GPUSkinning = true;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_GPUSkinning = false;
|
||||||
|
LOGMESSAGE("GPU skinning isn't supported on the current hardware.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CRenderer::IsInitialised())
|
if (CRenderer::IsInitialised())
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
struct SScreenRect
|
struct SScreenRect
|
||||||
{
|
{
|
||||||
|
|
@ -115,78 +116,192 @@ public:
|
||||||
|
|
||||||
SilhouetteRenderer silhouetteRenderer;
|
SilhouetteRenderer silhouetteRenderer;
|
||||||
|
|
||||||
/// Various model renderers
|
// Various model renderers
|
||||||
struct Models
|
struct Models
|
||||||
{
|
{
|
||||||
// NOTE: The current renderer design (with ModelRenderer, ModelVertexRenderer,
|
|
||||||
// RenderModifier, etc) is mostly a relic of an older design that implemented
|
|
||||||
// the different materials and rendering modes through extensive subclassing
|
|
||||||
// and hooking objects together in various combinations.
|
|
||||||
// The new design uses the CShaderManager API to abstract away the details
|
|
||||||
// of rendering, and uses a data-driven approach to materials, so there are
|
|
||||||
// now a small number of generic subclasses instead of many specialised subclasses,
|
|
||||||
// but most of the old infrastructure hasn't been refactored out yet and leads to
|
|
||||||
// some unwanted complexity.
|
|
||||||
|
|
||||||
// Submitted models are split on two axes:
|
// Submitted models are split on two axes:
|
||||||
// - Normal vs Transp[arent] - alpha-blended models are stored in a separate
|
// - Opaque vs Transparent - alpha-blended models are stored in a separate
|
||||||
// list so we can draw them above/below the alpha-blended water plane correctly
|
// list so we can draw them above/below the alpha-blended water plane correctly
|
||||||
// - Skinned vs Unskinned - with hardware lighting we don't need to
|
// - Skinned vs Unskinned - we don't need to duplicate mesh data per
|
||||||
// duplicate mesh data per model instance (except for skinned models),
|
// model instance (except for skinned models), so non-skinned models
|
||||||
// so non-skinned models get different ModelVertexRenderers
|
// get different ModelVertexRenderers
|
||||||
|
|
||||||
ModelRendererPtr NormalSkinned;
|
struct Submissions
|
||||||
ModelRendererPtr NormalUnskinned; // == NormalSkinned if unskinned shader instancing not supported
|
{
|
||||||
ModelRendererPtr TranspSkinned;
|
std::vector<CModel*> submissions[CSceneRenderer::CULL_MAX];
|
||||||
ModelRendererPtr TranspUnskinned; // == TranspSkinned if unskinned shader instancing not supported
|
};
|
||||||
|
|
||||||
ModelVertexRendererPtr VertexRendererShader;
|
// Unskinned submissions should be prepared and rendered with
|
||||||
ModelVertexRendererPtr VertexInstancingShader;
|
// VertexInstancingShader. Skinned - with Vertex*SkinningShader
|
||||||
ModelVertexRendererPtr VertexGPUSkinningShader;
|
// depending on whether GPU skinning is enabled.
|
||||||
|
Submissions OpaqueSkinned;
|
||||||
|
Submissions OpaqueUnskinned;
|
||||||
|
Submissions TransparentSkinned;
|
||||||
|
Submissions TransparentUnskinned;
|
||||||
|
|
||||||
LitRenderModifierPtr ModShader;
|
ModelRenderer modelRenderer;
|
||||||
|
|
||||||
|
InstancingModelRenderer VertexInstancingShader;
|
||||||
|
CPUSkinnedModelVertexRenderer VertexCPUSkinningShader;
|
||||||
|
// We can't create GPU skinning renderer for renderer devices without
|
||||||
|
// its support.
|
||||||
|
std::optional<GPUSkinnedModelModelRenderer> VertexGPUSkinningShader;
|
||||||
|
|
||||||
|
ShaderRenderModifier ModShader;
|
||||||
|
|
||||||
|
bool GPUSkinningEnabled{false};
|
||||||
} Model;
|
} Model;
|
||||||
|
|
||||||
CShaderDefines globalContext;
|
CShaderDefines globalContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload renderer data for all previously submitted models to backend.
|
||||||
|
*
|
||||||
|
* Must be called before any rendering calls and after all models
|
||||||
|
* for this frame have been prepared.
|
||||||
|
*/
|
||||||
|
void UploadModels(Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
|
||||||
|
{
|
||||||
|
PROFILE3("upload models");
|
||||||
|
|
||||||
|
ModelVertexRenderer& modelVertexSkinningRenderer{
|
||||||
|
Model.GPUSkinningEnabled
|
||||||
|
? static_cast<ModelVertexRenderer&>(*Model.VertexGPUSkinningShader)
|
||||||
|
: static_cast<ModelVertexRenderer&>(Model.VertexCPUSkinningShader)};
|
||||||
|
|
||||||
|
for (int cullGroup{0}; cullGroup < CSceneRenderer::CULL_MAX; ++cullGroup)
|
||||||
|
{
|
||||||
|
modelVertexSkinningRenderer.UploadModelsData(deviceCommandContext, Model.OpaqueSkinned.submissions[cullGroup]);
|
||||||
|
modelVertexSkinningRenderer.UploadModelsData(deviceCommandContext, Model.TransparentSkinned.submissions[cullGroup]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int cullGroup{0}; cullGroup < CSceneRenderer::CULL_MAX; ++cullGroup)
|
||||||
|
{
|
||||||
|
Model.VertexInstancingShader.UploadModelsData(deviceCommandContext, Model.OpaqueUnskinned.submissions[cullGroup]);
|
||||||
|
Model.VertexInstancingShader.UploadModelsData(deviceCommandContext, Model.TransparentUnskinned.submissions[cullGroup]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PrepareModels: Calculate renderer data for all previously
|
||||||
|
* submitted models.
|
||||||
|
*
|
||||||
|
* Must be called before any rendering calls and after all models
|
||||||
|
* for this frame have been submitted.
|
||||||
|
*/
|
||||||
|
void PrepareModels(
|
||||||
|
Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
|
||||||
|
{
|
||||||
|
PROFILE3("prepare models");
|
||||||
|
|
||||||
|
ModelVertexRenderer& modelVertexSkinningRenderer{
|
||||||
|
Model.GPUSkinningEnabled
|
||||||
|
? static_cast<ModelVertexRenderer&>(*Model.VertexGPUSkinningShader)
|
||||||
|
: static_cast<ModelVertexRenderer&>(Model.VertexCPUSkinningShader)};
|
||||||
|
|
||||||
|
for (int cullGroup{0}; cullGroup < CSceneRenderer::CULL_MAX; ++cullGroup)
|
||||||
|
{
|
||||||
|
PrepareModels(deviceCommandContext, modelVertexSkinningRenderer, Model.OpaqueSkinned.submissions[cullGroup]);
|
||||||
|
PrepareModels(deviceCommandContext, modelVertexSkinningRenderer, Model.TransparentSkinned.submissions[cullGroup]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int cullGroup{0}; cullGroup < CSceneRenderer::CULL_MAX; ++cullGroup)
|
||||||
|
{
|
||||||
|
PrepareModels(deviceCommandContext, Model.VertexInstancingShader, Model.OpaqueUnskinned.submissions[cullGroup]);
|
||||||
|
PrepareModels(deviceCommandContext, Model.VertexInstancingShader, Model.TransparentUnskinned.submissions[cullGroup]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrepareModels(
|
||||||
|
Renderer::Backend::IDeviceCommandContext* deviceCommandContext, ModelVertexRenderer& modelVertexRenderer, std::span<CModel*> submissions)
|
||||||
|
{
|
||||||
|
for (CModel* model : submissions)
|
||||||
|
{
|
||||||
|
model->ValidatePosition();
|
||||||
|
|
||||||
|
CModelRData* rdata = static_cast<CModelRData*>(model->GetRenderData());
|
||||||
|
ENSURE(rdata->GetKey() == &modelVertexRenderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
modelVertexRenderer.UpdateModelsData(deviceCommandContext, submissions);
|
||||||
|
|
||||||
|
for (CModel* model : submissions)
|
||||||
|
{
|
||||||
|
CModelRData* rdata = static_cast<CModelRData*>(model->GetRenderData());
|
||||||
|
rdata->m_UpdateFlags = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit: Submit a model for rendering this frame.
|
||||||
|
*
|
||||||
|
* preconditions : The model must not have been submitted to any
|
||||||
|
* ModelRenderer in this frame. Submit may only be called
|
||||||
|
* after EndFrame and before PrepareModels.
|
||||||
|
*
|
||||||
|
* @param model The model that will be added to the list of models
|
||||||
|
* submitted this frame.
|
||||||
|
*/
|
||||||
|
void Submit(const int cullGroup, ModelVertexRenderer& modelVertexRenderer, Models::Submissions& submissions, CModel* model)
|
||||||
|
{
|
||||||
|
CModelRData* rdata{static_cast<CModelRData*>(model->GetRenderData())};
|
||||||
|
|
||||||
|
// Ensure model data is valid.
|
||||||
|
// TODO: using a pointer as a key is unsafe.
|
||||||
|
const void* key{&modelVertexRenderer};
|
||||||
|
if (!rdata || rdata->GetKey() != key)
|
||||||
|
{
|
||||||
|
model->InvalidatePosition();
|
||||||
|
rdata = modelVertexRenderer.CreateModelData(key, model);
|
||||||
|
model->SetRenderData(rdata);
|
||||||
|
model->SetDirty(~0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
submissions.submissions[cullGroup].push_back(model);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders all non-alpha-blended models with the given context.
|
* Renders all non-alpha-blended models with the given context.
|
||||||
*/
|
*/
|
||||||
void CallModelRenderers(
|
void CallOpaqueModelRenderers(
|
||||||
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
|
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
|
||||||
const CShaderDefines& context, int cullGroup, int flags, const ERenderMode renderMode)
|
const CShaderDefines& context, int cullGroup, int flags, const ERenderMode renderMode)
|
||||||
{
|
{
|
||||||
CShaderDefines contextSkinned = context;
|
ModelVertexRenderer& modelVertexSkinningRenderer{
|
||||||
if (g_RenderingOptions.GetGPUSkinning())
|
Model.GPUSkinningEnabled
|
||||||
contextSkinned.Add(str_USE_INSTANCING, str_1);
|
? static_cast<ModelVertexRenderer&>(*Model.VertexGPUSkinningShader)
|
||||||
Model.NormalSkinned->Render(deviceCommandContext, Model.ModShader, contextSkinned, cullGroup, flags, renderMode);
|
: static_cast<ModelVertexRenderer&>(Model.VertexCPUSkinningShader)};
|
||||||
|
|
||||||
if (Model.NormalUnskinned != Model.NormalSkinned)
|
CShaderDefines contextSkinned = context;
|
||||||
{
|
if (Model.GPUSkinningEnabled)
|
||||||
CShaderDefines contextUnskinned = context;
|
contextSkinned.Add(str_USE_INSTANCING, str_1);
|
||||||
contextUnskinned.Add(str_USE_INSTANCING, str_1);
|
Model.modelRenderer.Render(deviceCommandContext, modelVertexSkinningRenderer, Model.ModShader, contextSkinned, cullGroup, flags, renderMode, Model.OpaqueSkinned.submissions[cullGroup]);
|
||||||
Model.NormalUnskinned->Render(deviceCommandContext, Model.ModShader, contextUnskinned, cullGroup, flags, renderMode);
|
|
||||||
}
|
CShaderDefines contextUnskinned = context;
|
||||||
|
contextUnskinned.Add(str_USE_INSTANCING, str_1);
|
||||||
|
Model.modelRenderer.Render(deviceCommandContext, Model.VertexInstancingShader, Model.ModShader, contextUnskinned, cullGroup, flags, renderMode, Model.OpaqueUnskinned.submissions[cullGroup]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders all alpha-blended models with the given context.
|
* Renders all alpha-blended models with the given context.
|
||||||
*/
|
*/
|
||||||
void CallTranspModelRenderers(
|
void CallTransparentModelRenderers(
|
||||||
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
|
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
|
||||||
const CShaderDefines& context, int cullGroup, int flags, const ERenderMode renderMode)
|
const CShaderDefines& context, int cullGroup, int flags, const ERenderMode renderMode)
|
||||||
{
|
{
|
||||||
CShaderDefines contextSkinned = context;
|
ModelVertexRenderer& modelVertexSkinningRenderer{
|
||||||
if (g_RenderingOptions.GetGPUSkinning())
|
Model.GPUSkinningEnabled
|
||||||
contextSkinned.Add(str_USE_INSTANCING, str_1);
|
? static_cast<ModelVertexRenderer&>(*Model.VertexGPUSkinningShader)
|
||||||
Model.TranspSkinned->Render(deviceCommandContext, Model.ModShader, contextSkinned, cullGroup, flags, renderMode);
|
: static_cast<ModelVertexRenderer&>(Model.VertexCPUSkinningShader)};
|
||||||
|
|
||||||
if (Model.TranspUnskinned != Model.TranspSkinned)
|
CShaderDefines contextSkinned = context;
|
||||||
{
|
if (Model.GPUSkinningEnabled)
|
||||||
CShaderDefines contextUnskinned = context;
|
contextSkinned.Add(str_USE_INSTANCING, str_1);
|
||||||
contextUnskinned.Add(str_USE_INSTANCING, str_1);
|
Model.modelRenderer.Render(deviceCommandContext, modelVertexSkinningRenderer, Model.ModShader, contextSkinned, cullGroup, flags, renderMode, Model.TransparentSkinned.submissions[cullGroup]);
|
||||||
Model.TranspUnskinned->Render(deviceCommandContext, Model.ModShader, contextUnskinned, cullGroup, flags, renderMode);
|
|
||||||
}
|
CShaderDefines contextUnskinned = context;
|
||||||
|
contextUnskinned.Add(str_USE_INSTANCING, str_1);
|
||||||
|
Model.modelRenderer.Render(deviceCommandContext, Model.VertexInstancingShader, Model.ModShader, contextUnskinned, cullGroup, flags, renderMode, Model.TransparentUnskinned.submissions[cullGroup]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -239,26 +354,12 @@ void CSceneRenderer::ReloadShaders([[maybe_unused]] Renderer::Backend::IDevice*
|
||||||
m->globalContext.Add(str_RENDER_DEBUG_MODE,
|
m->globalContext.Add(str_RENDER_DEBUG_MODE,
|
||||||
RenderDebugModeEnum::ToString(g_RenderingOptions.GetRenderDebugMode()));
|
RenderDebugModeEnum::ToString(g_RenderingOptions.GetRenderDebugMode()));
|
||||||
|
|
||||||
m->Model.ModShader = LitRenderModifierPtr(new ShaderRenderModifier());
|
m->Model.GPUSkinningEnabled = g_RenderingOptions.GetGPUSkinning();
|
||||||
|
if (m->Model.GPUSkinningEnabled)
|
||||||
m->Model.VertexRendererShader = ModelVertexRendererPtr(new CPUSkinnedModelVertexRenderer());
|
|
||||||
m->Model.VertexInstancingShader = ModelVertexRendererPtr(new InstancingModelRenderer());
|
|
||||||
|
|
||||||
if (g_RenderingOptions.GetGPUSkinning())
|
|
||||||
{
|
{
|
||||||
m->Model.VertexGPUSkinningShader = ModelVertexRendererPtr(new GPUSkinnedModelModelRenderer());
|
if (!m->Model.VertexGPUSkinningShader.has_value())
|
||||||
m->Model.NormalSkinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexGPUSkinningShader));
|
m->Model.VertexGPUSkinningShader.emplace();
|
||||||
m->Model.TranspSkinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexGPUSkinningShader));
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
m->Model.VertexGPUSkinningShader.reset();
|
|
||||||
m->Model.NormalSkinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexRendererShader));
|
|
||||||
m->Model.TranspSkinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexRendererShader));
|
|
||||||
}
|
|
||||||
|
|
||||||
m->Model.NormalUnskinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexInstancingShader));
|
|
||||||
m->Model.TranspUnskinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexInstancingShader));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSceneRenderer::Initialize()
|
void CSceneRenderer::Initialize()
|
||||||
|
|
@ -282,8 +383,8 @@ void CSceneRenderer::Resize(int /*width*/, int /*height*/)
|
||||||
void CSceneRenderer::BeginFrame()
|
void CSceneRenderer::BeginFrame()
|
||||||
{
|
{
|
||||||
// choose model renderers for this frame
|
// choose model renderers for this frame
|
||||||
m->Model.ModShader->SetShadowMap(&m->shadow);
|
m->Model.ModShader.SetShadowMap(&m->shadow);
|
||||||
m->Model.ModShader->SetLightEnv(m_LightEnv);
|
m->Model.ModShader.SetLightEnv(m_LightEnv);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSceneRenderer::SetSimulation(CSimulation2* simulation)
|
void CSceneRenderer::SetSimulation(CSimulation2* simulation)
|
||||||
|
|
@ -315,12 +416,12 @@ void CSceneRenderer::RenderShadowMap(
|
||||||
|
|
||||||
{
|
{
|
||||||
PROFILE("render models");
|
PROFILE("render models");
|
||||||
m->CallModelRenderers(deviceCommandContext, {}, cullGroup, ModelFlag::CAST_SHADOWS, ERenderMode::SOLID);
|
m->CallOpaqueModelRenderers(deviceCommandContext, {}, cullGroup, ModelFlag::CAST_SHADOWS, ERenderMode::SOLID);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
PROFILE("render transparent models");
|
PROFILE("render transparent models");
|
||||||
m->CallTranspModelRenderers(deviceCommandContext, {}, cullGroup, ModelFlag::CAST_SHADOWS, ERenderMode::SOLID);
|
m->CallTransparentModelRenderers(deviceCommandContext, {}, cullGroup, ModelFlag::CAST_SHADOWS, ERenderMode::SOLID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -363,10 +464,10 @@ void CSceneRenderer::RenderModels(
|
||||||
const ERenderMode modelRenderMode{
|
const ERenderMode modelRenderMode{
|
||||||
m_ModelRenderMode == WIREFRAME ? WIREFRAME : SOLID};
|
m_ModelRenderMode == WIREFRAME ? WIREFRAME : SOLID};
|
||||||
|
|
||||||
m->CallModelRenderers(deviceCommandContext, context, cullGroup, flags, modelRenderMode);
|
m->CallOpaqueModelRenderers(deviceCommandContext, context, cullGroup, flags, modelRenderMode);
|
||||||
|
|
||||||
if (m_ModelRenderMode == EDGED_FACES)
|
if (m_ModelRenderMode == EDGED_FACES)
|
||||||
m->CallModelRenderers(deviceCommandContext, {}, cullGroup, flags, EDGED_FACES);
|
m->CallOpaqueModelRenderers(deviceCommandContext, {}, cullGroup, flags, EDGED_FACES);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSceneRenderer::RenderTransparentModels(
|
void CSceneRenderer::RenderTransparentModels(
|
||||||
|
|
@ -388,13 +489,13 @@ void CSceneRenderer::RenderTransparentModels(
|
||||||
m_ModelRenderMode == WIREFRAME ? WIREFRAME : SOLID};
|
m_ModelRenderMode == WIREFRAME ? WIREFRAME : SOLID};
|
||||||
|
|
||||||
if (transparentMode == TRANSPARENT || transparentMode == TRANSPARENT_OPAQUE)
|
if (transparentMode == TRANSPARENT || transparentMode == TRANSPARENT_OPAQUE)
|
||||||
m->CallTranspModelRenderers(deviceCommandContext, contextOpaque, cullGroup, flags, modelRenderMode);
|
m->CallTransparentModelRenderers(deviceCommandContext, contextOpaque, cullGroup, flags, modelRenderMode);
|
||||||
|
|
||||||
if (transparentMode == TRANSPARENT || transparentMode == TRANSPARENT_BLEND)
|
if (transparentMode == TRANSPARENT || transparentMode == TRANSPARENT_BLEND)
|
||||||
m->CallTranspModelRenderers(deviceCommandContext, contextBlend, cullGroup, flags, modelRenderMode);
|
m->CallTransparentModelRenderers(deviceCommandContext, contextBlend, cullGroup, flags, modelRenderMode);
|
||||||
|
|
||||||
if (m_ModelRenderMode == EDGED_FACES)
|
if (m_ModelRenderMode == EDGED_FACES)
|
||||||
m->CallTranspModelRenderers(deviceCommandContext, {}, cullGroup, flags, EDGED_FACES);
|
m->CallTransparentModelRenderers(deviceCommandContext, {}, cullGroup, flags, EDGED_FACES);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetObliqueFrustumClipping: change the near plane to the given clip plane (in world space)
|
// SetObliqueFrustumClipping: change the near plane to the given clip plane (in world space)
|
||||||
|
|
@ -709,24 +810,24 @@ void CSceneRenderer::RenderSilhouettes(
|
||||||
|
|
||||||
{
|
{
|
||||||
PROFILE("render model occluders");
|
PROFILE("render model occluders");
|
||||||
m->CallModelRenderers(deviceCommandContext, {}, CULL_SILHOUETTE_OCCLUDER, 0, ERenderMode::SOLID);
|
m->CallOpaqueModelRenderers(deviceCommandContext, {}, CULL_SILHOUETTE_OCCLUDER, 0, ERenderMode::SOLID);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
PROFILE("render transparent occluders");
|
PROFILE("render transparent occluders");
|
||||||
m->CallTranspModelRenderers(deviceCommandContext, {}, CULL_SILHOUETTE_OCCLUDER, 0, ERenderMode::SOLID);
|
m->CallTransparentModelRenderers(deviceCommandContext, {}, CULL_SILHOUETTE_OCCLUDER, 0, ERenderMode::SOLID);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since we can't sort, we'll use the stencil buffer to ensure we only draw
|
// Since we can't sort, we'll use the stencil buffer to ensure we only draw
|
||||||
// a pixel once (using the color of whatever model happens to be drawn first).
|
// a pixel once (using the color of whatever model happens to be drawn first).
|
||||||
{
|
{
|
||||||
PROFILE("render model casters");
|
PROFILE("render model casters");
|
||||||
m->CallModelRenderers(deviceCommandContext, {}, CULL_SILHOUETTE_CASTER, 0, ERenderMode::SOLID);
|
m->CallOpaqueModelRenderers(deviceCommandContext, {}, CULL_SILHOUETTE_CASTER, 0, ERenderMode::SOLID);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
PROFILE("render transparent casters");
|
PROFILE("render transparent casters");
|
||||||
m->CallTranspModelRenderers(deviceCommandContext, {}, CULL_SILHOUETTE_CASTER, 0, ERenderMode::SOLID);
|
m->CallTransparentModelRenderers(deviceCommandContext, {}, CULL_SILHOUETTE_CASTER, 0, ERenderMode::SOLID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -770,15 +871,7 @@ void CSceneRenderer::PrepareSubmissions(
|
||||||
CShaderDefines context = m->globalContext;
|
CShaderDefines context = m->globalContext;
|
||||||
|
|
||||||
// Prepare model renderers
|
// Prepare model renderers
|
||||||
{
|
m->PrepareModels(deviceCommandContext);
|
||||||
PROFILE3("prepare models");
|
|
||||||
m->Model.NormalSkinned->PrepareModels(deviceCommandContext);
|
|
||||||
m->Model.TranspSkinned->PrepareModels(deviceCommandContext);
|
|
||||||
if (m->Model.NormalUnskinned != m->Model.NormalSkinned)
|
|
||||||
m->Model.NormalUnskinned->PrepareModels(deviceCommandContext);
|
|
||||||
if (m->Model.TranspUnskinned != m->Model.TranspSkinned)
|
|
||||||
m->Model.TranspUnskinned->PrepareModels(deviceCommandContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
m->terrainRenderer.PrepareForRendering();
|
m->terrainRenderer.PrepareForRendering();
|
||||||
|
|
||||||
|
|
@ -786,15 +879,7 @@ void CSceneRenderer::PrepareSubmissions(
|
||||||
|
|
||||||
m->particleRenderer.PrepareForRendering(context);
|
m->particleRenderer.PrepareForRendering(context);
|
||||||
|
|
||||||
{
|
m->UploadModels(deviceCommandContext);
|
||||||
PROFILE3("upload models");
|
|
||||||
m->Model.NormalSkinned->UploadModels(deviceCommandContext);
|
|
||||||
m->Model.TranspSkinned->UploadModels(deviceCommandContext);
|
|
||||||
if (m->Model.NormalUnskinned != m->Model.NormalSkinned)
|
|
||||||
m->Model.NormalUnskinned->UploadModels(deviceCommandContext);
|
|
||||||
if (m->Model.TranspUnskinned != m->Model.TranspSkinned)
|
|
||||||
m->Model.TranspUnskinned->UploadModels(deviceCommandContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
m->overlayRenderer.Upload(deviceCommandContext);
|
m->overlayRenderer.Upload(deviceCommandContext);
|
||||||
|
|
||||||
|
|
@ -908,13 +993,13 @@ void CSceneRenderer::EndFrame()
|
||||||
m->particleRenderer.EndFrame();
|
m->particleRenderer.EndFrame();
|
||||||
m->silhouetteRenderer.EndFrame();
|
m->silhouetteRenderer.EndFrame();
|
||||||
|
|
||||||
// Finish model renderers
|
for (int cullGroup{0}; cullGroup < CSceneRenderer::CULL_MAX; ++cullGroup)
|
||||||
m->Model.NormalSkinned->EndFrame();
|
{
|
||||||
m->Model.TranspSkinned->EndFrame();
|
m->Model.OpaqueSkinned.submissions[cullGroup].clear();
|
||||||
if (m->Model.NormalUnskinned != m->Model.NormalSkinned)
|
m->Model.TransparentSkinned.submissions[cullGroup].clear();
|
||||||
m->Model.NormalUnskinned->EndFrame();
|
m->Model.OpaqueUnskinned.submissions[cullGroup].clear();
|
||||||
if (m->Model.TranspUnskinned != m->Model.TranspSkinned)
|
m->Model.TransparentUnskinned.submissions[cullGroup].clear();
|
||||||
m->Model.TranspUnskinned->EndFrame();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSceneRenderer::DisplayFrustum(Renderer::Backend::IDeviceCommandContext& deviceCommandContext)
|
void CSceneRenderer::DisplayFrustum(Renderer::Backend::IDeviceCommandContext& deviceCommandContext)
|
||||||
|
|
@ -1029,21 +1114,25 @@ void CSceneRenderer::SubmitNonRecursive(CModel* model)
|
||||||
m->shadow.AddShadowCasterBound(cascade, model->GetWorldBounds());
|
m->shadow.AddShadowCasterBound(cascade, model->GetWorldBounds());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool requiresSkinning = (model->GetModelDef()->GetNumBones() != 0);
|
const bool requiresSkinning{model->GetModelDef()->GetNumBones() != 0};
|
||||||
|
ModelVertexRenderer& modelVertexSkinningRenderer{
|
||||||
|
m->Model.GPUSkinningEnabled
|
||||||
|
? static_cast<ModelVertexRenderer&>(*m->Model.VertexGPUSkinningShader)
|
||||||
|
: static_cast<ModelVertexRenderer&>(m->Model.VertexCPUSkinningShader)};
|
||||||
|
|
||||||
if (model->GetMaterial().UsesAlphaBlending())
|
if (model->GetMaterial().UsesAlphaBlending())
|
||||||
{
|
{
|
||||||
if (requiresSkinning)
|
if (requiresSkinning)
|
||||||
m->Model.TranspSkinned->Submit(m_CurrentCullGroup, model);
|
m->Submit(m_CurrentCullGroup, modelVertexSkinningRenderer, m->Model.TransparentSkinned, model);
|
||||||
else
|
else
|
||||||
m->Model.TranspUnskinned->Submit(m_CurrentCullGroup, model);
|
m->Submit(m_CurrentCullGroup, m->Model.VertexInstancingShader, m->Model.TransparentUnskinned, model);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (requiresSkinning)
|
if (requiresSkinning)
|
||||||
m->Model.NormalSkinned->Submit(m_CurrentCullGroup, model);
|
m->Submit(m_CurrentCullGroup, modelVertexSkinningRenderer, m->Model.OpaqueSkinned, model);
|
||||||
else
|
else
|
||||||
m->Model.NormalUnskinned->Submit(m_CurrentCullGroup, model);
|
m->Submit(m_CurrentCullGroup, m->Model.VertexInstancingShader, m->Model.OpaqueUnskinned, model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,21 +90,21 @@ CStr CScriptStatsTable::GetCellText(size_t row, size_t col)
|
||||||
if (col == 0)
|
if (col == 0)
|
||||||
return "max nominal heap bytes";
|
return "max nominal heap bytes";
|
||||||
uint32_t n = JS_GetGCParameter(m_ScriptInterfaces.at(col-1).first->GetGeneralJSContext(), JSGC_MAX_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:
|
case Row_Bytes:
|
||||||
{
|
{
|
||||||
if (col == 0)
|
if (col == 0)
|
||||||
return "allocated bytes";
|
return "allocated bytes";
|
||||||
uint32_t n = JS_GetGCParameter(m_ScriptInterfaces.at(col-1).first->GetGeneralJSContext(), JSGC_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:
|
case Row_NumberGC:
|
||||||
{
|
{
|
||||||
if (col == 0)
|
if (col == 0)
|
||||||
return "number of GCs";
|
return "number of GCs";
|
||||||
uint32_t n = JS_GetGCParameter(m_ScriptInterfaces.at(col-1).first->GetGeneralJSContext(), JSGC_NUMBER);
|
uint32_t n = JS_GetGCParameter(m_ScriptInterfaces.at(col-1).first->GetGeneralJSContext(), JSGC_NUMBER);
|
||||||
return CStr::FromUInt(n);
|
return std::to_string(n);
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return "???";
|
return "???";
|
||||||
|
|
@ -116,4 +116,4 @@ AbstractProfileTable* CScriptStatsTable::GetChild(size_t /*row*/)
|
||||||
return 0;
|
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
|
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 minRange;
|
||||||
entity_pos_t maxRange;
|
entity_pos_t maxRange;
|
||||||
|
entity_pos_t baseRange; // Non-parabolic detection range
|
||||||
entity_pos_t yOrigin; // Used for parabolas only.
|
entity_pos_t yOrigin; // Used for parabolas only.
|
||||||
u32 ownersMask;
|
u32 ownersMask;
|
||||||
i32 interface;
|
i32 interface;
|
||||||
|
|
@ -195,6 +196,7 @@ struct Query
|
||||||
bool enabled;
|
bool enabled;
|
||||||
bool parabolic;
|
bool parabolic;
|
||||||
bool accountForSize; // If true, the query accounts for unit sizes, otherwise it treats all entities as points.
|
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("min range", value.minRange);
|
||||||
serialize.NumberFixed_Unbounded("max range", value.maxRange);
|
serialize.NumberFixed_Unbounded("max range", value.maxRange);
|
||||||
|
serialize.NumberFixed_Unbounded("baseRange", value.baseRange);
|
||||||
serialize.NumberFixed_Unbounded("yOrigin", value.yOrigin);
|
serialize.NumberFixed_Unbounded("yOrigin", value.yOrigin);
|
||||||
serialize.NumberU32_Unbounded("owners mask", value.ownersMask);
|
serialize.NumberU32_Unbounded("owners mask", value.ownersMask);
|
||||||
serialize.NumberI32_Unbounded("interface", value.interface);
|
serialize.NumberI32_Unbounded("interface", value.interface);
|
||||||
|
|
@ -357,6 +360,7 @@ struct SerializeHelper<Query>
|
||||||
serialize.Bool("enabled", value.enabled);
|
serialize.Bool("enabled", value.enabled);
|
||||||
serialize.Bool("parabolic",value.parabolic);
|
serialize.Bool("parabolic",value.parabolic);
|
||||||
serialize.Bool("account for size",value.accountForSize);
|
serialize.Bool("account for size",value.accountForSize);
|
||||||
|
serialize.Bool("preferMirages", value.preferMirages);
|
||||||
}
|
}
|
||||||
|
|
||||||
void operator()(ISerializer& serialize, const char* name, Query& value, const CSimContext&)
|
void operator()(ISerializer& serialize, const char* name, Query& value, const CSimContext&)
|
||||||
|
|
@ -992,21 +996,20 @@ public:
|
||||||
|
|
||||||
tag_t CreateActiveQuery(entity_id_t source,
|
tag_t CreateActiveQuery(entity_id_t source,
|
||||||
entity_pos_t minRange, entity_pos_t maxRange,
|
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++;
|
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;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
tag_t CreateActiveParabolicQuery(entity_id_t source,
|
tag_t CreateActiveParabolicQuery(entity_id_t source,
|
||||||
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin,
|
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) override
|
const std::vector<int>& owners, int requiredInterface, u8 flags, bool preferMirages = false) override
|
||||||
{
|
{
|
||||||
tag_t id = m_QueryNext++;
|
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;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1297,10 +1300,35 @@ public:
|
||||||
if (id == q.source.GetId())
|
if (id == q.source.GetId())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Ignore if it's missing the required interface
|
// Check if this is a mirage entity
|
||||||
if (q.interface && !GetSimContext().GetComponentManager().QueryInterface(id, q.interface))
|
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;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1324,13 +1352,18 @@ public:
|
||||||
// Not the entire world, so check a parabolic range, or a regular range.
|
// Not the entire world, so check a parabolic range, or a regular range.
|
||||||
else if (q.parabolic)
|
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);
|
CmpPtr<ICmpPosition> cmpSourcePosition(q.source);
|
||||||
CFixedVector3D pos3d = cmpSourcePosition->GetPosition()+
|
CFixedVector3D pos3d = cmpSourcePosition->GetPosition() +
|
||||||
CFixedVector3D(entity_pos_t::Zero(), q.yOrigin, entity_pos_t::Zero()) ;
|
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.
|
// 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();
|
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)
|
for (size_t i = 0; i < subdivisionResultsBuffer.size(); ++i)
|
||||||
{
|
{
|
||||||
|
|
@ -1340,6 +1373,20 @@ public:
|
||||||
if (!TestEntityQuery(q, it->first, it->second))
|
if (!TestEntityQuery(q, it->first, it->second))
|
||||||
continue;
|
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]);
|
CmpPtr<ICmpPosition> cmpSecondPosition(GetSimContext(), subdivisionResultsBuffer[i]);
|
||||||
if (!cmpSecondPosition || !cmpSecondPosition->IsInWorld())
|
if (!cmpSecondPosition || !cmpSecondPosition->IsInWorld())
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1357,7 +1404,7 @@ public:
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!q.minRange.IsZero())
|
if (!q.minRange.IsZero())
|
||||||
if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange) < 0)
|
if (delta2D.CompareLength(q.minRange) < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
r.push_back(it->first);
|
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
|
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.
|
// For non-positive ranges, just return the range.
|
||||||
|
|
@ -1408,13 +1471,32 @@ public:
|
||||||
if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld())
|
if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld())
|
||||||
return NEVER_IN_RANGE;
|
return NEVER_IN_RANGE;
|
||||||
|
|
||||||
entity_pos_t heightDifference = cmpSourcePosition->GetHeightOffset() - cmpTargetPosition->GetHeightOffset() + yOrigin;
|
// GetPosition() returns the world height (terrain + water + offset)
|
||||||
if (heightDifference < -range / 2)
|
CFixedVector3D sourcePos = cmpSourcePosition->GetPosition();
|
||||||
return NEVER_IN_RANGE;
|
CFixedVector3D targetPos = cmpTargetPosition->GetPosition();
|
||||||
|
|
||||||
entity_pos_t effectiveRange;
|
entity_pos_t heightDiff = sourcePos.Y - targetPos.Y + yOrigin;
|
||||||
effectiveRange.SetInternalValue(static_cast<i32>(isqrt64(SQUARE_U64_FIXED(range) + static_cast<i64>(heightDifference.GetInternalValue()) * static_cast<i64>(range.GetInternalValue()) * 2)));
|
return ComputeParabolicRange(range, heightDiff);
|
||||||
return effectiveRange;
|
}
|
||||||
|
|
||||||
|
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
|
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,
|
Query ConstructQuery(entity_id_t source,
|
||||||
entity_pos_t minRange, entity_pos_t maxRange,
|
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.
|
// Min range must be non-negative.
|
||||||
if (minRange < entity_pos_t::Zero())
|
if (minRange < entity_pos_t::Zero())
|
||||||
|
|
@ -1530,6 +1613,8 @@ public:
|
||||||
if (maxRange < entity_pos_t::Zero() && maxRange != ALWAYS_IN_RANGE)
|
if (maxRange < entity_pos_t::Zero() && maxRange != ALWAYS_IN_RANGE)
|
||||||
LOGWARNING("CCmpRangeManager: Invalid max range %f in query for entity %u", maxRange.ToDouble(), source);
|
LOGWARNING("CCmpRangeManager: Invalid max range %f in query for entity %u", maxRange.ToDouble(), source);
|
||||||
|
|
||||||
|
CmpPtr<ICmpOwnership> cmpOwnership(GetSimContext(), source);
|
||||||
|
|
||||||
Query q;
|
Query q;
|
||||||
q.enabled = false;
|
q.enabled = false;
|
||||||
q.parabolic = false;
|
q.parabolic = false;
|
||||||
|
|
@ -1538,6 +1623,7 @@ public:
|
||||||
q.maxRange = maxRange;
|
q.maxRange = maxRange;
|
||||||
q.yOrigin = entity_pos_t::Zero();
|
q.yOrigin = entity_pos_t::Zero();
|
||||||
q.accountForSize = accountForSize;
|
q.accountForSize = accountForSize;
|
||||||
|
q.preferMirages = preferMirages;
|
||||||
|
|
||||||
if (q.accountForSize && q.source.GetId() != INVALID_ENTITY && q.maxRange != ALWAYS_IN_RANGE)
|
if (q.accountForSize && q.source.GetId() != INVALID_ENTITY && q.maxRange != ALWAYS_IN_RANGE)
|
||||||
{
|
{
|
||||||
|
|
@ -1574,12 +1660,14 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
Query ConstructParabolicQuery(entity_id_t source,
|
Query ConstructParabolicQuery(entity_id_t source,
|
||||||
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin,
|
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) const
|
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.parabolic = true;
|
||||||
q.yOrigin = yOrigin;
|
q.yOrigin = yOrigin;
|
||||||
|
q.baseRange = baseRange;
|
||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright (C) 2025 Wildfire Games.
|
/* Copyright (C) 2026 Wildfire Games.
|
||||||
* This file is part of 0 A.D.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 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)
|
BEGIN_INTERFACE_WRAPPER(Obstruction)
|
||||||
DEFINE_INTERFACE_METHOD("GetSize", ICmpObstruction, GetSize)
|
DEFINE_INTERFACE_METHOD("GetSize", ICmpObstruction, GetSize)
|
||||||
DEFINE_INTERFACE_METHOD("CheckShorePlacement", ICmpObstruction, CheckShorePlacement)
|
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("GetEntitiesDeletedUponConstruction", ICmpObstruction, GetEntitiesDeletedUponConstruction)
|
||||||
DEFINE_INTERFACE_METHOD("SetActive", ICmpObstruction, SetActive)
|
DEFINE_INTERFACE_METHOD("SetActive", ICmpObstruction, SetActive)
|
||||||
DEFINE_INTERFACE_METHOD("SetDisableBlockMovementPathfinding", ICmpObstruction, SetDisableBlockMovementPathfinding)
|
DEFINE_INTERFACE_METHOD("SetDisableBlockMovementPathfinding", ICmpObstruction, SetDisableBlockMovementPathfinding)
|
||||||
|
DEFINE_INTERFACE_METHOD("GetObstructionHalfSizes", ICmpObstruction, GetObstructionHalfSizes_wrapper)
|
||||||
DEFINE_INTERFACE_METHOD("GetBlockMovementFlag", ICmpObstruction, GetBlockMovementFlag)
|
DEFINE_INTERFACE_METHOD("GetBlockMovementFlag", ICmpObstruction, GetBlockMovementFlag)
|
||||||
DEFINE_INTERFACE_METHOD("SetControlGroup", ICmpObstruction, SetControlGroup)
|
DEFINE_INTERFACE_METHOD("SetControlGroup", ICmpObstruction, SetControlGroup)
|
||||||
DEFINE_INTERFACE_METHOD("GetControlGroup", ICmpObstruction, GetControlGroup)
|
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.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 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;
|
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
|
* Test whether this entity is colliding with any obstructions that share its
|
||||||
* control groups and block the creation of foundations.
|
* 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("SetLosRevealWholeMapForAll", ICmpRangeManager, SetLosRevealWholeMapForAll)
|
||||||
DEFINE_INTERFACE_METHOD("GetLosRevealWholeMapForAll", ICmpRangeManager, GetLosRevealWholeMapForAll)
|
DEFINE_INTERFACE_METHOD("GetLosRevealWholeMapForAll", ICmpRangeManager, GetLosRevealWholeMapForAll)
|
||||||
DEFINE_INTERFACE_METHOD("GetEffectiveParabolicRange", ICmpRangeManager, GetEffectiveParabolicRange)
|
DEFINE_INTERFACE_METHOD("GetEffectiveParabolicRange", ICmpRangeManager, GetEffectiveParabolicRange)
|
||||||
|
DEFINE_INTERFACE_METHOD("GetMaxReachableParabolicHeight", ICmpRangeManager, GetMaxReachableParabolicHeight)
|
||||||
DEFINE_INTERFACE_METHOD("GetElevationAdaptedRange", ICmpRangeManager, GetElevationAdaptedRange)
|
DEFINE_INTERFACE_METHOD("GetElevationAdaptedRange", ICmpRangeManager, GetElevationAdaptedRange)
|
||||||
DEFINE_INTERFACE_METHOD("ActivateScriptedVisibility", ICmpRangeManager, ActivateScriptedVisibility)
|
DEFINE_INTERFACE_METHOD("ActivateScriptedVisibility", ICmpRangeManager, ActivateScriptedVisibility)
|
||||||
DEFINE_INTERFACE_METHOD("GetLosVisibility", ICmpRangeManager, GetLosVisibility_wrapper)
|
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 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 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 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.
|
* @return unique non-zero identifier of query.
|
||||||
*/
|
*/
|
||||||
virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange,
|
virtual tag_t CreateActiveQuery(entity_id_t source,
|
||||||
const std::vector<int>& owners, int requiredInterface, u8 flags, bool accountForSize) = 0;
|
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.
|
* The query will be disabled by default.
|
||||||
* @param source the entity around which the range will be computed.
|
* @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 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;
|
* @param maxRange non-negative maximum distance in metres (inclusive) for units on the same elevation;
|
||||||
* or -1.0 to ignore distance.
|
* 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
|
* 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 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 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 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 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.
|
* 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.
|
* @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,
|
virtual tag_t CreateActiveParabolicQuery(entity_id_t source,
|
||||||
const std::vector<int>& owners, int requiredInterface, u8 flags) = 0;
|
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.
|
* 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;
|
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
|
* Get the average elevation over 8 points on distance range around the entity
|
||||||
* @param id the entity id to look around
|
* @param id the entity id to look around
|
||||||
|
|
|
||||||
|
|
@ -63,13 +63,13 @@ public:
|
||||||
entity_id_t GetTurretParent() const override {return INVALID_ENTITY;}
|
entity_id_t GetTurretParent() const override {return INVALID_ENTITY;}
|
||||||
void UpdateTurretPosition() override {}
|
void UpdateTurretPosition() override {}
|
||||||
std::set<entity_id_t>* GetTurrets() override { return nullptr; }
|
std::set<entity_id_t>* GetTurrets() override { return nullptr; }
|
||||||
bool IsInWorld() const override { return true; }
|
bool IsInWorld() const override { return m_InWorld; }
|
||||||
void MoveOutOfWorld() override { }
|
void MoveOutOfWorld() override { m_InWorld = false; }
|
||||||
void MoveTo(entity_pos_t /*x*/, entity_pos_t /*z*/) override { }
|
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 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 JumpTo(entity_pos_t /*x*/, entity_pos_t /*z*/) override { }
|
||||||
void SetHeightOffset(entity_pos_t /*dy*/) override { }
|
void SetHeightOffset(entity_pos_t dy) override { m_HeightOffset = dy; }
|
||||||
entity_pos_t GetHeightOffset() const override { return entity_pos_t::Zero(); }
|
entity_pos_t GetHeightOffset() const override { return m_HeightOffset; }
|
||||||
void SetHeightFixed(entity_pos_t /*y*/) override { }
|
void SetHeightFixed(entity_pos_t /*y*/) override { }
|
||||||
entity_pos_t GetHeightFixed() const override { return entity_pos_t::Zero(); }
|
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(); }
|
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(); }
|
CMatrix3D GetInterpolatedTransform(float /*frameOffset*/) const override { return CMatrix3D(); }
|
||||||
|
|
||||||
CFixedVector3D m_Pos;
|
CFixedVector3D m_Pos;
|
||||||
|
entity_pos_t m_HeightOffset = entity_pos_t::Zero();
|
||||||
|
bool m_InWorld = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
class MockObstructionRgm : public ICmpObstruction
|
class MockObstructionRgm : public ICmpObstruction
|
||||||
|
|
@ -154,7 +156,7 @@ public:
|
||||||
{
|
{
|
||||||
ComponentTestHelper test(*g_ScriptContext);
|
ComponentTestHelper test(*g_ScriptContext);
|
||||||
|
|
||||||
ICmpRangeManager* cmp = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
ICmpRangeManager* rangeManager = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
||||||
|
|
||||||
MockVisionRgm vision;
|
MockVisionRgm vision;
|
||||||
test.AddMock(100, IID_Vision, vision);
|
test.AddMock(100, IID_Vision, vision);
|
||||||
|
|
@ -165,41 +167,41 @@ public:
|
||||||
// This tests that the incremental computation produces the correct result
|
// This tests that the incremental computation produces the correct result
|
||||||
// in various edge cases
|
// 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));
|
rangeManager->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512));
|
||||||
cmp->Verify();
|
rangeManager->Verify();
|
||||||
{ CMessageCreate msg(100); cmp->HandleMessage(msg, false); }
|
{ CMessageCreate msg(100); rangeManager->HandleMessage(msg, false); }
|
||||||
cmp->Verify();
|
rangeManager->Verify();
|
||||||
{ CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); }
|
{ CMessageOwnershipChanged msg(100, -1, 1); rangeManager->HandleMessage(msg, false); }
|
||||||
cmp->Verify();
|
rangeManager->Verify();
|
||||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromDouble(257.95), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromDouble(257.95), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||||
cmp->Verify();
|
rangeManager->Verify();
|
||||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromInt(253), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromInt(253), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||||
cmp->Verify();
|
rangeManager->Verify();
|
||||||
|
|
||||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||||
cmp->Verify();
|
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); }
|
{ 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); }
|
||||||
cmp->Verify();
|
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); }
|
{ 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); }
|
||||||
cmp->Verify();
|
rangeManager->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); }
|
{ 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); }
|
||||||
cmp->Verify();
|
rangeManager->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); }
|
{ 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); }
|
||||||
cmp->Verify();
|
rangeManager->Verify();
|
||||||
|
|
||||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(383), entity_pos_t::FromInt(84), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(383), entity_pos_t::FromInt(84), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||||
cmp->Verify();
|
rangeManager->Verify();
|
||||||
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(348), entity_pos_t::FromInt(83), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(348), entity_pos_t::FromInt(83), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||||
cmp->Verify();
|
rangeManager->Verify();
|
||||||
|
|
||||||
std::mt19937 rng;
|
std::mt19937 rng;
|
||||||
for (size_t i = 0; i < 1024; ++i)
|
for (size_t i = 0; i < 1024; ++i)
|
||||||
{
|
{
|
||||||
double x = std::uniform_real_distribution<double>(0.0, 512.0)(rng);
|
double x = std::uniform_real_distribution<double>(0.0, 512.0)(rng);
|
||||||
double z = 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); }
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromDouble(x), entity_pos_t::FromDouble(z), entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||||
cmp->Verify();
|
rangeManager->Verify();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test OwnershipChange, GetEntitiesByPlayer, GetNonGaiaEntities
|
// Test OwnershipChange, GetEntitiesByPlayer, GetNonGaiaEntities
|
||||||
|
|
@ -208,22 +210,22 @@ public:
|
||||||
for (player_id_t newOwner = 0; newOwner < 8; ++newOwner)
|
for (player_id_t newOwner = 0; newOwner < 8; ++newOwner)
|
||||||
{
|
{
|
||||||
CMessageOwnershipChanged msg(100, previousOwner, newOwner);
|
CMessageOwnershipChanged msg(100, previousOwner, newOwner);
|
||||||
cmp->HandleMessage(msg, false);
|
rangeManager->HandleMessage(msg, false);
|
||||||
|
|
||||||
for (player_id_t i = 0; i < 8; ++i)
|
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;
|
previousOwner = newOwner;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_queries()
|
void test_range_queries_distance_only()
|
||||||
{
|
{
|
||||||
ComponentTestHelper test(*g_ScriptContext);
|
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;
|
MockVisionRgm vision, vision2;
|
||||||
MockPositionRgm position, position2;
|
MockPositionRgm position, position2;
|
||||||
|
|
@ -236,101 +238,204 @@ public:
|
||||||
test.AddMock(101, IID_Position, position2);
|
test.AddMock(101, IID_Position, position2);
|
||||||
test.AddMock(101, IID_Obstruction, obs2);
|
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));
|
rangeManager->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512));
|
||||||
cmp->Verify();
|
rangeManager->Verify();
|
||||||
{ CMessageCreate msg(100); cmp->HandleMessage(msg, false); }
|
{ CMessageCreate msg(100); rangeManager->HandleMessage(msg, false); }
|
||||||
{ CMessageCreate msg(101); cmp->HandleMessage(msg, false); }
|
{ CMessageCreate msg(101); rangeManager->HandleMessage(msg, false); }
|
||||||
|
|
||||||
{ CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); }
|
// Don't set ownership for either entity - leave both as INVALID_PLAYER.
|
||||||
{ CMessageOwnershipChanged msg(101, -1, 1); cmp->HandleMessage(msg, false); }
|
// 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);
|
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(100, position, fixed::FromInt(10), fixed::FromInt(10));
|
||||||
move(101, position2, fixed::FromInt(10), fixed::FromInt(20));
|
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>{});
|
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});
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||||
|
|
||||||
move(101, position2, fixed::FromInt(10), fixed::FromInt(10));
|
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});
|
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>{});
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
||||||
|
|
||||||
move(101, position2, fixed::FromInt(10), fixed::FromInt(13));
|
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});
|
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>{});
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
||||||
|
|
||||||
move(101, position2, fixed::FromInt(10), fixed::FromInt(15));
|
move(101, position2, fixed::FromInt(10), fixed::FromInt(15));
|
||||||
// In range thanks to self obstruction size.
|
// 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});
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||||
// In range thanks to target obstruction size.
|
// 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});
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{100});
|
||||||
|
|
||||||
// Trickier: min-range is closest-to-closest, but rotation may change the real distance.
|
// 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});
|
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});
|
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>{});
|
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});
|
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>{});
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_IsInTargetParabolicRange()
|
void test_range_queries_visibility_filtering()
|
||||||
{
|
{
|
||||||
ComponentTestHelper test(*g_ScriptContext);
|
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 source = 200;
|
||||||
const entity_id_t target = 201;
|
const entity_id_t target = 201;
|
||||||
entity_pos_t range = fixed::FromInt(-3);
|
entity_pos_t range{fixed::FromInt(-3)};
|
||||||
entity_pos_t yOrigin = fixed::FromInt(-20);
|
entity_pos_t yOrigin{fixed::FromInt(-20)};
|
||||||
|
|
||||||
// Invalid range.
|
// 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.
|
// No source ICmpPosition.
|
||||||
range = fixed::FromInt(10);
|
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.
|
// No target ICmpPosition.
|
||||||
MockPositionRgm cmpSourcePosition;
|
MockPositionRgm cmpSourcePosition;
|
||||||
test.AddMock(source, IID_Position, 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.
|
// Too much height difference.
|
||||||
MockPositionRgm cmpTargetPosition;
|
MockPositionRgm cmpTargetPosition;
|
||||||
test.AddMock(target, IID_Position, 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.
|
// If no offset we get the range.
|
||||||
range = fixed::FromInt(20);
|
range = fixed::FromInt(20);
|
||||||
yOrigin = fixed::Zero();
|
yOrigin = fixed::Zero();
|
||||||
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), range);
|
TS_ASSERT_EQUALS(rangeManager->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, fixed::Zero(), yOrigin), fixed::Zero());
|
||||||
|
|
||||||
// Normal case.
|
// Normal case with yOrigin only (no terrain difference)
|
||||||
yOrigin = fixed::FromInt(5);
|
yOrigin = fixed::FromInt(5);
|
||||||
range = fixed::FromInt(10);
|
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.
|
// Big range.
|
||||||
range = fixed::FromInt(260);
|
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()
|
void test_ExploreCircle()
|
||||||
|
|
@ -367,4 +472,72 @@ public:
|
||||||
|
|
||||||
cmp->Verify();
|
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());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -273,8 +273,16 @@ private:
|
||||||
|
|
||||||
if (evt.GetWheelRotation())
|
if (evt.GetWheelRotation())
|
||||||
{
|
{
|
||||||
float speed = 16.f * ScenarioEditor::GetSpeedModifier();
|
if (evt.GetWheelAxis() == wxMOUSE_WHEEL_VERTICAL)
|
||||||
POST_MESSAGE(SmoothZoom, (eRenderView::GAME, evt.GetWheelRotation() * speed / evt.GetWheelDelta()));
|
{
|
||||||
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -45,21 +45,15 @@
|
||||||
|
|
||||||
using AtlasMessage::Shareable;
|
using AtlasMessage::Shareable;
|
||||||
|
|
||||||
enum {
|
|
||||||
ID_PathsDrawing,
|
|
||||||
ID_PathsList,
|
|
||||||
ID_AddPath,
|
|
||||||
ID_DeletePath
|
|
||||||
};
|
|
||||||
|
|
||||||
CinemaSidebar::CinemaSidebar(ScenarioEditor& scenarioEditor, wxWindow* sidebarContainer, wxWindow* bottomBarContainer)
|
CinemaSidebar::CinemaSidebar(ScenarioEditor& scenarioEditor, wxWindow* sidebarContainer, wxWindow* bottomBarContainer)
|
||||||
: Sidebar(scenarioEditor, sidebarContainer, bottomBarContainer)
|
: Sidebar(scenarioEditor, sidebarContainer, bottomBarContainer)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
auto* sizer = new wxStaticBoxSizer(wxVERTICAL, this, _T("Common settings"));
|
auto* sizer = new wxStaticBoxSizer(wxVERTICAL, this, _T("Common settings"));
|
||||||
|
|
||||||
m_DrawPath = new wxCheckBox(sizer->GetStaticBox(), ID_PathsDrawing, _("Draw all paths"));
|
m_DrawPath = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Draw all paths"));
|
||||||
m_DrawPath->SetToolTip(_("Display every cinematic path added to the map"));
|
m_DrawPath->SetToolTip(_("Display every cinematic path added to the map"));
|
||||||
|
m_DrawPath->Bind(wxEVT_CHECKBOX, [this](auto&){ SetPathsDrawing(m_DrawPath->IsChecked()); });
|
||||||
|
|
||||||
sizer->Add(m_DrawPath, wxSizerFlags().Expand().Border(wxALL, Atlas::Style::STATICBOX_PADDING));
|
sizer->Add(m_DrawPath, wxSizerFlags().Expand().Border(wxALL, Atlas::Style::STATICBOX_PADDING));
|
||||||
|
|
||||||
|
|
@ -70,20 +64,22 @@ CinemaSidebar::CinemaSidebar(ScenarioEditor& scenarioEditor, wxWindow* sidebarCo
|
||||||
auto* boxSizer = new wxStaticBoxSizer(wxVERTICAL, this, _T("Paths"));
|
auto* boxSizer = new wxStaticBoxSizer(wxVERTICAL, this, _T("Paths"));
|
||||||
auto* box = boxSizer->GetStaticBox();
|
auto* box = boxSizer->GetStaticBox();
|
||||||
|
|
||||||
m_PathList = new wxListBox(box, ID_PathsList, wxDefaultPosition, wxDefaultSize, 0, NULL, wxLB_SINGLE | wxLB_SORT);
|
m_PathList = new wxListBox(box, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, NULL, wxLB_SINGLE | wxLB_SORT);
|
||||||
|
|
||||||
auto* deleteButton = new wxButton(box, ID_DeletePath, _("Delete"));
|
auto* deleteButton = new wxButton(box, wxID_ANY, _("Delete"));
|
||||||
deleteButton->SetToolTip(_T("Delete selected path"));
|
deleteButton->SetToolTip(_T("Delete selected path"));
|
||||||
|
deleteButton->Bind(wxEVT_BUTTON, [this](auto&){ DeleteSelectedPath(); });
|
||||||
|
|
||||||
m_NewPathName = new wxTextCtrl(box, wxID_ANY);
|
auto* newPathName = new wxTextCtrl(box, wxID_ANY);
|
||||||
|
|
||||||
auto* addButton = new wxButton(box, ID_AddPath, _("Add"));
|
auto* addButton = new wxButton(box, wxID_ANY, _("Add"));
|
||||||
|
addButton->Bind(wxEVT_BUTTON, [this, newPathName](auto&){ AddPath(newPathName->GetValue()); newPathName->Clear(); });
|
||||||
|
|
||||||
wxFlexGridSizer* pathsSizer = new wxFlexGridSizer(1, 5, 5);
|
wxFlexGridSizer* pathsSizer = new wxFlexGridSizer(1, 5, 5);
|
||||||
pathsSizer->AddGrowableCol(0);
|
pathsSizer->AddGrowableCol(0);
|
||||||
pathsSizer->Add(m_PathList, wxSizerFlags().Proportion(1).Expand());
|
pathsSizer->Add(m_PathList, wxSizerFlags().Proportion(1).Expand());
|
||||||
pathsSizer->Add(deleteButton, wxSizerFlags().Expand());
|
pathsSizer->Add(deleteButton, wxSizerFlags().Expand());
|
||||||
pathsSizer->Add(m_NewPathName, wxSizerFlags().Expand());
|
pathsSizer->Add(newPathName, wxSizerFlags().Expand());
|
||||||
pathsSizer->Add(addButton, wxSizerFlags().Expand());
|
pathsSizer->Add(addButton, wxSizerFlags().Expand());
|
||||||
|
|
||||||
boxSizer->Add(pathsSizer, wxSizerFlags().Expand().Border(wxALL, Atlas::Style::STATICBOX_PADDING));
|
boxSizer->Add(pathsSizer, wxSizerFlags().Expand().Border(wxALL, Atlas::Style::STATICBOX_PADDING));
|
||||||
|
|
@ -106,22 +102,21 @@ void CinemaSidebar::OnMapReload()
|
||||||
ReloadPathList();
|
ReloadPathList();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CinemaSidebar::OnTogglePathsDrawing(wxCommandEvent& evt)
|
void CinemaSidebar::SetPathsDrawing(const bool enable)
|
||||||
{
|
{
|
||||||
POST_COMMAND(SetCinemaPathsDrawing, (evt.IsChecked()));
|
POST_COMMAND(SetCinemaPathsDrawing, (enable));
|
||||||
}
|
}
|
||||||
|
|
||||||
void CinemaSidebar::OnAddPath(wxCommandEvent&)
|
void CinemaSidebar::AddPath(wxString name)
|
||||||
{
|
{
|
||||||
if (m_NewPathName->GetValue().empty())
|
if (name.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
POST_COMMAND(AddCinemaPath, (m_NewPathName->GetValue().ToStdWstring()));
|
POST_COMMAND(AddCinemaPath, (name.ToStdWstring()));
|
||||||
m_NewPathName->Clear();
|
|
||||||
ReloadPathList();
|
ReloadPathList();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CinemaSidebar::OnDeletePath(wxCommandEvent&)
|
void CinemaSidebar::DeleteSelectedPath()
|
||||||
{
|
{
|
||||||
int index = m_PathList->GetSelection();
|
int index = m_PathList->GetSelection();
|
||||||
if (index < 0)
|
if (index < 0)
|
||||||
|
|
@ -148,9 +143,3 @@ void CinemaSidebar::ReloadPathList()
|
||||||
|
|
||||||
m_PathList->SetStringSelection(selection);
|
m_PathList->SetStringSelection(selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
wxBEGIN_EVENT_TABLE(CinemaSidebar, Sidebar)
|
|
||||||
EVT_CHECKBOX(ID_PathsDrawing, CinemaSidebar::OnTogglePathsDrawing)
|
|
||||||
EVT_BUTTON(ID_AddPath, CinemaSidebar::OnAddPath)
|
|
||||||
EVT_BUTTON(ID_DeletePath, CinemaSidebar::OnDeletePath)
|
|
||||||
wxEND_EVENT_TABLE();
|
|
||||||
|
|
|
||||||
|
|
@ -37,15 +37,12 @@ protected:
|
||||||
void OnFirstDisplay() override;
|
void OnFirstDisplay() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void OnTogglePathsDrawing(wxCommandEvent& evt);
|
void SetPathsDrawing(const bool enable);
|
||||||
void OnAddPath(wxCommandEvent& evt);
|
void AddPath(wxString name);
|
||||||
void OnDeletePath(wxCommandEvent& evt);
|
void DeleteSelectedPath();
|
||||||
|
|
||||||
void ReloadPathList();
|
void ReloadPathList();
|
||||||
|
|
||||||
wxCheckBox* m_DrawPath;
|
wxCheckBox* m_DrawPath;
|
||||||
wxListBox* m_PathList;
|
wxListBox* m_PathList;
|
||||||
wxTextCtrl* m_NewPathName;
|
|
||||||
|
|
||||||
wxDECLARE_EVENT_TABLE();
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,7 @@ public:
|
||||||
wxGridSizer* gridSizer = new wxGridSizer(3, 5, 5);
|
wxGridSizer* gridSizer = new wxGridSizer(3, 5, 5);
|
||||||
wxButton* cameraSet = new wxButton(cameraSizer->GetStaticBox(), ID_CameraSet, _("Set"), wxDefaultPosition, wxSize(48, -1));
|
wxButton* cameraSet = new wxButton(cameraSizer->GetStaticBox(), ID_CameraSet, _("Set"), wxDefaultPosition, wxSize(48, -1));
|
||||||
gridSizer->Add(Tooltipped(cameraSet,
|
gridSizer->Add(Tooltipped(cameraSet,
|
||||||
_("Set player camera to cameraSizer->GetStaticBox() view")), wxSizerFlags().Expand());
|
_("Set player camera to this view")), wxSizerFlags().Expand());
|
||||||
wxButton* cameraView = new wxButton(cameraSizer->GetStaticBox(), ID_CameraView, _("View"), wxDefaultPosition, wxSize(48, -1));
|
wxButton* cameraView = new wxButton(cameraSizer->GetStaticBox(), ID_CameraView, _("View"), wxDefaultPosition, wxSize(48, -1));
|
||||||
cameraView->Enable(false);
|
cameraView->Enable(false);
|
||||||
gridSizer->Add(Tooltipped(cameraView,
|
gridSizer->Add(Tooltipped(cameraView,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright (C) 2012 Wildfire Games.
|
/* Copyright (C) 2026 Wildfire Games.
|
||||||
* This file is part of 0 A.D.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -41,7 +41,8 @@ struct GameLoopState
|
||||||
struct Input
|
struct Input
|
||||||
{
|
{
|
||||||
float scrollSpeed[6]; // [fwd, bwd, left, right, cw-rotation, ccw-rotation]. 0.0f for disabled.
|
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;
|
} 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)
|
MESSAGEHANDLER(LookAt)
|
||||||
{
|
{
|
||||||
// TODO: different camera depending on msg->view
|
// TODO: different camera depending on msg->view
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,35 @@ bool InputProcessor::ProcessInput(GameLoopState* state)
|
||||||
moved = true;
|
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)
|
if (moved)
|
||||||
{
|
{
|
||||||
camera.UpdateFrustum();
|
camera.UpdateFrustum();
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue