mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
Compare commits
36 commits
6a198e1c47
...
c4e7364802
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4e7364802 | ||
|
|
867bdf318b | ||
|
|
e64a138f92 | ||
|
|
4a7b7e7df1 | ||
|
|
fa90ddc8b6 | ||
|
|
764a420797 | ||
|
|
8a2bb10827 | ||
|
|
af9d102126 | ||
|
|
822092fcb4 | ||
|
|
6e79dc2f70 | ||
|
|
6fdcd68172 | ||
|
|
e4f4030a5e | ||
|
|
2fb6347125 | ||
|
|
7aa756f6a4 | ||
|
|
e712601602 | ||
|
|
865ca4cd55 | ||
|
|
dcb7cef308 | ||
|
|
71be79791f | ||
|
|
9ab89b01c2 | ||
|
|
dcce6666ae | ||
|
|
202bdb133a | ||
|
|
46981d2b90 | ||
|
|
49c887b7ca | ||
|
|
91a37e32f6 | ||
|
|
139b08fe6d | ||
|
|
dc6da217fd | ||
|
|
e41dd2d3a4 | ||
|
|
c7953026dd | ||
|
|
0340cc9abd | ||
|
|
aeeda05433 | ||
|
|
0da1e0c398 | ||
|
|
dc1e7179a3 | ||
|
|
b6d9d87ba9 | ||
|
|
839226d542 | ||
|
|
4125e5b645 | ||
|
|
f781181899 |
119 changed files with 2298 additions and 893 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'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,17 @@ jobs:
|
||||||
build:
|
build:
|
||||||
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
|
- id: restore-pip-cache
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v6
|
||||||
with:
|
with:
|
||||||
key: pip-cache-v1-${{ github.workflow }}
|
key: pip-cache-v1-${{ github.workflow }}
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
- uses: pre-commit/action@v3.0.1
|
- uses: pre-commit/action@v3.0.1
|
||||||
- uses: actions/cache/save@v4
|
- uses: actions/cache/save@v6
|
||||||
if: steps.restore-pip-cache.outcome == 'success'
|
if: steps.restore-pip-cache.outcome == 'success'
|
||||||
with:
|
with:
|
||||||
key: pip-cache-v1-${{ github.workflow }}
|
key: pip-cache-v1-${{ github.workflow }}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,9 @@
|
||||||
<optional>
|
<optional>
|
||||||
<attribute name="define"/>
|
<attribute name="define"/>
|
||||||
</optional>
|
</optional>
|
||||||
|
<optional>
|
||||||
|
<attribute name="srgb"/>
|
||||||
|
</optional>
|
||||||
</element>
|
</element>
|
||||||
</zeroOrMore>
|
</zeroOrMore>
|
||||||
</interleave>
|
</interleave>
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,18 @@
|
||||||
</textures>
|
</textures>
|
||||||
</variant>
|
</variant>
|
||||||
</group>
|
</group>
|
||||||
|
<group>
|
||||||
|
<variant name="ungarrisoned" frequency="1" />
|
||||||
|
<variant name="garrisoned">
|
||||||
|
<props>
|
||||||
|
<prop
|
||||||
|
actor="props/special/common/garrison_flag_achaemenids.xml"
|
||||||
|
attachpoint="garrisoned"
|
||||||
|
selectable="false"
|
||||||
|
/>
|
||||||
|
</props>
|
||||||
|
</variant>
|
||||||
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<variant name="alive" frequency="1"/>
|
<variant name="alive" frequency="1"/>
|
||||||
<variant file="structures/destruction_small.xml"/>
|
<variant file="structures/destruction_small.xml"/>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,18 @@
|
||||||
</textures>
|
</textures>
|
||||||
</variant>
|
</variant>
|
||||||
</group>
|
</group>
|
||||||
|
<group>
|
||||||
|
<variant frequency="1" name="ungarrisoned" />
|
||||||
|
<variant name="garrisoned">
|
||||||
|
<props>
|
||||||
|
<prop
|
||||||
|
actor="props/special/common/garrison_flag_celt.xml"
|
||||||
|
attachpoint="garrisoned"
|
||||||
|
selectable="false"
|
||||||
|
/>
|
||||||
|
</props>
|
||||||
|
</variant>
|
||||||
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<variant name="alive" frequency="1"/>
|
<variant name="alive" frequency="1"/>
|
||||||
<variant file="structures/destruction_small.xml"/>
|
<variant file="structures/destruction_small.xml"/>
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,18 @@
|
||||||
</textures>
|
</textures>
|
||||||
</variant>
|
</variant>
|
||||||
</group>
|
</group>
|
||||||
|
<group>
|
||||||
|
<variant name="ungarrisoned" frequency="1" />
|
||||||
|
<variant name="garrisoned">
|
||||||
|
<props>
|
||||||
|
<prop
|
||||||
|
actor="props/special/common/garrison_flag_celt.xml"
|
||||||
|
attachpoint="garrisoned"
|
||||||
|
selectable="false"
|
||||||
|
/>
|
||||||
|
</props>
|
||||||
|
</variant>
|
||||||
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<variant name="alive" frequency="1"/>
|
<variant name="alive" frequency="1"/>
|
||||||
<variant file="structures/destruction_small.xml"/>
|
<variant file="structures/destruction_small.xml"/>
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,20 @@
|
||||||
</props>
|
</props>
|
||||||
</variant>
|
</variant>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<variant frequency="1" name="ungarrisoned" />
|
||||||
|
<variant name="garrisoned">
|
||||||
|
<props>
|
||||||
|
<prop
|
||||||
|
actor="props/special/common/garrison_flag_germ.xml"
|
||||||
|
attachpoint="garrisoned"
|
||||||
|
selectable="false"
|
||||||
|
/>
|
||||||
|
</props>
|
||||||
|
</variant>
|
||||||
|
</group>
|
||||||
|
|
||||||
<group>
|
<group>
|
||||||
<variant name="alive" frequency="1"/>
|
<variant name="alive" frequency="1"/>
|
||||||
<variant file="structures/destruction_small.xml"/>
|
<variant file="structures/destruction_small.xml"/>
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,18 @@
|
||||||
</props>
|
</props>
|
||||||
</variant>
|
</variant>
|
||||||
</group>
|
</group>
|
||||||
|
<group>
|
||||||
|
<variant name="ungarrisoned" frequency="1" />
|
||||||
|
<variant name="garrisoned">
|
||||||
|
<props>
|
||||||
|
<prop
|
||||||
|
actor="props/special/common/garrison_flag_iber.xml"
|
||||||
|
attachpoint="garrisoned"
|
||||||
|
selectable="false"
|
||||||
|
/>
|
||||||
|
</props>
|
||||||
|
</variant>
|
||||||
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<variant name="alive" frequency="1"/>
|
<variant name="alive" frequency="1"/>
|
||||||
<variant file="structures/destruction_small.xml"/>
|
<variant file="structures/destruction_small.xml"/>
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,18 @@
|
||||||
</textures>
|
</textures>
|
||||||
</variant>
|
</variant>
|
||||||
</group>
|
</group>
|
||||||
|
<group>
|
||||||
|
<variant name="ungarrisoned" frequency="1" />
|
||||||
|
<variant name="garrisoned">
|
||||||
|
<props>
|
||||||
|
<prop
|
||||||
|
actor="props/special/common/garrison_flag_rome.xml"
|
||||||
|
attachpoint="garrisoned"
|
||||||
|
selectable="false"
|
||||||
|
/>
|
||||||
|
</props>
|
||||||
|
</variant>
|
||||||
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<variant name="alive" frequency="1"/>
|
<variant name="alive" frequency="1"/>
|
||||||
<variant file="structures/destruction_small.xml"/>
|
<variant file="structures/destruction_small.xml"/>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,18 @@
|
||||||
</textures>
|
</textures>
|
||||||
</variant>
|
</variant>
|
||||||
</group>
|
</group>
|
||||||
|
<group>
|
||||||
|
<variant frequency="1" name="ungarrisoned" />
|
||||||
|
<variant name="garrisoned">
|
||||||
|
<props>
|
||||||
|
<prop
|
||||||
|
actor="props/special/common/garrison_flag_seleucids.xml"
|
||||||
|
attachpoint="garrisoned"
|
||||||
|
selectable="false"
|
||||||
|
/>
|
||||||
|
</props>
|
||||||
|
</variant>
|
||||||
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<variant name="alive" frequency="1"/>
|
<variant name="alive" frequency="1"/>
|
||||||
<variant file="structures/sele/light_damage.xml"/>
|
<variant file="structures/sele/light_damage.xml"/>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:995da95e21261ae0a2718baee69d76ae5f6823d21eaefaa708a89651f2099bb0
|
oid sha256:ed243a229225ec51ee090588a63307cbd2efedecca06f4f1a0922a4d51ee3967
|
||||||
size 586243
|
size 586427
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d8e022a3ac132a32061583743d1322fc8792cdd6e7be57857547a8ed3cefbed6
|
oid sha256:3ce649aa943771357d7c19f48294af719c50ca14178c32be60992f17845d685d
|
||||||
size 525300
|
size 525483
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:21971f20f5567f820e0f3fffdd21015e2a5ffaee309be7b6efc38d3fc56aecd3
|
oid sha256:d1d867c2e9f3db233847da547c466325ba9f0a9e7478175281f3538cbb694709
|
||||||
size 503936
|
size 504112
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3c37b1b7f12a429cd07873d0fdb7cc8692f6578b3e5320a6be0e68420fb4688e
|
oid sha256:cc9c95992889a4c2930a39bd20f7c86b73f7c7ddb3b36492650afae2ae424641
|
||||||
size 780868
|
size 781051
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1230a92373c4e4684f791594efb35bb7d6c091377a11bd8f0daafd13816d04ec
|
oid sha256:6779d6ca2067d6dd5594b3d2e5ef82d08de97ad058ce7084eb0bf8ae3290f4c2
|
||||||
size 56755
|
size 56953
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:30a395c3b5a50214a689abe968ad15edb60212c3ea14041a2edc07295a4462c6
|
oid sha256:9e6aca91d66df31b5820f39025c57937ce6a1593199b8a068722fdec5b0675f6
|
||||||
size 45317
|
size 45523
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ce554ad452cf75bd4dbb9da5de9830309a1dccf72f72f6a74475078af7f0e1c4
|
oid sha256:07013561c7e46b020acbd03d4f371300c38d3e364c99a1e7bbcf7467aa8a34e6
|
||||||
size 42851
|
size 43041
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:60f0614c1092395065c4ba8fe6bed64da873d38c579c9cbc400e9a9b8f30f3a8
|
oid sha256:1416efa429cd64471d50e969c43ee723382c031c676414433f8fcd58176cfa77
|
||||||
size 272099
|
size 272274
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c6e7d6264d7b3e03466bf1de58ab366a023120c6f78142aebc6570218a4803d9
|
oid sha256:9f9e6debd5f2a9659e19e74b5826d3488167edb9a897ae1ed6e4db10742d94e8
|
||||||
size 274339
|
size 274543
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0624fc07116aa70c14527457fced3afcf7b36df1535bbf10250455e6254aeed6
|
oid sha256:091d68b7800d9141e87ba8aaf0fa93c5bc7d260dd459774075de610ca34855e2
|
||||||
size 372821
|
size 372997
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6ef199b28b9d9ef19e0817b189d9e6f1e2f9861ca211890b8d714f9fe05f5c61
|
oid sha256:c1413de5371443f1249d87fa40a821c880c749513a0347d9128a99472bec7b33
|
||||||
size 420601
|
size 420808
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:adf219c08e65bd7a149726d325b6f2b4fd67effc3dd1bc211c3163774ac8308e
|
oid sha256:8aba3f3dc5a909ddadd9af5e7abfe9094f55ed6c97b39e7f97b09dc1cb951f4d
|
||||||
size 201143
|
size 201315
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f8c688df60c4880dc92b6898728d4cef443c86831a2a2e87e61dd44661b58240
|
oid sha256:0c3298efebf687a07d4693d9014c51e1e870e95fb9d9024c9126b40adfec75a5
|
||||||
size 205000
|
size 205172
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9d86be00b7e911108e6666422a56eb061a996e6cf0bfe489d6c99f84be752444
|
oid sha256:b97f6dff13f4639e746e1ce79c120169bccb2e09d4507dc900075cf85e788590
|
||||||
size 216525
|
size 216697
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5e258d0b3a7b5c3fe2275c2816c636b7478ee1051df31a2cbe2f516465c692f9
|
oid sha256:8d764a2d5a712f53f08560504f6841ed00b5268fea2a283d2d5254d57ce1df76
|
||||||
size 239166
|
size 239339
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:82ff53473c25d6e17aeb97f795b543df3ec1c423e8ecb8398d0d014beac57f4e
|
oid sha256:dccc1521e6e626e75ac2f05d2c8b2c2963ee751da08a9e819e4c28954b19f01a
|
||||||
size 101895
|
size 102099
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2377bddedc6251acbe8f03bf1820d368b21cbf88fc2783695ff31036af8ed4e3
|
oid sha256:ae1042461639bca3d3d73fb3e723ca404c8a897ee0e2ad417af1eebb5849d23f
|
||||||
size 82812
|
size 83027
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b5f1530adecb42af05b6b81719a5c4d00fa06a5eff33f50eda8312ce2e0fcd5f
|
oid sha256:b71b8848a0a2e503adbff9c1e09c06c1e76e16537f38af5b35395a04f11b365a
|
||||||
size 77866
|
size 78066
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c393e007e24473c1332c7d0c3a8093e0b542974da00b51b853ee413e45818ade
|
oid sha256:256b3864edc754955814d5eec113e73fded49525d2e855a1171a4e4b495ffcc4
|
||||||
size 91109
|
size 91304
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e8fb0f356f48284704d81b601c7947f32e91995757e8c2fc390ecaf772fbd950
|
oid sha256:4ef0415cce15f4d1ff4e342c23a995ee5081d21b7f84eb6c19fb89c1d63f84a7
|
||||||
size 84604
|
size 84789
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7c2b0d0a461535e0802d2a07395afb4546515ef4d3ccc3eab18d55a0993bc165
|
oid sha256:2ab540ad518de662ba9a4dbe4d0d0590b6ee0b771bc482073296c949ee33d571
|
||||||
size 184407
|
size 184610
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:828e2e63b9686a601b3641be10f26e3b099ced5b8dc8d453ab94d41270f22b39
|
oid sha256:39a6a575b8bbb0f82ed8cf4056e0df932a59fd6e3343005625b3b76644c6879c
|
||||||
size 213385
|
size 213561
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4825c81a245ddae5637f3b2dee78f7749e27a48d2efa244bc95af075ddabcd1a
|
oid sha256:93341f17ad185e28ce62a533f1db457aec4b69bb2f67c98e4b89e4bcf9503b6a
|
||||||
size 144598
|
size 144784
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:427f9512af696afaae0721374a86ceb0e1432602013cb3dde818bfcd2791c163
|
oid sha256:dc27dba302816891bb8f33bc65d9d68b2b330f946256aa966e5d684714af9fe2
|
||||||
size 212264
|
size 212447
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:19ca2828e5e6b99065d3d901c1a90f1b73df065dc94b3a4b76b4e5804890c137
|
oid sha256:270fe9095c3894fb448cb91668ad48d886088006c689f0efa148e8a78a3de043
|
||||||
size 349847
|
size 350053
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:57039fff36fbba71de79b89614fea988a98bdc85b2bdc5ef88ce3a632da95a55
|
oid sha256:977b2ffb5dc9cf0364d125f0b3dd17e1fb0cd482b5c96fbf7c61b6e6a030db86
|
||||||
size 340693
|
size 340906
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:221eeca4ee2e46023d346e80c7a3b4b3852699e0c193e31fa58e0619e858f7a2
|
oid sha256:f177172ef923455050064af60377f2e1b171ff4ec72fa9aa5fdc5c4c28000550
|
||||||
size 272252
|
size 272435
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:afc01d8e7e7efc988b2f78b58cfb6b48b5453b8786609e9ce5db38bc617a7984
|
oid sha256:acdaaf09848dc5cb7cf207c7bb535865aa4199a5a50609ca3b632ce5d88d95a2
|
||||||
size 279942
|
size 280139
|
||||||
|
|
|
||||||
|
|
@ -330,3 +330,20 @@ Math.euclidDistance3D = function(x1, y1, z1, x2, y2, z2)
|
||||||
{
|
{
|
||||||
return Math.sqrt(Math.euclidDistance3DSquared(x1, y1, z1, x2, y2, z2));
|
return Math.sqrt(Math.euclidDistance3DSquared(x1, y1, z1, x2, y2, z2));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clamps a value between a minimum and maximum range.
|
||||||
|
*
|
||||||
|
* @param {number} value - The value to clamp
|
||||||
|
* @param {number} min - Minimum allowed value
|
||||||
|
* @param {number} max - Maximum allowed value
|
||||||
|
* @returns {number} The clamped value (min <= result <= max)
|
||||||
|
*/
|
||||||
|
Math.clamp = function(value, min, max)
|
||||||
|
{
|
||||||
|
if (value < min)
|
||||||
|
return min;
|
||||||
|
if (value > max)
|
||||||
|
return max;
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -442,3 +442,17 @@ Vector3D.div = function(v, f)
|
||||||
{
|
{
|
||||||
return new Vector3D(v.x / f, v.y / f, v.z / f);
|
return new Vector3D(v.x / f, v.y / f, v.z / f);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linear interpolation between this vector and another.
|
||||||
|
* @param {Vector2D} v - The target vector.
|
||||||
|
* @param {number} t - Interpolation factor (0 = this, 1 = v).
|
||||||
|
* @returns {Vector2D} A new vector at the interpolated position.
|
||||||
|
*/
|
||||||
|
Vector2D.prototype.lerp = function(v, t)
|
||||||
|
{
|
||||||
|
return new Vector2D(
|
||||||
|
this.x + (v.x - this.x) * t,
|
||||||
|
this.y + (v.y - this.y) * t
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@
|
||||||
"List": [
|
"List": [
|
||||||
{ "nick": "Alexandermb", "name": "Daniel Morgado" },
|
{ "nick": "Alexandermb", "name": "Daniel Morgado" },
|
||||||
{ "nick": "Athos" },
|
{ "nick": "Athos" },
|
||||||
|
{ "nick": "Atrik" },
|
||||||
{ "nick": "Basshunter", "name": "Sebastián Gómez" },
|
{ "nick": "Basshunter", "name": "Sebastián Gómez" },
|
||||||
{ "nick": "BigTiger", "name": "Nathanael P. Moore" },
|
{ "nick": "BigTiger", "name": "Nathanael P. Moore" },
|
||||||
{ "nick": "Brightgalrs", "name": "Robert D. Schultz" },
|
{ "nick": "Brightgalrs", "name": "Robert D. Schultz" },
|
||||||
|
|
@ -80,6 +81,7 @@
|
||||||
{ "nick": "Enrique", "name": "Enrique Keykens Espolio" },
|
{ "nick": "Enrique", "name": "Enrique Keykens Espolio" },
|
||||||
{ "nick": "Gen.Kenobi", "name": "Daniel Schubert" },
|
{ "nick": "Gen.Kenobi", "name": "Daniel Schubert" },
|
||||||
{ "nick": "Historicity", "name": "Shane" },
|
{ "nick": "Historicity", "name": "Shane" },
|
||||||
|
{ "nick": "Langbart" },
|
||||||
{ "nick": "Leyto", "name": "Valentin Levalet" },
|
{ "nick": "Leyto", "name": "Valentin Levalet" },
|
||||||
{ "nick": "LordGood", "name": "Johnathan B. Good" },
|
{ "nick": "LordGood", "name": "Johnathan B. Good" },
|
||||||
{ "nick": "m7600" },
|
{ "nick": "m7600" },
|
||||||
|
|
|
||||||
|
|
@ -507,6 +507,49 @@ var unitFilters = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Choose, inside a list of entities, which ones are targetable foes for boxtargeting.
|
||||||
|
// We also filter out entities that aren't visible, or low priority like livestocks
|
||||||
|
function getTargetableEntities(ents)
|
||||||
|
{
|
||||||
|
const player = g_ViewedPlayer;
|
||||||
|
const simState = GetSimState();
|
||||||
|
const isEnemy = simState.players[player].isEnemy;
|
||||||
|
|
||||||
|
// Filter valid target candidates
|
||||||
|
const candidates = ents.filter(entity =>
|
||||||
|
{
|
||||||
|
const entState = GetEntityState(entity);
|
||||||
|
return entState &&
|
||||||
|
unitFilters.isUnit(entity) &&
|
||||||
|
entState.visibility != "hidden";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enemy players
|
||||||
|
const enemyPlayers = candidates.filter(entity =>
|
||||||
|
{
|
||||||
|
const entState = GetEntityState(entity);
|
||||||
|
return isEnemy[entState.player] && entState.player !== 0 && !hasClass(entState, "Domestic");
|
||||||
|
});
|
||||||
|
if (enemyPlayers.length > 0)
|
||||||
|
return enemyPlayers;
|
||||||
|
|
||||||
|
// Gaia
|
||||||
|
const gaiaUnits = candidates.filter(entity =>
|
||||||
|
{
|
||||||
|
const entState = GetEntityState(entity);
|
||||||
|
return entState.player === 0;
|
||||||
|
});
|
||||||
|
if (gaiaUnits.length > 0)
|
||||||
|
return gaiaUnits;
|
||||||
|
|
||||||
|
// Domestic animals
|
||||||
|
return candidates.filter(entity =>
|
||||||
|
{
|
||||||
|
const entState = GetEntityState(entity);
|
||||||
|
return hasClass(entState, "Domestic");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Choose, inside a list of entities, which ones will be selected.
|
// Choose, inside a list of entities, which ones will be selected.
|
||||||
// We may use several entity filters, until one returns at least one element.
|
// We may use several entity filters, until one returns at least one element.
|
||||||
function getPreferredEntities(ents)
|
function getPreferredEntities(ents)
|
||||||
|
|
@ -572,16 +615,19 @@ function handleInputBeforeGui(ev, hoveredObject)
|
||||||
case INPUT_BANDBOXING:
|
case INPUT_BANDBOXING:
|
||||||
{
|
{
|
||||||
const bandbox = Engine.GetGUIObjectByName("bandbox");
|
const bandbox = Engine.GetGUIObjectByName("bandbox");
|
||||||
|
const isAttackMode = Engine.HotkeyIsPressed("session.attack") && !g_IsObserver;
|
||||||
switch (ev.type)
|
switch (ev.type)
|
||||||
{
|
{
|
||||||
case "mousemotion":
|
case "mousemotion":
|
||||||
{
|
{
|
||||||
const rect = updateBandbox(bandbox, ev, false);
|
const rect = updateBandbox(bandbox, ev, false);
|
||||||
|
|
||||||
const ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer);
|
const player = isAttackMode ? -1 : g_ViewedPlayer;
|
||||||
const preferredEntities = getPreferredEntities(ents);
|
|
||||||
g_Selection.setHighlightList(preferredEntities);
|
|
||||||
|
|
||||||
|
const ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], player);
|
||||||
|
const highlightEntities = isAttackMode ? getTargetableEntities(ents) : getPreferredEntities(ents);
|
||||||
|
|
||||||
|
g_Selection.setHighlightList(highlightEntities);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -589,6 +635,30 @@ function handleInputBeforeGui(ev, hoveredObject)
|
||||||
if (ev.button == SDL_BUTTON_LEFT)
|
if (ev.button == SDL_BUTTON_LEFT)
|
||||||
{
|
{
|
||||||
const rect = updateBandbox(bandbox, ev, true);
|
const rect = updateBandbox(bandbox, ev, true);
|
||||||
|
if (isAttackMode)
|
||||||
|
{
|
||||||
|
// Box attack behavior
|
||||||
|
const ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], -1);
|
||||||
|
|
||||||
|
// Filter to only enemy units
|
||||||
|
const enemyUnits = getTargetableEntities(ents);
|
||||||
|
|
||||||
|
// Get currently selected friendly units
|
||||||
|
const selectedUnits = g_Selection.toList();
|
||||||
|
|
||||||
|
if (selectedUnits.length && enemyUnits.length)
|
||||||
|
{
|
||||||
|
// Sort entities by position
|
||||||
|
const sortedCombattants = sortEntitiesForEngagement(selectedUnits, enemyUnits);
|
||||||
|
// Distribute attack orders
|
||||||
|
distributeAttackOrders(sortedCombattants.attackers, sortedCombattants.targets);
|
||||||
|
}
|
||||||
|
g_Selection.setHighlightList([]);
|
||||||
|
inputState = INPUT_NORMAL;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer));
|
const ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer));
|
||||||
g_Selection.setHighlightList([]);
|
g_Selection.setHighlightList([]);
|
||||||
|
|
||||||
|
|
@ -1371,6 +1441,7 @@ function positionUnitsFreehandSelectionMouseUp(ev)
|
||||||
for (let i = 1; i < inputLine.length; ++i)
|
for (let i = 1; i < inputLine.length; ++i)
|
||||||
lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]);
|
lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]);
|
||||||
|
|
||||||
|
// Filter units that can move
|
||||||
const selection = g_Selection.filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b);
|
const selection = g_Selection.filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b);
|
||||||
|
|
||||||
// Checking the line for a minimum length to save performance.
|
// Checking the line for a minimum length to save performance.
|
||||||
|
|
@ -1380,39 +1451,43 @@ function positionUnitsFreehandSelectionMouseUp(ev)
|
||||||
return !!action && doAction(action, ev);
|
return !!action && doAction(action, ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Even distribution of the units on the line.
|
// Sort units by their projection onto the line direction (parallel sorting)
|
||||||
let p0 = inputLine[0];
|
const sortedSelection = sortEntitiesAlongLine(selection, inputLine[0], inputLine[inputLine.length - 1], true);
|
||||||
let entityDistribution = [p0];
|
|
||||||
const distanceBetweenEnts = lengthOfLine / (selection.length - 1);
|
|
||||||
let freeDist = -distanceBetweenEnts;
|
|
||||||
|
|
||||||
for (let i = 1; i < inputLine.length; ++i)
|
const selectionCount = sortedSelection.length;
|
||||||
|
const targetPositions = [];
|
||||||
|
|
||||||
|
// Distribute units evenly along the polyline
|
||||||
|
for (let i = 0; i < selectionCount; ++i)
|
||||||
{
|
{
|
||||||
const p1 = inputLine[i];
|
const t = i / (selectionCount - 1);
|
||||||
freeDist += inputLine[i - 1].distanceTo(p1);
|
const targetDistance = t * lengthOfLine;
|
||||||
|
|
||||||
while (freeDist >= 0)
|
let accumulated = 0;
|
||||||
|
let targetPoint = inputLine[0];
|
||||||
|
for (let j = 1; j < inputLine.length; ++j)
|
||||||
{
|
{
|
||||||
p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1);
|
const segStart = inputLine[j - 1];
|
||||||
entityDistribution.push(p0);
|
const segEnd = inputLine[j];
|
||||||
freeDist -= distanceBetweenEnts;
|
const segLength = segEnd.distanceTo(segStart);
|
||||||
|
|
||||||
|
if (accumulated + segLength >= targetDistance)
|
||||||
|
{
|
||||||
|
// Interpolate within this segment
|
||||||
|
const remaining = targetDistance - accumulated;
|
||||||
|
const frac = remaining / segLength;
|
||||||
|
targetPoint = segStart.lerp(segEnd, frac);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
accumulated += segLength;
|
||||||
}
|
}
|
||||||
|
targetPositions.push(targetPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rounding errors can lead to missing or too many points.
|
|
||||||
entityDistribution = entityDistribution.slice(0, selection.length);
|
|
||||||
entityDistribution = entityDistribution.concat(new Array(selection.length - entityDistribution.length).fill(inputLine[inputLine.length - 1]));
|
|
||||||
|
|
||||||
if (Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[0]) +
|
|
||||||
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[selection.length - 1]) >
|
|
||||||
Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[selection.length - 1]) +
|
|
||||||
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[0]))
|
|
||||||
entityDistribution.reverse();
|
|
||||||
|
|
||||||
Engine.PostNetworkCommand({
|
Engine.PostNetworkCommand({
|
||||||
"type": isAttackMovePressed() ? "attack-walk-custom" : "walk-custom",
|
"type": isAttackMovePressed() ? "attack-walk-custom" : "walk-custom",
|
||||||
"entities": selection,
|
"entities": sortedSelection,
|
||||||
"targetPositions": entityDistribution.map(pos => pos.toFixed(2)),
|
"targetPositions": targetPositions.map(pos => pos.toFixed(2)),
|
||||||
"targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] },
|
"targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] },
|
||||||
"queued": Engine.HotkeyIsPressed("session.queue"),
|
"queued": Engine.HotkeyIsPressed("session.queue"),
|
||||||
"pushFront": Engine.HotkeyIsPressed("session.pushorderfront"),
|
"pushFront": Engine.HotkeyIsPressed("session.pushorderfront"),
|
||||||
|
|
@ -1420,9 +1495,10 @@ function positionUnitsFreehandSelectionMouseUp(ev)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add target markers with a minimum distance of 5 to each other.
|
// Add target markers with a minimum distance of 5 to each other.
|
||||||
const entitiesBetweenMarker = Math.ceil(5 / distanceBetweenEnts);
|
const stepDistance = lengthOfLine / (selectionCount - 1);
|
||||||
for (let i = 0; i < entityDistribution.length; i += entitiesBetweenMarker)
|
const entitiesBetweenMarker = Math.max(1, Math.ceil(5 / stepDistance));
|
||||||
DrawTargetMarker({ "x": entityDistribution[i].x, "z": entityDistribution[i].y });
|
for (let i = 0; i < targetPositions.length; i += entitiesBetweenMarker)
|
||||||
|
DrawTargetMarker({ "x": targetPositions[i].x, "z": targetPositions[i].y });
|
||||||
|
|
||||||
Engine.GuiInterfaceCall("PlaySound", {
|
Engine.GuiInterfaceCall("PlaySound", {
|
||||||
"name": "order_walk",
|
"name": "order_walk",
|
||||||
|
|
@ -1929,3 +2005,108 @@ function clearSelection()
|
||||||
g_Selection.reset();
|
g_Selection.reset();
|
||||||
preSelectedAction = ACTION_NONE;
|
preSelectedAction = ACTION_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function distributeAttackOrders(attackers, targets)
|
||||||
|
{
|
||||||
|
if (targets.length < 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Play attack sounds from a sample of units in selection
|
||||||
|
const soundCount = Math.min(3, attackers.length);
|
||||||
|
for (let i = 0; i < soundCount; i++)
|
||||||
|
{
|
||||||
|
const t = soundCount === 1 ? 0 : i / (soundCount - 1);
|
||||||
|
const attackerIndex = Math.floor(t * (attackers.length - 1));
|
||||||
|
|
||||||
|
setTimeout(() =>
|
||||||
|
{
|
||||||
|
Engine.GuiInterfaceCall("PlaySound", {
|
||||||
|
"name": "order_attack",
|
||||||
|
"entity": attackers[attackerIndex]
|
||||||
|
});
|
||||||
|
}, i * 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
Engine.PostNetworkCommand({
|
||||||
|
"type": "attack-group",
|
||||||
|
"entities": attackers,
|
||||||
|
"targets": targets,
|
||||||
|
"queued": Engine.HotkeyIsPressed("session.queue"),
|
||||||
|
"allowCapture": false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts attackers and targets for left-to-right engagement across the battle line.
|
||||||
|
*
|
||||||
|
* Entities are sorted by their perpendicular distance to the line connecting the
|
||||||
|
* two army centers, creating natural pairings across the battle front.
|
||||||
|
*
|
||||||
|
* @param {number[]} attackers - Array of attacking entity IDs.
|
||||||
|
* @param {number[]} targets - Array of target entity IDs.
|
||||||
|
* @returns {Object} { attackers: number[], targets: number[] } - Both arrays sorted
|
||||||
|
* for cross-line engagement (targets automatically reversed).
|
||||||
|
*/
|
||||||
|
function sortEntitiesForEngagement(attackers, targets)
|
||||||
|
{
|
||||||
|
const getPosition = (id) => GetEntityState(id).position;
|
||||||
|
|
||||||
|
const computeAveragePosition = (entities) =>
|
||||||
|
{
|
||||||
|
if (entities.length === 0) return new Vector2D(0, 0);
|
||||||
|
const vectors = entities.map(id => Vector2D.from3D(getPosition(id)));
|
||||||
|
return Vector2D.average(vectors);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate the axis of engagement - the line between the groups' average positions
|
||||||
|
const avgAttackers = computeAveragePosition(attackers);
|
||||||
|
const avgTargets = computeAveragePosition(targets);
|
||||||
|
|
||||||
|
// Sort each group by perpendicular distance to the engagement line
|
||||||
|
const sortedAttackers = sortEntitiesAlongLine(attackers, avgAttackers, avgTargets);
|
||||||
|
const sortedTargets = sortEntitiesAlongLine(targets, avgAttackers, avgTargets);
|
||||||
|
|
||||||
|
// Reverse targets so the leftmost attacker pairs with leftmost target
|
||||||
|
return {
|
||||||
|
"attackers": sortedAttackers,
|
||||||
|
"targets": sortedTargets.reverse()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts entity IDs by their projection onto a line's direction or perpendicular axis.
|
||||||
|
*
|
||||||
|
* @param {number[]} entities - Array of entity IDs.
|
||||||
|
* @param {Vector2D} lineStart - Start point of the reference line.
|
||||||
|
* @param {Vector2D} lineEnd - End point of the reference line.
|
||||||
|
* @param {boolean} sortByDirection - If true, sort by parallel projection (along line);
|
||||||
|
* If false, sort by perpendicular distance (default).
|
||||||
|
* @returns {number[]} Sorted entity IDs.
|
||||||
|
*/
|
||||||
|
function sortEntitiesAlongLine(entities, lineStart, lineEnd, sortByDirection = false)
|
||||||
|
{
|
||||||
|
const dir = lineEnd.sub(lineStart);
|
||||||
|
const length = dir.length();
|
||||||
|
if (length === 0)
|
||||||
|
return entities.slice();
|
||||||
|
|
||||||
|
// Choose basis vector: either parallel or perpendicular to the line
|
||||||
|
let basis;
|
||||||
|
if (sortByDirection)
|
||||||
|
basis = dir.mult(1 / length); // Unit direction vector
|
||||||
|
else
|
||||||
|
basis = new Vector2D(-dir.y, dir.x).normalize(); // Perpendicular
|
||||||
|
|
||||||
|
const withCoord = entities.map(id =>
|
||||||
|
{
|
||||||
|
const pos = GetEntityState(id).position;
|
||||||
|
const point2D = Vector2D.from3D(pos);
|
||||||
|
const rel = point2D.sub(lineStart);
|
||||||
|
const coord = rel.dot(basis); // Project onto basis vector
|
||||||
|
return { id, coord };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by that coordinate
|
||||||
|
withCoord.sort((a, b) => a.coord - b.coord);
|
||||||
|
return withCoord.map(item => item.id);
|
||||||
|
}
|
||||||
|
|
@ -603,4 +603,147 @@ TriggerHelper.SpawnAndTurretAtClasses = function(playerID, classes, templates, c
|
||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes an entity permanently visible to all players by:
|
||||||
|
* - Setting alwaysVisible flag
|
||||||
|
* - Deactivating fogging (no mirages)
|
||||||
|
* - Exploring the surrounding area
|
||||||
|
* - Forcing a visibility update
|
||||||
|
*
|
||||||
|
* @param {number} ent - Entity ID to make permanently visible
|
||||||
|
*/
|
||||||
|
TriggerHelper.MakeEntityPermanentlyVisible = function(ent)
|
||||||
|
{
|
||||||
|
const cmpVisibility = Engine.QueryInterface(ent, IID_Visibility);
|
||||||
|
if (cmpVisibility)
|
||||||
|
{
|
||||||
|
cmpVisibility.alwaysVisible = true;
|
||||||
|
cmpVisibility.SetActivated(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmpFogging = Engine.QueryInterface(ent, IID_Fogging);
|
||||||
|
if (cmpFogging)
|
||||||
|
cmpFogging.PermanentlyReveal("all");
|
||||||
|
|
||||||
|
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||||
|
const cmpPosition = Engine.QueryInterface(ent, IID_Position);
|
||||||
|
if (cmpRangeManager && cmpPosition && cmpPosition.IsInWorld())
|
||||||
|
{
|
||||||
|
const pos = cmpPosition.GetPosition2D();
|
||||||
|
const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
|
||||||
|
|
||||||
|
const exploreRadius = TriggerHelper.CalculateExplorationRadius(ent);
|
||||||
|
|
||||||
|
for (let i = 1; i < numPlayers; ++i)
|
||||||
|
cmpRangeManager.ExploreCircle(i, pos.x, pos.y, exploreRadius);
|
||||||
|
|
||||||
|
cmpRangeManager.RequestVisibilityUpdate(ent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate a reasonable exploration radius from an entity's footprint.
|
||||||
|
* Calculates the radius as the entity's full extent to reveal the surrounding area.
|
||||||
|
* This is equivalent to (extent/2) * 2, giving a 2x margin around the entity.
|
||||||
|
*/
|
||||||
|
TriggerHelper.CalculateExplorationRadius = function(ent)
|
||||||
|
{
|
||||||
|
const cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
|
||||||
|
if (!cmpFootprint)
|
||||||
|
return 50;
|
||||||
|
|
||||||
|
const shapeInfo = cmpFootprint.GetShape();
|
||||||
|
|
||||||
|
let halfExtent; // The radius of the entity (half its size)
|
||||||
|
if (shapeInfo && shapeInfo.type === "circle")
|
||||||
|
halfExtent = shapeInfo.radius || shapeInfo.size0;
|
||||||
|
else if (shapeInfo && shapeInfo.type === "square")
|
||||||
|
halfExtent = Math.max(shapeInfo.width || shapeInfo.size0, shapeInfo.depth || shapeInfo.size1) / 2;
|
||||||
|
else
|
||||||
|
return 50;
|
||||||
|
|
||||||
|
// Use 2x the entity's radius to reveal surrounding area
|
||||||
|
// This gives a comfortable margin around the entity
|
||||||
|
const radius = halfExtent * 2;
|
||||||
|
return radius;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a notification to specific players.
|
||||||
|
*
|
||||||
|
* @param {string} message - The message to display (use markForTranslation)
|
||||||
|
* @param {number[]} players - Array of player IDs to send the notification to
|
||||||
|
* @param {number} duration - Duration in milliseconds
|
||||||
|
* @param {Object} parameters - Translation parameters (optional)
|
||||||
|
*/
|
||||||
|
TriggerHelper.SendNotification = function(message, players, duration = 30000, parameters = {}, translateParameters = [])
|
||||||
|
{
|
||||||
|
const cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
|
||||||
|
if (!cmpGuiInterface)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Ensure all parameter values are primitive types
|
||||||
|
const primitiveParameters = {};
|
||||||
|
for (const key in parameters)
|
||||||
|
{
|
||||||
|
const value = parameters[key];
|
||||||
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
||||||
|
primitiveParameters[key] = value;
|
||||||
|
else
|
||||||
|
primitiveParameters[key] = String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
cmpGuiInterface.AddTimeNotification(
|
||||||
|
{
|
||||||
|
"message": message,
|
||||||
|
"players": players,
|
||||||
|
"parameters": primitiveParameters,
|
||||||
|
"translateMessage": true,
|
||||||
|
"translateParameters": translateParameters
|
||||||
|
},
|
||||||
|
duration
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a notification to a specific player and a different notification to all other active players.
|
||||||
|
*
|
||||||
|
* @param {number} player - The player ID who gets the 'owner' message
|
||||||
|
* @param {string} ownerMessage - Message for the specified player (use markForTranslation)
|
||||||
|
* @param {string} othersMessage - Message for all other active players (use markForTranslation)
|
||||||
|
* @param {number} duration - Duration in milliseconds
|
||||||
|
* @param {Object} parameters - Translation parameters shared by both messages (optional)
|
||||||
|
* @param {Object} ownerParameters - Additional parameters for the owner message (optional)
|
||||||
|
* @param {Object} othersParameters - Additional parameters for the others message (optional)
|
||||||
|
*/
|
||||||
|
TriggerHelper.SendDualNotification = function(player, ownerMessage, othersMessage, duration = 30000, parameters = {}, ownerParameters = {}, othersParameters = {})
|
||||||
|
{
|
||||||
|
const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
|
||||||
|
|
||||||
|
const others = [-1]; // Include observers (player id -1)
|
||||||
|
for (let playerID = 1; playerID < numPlayers; ++playerID)
|
||||||
|
{
|
||||||
|
if (playerID == player)
|
||||||
|
continue;
|
||||||
|
const cmpPlayer = QueryPlayerIDInterface(playerID);
|
||||||
|
if (cmpPlayer && cmpPlayer.GetState() == "defeated")
|
||||||
|
continue;
|
||||||
|
others.push(playerID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedOwnerParams = Object.assign({}, parameters, ownerParameters);
|
||||||
|
const mergedOthersParams = Object.assign({}, parameters, othersParameters);
|
||||||
|
|
||||||
|
// Auto-detect translatable parameters (those starting with "_" or named "player")
|
||||||
|
const translateParameters = Object.keys(mergedOthersParams).filter(key => key.startsWith("_") || key == "player");
|
||||||
|
|
||||||
|
TriggerHelper.SendNotification(ownerMessage, [player], duration, mergedOwnerParams, translateParameters);
|
||||||
|
TriggerHelper.SendNotification(othersMessage, others, duration, mergedOthersParams, translateParameters);
|
||||||
|
};
|
||||||
|
|
||||||
|
TriggerHelper.SendWonderNotifications = function(owner, othersMessage, ownerMessage)
|
||||||
|
{
|
||||||
|
TriggerHelper.SendDualNotification(owner, ownerMessage, othersMessage, 30000, { "_player_": owner });
|
||||||
|
};
|
||||||
|
|
||||||
Engine.RegisterGlobal("TriggerHelper", TriggerHelper);
|
Engine.RegisterGlobal("TriggerHelper", TriggerHelper);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,42 @@
|
||||||
Trigger.prototype.WonderVictoryEntityRenamed = function(data)
|
Trigger.prototype.WonderVictoryEntityRenamed = function(data)
|
||||||
{
|
{
|
||||||
if (this.wonderVictoryMessages[data.entity] && Engine.QueryInterface(data.newentity, IID_Wonder))
|
if (!Engine.QueryInterface(data.newentity, IID_Wonder))
|
||||||
{
|
return;
|
||||||
// When an entity is renamed, we first create a new entity,
|
|
||||||
// which in case it is a wonder will receive a timer.
|
|
||||||
// However on a rename we want to use the timer from the old entity,
|
|
||||||
// so we need to remove the timer of the new entity.
|
|
||||||
this.WonderVictoryDeleteTimer(data.newentity);
|
|
||||||
|
|
||||||
|
// Check if wonder victory is enabled
|
||||||
|
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
|
||||||
|
if (!cmpEndGameManager)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
|
||||||
|
if (!victoryConditions || !victoryConditions.includes("wonder"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Handle timer transfer (only if old entity had the timer)
|
||||||
|
if (this.wonderVictoryMessages[data.entity])
|
||||||
|
{
|
||||||
|
this.WonderVictoryDeleteTimer(data.newentity);
|
||||||
this.wonderVictoryMessages[data.newentity] = this.wonderVictoryMessages[data.entity];
|
this.wonderVictoryMessages[data.newentity] = this.wonderVictoryMessages[data.entity];
|
||||||
delete this.wonderVictoryMessages[data.entity];
|
delete this.wonderVictoryMessages[data.entity];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make the completed wonder permanently visible
|
||||||
|
TriggerHelper.MakeEntityPermanentlyVisible(data.newentity);
|
||||||
|
|
||||||
|
// Send "wonder completed" notifications (only for non-Gaia wonders)
|
||||||
|
const cmpOwnership = Engine.QueryInterface(data.newentity, IID_Ownership);
|
||||||
|
if (cmpOwnership)
|
||||||
|
{
|
||||||
|
const owner = cmpOwnership.GetOwner();
|
||||||
|
if (owner > 0)
|
||||||
|
{
|
||||||
|
TriggerHelper.SendWonderNotifications(
|
||||||
|
owner,
|
||||||
|
markForTranslation("%(_player_)s has built a Wonder!"),
|
||||||
|
markForTranslation("You have built a Wonder!")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Trigger.prototype.WonderVictoryOwnershipChanged = function(data)
|
Trigger.prototype.WonderVictoryOwnershipChanged = function(data)
|
||||||
|
|
@ -18,6 +44,15 @@ Trigger.prototype.WonderVictoryOwnershipChanged = function(data)
|
||||||
if (!Engine.QueryInterface(data.entity, IID_Wonder))
|
if (!Engine.QueryInterface(data.entity, IID_Wonder))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Check if wonder victory is enabled
|
||||||
|
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
|
||||||
|
if (!cmpEndGameManager)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
|
||||||
|
if (!victoryConditions || !victoryConditions.includes("wonder"))
|
||||||
|
return;
|
||||||
|
|
||||||
this.WonderVictoryDeleteTimer(data.entity);
|
this.WonderVictoryDeleteTimer(data.entity);
|
||||||
|
|
||||||
if (data.to > 0)
|
if (data.to > 0)
|
||||||
|
|
@ -146,11 +181,69 @@ Trigger.prototype.WonderVictorySetWinner = function(playerID)
|
||||||
n));
|
n));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Trigger.prototype.WonderStartNotification = function(data)
|
||||||
|
{
|
||||||
|
// Check if wonder victory is enabled in game settings
|
||||||
|
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
|
||||||
|
if (!cmpEndGameManager)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Don't show the wonder foundation if it's not a wonder victory game
|
||||||
|
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
|
||||||
|
if (!victoryConditions || !victoryConditions.includes("wonder"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
|
||||||
|
if (!cmpTemplateManager)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const template = cmpTemplateManager.GetTemplate(data.template);
|
||||||
|
|
||||||
|
if (!template || !("Wonder" in template))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const owner = TriggerHelper.GetOwner(data.foundation);
|
||||||
|
if (owner <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TriggerHelper.MakeEntityPermanentlyVisible(data.foundation);
|
||||||
|
|
||||||
|
TriggerHelper.SendWonderNotifications(
|
||||||
|
owner,
|
||||||
|
markForTranslation("%(_player_)s has started building a Wonder."),
|
||||||
|
markForTranslation("You have started building a Wonder.")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Trigger.prototype.RevealGaiaWonders = function()
|
||||||
|
{
|
||||||
|
Engine.ProfileStart("RevealGaiaWonders");
|
||||||
|
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
|
||||||
|
if (!cmpEndGameManager)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
|
||||||
|
if (!victoryConditions || !victoryConditions.includes("wonder"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Find and reveal all Gaia wonders
|
||||||
|
const gaiaWonders = Engine.GetEntitiesWithInterface(IID_Wonder).filter(
|
||||||
|
ent => TriggerHelper.GetOwner(ent) === 0
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const ent of gaiaWonders)
|
||||||
|
TriggerHelper.MakeEntityPermanentlyVisible(ent);
|
||||||
|
|
||||||
|
Engine.ProfileStop();
|
||||||
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
|
const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
|
||||||
cmpTrigger.RegisterTrigger("OnEntityRenamed", "WonderVictoryEntityRenamed", { "enabled": true });
|
cmpTrigger.RegisterTrigger("OnEntityRenamed", "WonderVictoryEntityRenamed", { "enabled": true });
|
||||||
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "WonderVictoryOwnershipChanged", { "enabled": true });
|
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "WonderVictoryOwnershipChanged", { "enabled": true });
|
||||||
cmpTrigger.RegisterTrigger("OnDiplomacyChanged", "WonderVictoryDiplomacyChanged", { "enabled": true });
|
cmpTrigger.RegisterTrigger("OnDiplomacyChanged", "WonderVictoryDiplomacyChanged", { "enabled": true });
|
||||||
cmpTrigger.RegisterTrigger("OnPlayerWon", "WonderVictoryPlayerWon", { "enabled": true });
|
cmpTrigger.RegisterTrigger("OnPlayerWon", "WonderVictoryPlayerWon", { "enabled": true });
|
||||||
|
cmpTrigger.RegisterTrigger("OnConstructionStarted", "WonderStartNotification", { "enabled": true });
|
||||||
|
cmpTrigger.DoAfterDelay(0, "RevealGaiaWonders", {});
|
||||||
cmpTrigger.wonderVictoryMessages = {};
|
cmpTrigger.wonderVictoryMessages = {};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ Fogging.prototype.Init = function()
|
||||||
this.mirages = [];
|
this.mirages = [];
|
||||||
this.miraged = [];
|
this.miraged = [];
|
||||||
this.seen = [];
|
this.seen = [];
|
||||||
|
this.revealed = [];
|
||||||
|
|
||||||
const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
|
const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
|
||||||
for (let player = 0; player < numPlayers; ++player)
|
for (let player = 0; player < numPlayers; ++player)
|
||||||
|
|
@ -36,6 +37,7 @@ Fogging.prototype.Init = function()
|
||||||
this.mirages.push(INVALID_ENTITY);
|
this.mirages.push(INVALID_ENTITY);
|
||||||
this.miraged.push(false);
|
this.miraged.push(false);
|
||||||
this.seen.push(false);
|
this.seen.push(false);
|
||||||
|
this.revealed.push(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -169,6 +171,45 @@ Fogging.prototype.WasSeen = function(player)
|
||||||
return this.seen[player];
|
return this.seen[player];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permanently reveal this entity to one or all players.
|
||||||
|
* Destroys existing mirages and optionally deactivates fogging entirely.
|
||||||
|
* @param {number|string} player - Player ID to reveal to, or "all" for all players.
|
||||||
|
*/
|
||||||
|
Fogging.prototype.PermanentlyReveal = function(player = "all")
|
||||||
|
{
|
||||||
|
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||||
|
|
||||||
|
// Validate single player case
|
||||||
|
if (player !== "all" && (player < 0 || player >= this.mirages.length))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Determine which players to reveal
|
||||||
|
const startPlayer = (player === "all") ? 0 : player;
|
||||||
|
const endPlayer = (player === "all") ? this.mirages.length : player + 1;
|
||||||
|
|
||||||
|
// Reveal each player in the range
|
||||||
|
for (let i = startPlayer; i < endPlayer; ++i)
|
||||||
|
{
|
||||||
|
if (this.mirages[i] != INVALID_ENTITY)
|
||||||
|
{
|
||||||
|
Engine.DestroyEntity(this.mirages[i]);
|
||||||
|
this.mirages[i] = INVALID_ENTITY;
|
||||||
|
this.miraged[i] = false;
|
||||||
|
}
|
||||||
|
this.revealed[i] = true;
|
||||||
|
this.seen[i] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deactivate fogging if revealing to all players
|
||||||
|
if (player === "all")
|
||||||
|
this.activated = false;
|
||||||
|
|
||||||
|
// Request visibility update
|
||||||
|
if (cmpRangeManager)
|
||||||
|
cmpRangeManager.RequestVisibilityUpdate(this.entity);
|
||||||
|
};
|
||||||
|
|
||||||
Fogging.prototype.OnOwnershipChanged = function(msg)
|
Fogging.prototype.OnOwnershipChanged = function(msg)
|
||||||
{
|
{
|
||||||
// Always activate fogging for non-Gaia entities.
|
// Always activate fogging for non-Gaia entities.
|
||||||
|
|
@ -209,7 +250,8 @@ Fogging.prototype.OnVisibilityChanged = function(msg)
|
||||||
this.seen[msg.player] = true;
|
this.seen[msg.player] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.newVisibility == VIS_FOGGED && this.activated)
|
// Only create a mirage if the player hasn’t been permanently revealed
|
||||||
|
if (msg.newVisibility == VIS_FOGGED && this.activated && !this.revealed[msg.player])
|
||||||
this.LoadMirage(msg.player);
|
this.LoadMirage(msg.player);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -424,10 +424,42 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||||
|
|
||||||
"Order.Attack": function(msg)
|
"Order.Attack": function(msg)
|
||||||
{
|
{
|
||||||
const type = this.GetBestAttackAgainst(msg.data.target, msg.data.allowCapture);
|
// Check if this is a group attack
|
||||||
if (!type)
|
const isGroupAttack = !!msg.data.targetArray;
|
||||||
return this.FinishOrder();
|
|
||||||
|
|
||||||
|
// For group attacks with invalid initial target, find one from the array
|
||||||
|
if (isGroupAttack && (!msg.data.target || msg.data.target === INVALID_ENTITY))
|
||||||
|
{
|
||||||
|
if (!this.FindAndSetNextTarget(msg.data))
|
||||||
|
{
|
||||||
|
this.FinishOrder();
|
||||||
|
return ACCEPT_ORDER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the attack type for the current target
|
||||||
|
const type = this.GetBestAttackAgainst(msg.data.target, msg.data.allowCapture);
|
||||||
|
|
||||||
|
if (!type)
|
||||||
|
{
|
||||||
|
// Current target can't be attacked
|
||||||
|
if (isGroupAttack)
|
||||||
|
{
|
||||||
|
if (this.FindAndSetNextTarget(msg.data))
|
||||||
|
{
|
||||||
|
msg.data.attackType = this.GetBestAttackAgainst(msg.data.target, msg.data.allowCapture);
|
||||||
|
if (msg.data.attackType)
|
||||||
|
{
|
||||||
|
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
|
||||||
|
return ACCEPT_ORDER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.FinishOrder();
|
||||||
|
return ACCEPT_ORDER;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set attackType before range check
|
||||||
msg.data.attackType = type;
|
msg.data.attackType = type;
|
||||||
|
|
||||||
this.RememberTargetPosition();
|
this.RememberTargetPosition();
|
||||||
|
|
@ -1366,6 +1398,12 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||||
// Wait for individual members to finish
|
// Wait for individual members to finish
|
||||||
"enter": function(msg)
|
"enter": function(msg)
|
||||||
{
|
{
|
||||||
|
if (this.order.data.targetArray)
|
||||||
|
{
|
||||||
|
// For group attacks, we're done once we distributed targets
|
||||||
|
this.FinishOrder();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const target = this.order.data.target;
|
const target = this.order.data.target;
|
||||||
if (!this.CheckFormationTargetAttackRange(target))
|
if (!this.CheckFormationTargetAttackRange(target))
|
||||||
{
|
{
|
||||||
|
|
@ -1602,6 +1640,26 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||||
"INDIVIDUAL": {
|
"INDIVIDUAL": {
|
||||||
"Attacked": function(msg)
|
"Attacked": function(msg)
|
||||||
{
|
{
|
||||||
|
// First, check if we have a group attack order and the attacker is in our target array
|
||||||
|
if (this.order?.type === "Attack" &&
|
||||||
|
this.order.data?.targetArray?.includes(msg.data.attacker))
|
||||||
|
{
|
||||||
|
// Switch to attack the entity that's attacking us
|
||||||
|
this.order.data.target = msg.data.attacker;
|
||||||
|
this.order.data.attackType = this.GetBestAttackAgainst(msg.data.attacker, this.order.data.allowCapture);
|
||||||
|
|
||||||
|
// Update the current target index
|
||||||
|
const currentIndex = this.order.data.targetArray.indexOf(msg.data.attacker);
|
||||||
|
if (currentIndex !== -1)
|
||||||
|
this.order.data.currentTargetIndex = currentIndex;
|
||||||
|
|
||||||
|
// Make sure we're in the right state to handle this
|
||||||
|
const currentState = this.GetCurrentState();
|
||||||
|
if (currentState === "INDIVIDUAL.IDLE" || currentState === "INDIVIDUAL.WALKING")
|
||||||
|
this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force)
|
if (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force)
|
||||||
this.RespondToTargetedEntities([msg.data.attacker]);
|
this.RespondToTargetedEntities([msg.data.attacker]);
|
||||||
},
|
},
|
||||||
|
|
@ -2150,6 +2208,35 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||||
|
|
||||||
"Attacked": function(msg)
|
"Attacked": function(msg)
|
||||||
{
|
{
|
||||||
|
// Check if we're doing a group attack and the attacker is in our target array
|
||||||
|
if (!this.order.data.targetArray ||
|
||||||
|
!this.order.data.targetArray.includes(msg.data.attacker) ||
|
||||||
|
msg.data.attacker === this.order.data.target)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Switch to attack the entity that's attacking us
|
||||||
|
this.order.data.target = msg.data.attacker;
|
||||||
|
this.order.data.attackType = this.GetBestAttackAgainst(msg.data.attacker, this.order.data.allowCapture);
|
||||||
|
|
||||||
|
// Update the current target index
|
||||||
|
const currentIndex = this.order.data.targetArray.indexOf(msg.data.attacker);
|
||||||
|
if (currentIndex !== -1)
|
||||||
|
this.order.data.currentTargetIndex = currentIndex;
|
||||||
|
|
||||||
|
// Re-evaluate our approach to the new target
|
||||||
|
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
|
||||||
|
{
|
||||||
|
if (this.CanUnpack())
|
||||||
|
{
|
||||||
|
this.PushOrderFront("Unpack", { "force": true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.SetNextState("ATTACKING");
|
||||||
|
}
|
||||||
|
// Continue approaching the new target
|
||||||
|
else if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
|
||||||
|
this.FinishOrder();
|
||||||
|
|
||||||
// If we're already in combat mode, ignore anyone else who's attacking us
|
// If we're already in combat mode, ignore anyone else who's attacking us
|
||||||
// unless it's a melee attack since they may be blocking our way to the target
|
// unless it's a melee attack since they may be blocking our way to the target
|
||||||
if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || !this.order.data.force))
|
if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || !this.order.data.force))
|
||||||
|
|
@ -2209,7 +2296,23 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||||
|
|
||||||
"MovementUpdate": function(msg)
|
"MovementUpdate": function(msg)
|
||||||
{
|
{
|
||||||
if (msg.likelyFailure)
|
if (!msg.likelyFailure)
|
||||||
|
{
|
||||||
|
if (!this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (this.CanUnpack())
|
||||||
|
{
|
||||||
|
this.PushOrderFront("Unpack", { "force": true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.SetNextState("ATTACKING");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For group attacks, try next target instead of moving to last position
|
||||||
|
if (!this.order.data.targetArray || this.order.data.currentTargetIndex === undefined)
|
||||||
{
|
{
|
||||||
// This also handles hunting.
|
// This also handles hunting.
|
||||||
if (this.orderQueue.length > 1)
|
if (this.orderQueue.length > 1)
|
||||||
|
|
@ -2217,11 +2320,13 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||||
this.FinishOrder();
|
this.FinishOrder();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (!this.order.data.force || !this.order.data.lastPos)
|
|
||||||
|
if (!this.order.data.force || !this.order.data.lastPos)
|
||||||
{
|
{
|
||||||
this.SetNextState("COMBAT.FINDINGNEWTARGET");
|
this.SetNextState("COMBAT.FINDINGNEWTARGET");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the order was forced, try moving to the target position,
|
// If the order was forced, try moving to the target position,
|
||||||
// under the assumption that this is desirable if the target
|
// under the assumption that this is desirable if the target
|
||||||
// was somewhat far away - we'll likely end up closer to where
|
// was somewhat far away - we'll likely end up closer to where
|
||||||
|
|
@ -2234,21 +2339,30 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
|
// Handle group attack case
|
||||||
|
// Remove current unreachable target from array
|
||||||
|
const currentIndex = this.order.data.currentTargetIndex;
|
||||||
|
if (currentIndex < this.order.data.targetArray.length)
|
||||||
|
this.order.data.targetArray.splice(currentIndex, 1);
|
||||||
|
|
||||||
|
// Try to find next valid target
|
||||||
|
if (this.order.data.targetArray.length === 0 || !this.FindAndSetNextTarget(this.order.data))
|
||||||
{
|
{
|
||||||
if (this.CanUnpack())
|
this.FinishOrder();
|
||||||
{
|
return;
|
||||||
this.PushOrderFront("Unpack", { "force": true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.SetNextState("ATTACKING");
|
|
||||||
}
|
}
|
||||||
else if (msg.likelySuccess)
|
|
||||||
// Try moving again,
|
this.order.data.attackType = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
|
||||||
// attack range uses a height-related formula and our actual max range might have changed.
|
|
||||||
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
|
// Try approaching the new target
|
||||||
this.FinishOrder();
|
if (this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
|
||||||
},
|
return;
|
||||||
|
|
||||||
|
// If we can't approach the new target either, remove it and continue
|
||||||
|
this.order.data.targetArray.splice(this.order.data.currentTargetIndex, 1);
|
||||||
|
if (this.order.data.targetArray.length === 0)
|
||||||
|
this.FinishOrder();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"ATTACKING": {
|
"ATTACKING": {
|
||||||
|
|
@ -2322,6 +2436,18 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||||
|
|
||||||
"OutOfRange": function()
|
"OutOfRange": function()
|
||||||
{
|
{
|
||||||
|
// Check if the target is garrisoned or in a turret (turreted entities cannot be attacked by melee units)
|
||||||
|
const cmpTargetUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
|
||||||
|
const isTargetGarrisoned = cmpTargetUnitAI && cmpTargetUnitAI.isGarrisoned;
|
||||||
|
const isTargetInTurret = cmpTargetUnitAI && cmpTargetUnitAI.IsTurret();
|
||||||
|
|
||||||
|
// If target is garrisoned or in a turret (and we're not ranged), treat as invalid
|
||||||
|
if (isTargetGarrisoned || (isTargetInTurret && this.order.data.attackType !== "Ranged"))
|
||||||
|
{
|
||||||
|
this.ProcessMessage("TargetInvalidated");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force))
|
if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force))
|
||||||
{
|
{
|
||||||
if (this.CanPack())
|
if (this.CanPack())
|
||||||
|
|
@ -2332,11 +2458,53 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||||
this.SetNextState("CHASING");
|
this.SetNextState("CHASING");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For group attacks, try next target instead of finding new ones
|
||||||
|
if (this.order.data.targetArray && this.order.data.currentTargetIndex !== undefined)
|
||||||
|
{
|
||||||
|
const currentIndex = this.order.data.currentTargetIndex;
|
||||||
|
|
||||||
|
// Remove current target since it's out of range and we can't chase
|
||||||
|
this.order.data.targetArray.splice(currentIndex, 1);
|
||||||
|
|
||||||
|
// Try to find next valid target in sequence
|
||||||
|
if (this.order.data.targetArray.length > 0 && this.FindAndSetNextTarget(this.order.data))
|
||||||
|
{
|
||||||
|
this.order.data.attackType = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
|
||||||
|
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.SetNextState("FINDINGNEWTARGET");
|
this.SetNextState("FINDINGNEWTARGET");
|
||||||
},
|
},
|
||||||
|
|
||||||
"TargetInvalidated": function()
|
"TargetInvalidated": function()
|
||||||
{
|
{
|
||||||
|
// For group attacks, remove current target and move to next in array
|
||||||
|
if (this.order.data.targetArray && this.order.data.currentTargetIndex !== undefined)
|
||||||
|
{
|
||||||
|
// Remove the current target from the array
|
||||||
|
const currentIndex = this.order.data.currentTargetIndex;
|
||||||
|
if (currentIndex < this.order.data.targetArray.length)
|
||||||
|
{
|
||||||
|
this.order.data.targetArray.splice(currentIndex, 1);
|
||||||
|
|
||||||
|
// If we have more targets, continue with the next one
|
||||||
|
if (this.order.data.targetArray.length > 0)
|
||||||
|
{
|
||||||
|
// Try to find next valid target in sequence
|
||||||
|
if (this.FindAndSetNextTarget(this.order.data))
|
||||||
|
{
|
||||||
|
this.order.data.attackType = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
|
||||||
|
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No more targets or not a group attack - use original behavior
|
||||||
this.SetNextState("FINDINGNEWTARGET");
|
this.SetNextState("FINDINGNEWTARGET");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -2360,7 +2528,26 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||||
|
|
||||||
"enter": function()
|
"enter": function()
|
||||||
{
|
{
|
||||||
// Check if we are attacking a formation.
|
// First, check if we have pending targets in a group attack
|
||||||
|
if (this.order.data.targetArray && this.order.data.currentTargetIndex !== undefined)
|
||||||
|
{
|
||||||
|
// Try to find next valid target from the array
|
||||||
|
if (this.FindAndSetNextTarget(this.order.data))
|
||||||
|
{
|
||||||
|
this.order.data.attackType = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
|
||||||
|
this.SetNextState("COMBAT.ATTACKING");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No valid targets in array - clean up and continue with normal behavior
|
||||||
|
this.order.data.targetArray = [];
|
||||||
|
this.order.data.currentTargetIndex = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.order.data.target)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Try to find the formation the target was a part of.
|
||||||
let cmpFormation = Engine.QueryInterface(this.order.data.target, IID_Formation);
|
let cmpFormation = Engine.QueryInterface(this.order.data.target, IID_Formation);
|
||||||
if (cmpFormation)
|
if (cmpFormation)
|
||||||
this.order.data.formationTarget = this.order.data.target;
|
this.order.data.formationTarget = this.order.data.target;
|
||||||
|
|
@ -2468,6 +2655,12 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||||
{
|
{
|
||||||
if (msg.likelyFailure)
|
if (msg.likelyFailure)
|
||||||
{
|
{
|
||||||
|
// If this is a group attack with pending targets, try the next one
|
||||||
|
if (this.order.data.targetArray && this.order.data.targetArray.length > 0)
|
||||||
|
{
|
||||||
|
this.SetNextState("COMBAT.FINDINGNEWTARGET");
|
||||||
|
return;
|
||||||
|
}
|
||||||
// This also handles hunting.
|
// This also handles hunting.
|
||||||
if (this.orderQueue.length > 1)
|
if (this.orderQueue.length > 1)
|
||||||
{
|
{
|
||||||
|
|
@ -3887,6 +4080,9 @@ UnitAI.prototype.OnOwnershipChanged = function(msg)
|
||||||
|
|
||||||
UnitAI.prototype.OnDestroy = function()
|
UnitAI.prototype.OnDestroy = function()
|
||||||
{
|
{
|
||||||
|
// Clean up all group attack states before destruction
|
||||||
|
this.CleanupAllGroupAttackStates();
|
||||||
|
|
||||||
// Switch to an empty state to let states execute their leave handlers.
|
// Switch to an empty state to let states execute their leave handlers.
|
||||||
this.UnitFsm.SwitchToNextState(this, "");
|
this.UnitFsm.SwitchToNextState(this, "");
|
||||||
|
|
||||||
|
|
@ -4093,6 +4289,9 @@ UnitAI.prototype.FsmStateNameChanged = function(state)
|
||||||
*/
|
*/
|
||||||
UnitAI.prototype.FinishOrder = function()
|
UnitAI.prototype.FinishOrder = function()
|
||||||
{
|
{
|
||||||
|
// Clean up any group attack state before removing order
|
||||||
|
this.CleanupGroupAttackState(this.order);
|
||||||
|
|
||||||
if (!this.orderQueue.length)
|
if (!this.orderQueue.length)
|
||||||
{
|
{
|
||||||
const stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line
|
const stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line
|
||||||
|
|
@ -4318,15 +4517,24 @@ UnitAI.prototype.ReplaceOrder = function(type, data)
|
||||||
const idx = this.orderQueue.findIndex(o => o.type == "LeaveFormation");
|
const idx = this.orderQueue.findIndex(o => o.type == "LeaveFormation");
|
||||||
if (idx === -1)
|
if (idx === -1)
|
||||||
{
|
{
|
||||||
|
// Clean up all queued orders before clearing them
|
||||||
|
this.CleanupAllGroupAttackStates();
|
||||||
this.orderQueue = [];
|
this.orderQueue = [];
|
||||||
this.order = undefined;
|
this.order = undefined;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
// Clean up orders being discarded
|
||||||
|
for (let i = 0; i < idx; ++i)
|
||||||
|
this.CleanupGroupAttackState(this.orderQueue[i]);
|
||||||
this.orderQueue.splice(0, idx);
|
this.orderQueue.splice(0, idx);
|
||||||
|
}
|
||||||
this.PushOrderFront(type, data);
|
this.PushOrderFront(type, data);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Clean up all queued orders before clearing them
|
||||||
|
this.CleanupAllGroupAttackStates();
|
||||||
this.orderQueue = [];
|
this.orderQueue = [];
|
||||||
this.PushOrder(type, data);
|
this.PushOrder(type, data);
|
||||||
}
|
}
|
||||||
|
|
@ -4405,6 +4613,7 @@ UnitAI.prototype.BackToWork = function()
|
||||||
if (this.IsTurret(cmpTurretable) && !cmpTurretable.LeaveTurret())
|
if (this.IsTurret(cmpTurretable) && !cmpTurretable.LeaveTurret())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
this.CleanupAllGroupAttackStates();
|
||||||
this.orderQueue = [];
|
this.orderQueue = [];
|
||||||
|
|
||||||
this.AddOrders(this.workOrders);
|
this.AddOrders(this.workOrders);
|
||||||
|
|
@ -5060,6 +5269,9 @@ UnitAI.prototype.CheckTargetRange = function(target, iid, type)
|
||||||
*/
|
*/
|
||||||
UnitAI.prototype.CheckTargetAttackRange = function(target, type)
|
UnitAI.prototype.CheckTargetAttackRange = function(target, type)
|
||||||
{
|
{
|
||||||
|
if (!target || target === INVALID_ENTITY || !type)
|
||||||
|
return false;
|
||||||
|
|
||||||
// for formation members, the formation will take care of the range check
|
// for formation members, the formation will take care of the range check
|
||||||
if (this.IsFormationMember())
|
if (this.IsFormationMember())
|
||||||
{
|
{
|
||||||
|
|
@ -5793,6 +6005,75 @@ UnitAI.prototype.Attack = function(target, allowCapture = this.DEFAULT_CAPTURE,
|
||||||
this.AddOrder("Attack", order, queued, pushFront);
|
this.AddOrder("Attack", order, queued, pushFront);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a group attack order to the queue.
|
||||||
|
* Used when some unit(s) are ordered to attack multiple targets.
|
||||||
|
* Each unit receives the full target list (from Commands) but starts at a different position
|
||||||
|
* based on the startingIndex in the attacking group.
|
||||||
|
*
|
||||||
|
* @param {Array} targets - Array of target entity IDs to attack (pre-sorted by distance)
|
||||||
|
* @param {number} startingIndex - This unit's position in the attacking group (0-based)
|
||||||
|
* @param {number} unitCount - Total number of units in the attacking group
|
||||||
|
* @param {boolean} allowCapture - Whether capturing is allowed
|
||||||
|
* @param {boolean} queued - If true, add to end of queue instead of replacing current order
|
||||||
|
* @param {boolean} pushFront - If true, add to front of queue (interrupt current order)
|
||||||
|
*/
|
||||||
|
UnitAI.prototype.AttackGroup = function(targets, startingIndex, unitCount, allowCapture = this.DEFAULT_CAPTURE, queued = false, pushFront = false)
|
||||||
|
{
|
||||||
|
if (!targets?.length)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Check if adding these targets would exceed the limit
|
||||||
|
const currentTotal = this.CountTotalTargetsInQueue();
|
||||||
|
const newTargetCount = targets.length;
|
||||||
|
|
||||||
|
if (currentTotal + newTargetCount > this.MAX_TOTAL_TARGETS)
|
||||||
|
{
|
||||||
|
// Trim the targets to fit within the limit
|
||||||
|
const allowedNewTargets = Math.max(0, this.MAX_TOTAL_TARGETS - currentTotal);
|
||||||
|
if (allowedNewTargets === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Trim the target array
|
||||||
|
targets = targets.slice(0, allowedNewTargets);
|
||||||
|
|
||||||
|
// If trimming results in no targets, just return
|
||||||
|
if (targets.length === 0)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this unit is in a formation, move the formation controller out of the world
|
||||||
|
// This prevents the controller from staying at the coordinates where the units received the order
|
||||||
|
// That can potentially be far apart from where the fighting will occur.
|
||||||
|
// Result: The formation will reform into a more logical position once members are done (idle).
|
||||||
|
this.MoveFormationControllerOutOfWorld();
|
||||||
|
|
||||||
|
const stride = targets.length / unitCount;
|
||||||
|
const startIdx = Math.min(Math.round((startingIndex + 0.5) * stride), targets.length - 1);
|
||||||
|
|
||||||
|
const order = {
|
||||||
|
"force": true,
|
||||||
|
"allowCapture": allowCapture,
|
||||||
|
"targetArray": targets.slice(),
|
||||||
|
"target": INVALID_ENTITY,
|
||||||
|
"currentTargetIndex": startIdx,
|
||||||
|
"originalFirstTarget": targets[0],
|
||||||
|
};
|
||||||
|
|
||||||
|
this.RememberTargetPosition(order);
|
||||||
|
this.AddOrder("Attack", order, queued, pushFront);
|
||||||
|
};
|
||||||
|
|
||||||
|
UnitAI.prototype.MoveFormationControllerOutOfWorld = function()
|
||||||
|
{
|
||||||
|
if (!this.IsFormationMember() || !this.formationController)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const cmpControllerPosition = Engine.QueryInterface(this.formationController, IID_Position);
|
||||||
|
if (cmpControllerPosition?.IsInWorld())
|
||||||
|
cmpControllerPosition.MoveOutOfWorld();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds garrison order to the queue, forced by the player.
|
* Adds garrison order to the queue, forced by the player.
|
||||||
*/
|
*/
|
||||||
|
|
@ -6726,6 +7007,72 @@ UnitAI.prototype.AttackEntitiesByPreference = function(ents)
|
||||||
return this.RespondToTargetedEntities(entsWithoutPref);
|
return this.RespondToTargetedEntities(entsWithoutPref);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
UnitAI.prototype.FindAndSetNextTarget = function(data)
|
||||||
|
{
|
||||||
|
if (!this.EnsureValidTargetIndex(data))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const startIdx = data.currentTargetIndex;
|
||||||
|
const currentTarget = data.targetArray[startIdx];
|
||||||
|
|
||||||
|
// First check the starting target
|
||||||
|
if (this.CanAttack(currentTarget) && this.CheckTargetVisible(currentTarget))
|
||||||
|
return this.SetNextTarget(data, startIdx);
|
||||||
|
|
||||||
|
// Search forward using find
|
||||||
|
const forwardFound = data.targetArray
|
||||||
|
.slice(startIdx + 1)
|
||||||
|
.findIndex((target, i) =>
|
||||||
|
this.CanAttack(target) && this.CheckTargetVisible(target)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (forwardFound !== -1)
|
||||||
|
return this.SetNextTarget(data, startIdx + 1 + forwardFound);
|
||||||
|
|
||||||
|
// Search backward using find
|
||||||
|
const backwardFound = data.targetArray
|
||||||
|
.slice(0, startIdx)
|
||||||
|
.reverse()
|
||||||
|
.findIndex((target, i) =>
|
||||||
|
this.CanAttack(target) && this.CheckTargetVisible(target)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (backwardFound !== -1)
|
||||||
|
{
|
||||||
|
// Convert reverse index back to original index
|
||||||
|
const originalIndex = startIdx - 1 - backwardFound;
|
||||||
|
return this.SetNextTarget(data, originalIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
UnitAI.prototype.SetNextTarget = function(data, index)
|
||||||
|
{
|
||||||
|
const target = data.targetArray[index];
|
||||||
|
data.target = target;
|
||||||
|
data.attackType = this.GetBestAttackAgainst(target, data.allowCapture);
|
||||||
|
data.currentTargetIndex = index;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
UnitAI.prototype.EnsureValidTargetIndex = function(data)
|
||||||
|
{
|
||||||
|
if (!data.targetArray || data.currentTargetIndex === undefined)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// If array is empty, clear the index
|
||||||
|
if (data.targetArray.length === 0)
|
||||||
|
{
|
||||||
|
data.currentTargetIndex = undefined;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.currentTargetIndex = Math.clamp(data.currentTargetIndex, 0, data.targetArray.length - 1);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call UnitAI.funcname(args) on all formation members.
|
* Call UnitAI.funcname(args) on all formation members.
|
||||||
* @param resetFinishedEntities - If true, call ResetFinishedEntities first.
|
* @param resetFinishedEntities - If true, call ResetFinishedEntities first.
|
||||||
|
|
@ -6837,6 +7184,50 @@ UnitAI.prototype.ResetObstructionMitigationFlag = function()
|
||||||
delete this.obstructionMitigationAttempted;
|
delete this.obstructionMitigationAttempted;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the total number of target entities across all queued orders.
|
||||||
|
* This includes targetArray from group attacks and single targets.
|
||||||
|
* Each target reference is counted separately, even if the same entity
|
||||||
|
* appears in multiple orders or in both targetArray and target.
|
||||||
|
* @returns {number} - Total number of target entity references in the order queue
|
||||||
|
*/
|
||||||
|
UnitAI.prototype.CountTotalTargetsInQueue = function()
|
||||||
|
{
|
||||||
|
let total = 0;
|
||||||
|
for (const order of this.orderQueue)
|
||||||
|
{
|
||||||
|
if (order.data)
|
||||||
|
{
|
||||||
|
// Count group attack targets
|
||||||
|
if (order.data.targetArray && Array.isArray(order.data.targetArray))
|
||||||
|
total += order.data.targetArray.length;
|
||||||
|
|
||||||
|
// Add single target for consistency
|
||||||
|
if (order.data.target && order.data.target !== INVALID_ENTITY)
|
||||||
|
total++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
};
|
||||||
|
|
||||||
|
UnitAI.prototype.CleanupGroupAttackState = function(order)
|
||||||
|
{
|
||||||
|
if (order?.data?.targetArray)
|
||||||
|
{
|
||||||
|
delete order.data.targetArray;
|
||||||
|
delete order.data.currentTargetIndex;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
UnitAI.prototype.CleanupAllGroupAttackStates = function()
|
||||||
|
{
|
||||||
|
for (const order of this.orderQueue)
|
||||||
|
this.CleanupGroupAttackState(order);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Maximum number of target entities allowed across all queued orders
|
||||||
|
UnitAI.prototype.MAX_TOTAL_TARGETS = 1000;
|
||||||
|
|
||||||
UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec);
|
UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec);
|
||||||
|
|
||||||
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
|
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ function TestFormationExiting(mode)
|
||||||
"ResetActiveQuery": function(id) { if (mode == 0) return []; return [enemy]; },
|
"ResetActiveQuery": function(id) { if (mode == 0) return []; return [enemy]; },
|
||||||
"DisableActiveQuery": function(id) { },
|
"DisableActiveQuery": function(id) { },
|
||||||
"GetEntityFlagMask": function(identifier) { },
|
"GetEntityFlagMask": function(identifier) { },
|
||||||
|
"GetLosVisibility": (ent, player) => "visible"
|
||||||
});
|
});
|
||||||
|
|
||||||
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
|
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
|
||||||
|
|
@ -319,8 +320,11 @@ function TestMoveIntoFormationWhileAttacking()
|
||||||
"ResetActiveQuery": function(id) { return [enemy]; },
|
"ResetActiveQuery": function(id) { return [enemy]; },
|
||||||
"DisableActiveQuery": function(id) { },
|
"DisableActiveQuery": function(id) { },
|
||||||
"GetEntityFlagMask": function(identifier) { },
|
"GetEntityFlagMask": function(identifier) { },
|
||||||
|
"GetLosVisibility": (target, player) => "visible",
|
||||||
|
"GetLosVisibilityPosition": (x, z, player) => "visible"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
|
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
|
||||||
"GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; },
|
"GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; },
|
||||||
});
|
});
|
||||||
|
|
@ -406,6 +410,10 @@ function TestMoveIntoFormationWhileAttacking()
|
||||||
"GetHitpoints": function() { return 40; },
|
"GetHitpoints": function() { return 40; },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddMock(enemy, IID_Ownership, {
|
||||||
|
"GetOwner": () => 2 // Different player ID (enemy)
|
||||||
|
});
|
||||||
|
|
||||||
const controllerFormation = ConstructComponent(controller, "Formation", {
|
const controllerFormation = ConstructComponent(controller, "Formation", {
|
||||||
"FormationShape": "square",
|
"FormationShape": "square",
|
||||||
"ShiftRows": "false",
|
"ShiftRows": "false",
|
||||||
|
|
@ -421,6 +429,10 @@ function TestMoveIntoFormationWhileAttacking()
|
||||||
"DefaultStance": "aggressive"
|
"DefaultStance": "aggressive"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddMock(controller, IID_Ownership, {
|
||||||
|
"GetOwner": () => 1
|
||||||
|
});
|
||||||
|
|
||||||
AddMock(controller, IID_Position, {
|
AddMock(controller, IID_Position, {
|
||||||
"GetTurretParent": () => INVALID_ENTITY,
|
"GetTurretParent": () => INVALID_ENTITY,
|
||||||
"JumpTo": function(x, z) { this.x = x; this.z = z; },
|
"JumpTo": function(x, z) { this.x = x; this.z = z; },
|
||||||
|
|
@ -458,7 +470,7 @@ function TestMoveIntoFormationWhileAttacking()
|
||||||
|
|
||||||
controllerFormation.SetMembers(units);
|
controllerFormation.SetMembers(units);
|
||||||
|
|
||||||
controllerAI.Attack(enemy, []);
|
controllerAI.Attack(enemy);
|
||||||
|
|
||||||
for (const ent of unitAIs)
|
for (const ent of unitAIs)
|
||||||
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
|
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
|
||||||
|
|
@ -889,4 +901,362 @@ function TestAttemptObstructionMitigation()
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
TestAttemptObstructionMitigation();
|
TestAttemptObstructionMitigation();
|
||||||
|
function TestFindAndSetNextTargetBehavior()
|
||||||
|
{
|
||||||
|
ResetState();
|
||||||
|
|
||||||
|
const attacker = 10;
|
||||||
|
const targets = [20, 21, 22, 23, 24]; // Multiple targets for comprehensive testing
|
||||||
|
|
||||||
|
// Basic mocks needed for unitAI creation
|
||||||
|
AddMock(SYSTEM_ENTITY, IID_Timer, {
|
||||||
|
"SetInterval": () => {},
|
||||||
|
"SetTimeout": () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
||||||
|
"CreateActiveQuery": () => 1,
|
||||||
|
"EnableActiveQuery": () => {},
|
||||||
|
"ResetActiveQuery": () => [],
|
||||||
|
"DisableActiveQuery": () => {},
|
||||||
|
"GetEntityFlagMask": () => 0
|
||||||
|
});
|
||||||
|
|
||||||
|
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
|
||||||
|
"IsInTargetRange": () => false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the unitAI component
|
||||||
|
const unitAI = ConstructComponent(attacker, "UnitAI", {
|
||||||
|
"FormationController": "false",
|
||||||
|
"DefaultStance": "aggressive"
|
||||||
|
});
|
||||||
|
|
||||||
|
AddMock(attacker, IID_Attack, {
|
||||||
|
"GetBestAttackAgainst": () => "Melee"
|
||||||
|
});
|
||||||
|
|
||||||
|
unitAI.OnCreate();
|
||||||
|
|
||||||
|
// Mock the visibility and attackability checks
|
||||||
|
unitAI.CanAttack = (target) =>
|
||||||
|
{
|
||||||
|
// Target 21 is always unattackable (e.g., garrisoned, dead, etc.)
|
||||||
|
return target !== 21;
|
||||||
|
};
|
||||||
|
|
||||||
|
unitAI.CheckTargetVisible = (target) =>
|
||||||
|
{
|
||||||
|
// Target 22 is always not visible
|
||||||
|
return target !== 22;
|
||||||
|
};
|
||||||
|
|
||||||
|
// First target is valid
|
||||||
|
(function()
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
"targetArray": [20, 21, 22, 23, 24],
|
||||||
|
"currentTargetIndex": 0, // Currently targeting 20
|
||||||
|
"target": 20,
|
||||||
|
"allowCapture": false
|
||||||
|
};
|
||||||
|
|
||||||
|
const found = unitAI.FindAndSetNextTarget(data);
|
||||||
|
TS_ASSERT(found);
|
||||||
|
TS_ASSERT_EQUALS(data.target, 20);
|
||||||
|
TS_ASSERT_EQUALS(data.currentTargetIndex, 0);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// End of array behavior
|
||||||
|
(function()
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
"targetArray": [18, 19, 20, 21, 22],
|
||||||
|
"currentTargetIndex": 2, // Currently targeting 22
|
||||||
|
"allowCapture": false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 21 and 22 are invalid, should go backward and find 20
|
||||||
|
const found = unitAI.FindAndSetNextTarget(data);
|
||||||
|
TS_ASSERT(found);
|
||||||
|
TS_ASSERT_EQUALS(data.target, 20); // Should be assigned targetArray[initialTarget - 1]
|
||||||
|
})();
|
||||||
|
|
||||||
|
// No valid targets found
|
||||||
|
(function()
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
"targetArray": [21, 22], // Both are invalid (21 unattackable, 22 not visible)
|
||||||
|
"currentTargetIndex": 0,
|
||||||
|
"target": 21,
|
||||||
|
"allowCapture": false
|
||||||
|
};
|
||||||
|
|
||||||
|
const found = unitAI.FindAndSetNextTarget(data);
|
||||||
|
TS_ASSERT(!found);
|
||||||
|
TS_ASSERT_EQUALS(data.target, 21); // Should remain unchanged
|
||||||
|
TS_ASSERT_EQUALS(data.currentTargetIndex, 0);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Empty target array
|
||||||
|
(function()
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
"targetArray": [],
|
||||||
|
"currentTargetIndex": 0,
|
||||||
|
"target": 20,
|
||||||
|
"allowCapture": false
|
||||||
|
};
|
||||||
|
|
||||||
|
const found = unitAI.FindAndSetNextTarget(data);
|
||||||
|
TS_ASSERT(!found);
|
||||||
|
TS_ASSERT_EQUALS(data.target, 20); // Should remain unchanged
|
||||||
|
TS_ASSERT_EQUALS(data.currentTargetIndex, void 0); // Should nullify currentTargetIndex
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Current target is last in array
|
||||||
|
(function()
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
"targetArray": [20, 21, 22, 23, 24],
|
||||||
|
"currentTargetIndex": 4, // Currently targeting 24 (last element)
|
||||||
|
"allowCapture": false
|
||||||
|
};
|
||||||
|
|
||||||
|
const found = unitAI.FindAndSetNextTarget(data);
|
||||||
|
TS_ASSERT(found);
|
||||||
|
TS_ASSERT_EQUALS(data.target, 24); // Should remain unchanged
|
||||||
|
TS_ASSERT_EQUALS(data.currentTargetIndex, 4);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Single invalid target
|
||||||
|
(function()
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
"targetArray": [21], // Only one target, which is invalid
|
||||||
|
"currentTargetIndex": 0,
|
||||||
|
"allowCapture": false
|
||||||
|
};
|
||||||
|
|
||||||
|
const found = unitAI.FindAndSetNextTarget(data);
|
||||||
|
TS_ASSERT(!found);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Verify if we don't do more calls then necessary
|
||||||
|
(function()
|
||||||
|
{
|
||||||
|
const searchOrder = [];
|
||||||
|
|
||||||
|
unitAI.CanAttack = (target) =>
|
||||||
|
{
|
||||||
|
searchOrder.push(target);
|
||||||
|
return target == 20;
|
||||||
|
};
|
||||||
|
|
||||||
|
unitAI.CheckTargetVisible = (target) => true; // All visible
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
"targetArray": [20, 21, 22, 23],
|
||||||
|
"currentTargetIndex": 1, // Currently targeting 21
|
||||||
|
"allowCapture": false
|
||||||
|
};
|
||||||
|
|
||||||
|
const found = unitAI.FindAndSetNextTarget(data);
|
||||||
|
TS_ASSERT(found);
|
||||||
|
TS_ASSERT_EQUALS(searchOrder.length, 4); // Should find at 20 (4 calls)
|
||||||
|
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
TestFindAndSetNextTargetBehavior();
|
||||||
|
|
||||||
|
function TestAttackGroupBehavior()
|
||||||
|
{
|
||||||
|
ResetState();
|
||||||
|
|
||||||
|
const attacker1 = 100;
|
||||||
|
const attacker2 = 101;
|
||||||
|
const targets = [200, 201, 202, 203, 204];
|
||||||
|
|
||||||
|
// Basic mocks needed for unitAI creation
|
||||||
|
AddMock(SYSTEM_ENTITY, IID_Timer, {
|
||||||
|
"SetInterval": () => {},
|
||||||
|
"SetTimeout": () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
||||||
|
"CreateActiveQuery": () => 1,
|
||||||
|
"EnableActiveQuery": () => {},
|
||||||
|
"ResetActiveQuery": () => [],
|
||||||
|
"DisableActiveQuery": () => {},
|
||||||
|
"GetEntityFlagMask": () => 0
|
||||||
|
});
|
||||||
|
|
||||||
|
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
|
||||||
|
"IsInTargetRange": () => false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create two unitAI components
|
||||||
|
const unitAI1 = ConstructComponent(attacker1, "UnitAI", {
|
||||||
|
"FormationController": "false",
|
||||||
|
"DefaultStance": "aggressive"
|
||||||
|
});
|
||||||
|
|
||||||
|
const unitAI2 = ConstructComponent(attacker2, "UnitAI", {
|
||||||
|
"FormationController": "false",
|
||||||
|
"DefaultStance": "aggressive"
|
||||||
|
});
|
||||||
|
|
||||||
|
AddMock(attacker1, IID_Attack, {
|
||||||
|
"GetBestAttackAgainst": () => "Melee"
|
||||||
|
});
|
||||||
|
|
||||||
|
AddMock(attacker2, IID_Attack, {
|
||||||
|
"GetBestAttackAgainst": () => "Melee"
|
||||||
|
});
|
||||||
|
|
||||||
|
unitAI1.OnCreate();
|
||||||
|
unitAI2.OnCreate();
|
||||||
|
|
||||||
|
// Basic AttackGroup distribution
|
||||||
|
(function()
|
||||||
|
{
|
||||||
|
let orderAdded1 = null;
|
||||||
|
let orderAdded2 = null;
|
||||||
|
|
||||||
|
// Mock AddOrder to capture what orders are created
|
||||||
|
unitAI1.AddOrder = (type, data, queued, pushFront) =>
|
||||||
|
{
|
||||||
|
orderAdded1 = { type, data, queued, pushFront };
|
||||||
|
};
|
||||||
|
|
||||||
|
unitAI2.AddOrder = (type, data, queued, pushFront) =>
|
||||||
|
{
|
||||||
|
orderAdded2 = { type, data, queued, pushFront };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call AttackGroup for both units
|
||||||
|
unitAI1.AttackGroup(targets, 0, 2, false, false, false);
|
||||||
|
unitAI2.AttackGroup(targets, 1, 2, false, false, false);
|
||||||
|
|
||||||
|
// Verify unit1's order
|
||||||
|
TS_ASSERT(orderAdded1);
|
||||||
|
TS_ASSERT_EQUALS(orderAdded1.type, "Attack");
|
||||||
|
TS_ASSERT_EQUALS(orderAdded1.data.targetArray.length, 5);
|
||||||
|
TS_ASSERT_EQUALS(orderAdded1.data.target, INVALID_ENTITY);
|
||||||
|
// Unit 0 should start at position 1 ==> Math.round((0 + 0.5) * 2.5) = Math.round(1.25) = 1
|
||||||
|
TS_ASSERT_EQUALS(orderAdded1.data.currentTargetIndex, 1);
|
||||||
|
|
||||||
|
// Verify unit2's order
|
||||||
|
TS_ASSERT(orderAdded2);
|
||||||
|
TS_ASSERT_EQUALS(orderAdded2.type, "Attack");
|
||||||
|
TS_ASSERT_EQUALS(orderAdded2.data.targetArray.length, 5);
|
||||||
|
TS_ASSERT_EQUALS(orderAdded2.data.target, INVALID_ENTITY);
|
||||||
|
// Unit 1 should start at position 4 ==> Math.round((1 + 0.5) * 2.5) = Math.round(3.75) = 4
|
||||||
|
TS_ASSERT_EQUALS(orderAdded2.data.currentTargetIndex, 4);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// More units than targets
|
||||||
|
(function()
|
||||||
|
{
|
||||||
|
let orderAdded1 = null;
|
||||||
|
let orderAdded2 = null;
|
||||||
|
let orderAdded3 = null;
|
||||||
|
|
||||||
|
const unitAI3 = ConstructComponent(102, "UnitAI", {
|
||||||
|
"FormationController": "false",
|
||||||
|
"DefaultStance": "aggressive"
|
||||||
|
});
|
||||||
|
AddMock(102, IID_Attack, { "GetBestAttackAgainst": () => "Melee" });
|
||||||
|
unitAI3.OnCreate();
|
||||||
|
|
||||||
|
unitAI1.AddOrder = (type, data, queued, pushFront) =>
|
||||||
|
{
|
||||||
|
orderAdded1 = { type, data, queued, pushFront };
|
||||||
|
};
|
||||||
|
unitAI2.AddOrder = (type, data, queued, pushFront) =>
|
||||||
|
{
|
||||||
|
orderAdded2 = { type, data, queued, pushFront };
|
||||||
|
};
|
||||||
|
unitAI3.AddOrder = (type, data, queued, pushFront) =>
|
||||||
|
{
|
||||||
|
orderAdded3 = { type, data, queued, pushFront };
|
||||||
|
};
|
||||||
|
|
||||||
|
const smallTargets = [300, 301];
|
||||||
|
|
||||||
|
unitAI1.AttackGroup(smallTargets, 0, 3, true, true, false);
|
||||||
|
unitAI2.AttackGroup(smallTargets, 1, 3, true, true, false);
|
||||||
|
unitAI3.AttackGroup(smallTargets, 2, 3, true, true, false);
|
||||||
|
|
||||||
|
// Verify distribution when more units than targets
|
||||||
|
TS_ASSERT(orderAdded1);
|
||||||
|
TS_ASSERT_EQUALS(orderAdded1.data.currentTargetIndex, 0); // 0 * 2 / 3 = 0
|
||||||
|
TS_ASSERT_EQUALS(orderAdded1.data.allowCapture, true);
|
||||||
|
TS_ASSERT_EQUALS(orderAdded1.queued, true);
|
||||||
|
|
||||||
|
TS_ASSERT(orderAdded2);
|
||||||
|
TS_ASSERT_EQUALS(orderAdded2.data.currentTargetIndex, 1); // 1 * 2 / 3 = 0.66, round = 1
|
||||||
|
TS_ASSERT(orderAdded2.data.currentTargetIndex < smallTargets.length);
|
||||||
|
|
||||||
|
TS_ASSERT(orderAdded3);
|
||||||
|
TS_ASSERT_EQUALS(orderAdded3.data.currentTargetIndex, 1); // 2 * 2 / 3 = 1.33, round = 1
|
||||||
|
TS_ASSERT(orderAdded3.data.currentTargetIndex < smallTargets.length);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Edge case - single target
|
||||||
|
(function()
|
||||||
|
{
|
||||||
|
let orderAdded = null;
|
||||||
|
unitAI1.AddOrder = (type, data, queued, pushFront) =>
|
||||||
|
{
|
||||||
|
orderAdded = { type, data, queued, pushFront };
|
||||||
|
};
|
||||||
|
|
||||||
|
const singleTarget = [400];
|
||||||
|
|
||||||
|
unitAI1.AttackGroup(singleTarget, 0, 1, false, false, true);
|
||||||
|
|
||||||
|
TS_ASSERT(orderAdded);
|
||||||
|
TS_ASSERT_EQUALS(orderAdded.data.targetArray.length, 1);
|
||||||
|
TS_ASSERT_EQUALS(orderAdded.data.currentTargetIndex, 0);
|
||||||
|
TS_ASSERT_EQUALS(orderAdded.pushFront, true);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Empty targets array should do nothing
|
||||||
|
(function()
|
||||||
|
{
|
||||||
|
let callCount = 0;
|
||||||
|
unitAI1.AddOrder = () => { callCount++; };
|
||||||
|
|
||||||
|
unitAI1.AttackGroup([], 0, 1);
|
||||||
|
TS_ASSERT_EQUALS(callCount, 0);
|
||||||
|
|
||||||
|
unitAI1.AttackGroup(null, 0, 1);
|
||||||
|
TS_ASSERT_EQUALS(callCount, 0);
|
||||||
|
|
||||||
|
unitAI1.AttackGroup(undefined, 0, 1);
|
||||||
|
TS_ASSERT_EQUALS(callCount, 0);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Verify RememberTargetPosition is called
|
||||||
|
(function()
|
||||||
|
{
|
||||||
|
let rememberedOrder = null;
|
||||||
|
unitAI1.RememberTargetPosition = (order) =>
|
||||||
|
{
|
||||||
|
rememberedOrder = order;
|
||||||
|
};
|
||||||
|
|
||||||
|
unitAI1.AddOrder = () => {};
|
||||||
|
|
||||||
|
const testTargets = [500, 501];
|
||||||
|
unitAI1.AttackGroup(testTargets, 0, 1);
|
||||||
|
|
||||||
|
TS_ASSERT(rememberedOrder);
|
||||||
|
TS_ASSERT_EQUALS(rememberedOrder.targetArray.length, 2);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
TestAttackGroupBehavior();
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,28 @@ var g_Commands = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"attack-group": function(player, cmd, data)
|
||||||
|
{
|
||||||
|
const unitAIs = data.entities.flatMap(ent =>
|
||||||
|
{
|
||||||
|
const cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
|
||||||
|
const cmpAttack = Engine.QueryInterface(ent, IID_Attack);
|
||||||
|
|
||||||
|
// Check if unit can move and has attack capability
|
||||||
|
const canAttack = cmpAttack && cmpAttack.GetAttackTypes().length > 0;
|
||||||
|
return (cmpUnitAI && cmpUnitAI.AbleToMove() && canAttack) ? [cmpUnitAI] : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!unitAIs.length || !cmd.targets?.length)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Pass the entire target array and unit count to each UnitAI
|
||||||
|
unitAIs.forEach((cmpUnitAI, index) =>
|
||||||
|
{
|
||||||
|
cmpUnitAI.AttackGroup(cmd.targets, index, unitAIs.length, cmd.allowCapture, cmd.queued, cmd.pushFront);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
"patrol": function(player, cmd, data)
|
"patrol": function(player, cmd, data)
|
||||||
{
|
{
|
||||||
const ents = data.entities.length;
|
const ents = data.entities.length;
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -92,12 +92,12 @@ void CCinemaManager::DrawSpline(Renderer::Backend::IDeviceCommandContext& device
|
||||||
if (spline.GetAllNodes().size() == 2)
|
if (spline.GetAllNodes().size() == 2)
|
||||||
smoothness = 2;
|
smoothness = 2;
|
||||||
|
|
||||||
const float start = spline.MaxDistance.ToFloat() / smoothness;
|
const float start = spline.GetMaxDistance().ToFloat() / smoothness;
|
||||||
|
|
||||||
std::vector<CVector3D> line;
|
std::vector<CVector3D> line;
|
||||||
for (int i = 0; i <= smoothness; ++i)
|
for (int i = 0; i <= smoothness; ++i)
|
||||||
{
|
{
|
||||||
const float time = start * i / spline.MaxDistance.ToFloat();
|
const float time = start * i / spline.GetMaxDistance().ToFloat();
|
||||||
line.emplace_back(spline.GetPosition(time));
|
line.emplace_back(spline.GetPosition(time));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,7 +108,7 @@ void CCinemaManager::DrawSpline(Renderer::Backend::IDeviceCommandContext& device
|
||||||
{
|
{
|
||||||
for (int i = 0; i <= smoothness; ++i)
|
for (int i = 0; i <= smoothness; ++i)
|
||||||
{
|
{
|
||||||
const float time = start * i / spline.MaxDistance.ToFloat();
|
const float time = start * i / spline.GetMaxDistance().ToFloat();
|
||||||
const CVector3D tmp = spline.GetPosition(time);
|
const CVector3D tmp = spline.GetPosition(time);
|
||||||
const float groundY = g_Game->GetWorld()->GetTerrain().GetExactGroundLevel(tmp.X, tmp.Z);
|
const float groundY = g_Game->GetWorld()->GetTerrain().GetExactGroundLevel(tmp.X, tmp.Z);
|
||||||
g_Renderer.GetDebugRenderer().DrawLine(deviceCommandContext, tmp, CVector3D(tmp.X, groundY, tmp.Z), splineColor, 0.1f, false);
|
g_Renderer.GetDebugRenderer().DrawLine(deviceCommandContext, tmp, CVector3D(tmp.X, groundY, tmp.Z), splineColor, 0.1f, false);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,7 @@ void CMaterial::AddRenderQuery(const char* key)
|
||||||
m_RenderQueries.Add(key);
|
m_RenderQueries.Add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CMaterial::AddRequiredSampler(const CStr& samplerName)
|
void CMaterial::AddRequiredSampler(RequiredSampler sampler)
|
||||||
{
|
{
|
||||||
CStrIntern string(samplerName);
|
m_RequiredSamplers.emplace_back(std::move(sampler));
|
||||||
m_RequiredSamplers.push_back(string);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,12 @@ public:
|
||||||
CTexturePtr Sampler;
|
CTexturePtr Sampler;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct RequiredSampler
|
||||||
|
{
|
||||||
|
CStrIntern name;
|
||||||
|
bool sRGB;
|
||||||
|
};
|
||||||
|
|
||||||
typedef std::vector<TextureSampler> SamplersVector;
|
typedef std::vector<TextureSampler> SamplersVector;
|
||||||
|
|
||||||
CMaterial();
|
CMaterial();
|
||||||
|
|
@ -82,8 +88,8 @@ public:
|
||||||
void AddRenderQuery(const char* key);
|
void AddRenderQuery(const char* key);
|
||||||
const CShaderRenderQueries& GetRenderQueries() const { return m_RenderQueries; }
|
const CShaderRenderQueries& GetRenderQueries() const { return m_RenderQueries; }
|
||||||
|
|
||||||
void AddRequiredSampler(const CStr& samplerName);
|
void AddRequiredSampler(RequiredSampler sampler);
|
||||||
const std::vector<CStrIntern>& GetRequiredSampler() const { return m_RequiredSamplers; }
|
const std::vector<RequiredSampler>& GetRequiredSamplers() const { return m_RequiredSamplers; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
|
@ -92,7 +98,7 @@ private:
|
||||||
CTexturePtr m_DiffuseTexture;
|
CTexturePtr m_DiffuseTexture;
|
||||||
|
|
||||||
SamplersVector m_Samplers;
|
SamplersVector m_Samplers;
|
||||||
std::vector<CStrIntern> m_RequiredSamplers;
|
std::vector<RequiredSampler> m_RequiredSamplers;
|
||||||
|
|
||||||
std::array<CStrIntern, static_cast<size_t>(Pass::COUNT)> m_ShaderEffects;
|
std::array<CStrIntern, static_cast<size_t>(Pass::COUNT)> m_ShaderEffects;
|
||||||
CShaderDefines m_ShaderDefines;
|
CShaderDefines m_ShaderDefines;
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ CMaterial CMaterialManager::LoadMaterial(const VfsPath& pathname)
|
||||||
AT(material);
|
AT(material);
|
||||||
AT(name);
|
AT(name);
|
||||||
AT(pass);
|
AT(pass);
|
||||||
|
AT(srgb);
|
||||||
AT(value);
|
AT(value);
|
||||||
#undef AT
|
#undef AT
|
||||||
#undef EL
|
#undef EL
|
||||||
|
|
@ -161,7 +162,8 @@ CMaterial CMaterialManager::LoadMaterial(const VfsPath& pathname)
|
||||||
}
|
}
|
||||||
else if (token == el_required_texture)
|
else if (token == el_required_texture)
|
||||||
{
|
{
|
||||||
material.AddRequiredSampler(attrs.GetNamedItem(at_name));
|
const bool sRGB{attrs.GetNamedItem(at_srgb) == "true"};
|
||||||
|
material.AddRequiredSampler({CStrIntern{attrs.GetNamedItem(at_name)}, sRGB});
|
||||||
if (!attrs.GetNamedItem(at_define).empty())
|
if (!attrs.GetNamedItem(at_define).empty())
|
||||||
material.AddShaderDefine(CStrIntern(attrs.GetNamedItem(at_define)), str_1);
|
material.AddShaderDefine(CStrIntern(attrs.GetNamedItem(at_define)), str_1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,22 @@
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
bool ShouldInterpretTextureAsSRGB(const CObjectBase::Samp& sampler, const CMaterial& material)
|
||||||
|
{
|
||||||
|
const auto requiredSamplers{material.GetRequiredSamplers()};
|
||||||
|
auto it{std::find_if(requiredSamplers.begin(), requiredSamplers.end(),
|
||||||
|
[&](const CMaterial::RequiredSampler& requiredSampler)
|
||||||
|
{
|
||||||
|
return requiredSampler.name == sampler.m_SamplerName;
|
||||||
|
})};
|
||||||
|
return it != requiredSamplers.end() && it->sRGB;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
CObjectEntry::CObjectEntry(const std::shared_ptr<CObjectBase>& base, const CSimulation2& simulation) :
|
CObjectEntry::CObjectEntry(const std::shared_ptr<CObjectBase>& base, const CSimulation2& simulation) :
|
||||||
m_Base(base), m_Color(1.0f, 1.0f, 1.0f, 1.0f), m_Simulation(simulation)
|
m_Base(base), m_Color(1.0f, 1.0f, 1.0f, 1.0f), m_Simulation(simulation)
|
||||||
{
|
{
|
||||||
|
|
@ -155,6 +171,7 @@ bool CObjectEntry::BuildVariation(const std::vector<const std::set<CStr>*>& comp
|
||||||
{
|
{
|
||||||
CTextureProperties textureProps(samp.m_SamplerFile);
|
CTextureProperties textureProps(samp.m_SamplerFile);
|
||||||
textureProps.SetAddressMode(Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE);
|
textureProps.SetAddressMode(Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE);
|
||||||
|
textureProps.SetSRGB(ShouldInterpretTextureAsSRGB(samp, material));
|
||||||
CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps);
|
CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps);
|
||||||
// if we've loaded this model we're probably going to render it soon, so prefetch its texture.
|
// if we've loaded this model we're probably going to render it soon, so prefetch its texture.
|
||||||
// All textures are prefetched even in the fixed pipeline, including the normal maps etc.
|
// All textures are prefetched even in the fixed pipeline, including the normal maps etc.
|
||||||
|
|
@ -167,11 +184,14 @@ bool CObjectEntry::BuildVariation(const std::vector<const std::set<CStr>*>& comp
|
||||||
CModel* model = newModel.get();
|
CModel* model = newModel.get();
|
||||||
m_Model = std::move(newModel);
|
m_Model = std::move(newModel);
|
||||||
|
|
||||||
for (const CStrIntern& requSampName : model->GetMaterial().GetRequiredSampler())
|
for (const auto& requiredSampler : model->GetMaterial().GetRequiredSamplers())
|
||||||
{
|
{
|
||||||
if (std::find_if(m_Samplers.begin(), m_Samplers.end(),
|
if (std::find_if(m_Samplers.begin(), m_Samplers.end(),
|
||||||
[&](const CObjectBase::Samp& sampler) { return sampler.m_SamplerName == requSampName; }) == m_Samplers.end())
|
[&](const CObjectBase::Samp& sampler) { return sampler.m_SamplerName == requiredSampler.name; }) == m_Samplers.end())
|
||||||
LOGERROR("Actor %s: required texture sampler %s not found (material %s)", m_Base->GetIdentifier(), requSampName.string().c_str(), m_Base->m_Material.string8().c_str());
|
{
|
||||||
|
LOGERROR("Actor %s: required texture sampler %s not found (material %s)",
|
||||||
|
m_Base->GetIdentifier(), requiredSampler.name.string().c_str(), m_Base->m_Material.string8().c_str());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate initial object space bounds, based on vertex positions
|
// calculate initial object space bounds, based on vertex positions
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -60,7 +60,7 @@ namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded(
|
Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded(
|
||||||
Renderer::Backend::IDevice* device, Tex& textureData, const bool hasS3TC)
|
Renderer::Backend::IDevice* device, Tex& textureData, const bool hasS3TC, const bool sRGB)
|
||||||
{
|
{
|
||||||
const bool alpha = (textureData.m_Flags & TEX_ALPHA) != 0;
|
const bool alpha = (textureData.m_Flags & TEX_ALPHA) != 0;
|
||||||
const bool grey = (textureData.m_Flags & TEX_GREY) != 0;
|
const bool grey = (textureData.m_Flags & TEX_GREY) != 0;
|
||||||
|
|
@ -81,13 +81,13 @@ Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded(
|
||||||
switch (dxt)
|
switch (dxt)
|
||||||
{
|
{
|
||||||
case DXT1A:
|
case DXT1A:
|
||||||
return Renderer::Backend::Format::BC1_RGBA_UNORM;
|
return sRGB ? Renderer::Backend::Format::BC1_RGBA_SRGB : Renderer::Backend::Format::BC1_RGBA_UNORM;
|
||||||
case 1:
|
case 1:
|
||||||
return Renderer::Backend::Format::BC1_RGB_UNORM;
|
return sRGB ? Renderer::Backend::Format::BC1_RGB_SRGB : Renderer::Backend::Format::BC1_RGB_UNORM;
|
||||||
case 3:
|
case 3:
|
||||||
return Renderer::Backend::Format::BC2_UNORM;
|
return sRGB ? Renderer::Backend::Format::BC2_SRGB : Renderer::Backend::Format::BC2_UNORM;
|
||||||
case 5:
|
case 5:
|
||||||
return Renderer::Backend::Format::BC3_UNORM;
|
return sRGB ? Renderer::Backend::Format::BC3_SRGB : Renderer::Backend::Format::BC3_UNORM;
|
||||||
default:
|
default:
|
||||||
LOGERROR("Unknown DXT compression.");
|
LOGERROR("Unknown DXT compression.");
|
||||||
return Renderer::Backend::Format::UNDEFINED;
|
return Renderer::Backend::Format::UNDEFINED;
|
||||||
|
|
@ -101,9 +101,12 @@ Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded(
|
||||||
{
|
{
|
||||||
case 8:
|
case 8:
|
||||||
ENSURE(grey);
|
ENSURE(grey);
|
||||||
|
ENSURE(!sRGB);
|
||||||
return Renderer::Backend::Format::L8_UNORM;
|
return Renderer::Backend::Format::L8_UNORM;
|
||||||
case 24:
|
case 24:
|
||||||
|
ENSURE(grey);
|
||||||
ENSURE(!alpha);
|
ENSURE(!alpha);
|
||||||
|
ENSURE(!sRGB);
|
||||||
if (device->IsTextureFormatSupported(Renderer::Backend::Format::R8G8B8_UNORM))
|
if (device->IsTextureFormatSupported(Renderer::Backend::Format::R8G8B8_UNORM))
|
||||||
return Renderer::Backend::Format::R8G8B8_UNORM;
|
return Renderer::Backend::Format::R8G8B8_UNORM;
|
||||||
else
|
else
|
||||||
|
|
@ -114,7 +117,7 @@ Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded(
|
||||||
}
|
}
|
||||||
case 32:
|
case 32:
|
||||||
ENSURE(alpha);
|
ENSURE(alpha);
|
||||||
return Renderer::Backend::Format::R8G8B8A8_UNORM;
|
return sRGB ? Renderer::Backend::Format::R8G8B8A8_SRGB : Renderer::Backend::Format::R8G8B8A8_UNORM;
|
||||||
default:
|
default:
|
||||||
LOGERROR("Unsupported BPP: %zu", textureData.m_Bpp);
|
LOGERROR("Unsupported BPP: %zu", textureData.m_Bpp);
|
||||||
}
|
}
|
||||||
|
|
@ -336,6 +339,7 @@ struct TPhash
|
||||||
hash_combine(seed, textureProperties.m_AnisotropicFilterEnabled);
|
hash_combine(seed, textureProperties.m_AnisotropicFilterEnabled);
|
||||||
hash_combine(seed, textureProperties.m_FormatOverride);
|
hash_combine(seed, textureProperties.m_FormatOverride);
|
||||||
hash_combine(seed, textureProperties.m_IgnoreQuality);
|
hash_combine(seed, textureProperties.m_IgnoreQuality);
|
||||||
|
hash_combine(seed, textureProperties.m_SRGB);
|
||||||
return seed;
|
return seed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -358,7 +362,8 @@ struct TPequal_to
|
||||||
lhs.m_AddressModeV == rhs.m_AddressModeV &&
|
lhs.m_AddressModeV == rhs.m_AddressModeV &&
|
||||||
lhs.m_AnisotropicFilterEnabled == rhs.m_AnisotropicFilterEnabled &&
|
lhs.m_AnisotropicFilterEnabled == rhs.m_AnisotropicFilterEnabled &&
|
||||||
lhs.m_FormatOverride == rhs.m_FormatOverride &&
|
lhs.m_FormatOverride == rhs.m_FormatOverride &&
|
||||||
lhs.m_IgnoreQuality == rhs.m_IgnoreQuality;
|
lhs.m_IgnoreQuality == rhs.m_IgnoreQuality &&
|
||||||
|
lhs.m_SRGB == rhs.m_SRGB;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool operator()(const CTexturePtr& lhs, const CTexturePtr& rhs) const
|
bool operator()(const CTexturePtr& lhs, const CTexturePtr& rhs) const
|
||||||
|
|
@ -526,7 +531,8 @@ public:
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
format = ChooseFormatAndTransformTextureDataIfNeeded(m_Device, textureData, m_HasS3TC);
|
format = ChooseFormatAndTransformTextureDataIfNeeded(
|
||||||
|
m_Device, textureData, m_HasS3TC, texture->m_Properties.m_SRGB);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format == Renderer::Backend::Format::UNDEFINED)
|
if (format == Renderer::Backend::Format::UNDEFINED)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -245,6 +245,8 @@ public:
|
||||||
|
|
||||||
void SetIgnoreQuality(bool ignore) { m_IgnoreQuality = ignore; }
|
void SetIgnoreQuality(bool ignore) { m_IgnoreQuality = ignore; }
|
||||||
|
|
||||||
|
void SetSRGB(const bool sRGB) { m_SRGB = sRGB; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Must update TPhash, TPequal_to when changing these fields
|
// Must update TPhash, TPequal_to when changing these fields
|
||||||
VfsPath m_Path;
|
VfsPath m_Path;
|
||||||
|
|
@ -257,6 +259,7 @@ private:
|
||||||
Renderer::Backend::Format m_FormatOverride =
|
Renderer::Backend::Format m_FormatOverride =
|
||||||
Renderer::Backend::Format::UNDEFINED;
|
Renderer::Backend::Format::UNDEFINED;
|
||||||
bool m_IgnoreQuality = false;
|
bool m_IgnoreQuality = false;
|
||||||
|
bool m_SRGB{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -52,114 +52,111 @@ CVector3D GetPositionOnCubic(const CVector3D& startPos, const CVector3D& startVe
|
||||||
|
|
||||||
/*********************************** R N S **************************************************/
|
/*********************************** R N S **************************************************/
|
||||||
|
|
||||||
RNSpline::RNSpline()
|
RNSpline::RNSpline() = default;
|
||||||
: NodeCount(0)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
RNSpline::~RNSpline() = default;
|
RNSpline::~RNSpline() = default;
|
||||||
|
|
||||||
// adds node and updates segment length
|
// adds node and updates segment length
|
||||||
void RNSpline::AddNode(const CFixedVector3D& pos)
|
void RNSpline::AddNode(const CFixedVector3D& pos)
|
||||||
{
|
{
|
||||||
if (NodeCount >= MAX_SPLINE_NODES)
|
if (m_NodeCount >= MAX_SPLINE_NODES)
|
||||||
return;
|
return;
|
||||||
if (NodeCount == 0)
|
if (m_NodeCount == 0)
|
||||||
MaxDistance = fixed::Zero();
|
m_MaxDistance = fixed::Zero();
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Node[NodeCount-1].Distance = (Node[NodeCount-1].Position - pos).Length();
|
m_Nodes[m_NodeCount-1].Distance = (m_Nodes[m_NodeCount-1].Position - pos).Length();
|
||||||
MaxDistance += Node[NodeCount-1].Distance;
|
m_MaxDistance += m_Nodes[m_NodeCount-1].Distance;
|
||||||
}
|
}
|
||||||
SplineData temp;
|
SplineData temp;
|
||||||
temp.Position = pos;
|
temp.Position = pos;
|
||||||
Node.push_back(temp);
|
m_Nodes.push_back(temp);
|
||||||
++NodeCount;
|
++m_NodeCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// called after all nodes added. This function calculates the node velocities
|
// called after all nodes added. This function calculates the node velocities
|
||||||
void RNSpline::BuildSpline()
|
void RNSpline::BuildSpline()
|
||||||
{
|
{
|
||||||
if (NodeCount == 2)
|
if (m_NodeCount == 2)
|
||||||
{
|
{
|
||||||
Node[0].Velocity = GetStartVelocity(0);
|
m_Nodes[0].Velocity = GetStartVelocity(0);
|
||||||
Node[NodeCount-1].Velocity = GetEndVelocity(NodeCount-1);
|
m_Nodes[m_NodeCount-1].Velocity = GetEndVelocity(m_NodeCount-1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (NodeCount < 2)
|
else if (m_NodeCount < 2)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (int i = 1; i < NodeCount-1; ++i)
|
for (int i = 1; i < m_NodeCount-1; ++i)
|
||||||
{
|
{
|
||||||
CVector3D Next = Node[i+1].Position - Node[i].Position;
|
CVector3D Next = m_Nodes[i+1].Position - m_Nodes[i].Position;
|
||||||
CVector3D Previous = Node[i-1].Position - Node[i].Position;
|
CVector3D Previous = m_Nodes[i-1].Position - m_Nodes[i].Position;
|
||||||
Next.Normalize();
|
Next.Normalize();
|
||||||
Previous.Normalize();
|
Previous.Normalize();
|
||||||
|
|
||||||
// split the angle (figure 4)
|
// split the angle (figure 4)
|
||||||
Node[i].Velocity = Next - Previous;
|
m_Nodes[i].Velocity = Next - Previous;
|
||||||
Node[i].Velocity.Normalize();
|
m_Nodes[i].Velocity.Normalize();
|
||||||
}
|
}
|
||||||
// calculate start and end velocities
|
// calculate start and end velocities
|
||||||
Node[0].Velocity = GetStartVelocity(0);
|
m_Nodes[0].Velocity = GetStartVelocity(0);
|
||||||
Node[NodeCount-1].Velocity = GetEndVelocity(NodeCount-1);
|
m_Nodes[m_NodeCount-1].Velocity = GetEndVelocity(m_NodeCount-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// spline access function. time is 0 -> 1
|
// spline access function. time is 0 -> 1
|
||||||
CVector3D RNSpline::GetPosition(float time) const
|
CVector3D RNSpline::GetPosition(float time) const
|
||||||
{
|
{
|
||||||
if (NodeCount < 2)
|
if (m_NodeCount < 2)
|
||||||
return CVector3D(0.0f, 0.0f, 0.0f);
|
return CVector3D(0.0f, 0.0f, 0.0f);
|
||||||
if (time < 0.0f)
|
if (time < 0.0f)
|
||||||
time = 0.0f;
|
time = 0.0f;
|
||||||
if (time > 1.0f)
|
if (time > 1.0f)
|
||||||
time = 1.0f;
|
time = 1.0f;
|
||||||
float Distance = time * MaxDistance.ToFloat();
|
float Distance = time * m_MaxDistance.ToFloat();
|
||||||
float CurrentDistance = 0.f;
|
float CurrentDistance = 0.f;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
// Find which node we're on
|
// Find which node we're on
|
||||||
while (CurrentDistance + Node[i].Distance.ToFloat() < Distance && i < NodeCount - 2)
|
while (CurrentDistance + m_Nodes[i].Distance.ToFloat() < Distance && i < m_NodeCount - 2)
|
||||||
{
|
{
|
||||||
CurrentDistance += Node[i].Distance.ToFloat();
|
CurrentDistance += m_Nodes[i].Distance.ToFloat();
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
ENSURE(i < NodeCount - 1);
|
ENSURE(i < m_NodeCount - 1);
|
||||||
float t = Distance - CurrentDistance;
|
float t = Distance - CurrentDistance;
|
||||||
// TODO: reimplement CVector3D comparator (float comparing is bad without EPS)
|
// TODO: reimplement CVector3D comparator (float comparing is bad without EPS)
|
||||||
if (Node[i].Position == Node[i+1].Position || Node[i].Distance.ToFloat() < 1e-7) // distance too small or zero
|
if (m_Nodes[i].Position == m_Nodes[i+1].Position || m_Nodes[i].Distance.ToFloat() < 1e-7) // distance too small or zero
|
||||||
{
|
{
|
||||||
return Node[i+1].Position;
|
return m_Nodes[i+1].Position;
|
||||||
}
|
}
|
||||||
t /= Node[i].Distance.ToFloat(); // scale t in range 0 - 1
|
t /= m_Nodes[i].Distance.ToFloat(); // scale t in range 0 - 1
|
||||||
CVector3D startVel = Node[i].Velocity * Node[i].Distance.ToFloat();
|
CVector3D startVel = m_Nodes[i].Velocity * m_Nodes[i].Distance.ToFloat();
|
||||||
CVector3D endVel = Node[i+1].Velocity * Node[i].Distance.ToFloat();
|
CVector3D endVel = m_Nodes[i+1].Velocity * m_Nodes[i].Distance.ToFloat();
|
||||||
return GetPositionOnCubic(Node[i].Position, startVel,
|
return GetPositionOnCubic(m_Nodes[i].Position, startVel,
|
||||||
Node[i+1].Position, endVel, t);
|
m_Nodes[i+1].Position, endVel, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<SplineData>& RNSpline::GetAllNodes() const
|
const std::vector<SplineData>& RNSpline::GetAllNodes() const
|
||||||
{
|
{
|
||||||
return Node;
|
return m_Nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal. Based on Equation 14
|
// internal. Based on Equation 14
|
||||||
CVector3D RNSpline::GetStartVelocity(int index)
|
CVector3D RNSpline::GetStartVelocity(int index)
|
||||||
{
|
{
|
||||||
if (index >= NodeCount - 1 || index < 0)
|
if (index >= m_NodeCount - 1 || index < 0)
|
||||||
return CVector3D(0.0f, 0.0f, 0.0f);
|
return CVector3D(0.0f, 0.0f, 0.0f);
|
||||||
CVector3D temp = CVector3D(Node[index+1].Position - Node[index].Position) * 3.0f * (1.0f / Node[index].Distance.ToFloat());
|
CVector3D temp = CVector3D(m_Nodes[index+1].Position - m_Nodes[index].Position) * 3.0f * (1.0f / m_Nodes[index].Distance.ToFloat());
|
||||||
return (temp - Node[index+1].Velocity)*0.5f;
|
return (temp - m_Nodes[index+1].Velocity)*0.5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal. Based on Equation 15
|
// internal. Based on Equation 15
|
||||||
CVector3D RNSpline::GetEndVelocity(int index)
|
CVector3D RNSpline::GetEndVelocity(int index)
|
||||||
{
|
{
|
||||||
if (index >= NodeCount || index < 1)
|
if (index >= m_NodeCount || index < 1)
|
||||||
return CVector3D(0.0f, 0.0f, 0.0f);
|
return CVector3D(0.0f, 0.0f, 0.0f);
|
||||||
CVector3D temp = CVector3D(Node[index].Position - Node[index-1].Position) * 3.0f * (1.0f / Node[index-1].Distance.ToFloat());
|
CVector3D temp = CVector3D(m_Nodes[index].Position - m_Nodes[index-1].Position) * 3.0f * (1.0f / m_Nodes[index-1].Distance.ToFloat());
|
||||||
return (temp - Node[index-1].Velocity) * 0.5f;
|
return (temp - m_Nodes[index-1].Velocity) * 0.5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*********************************** S N S **************************************************/
|
/*********************************** S N S **************************************************/
|
||||||
|
|
@ -176,21 +173,21 @@ void SNSpline::BuildSpline()
|
||||||
// smoothing filter.
|
// smoothing filter.
|
||||||
void SNSpline::Smooth()
|
void SNSpline::Smooth()
|
||||||
{
|
{
|
||||||
if (NodeCount < 3)
|
if (m_NodeCount < 3)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
CVector3D newVel;
|
CVector3D newVel;
|
||||||
CVector3D oldVel = GetStartVelocity(0);
|
CVector3D oldVel = GetStartVelocity(0);
|
||||||
for (int i = 1; i < NodeCount-1; ++i)
|
for (int i = 1; i < m_NodeCount-1; ++i)
|
||||||
{
|
{
|
||||||
// Equation 12
|
// Equation 12
|
||||||
newVel = GetEndVelocity(i) * Node[i].Distance.ToFloat() + GetStartVelocity(i) * Node[i-1].Distance.ToFloat();
|
newVel = GetEndVelocity(i) * m_Nodes[i].Distance.ToFloat() + GetStartVelocity(i) * m_Nodes[i-1].Distance.ToFloat();
|
||||||
newVel = newVel * (1 / (Node[i-1].Distance + Node[i].Distance).ToFloat());
|
newVel = newVel * (1 / (m_Nodes[i-1].Distance + m_Nodes[i].Distance).ToFloat());
|
||||||
Node[i-1].Velocity = oldVel;
|
m_Nodes[i-1].Velocity = oldVel;
|
||||||
oldVel = newVel;
|
oldVel = newVel;
|
||||||
}
|
}
|
||||||
Node[NodeCount-1].Velocity = GetEndVelocity(NodeCount-1);
|
m_Nodes[m_NodeCount-1].Velocity = GetEndVelocity(m_NodeCount-1);
|
||||||
Node[NodeCount-2].Velocity = oldVel;
|
m_Nodes[m_NodeCount-2].Velocity = oldVel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*********************************** T N S **************************************************/
|
/*********************************** T N S **************************************************/
|
||||||
|
|
@ -201,15 +198,15 @@ TNSpline::~TNSpline() = default;
|
||||||
// ie time period is time from last node to this node
|
// ie time period is time from last node to this node
|
||||||
void TNSpline::AddNode(const CFixedVector3D& pos, const CFixedVector3D& rotation, fixed timePeriod)
|
void TNSpline::AddNode(const CFixedVector3D& pos, const CFixedVector3D& rotation, fixed timePeriod)
|
||||||
{
|
{
|
||||||
if (NodeCount >= MAX_SPLINE_NODES)
|
if (m_NodeCount >= MAX_SPLINE_NODES)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (NodeCount == 0)
|
if (m_NodeCount == 0)
|
||||||
MaxDistance = fixed::Zero();
|
m_MaxDistance = fixed::Zero();
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Node[NodeCount-1].Distance = timePeriod;
|
m_Nodes[m_NodeCount-1].Distance = timePeriod;
|
||||||
MaxDistance += Node[NodeCount-1].Distance;
|
m_MaxDistance += m_Nodes[m_NodeCount-1].Distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
SplineData temp;
|
SplineData temp;
|
||||||
|
|
@ -219,56 +216,56 @@ void TNSpline::AddNode(const CFixedVector3D& pos, const CFixedVector3D& rotation
|
||||||
temp.Distance = fixed::Zero();
|
temp.Distance = fixed::Zero();
|
||||||
temp.Velocity = CVector3D(0.0f, 0.0f, 0.0f);
|
temp.Velocity = CVector3D(0.0f, 0.0f, 0.0f);
|
||||||
temp.Rotation = rotation;
|
temp.Rotation = rotation;
|
||||||
Node.push_back(temp);
|
m_Nodes.push_back(temp);
|
||||||
++NodeCount;
|
++m_NodeCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Inserts node before position
|
//Inserts node before position
|
||||||
void TNSpline::InsertNode(const int index, const CFixedVector3D& pos, const CFixedVector3D& /*rotation*/,
|
void TNSpline::InsertNode(const int index, const CFixedVector3D& pos, const CFixedVector3D& /*rotation*/,
|
||||||
fixed timePeriod)
|
fixed timePeriod)
|
||||||
{
|
{
|
||||||
if (NodeCount >= MAX_SPLINE_NODES || index < 0 || index > NodeCount)
|
if (m_NodeCount >= MAX_SPLINE_NODES || index < 0 || index > m_NodeCount)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (NodeCount == 0)
|
if (m_NodeCount == 0)
|
||||||
MaxDistance = fixed::Zero();
|
m_MaxDistance = fixed::Zero();
|
||||||
else
|
else
|
||||||
MaxDistance += timePeriod;
|
m_MaxDistance += timePeriod;
|
||||||
|
|
||||||
SplineData temp;
|
SplineData temp;
|
||||||
temp.Position = pos;
|
temp.Position = pos;
|
||||||
temp.Distance = timePeriod;
|
temp.Distance = timePeriod;
|
||||||
Node.insert(Node.begin() + index, temp);
|
m_Nodes.insert(m_Nodes.begin() + index, temp);
|
||||||
if (index > 0)
|
if (index > 0)
|
||||||
std::swap(Node[index].Distance, Node[index - 1].Distance);
|
std::swap(m_Nodes[index].Distance, m_Nodes[index - 1].Distance);
|
||||||
++NodeCount;
|
++m_NodeCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Removes node at index
|
//Removes node at index
|
||||||
void TNSpline::RemoveNode(const int index)
|
void TNSpline::RemoveNode(const int index)
|
||||||
{
|
{
|
||||||
if (NodeCount == 0 || index > NodeCount - 1)
|
if (m_NodeCount == 0 || index > m_NodeCount - 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
MaxDistance -= Node[index].Distance;
|
m_MaxDistance -= m_Nodes[index].Distance;
|
||||||
Node.erase(Node.begin() + index);
|
m_Nodes.erase(m_Nodes.begin() + index);
|
||||||
--NodeCount;
|
--m_NodeCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TNSpline::UpdateNodeTime(const int index, fixed time)
|
void TNSpline::UpdateNodeTime(const int index, fixed time)
|
||||||
{
|
{
|
||||||
if (NodeCount == 0 || index > NodeCount - 1)
|
if (m_NodeCount == 0 || index > m_NodeCount - 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Node[index].Distance = time;
|
m_Nodes[index].Distance = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TNSpline::UpdateNodePos(const int index, const CFixedVector3D& pos)
|
void TNSpline::UpdateNodePos(const int index, const CFixedVector3D& pos)
|
||||||
{
|
{
|
||||||
if (NodeCount == 0 || index > NodeCount - 1)
|
if (m_NodeCount == 0 || index > m_NodeCount - 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Node[index].Position = pos;
|
m_Nodes[index].Position = pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TNSpline::BuildSpline()
|
void TNSpline::BuildSpline()
|
||||||
|
|
@ -289,15 +286,15 @@ void TNSpline::Smooth()
|
||||||
|
|
||||||
void TNSpline::Constrain()
|
void TNSpline::Constrain()
|
||||||
{
|
{
|
||||||
if (NodeCount < 3)
|
if (m_NodeCount < 3)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (int i = 1; i < NodeCount-1; ++i)
|
for (int i = 1; i < m_NodeCount-1; ++i)
|
||||||
{
|
{
|
||||||
// Equation 13
|
// Equation 13
|
||||||
float r0 = (Node[i].Position - Node[i - 1].Position).Length().ToFloat() / Node[i-1].Distance.ToFloat();
|
float r0 = (m_Nodes[i].Position - m_Nodes[i - 1].Position).Length().ToFloat() / m_Nodes[i-1].Distance.ToFloat();
|
||||||
float r1 = (Node[i+1].Position - Node[i].Position).Length().ToFloat() / Node[i].Distance.ToFloat();
|
float r1 = (m_Nodes[i+1].Position - m_Nodes[i].Position).Length().ToFloat() / m_Nodes[i].Distance.ToFloat();
|
||||||
Node[i].Velocity *= 4.0f*r0*r1 / ((r0 + r1)*(r0 + r1));
|
m_Nodes[i].Velocity *= 4.0f*r0*r1 / ((r0 + r1)*(r0 + r1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -63,12 +63,12 @@ public:
|
||||||
CVector3D GetRotation(float time) const;
|
CVector3D GetRotation(float time) const;
|
||||||
const std::vector<SplineData>& GetAllNodes() const;
|
const std::vector<SplineData>& GetAllNodes() const;
|
||||||
|
|
||||||
fixed MaxDistance;
|
fixed GetMaxDistance() const { return m_MaxDistance; }
|
||||||
int NodeCount;
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
fixed m_MaxDistance;
|
||||||
std::vector<SplineData> Node;
|
int m_NodeCount{0};
|
||||||
|
std::vector<SplineData> m_Nodes;
|
||||||
CVector3D GetStartVelocity(int index);
|
CVector3D GetStartVelocity(int index);
|
||||||
CVector3D GetEndVelocity(int index);
|
CVector3D GetEndVelocity(int index);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ void CNetClientTurnManager::PostCommand(JS::HandleValue data)
|
||||||
// TODO: we should do this when the server stops sending our commands back to us
|
// TODO: we should do this when the server stops sending our commands back to us
|
||||||
}
|
}
|
||||||
|
|
||||||
void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn)
|
void CNetClientTurnManager::NotifyFinishedOwnCommands(turn_id_t turn)
|
||||||
{
|
{
|
||||||
NETCLIENTTURN_LOG("NotifyFinishedOwnCommands(%d)\n", turn);
|
NETCLIENTTURN_LOG("NotifyFinishedOwnCommands(%d)\n", turn);
|
||||||
|
|
||||||
|
|
@ -79,7 +79,7 @@ void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn)
|
||||||
m_NetClient.SendMessage(&msg);
|
m_NetClient.SendMessage(&msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CNetClientTurnManager::NotifyFinishedUpdate(u32 turn, const UpdateCallback&)
|
void CNetClientTurnManager::NotifyFinishedUpdate(turn_id_t turn, const UpdateCallback&)
|
||||||
{
|
{
|
||||||
bool quick = !TurnNeedsFullHash(turn);
|
bool quick = !TurnNeedsFullHash(turn);
|
||||||
std::string hash;
|
std::string hash;
|
||||||
|
|
@ -110,10 +110,10 @@ void CNetClientTurnManager::OnDestroyConnection()
|
||||||
void CNetClientTurnManager::OnSimulationMessage(CSimulationMessage* msg)
|
void CNetClientTurnManager::OnSimulationMessage(CSimulationMessage* msg)
|
||||||
{
|
{
|
||||||
// Command received from the server - store it for later execution
|
// Command received from the server - store it for later execution
|
||||||
AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, msg->m_Turn);
|
AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, static_cast<turn_id_t>(msg->m_Turn));
|
||||||
}
|
}
|
||||||
|
|
||||||
void CNetClientTurnManager::OnSyncError(u32 turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames)
|
void CNetClientTurnManager::OnSyncError(turn_id_t turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames)
|
||||||
{
|
{
|
||||||
CStr expectedHashHex(Hexify(expectedHash));
|
CStr expectedHashHex(Hexify(expectedHash));
|
||||||
NETCLIENTTURN_LOG("OnSyncError(%d, %hs)\n", turn, expectedHashHex.c_str());
|
NETCLIENTTURN_LOG("OnSyncError(%d, %hs)\n", turn, expectedHashHex.c_str());
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,12 @@ public:
|
||||||
*/
|
*/
|
||||||
void OnDestroyConnection();
|
void OnDestroyConnection();
|
||||||
|
|
||||||
void OnSyncError(u32 turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames);
|
void OnSyncError(turn_id_t turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void NotifyFinishedOwnCommands(u32 turn) override;
|
void NotifyFinishedOwnCommands(turn_id_t turn) override;
|
||||||
|
|
||||||
void NotifyFinishedUpdate(u32 turn, const UpdateCallback& sendEventToAll) override;
|
void NotifyFinishedUpdate(turn_id_t turn, const UpdateCallback& sendEventToAll) override;
|
||||||
|
|
||||||
CNetClient& m_NetClient;
|
CNetClient& m_NetClient;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ public:
|
||||||
|
|
||||||
u32 m_Client;
|
u32 m_Client;
|
||||||
i32 m_Player;
|
i32 m_Player;
|
||||||
u32 m_Turn;
|
i32 m_Turn;
|
||||||
JS::PersistentRooted<JS::Value> m_Data;
|
JS::PersistentRooted<JS::Value> m_Data;
|
||||||
private:
|
private:
|
||||||
const Script::Interface& m_ScriptInterface;
|
const Script::Interface& m_ScriptInterface;
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ u8* CSimulationMessage::Serialize(u8* pBuffer) const
|
||||||
CBufferBinarySerializer serializer(m_ScriptInterface, pos);
|
CBufferBinarySerializer serializer(m_ScriptInterface, pos);
|
||||||
serializer.NumberU32_Unbounded("client", m_Client);
|
serializer.NumberU32_Unbounded("client", m_Client);
|
||||||
serializer.NumberI32_Unbounded("player", m_Player);
|
serializer.NumberI32_Unbounded("player", m_Player);
|
||||||
serializer.NumberU32_Unbounded("turn", m_Turn);
|
serializer.NumberI32_Unbounded("turn", m_Turn);
|
||||||
|
|
||||||
serializer.ScriptVal("command", const_cast<JS::PersistentRootedValue*>(&m_Data));
|
serializer.ScriptVal("command", const_cast<JS::PersistentRootedValue*>(&m_Data));
|
||||||
return serializer.GetBuffer();
|
return serializer.GetBuffer();
|
||||||
|
|
@ -165,7 +165,7 @@ const u8* CSimulationMessage::Deserialize(const u8* pStart, const u8* pEnd)
|
||||||
CStdDeserializer deserializer(m_ScriptInterface, stream);
|
CStdDeserializer deserializer(m_ScriptInterface, stream);
|
||||||
deserializer.NumberU32_Unbounded("client", m_Client);
|
deserializer.NumberU32_Unbounded("client", m_Client);
|
||||||
deserializer.NumberI32_Unbounded("player", m_Player);
|
deserializer.NumberI32_Unbounded("player", m_Player);
|
||||||
deserializer.NumberU32_Unbounded("turn", m_Turn);
|
deserializer.NumberI32_Unbounded("turn", m_Turn);
|
||||||
deserializer.ScriptVal("command", &m_Data);
|
deserializer.ScriptVal("command", &m_Data);
|
||||||
return pEnd;
|
return pEnd;
|
||||||
}
|
}
|
||||||
|
|
@ -177,7 +177,7 @@ size_t CSimulationMessage::GetSerializedLength() const
|
||||||
CLengthBinarySerializer serializer(m_ScriptInterface);
|
CLengthBinarySerializer serializer(m_ScriptInterface);
|
||||||
serializer.NumberU32_Unbounded("client", m_Client);
|
serializer.NumberU32_Unbounded("client", m_Client);
|
||||||
serializer.NumberI32_Unbounded("player", m_Player);
|
serializer.NumberI32_Unbounded("player", m_Player);
|
||||||
serializer.NumberU32_Unbounded("turn", m_Turn);
|
serializer.NumberI32_Unbounded("turn", m_Turn);
|
||||||
|
|
||||||
// TODO: The cast can probably be removed if and when ScriptVal can take a JS::HandleValue instead of
|
// TODO: The cast can probably be removed if and when ScriptVal can take a JS::HandleValue instead of
|
||||||
// a JS::MutableHandleValue (relies on JSAPI change). Also search for other casts like this one in that case.
|
// a JS::MutableHandleValue (relies on JSAPI change). Also search for other casts like this one in that case.
|
||||||
|
|
|
||||||
|
|
@ -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,7 +27,7 @@
|
||||||
|
|
||||||
#define PS_PROTOCOL_MAGIC 0x5073013f // 'P', 's', 0x01, '?'
|
#define PS_PROTOCOL_MAGIC 0x5073013f // 'P', 's', 0x01, '?'
|
||||||
#define PS_PROTOCOL_MAGIC_RESPONSE 0x50630121 // 'P', 'c', 0x01, '!'
|
#define PS_PROTOCOL_MAGIC_RESPONSE 0x50630121 // 'P', 'c', 0x01, '!'
|
||||||
#define PS_PROTOCOL_VERSION 0x01010019 // Arbitrary protocol
|
#define PS_PROTOCOL_VERSION 0x0101001A // Arbitrary protocol
|
||||||
#define PS_DEFAULT_PORT 0x5073 // 'P', 's'
|
#define PS_DEFAULT_PORT 0x5073 // 'P', 's'
|
||||||
|
|
||||||
// Set when lobby authentication is required. Used in the SrvHandshakeResponseMessage.
|
// Set when lobby authentication is required. Used in the SrvHandshakeResponseMessage.
|
||||||
|
|
@ -231,7 +231,7 @@ START_NMT_CLASS_(ClientPaused, NMT_CLIENT_PAUSED)
|
||||||
END_NMT_CLASS()
|
END_NMT_CLASS()
|
||||||
|
|
||||||
START_NMT_CLASS_(LoadedGame, NMT_LOADED_GAME)
|
START_NMT_CLASS_(LoadedGame, NMT_LOADED_GAME)
|
||||||
NMT_FIELD_INT(m_CurrentTurn, u32, 4)
|
NMT_FIELD_INT(m_CurrentTurn, i32, 4)
|
||||||
END_NMT_CLASS()
|
END_NMT_CLASS()
|
||||||
|
|
||||||
START_NMT_CLASS_(GameStart, NMT_GAME_START)
|
START_NMT_CLASS_(GameStart, NMT_GAME_START)
|
||||||
|
|
@ -243,17 +243,17 @@ START_NMT_CLASS_(GameSavedStart, NMT_SAVED_GAME_START)
|
||||||
END_NMT_CLASS()
|
END_NMT_CLASS()
|
||||||
|
|
||||||
START_NMT_CLASS_(EndCommandBatch, NMT_END_COMMAND_BATCH)
|
START_NMT_CLASS_(EndCommandBatch, NMT_END_COMMAND_BATCH)
|
||||||
NMT_FIELD_INT(m_Turn, u32, 4)
|
NMT_FIELD_INT(m_Turn, i32, 4)
|
||||||
NMT_FIELD_INT(m_TurnLength, u32, 2)
|
NMT_FIELD_INT(m_TurnLength, u32, 2)
|
||||||
END_NMT_CLASS()
|
END_NMT_CLASS()
|
||||||
|
|
||||||
START_NMT_CLASS_(SyncCheck, NMT_SYNC_CHECK)
|
START_NMT_CLASS_(SyncCheck, NMT_SYNC_CHECK)
|
||||||
NMT_FIELD_INT(m_Turn, u32, 4)
|
NMT_FIELD_INT(m_Turn, i32, 4)
|
||||||
NMT_FIELD(CStr, m_Hash)
|
NMT_FIELD(CStr, m_Hash)
|
||||||
END_NMT_CLASS()
|
END_NMT_CLASS()
|
||||||
|
|
||||||
START_NMT_CLASS_(SyncError, NMT_SYNC_ERROR)
|
START_NMT_CLASS_(SyncError, NMT_SYNC_ERROR)
|
||||||
NMT_FIELD_INT(m_Turn, u32, 4)
|
NMT_FIELD_INT(m_Turn, i32, 4)
|
||||||
NMT_FIELD(CStr, m_HashExpected)
|
NMT_FIELD(CStr, m_HashExpected)
|
||||||
NMT_START_ARRAY(m_PlayerNames)
|
NMT_START_ARRAY(m_PlayerNames)
|
||||||
NMT_FIELD(CStrW, m_Name)
|
NMT_FIELD(CStrW, m_Name)
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
#include "scriptinterface/Context.h"
|
#include "scriptinterface/Context.h"
|
||||||
#include "scriptinterface/Interface.h"
|
#include "scriptinterface/Interface.h"
|
||||||
#include "scriptinterface/Request.h"
|
#include "scriptinterface/Request.h"
|
||||||
|
#include "simulation2/helpers/SimulationCommand.h"
|
||||||
#include "simulation2/system/TurnManager.h"
|
#include "simulation2/system/TurnManager.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -1172,9 +1173,9 @@ bool CNetServerWorker::OnSimulationCommand(CNetServerSession* session, CFsmEvent
|
||||||
server.Multicast(message, { NSS_INGAME });
|
server.Multicast(message, { NSS_INGAME });
|
||||||
|
|
||||||
// Save all the received commands
|
// Save all the received commands
|
||||||
if (server.m_SavedCommands.size() < message->m_Turn + 1)
|
if (server.m_SavedCommands.size() < static_cast<size_t>(message->m_Turn) + 1)
|
||||||
server.m_SavedCommands.resize(message->m_Turn + 1);
|
server.m_SavedCommands.resize(static_cast<size_t>(message->m_Turn) + 1);
|
||||||
server.m_SavedCommands[message->m_Turn].push_back(*message);
|
server.m_SavedCommands[static_cast<size_t>(message->m_Turn)].push_back(*message);
|
||||||
|
|
||||||
// TODO: we shouldn't send the message back to the client that first sent it
|
// TODO: we shouldn't send the message back to the client that first sent it
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1396,14 +1397,14 @@ bool CNetServerWorker::OnJoinSyncingLoadedGame(CNetServerSession* session, CFsmE
|
||||||
|
|
||||||
CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
|
CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
|
||||||
|
|
||||||
u32 turn = message->m_CurrentTurn;
|
turn_id_t turn = message->m_CurrentTurn;
|
||||||
u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
|
turn_id_t readyTurn = server.m_ServerTurnManager->GetReadyTurn();
|
||||||
|
|
||||||
// Send them all commands received since their saved state,
|
// Send them all commands received since their saved state,
|
||||||
// and turn-ended messages for any turns that have already been processed
|
// and turn-ended messages for any turns that have already been processed
|
||||||
for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i)
|
for (turn_id_t i = turn + 1; i < std::max(readyTurn + 1, static_cast<turn_id_t>(server.m_SavedCommands.size())); ++i)
|
||||||
{
|
{
|
||||||
if (i < server.m_SavedCommands.size())
|
if (static_cast<size_t>(i) < server.m_SavedCommands.size())
|
||||||
for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
|
for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
|
||||||
session->SendMessage(&server.m_SavedCommands[i][j]);
|
session->SendMessage(&server.m_SavedCommands[i][j]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
#include "network/NetServerSession.h"
|
#include "network/NetServerSession.h"
|
||||||
#include "ps/CLogger.h"
|
#include "ps/CLogger.h"
|
||||||
#include "ps/ConfigDB.h"
|
#include "ps/ConfigDB.h"
|
||||||
|
#include "simulation2/helpers/SimulationCommand.h"
|
||||||
#include "simulation2/system/TurnManager.h"
|
#include "simulation2/system/TurnManager.h"
|
||||||
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
|
@ -50,7 +51,7 @@ CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server)
|
||||||
m_SavedTurnLengths.push_back(m_TurnLength);
|
m_SavedTurnLengths.push_back(m_TurnLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CNetServerTurnManager::NotifyFinishedClientCommands(CNetServerSession& session, u32 turn)
|
void CNetServerTurnManager::NotifyFinishedClientCommands(CNetServerSession& session, turn_id_t turn)
|
||||||
{
|
{
|
||||||
int client = session.GetHostID();
|
int client = session.GetHostID();
|
||||||
|
|
||||||
|
|
@ -105,11 +106,11 @@ void CNetServerTurnManager::CheckClientsReady()
|
||||||
msg.m_Turn = m_ReadyTurn;
|
msg.m_Turn = m_ReadyTurn;
|
||||||
m_NetServer.Multicast(&msg, { NSS_INGAME });
|
m_NetServer.Multicast(&msg, { NSS_INGAME });
|
||||||
|
|
||||||
ENSURE(m_SavedTurnLengths.size() == m_ReadyTurn);
|
ENSURE(std::cmp_equal(m_SavedTurnLengths.size(), m_ReadyTurn));
|
||||||
m_SavedTurnLengths.push_back(m_TurnLength);
|
m_SavedTurnLengths.push_back(m_TurnLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& session, u32 turn, const CStr& hash)
|
void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& session, turn_id_t turn, const CStr& hash)
|
||||||
{
|
{
|
||||||
|
|
||||||
int client = session.GetHostID();
|
int client = session.GetHostID();
|
||||||
|
|
@ -138,13 +139,13 @@ void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& sessio
|
||||||
m_ClientStateHashes[turn][client] = hash;
|
m_ClientStateHashes[turn][client] = hash;
|
||||||
|
|
||||||
// Find the newest turn which we know all clients have simulated
|
// Find the newest turn which we know all clients have simulated
|
||||||
u32 newest = std::numeric_limits<u32>::max();
|
turn_id_t newest = std::numeric_limits<turn_id_t>::max();
|
||||||
for (const std::pair<const int, Client>& clientData : m_ClientsData)
|
for (const std::pair<const int, Client>& clientData : m_ClientsData)
|
||||||
if (clientData.second.simulatedTurn < newest)
|
if (clientData.second.simulatedTurn < newest)
|
||||||
newest = clientData.second.simulatedTurn;
|
newest = clientData.second.simulatedTurn;
|
||||||
|
|
||||||
// For every set of state hashes that all clients have simulated, check for OOS
|
// For every set of state hashes that all clients have simulated, check for OOS
|
||||||
for (const std::pair<const u32, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
|
for (const std::pair<const turn_id_t, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
|
||||||
{
|
{
|
||||||
if (clientStateHash.first > newest)
|
if (clientStateHash.first > newest)
|
||||||
break;
|
break;
|
||||||
|
|
@ -187,7 +188,7 @@ void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& sessio
|
||||||
m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1));
|
m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1));
|
||||||
}
|
}
|
||||||
|
|
||||||
void CNetServerTurnManager::InitialiseClient(int client, u32 turn, bool observer)
|
void CNetServerTurnManager::InitialiseClient(int client, turn_id_t turn, bool observer)
|
||||||
{
|
{
|
||||||
NETSERVERTURN_LOG("InitialiseClient(client=%d, turn=%d)\n", client, turn);
|
NETSERVERTURN_LOG("InitialiseClient(client=%d, turn=%d)\n", client, turn);
|
||||||
|
|
||||||
|
|
@ -206,7 +207,7 @@ void CNetServerTurnManager::UninitialiseClient(int client)
|
||||||
bool checkOOS = m_ClientsData[client].isOOS;
|
bool checkOOS = m_ClientsData[client].isOOS;
|
||||||
m_ClientsData.erase(client);
|
m_ClientsData.erase(client);
|
||||||
|
|
||||||
for (std::pair<const u32, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
|
for (std::pair<const turn_id_t, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
|
||||||
clientStateHash.second.erase(client);
|
clientStateHash.second.erase(client);
|
||||||
|
|
||||||
// Check whether we're ready for the next turn now that we're not
|
// Check whether we're ready for the next turn now that we're not
|
||||||
|
|
@ -228,8 +229,8 @@ void CNetServerTurnManager::SetTurnLength(u32 msecs)
|
||||||
m_TurnLength = msecs;
|
m_TurnLength = msecs;
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 CNetServerTurnManager::GetSavedTurnLength(u32 turn)
|
u32 CNetServerTurnManager::GetSavedTurnLength(turn_id_t turn)
|
||||||
{
|
{
|
||||||
ENSURE(turn <= m_ReadyTurn);
|
ENSURE(turn <= m_ReadyTurn);
|
||||||
return m_SavedTurnLengths.at(turn);
|
return m_SavedTurnLengths.at(static_cast<size_t>(turn));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
#include "lib/code_annotation.h"
|
#include "lib/code_annotation.h"
|
||||||
#include "lib/types.h"
|
#include "lib/types.h"
|
||||||
#include "ps/CStr.h"
|
#include "ps/CStr.h"
|
||||||
|
#include "simulation2/helpers/SimulationCommand.h"
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
@ -44,15 +45,15 @@ class CNetServerTurnManager
|
||||||
public:
|
public:
|
||||||
CNetServerTurnManager(CNetServerWorker& server);
|
CNetServerTurnManager(CNetServerWorker& server);
|
||||||
|
|
||||||
void NotifyFinishedClientCommands(CNetServerSession& session, u32 turn);
|
void NotifyFinishedClientCommands(CNetServerSession& session, turn_id_t turn);
|
||||||
|
|
||||||
void NotifyFinishedClientUpdate(CNetServerSession& session, u32 turn, const CStr& hash);
|
void NotifyFinishedClientUpdate(CNetServerSession& session, turn_id_t turn, const CStr& hash);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inform the turn manager of a new client
|
* Inform the turn manager of a new client
|
||||||
* @param observer - whether this client is an observer.
|
* @param observer - whether this client is an observer.
|
||||||
*/
|
*/
|
||||||
void InitialiseClient(int client, u32 turn, bool observer);
|
void InitialiseClient(int client, turn_id_t turn, bool observer);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inform the turn manager that a previously-initialised client has left the game.
|
* Inform the turn manager that a previously-initialised client has left the game.
|
||||||
|
|
@ -65,13 +66,13 @@ public:
|
||||||
* Returns the latest turn for which all clients are ready;
|
* Returns the latest turn for which all clients are ready;
|
||||||
* they will have already been told to execute this turn.
|
* they will have already been told to execute this turn.
|
||||||
*/
|
*/
|
||||||
u32 GetReadyTurn() { return m_ReadyTurn; }
|
turn_id_t GetReadyTurn() { return m_ReadyTurn; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the turn length that was used for the given turn.
|
* Returns the turn length that was used for the given turn.
|
||||||
* Requires turn <= GetReadyTurn().
|
* Requires turn <= GetReadyTurn().
|
||||||
*/
|
*/
|
||||||
u32 GetSavedTurnLength(u32 turn);
|
u32 GetSavedTurnLength(turn_id_t turn);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void CheckClientsReady();
|
void CheckClientsReady();
|
||||||
|
|
@ -80,9 +81,9 @@ private:
|
||||||
{
|
{
|
||||||
CStrW playerName;
|
CStrW playerName;
|
||||||
// Latest turn for which all commands have been received.
|
// Latest turn for which all commands have been received.
|
||||||
u32 readyTurn;
|
turn_id_t readyTurn;
|
||||||
// Last known simulated turn.
|
// Last known simulated turn.
|
||||||
u32 simulatedTurn;
|
turn_id_t simulatedTurn;
|
||||||
bool isObserver;
|
bool isObserver;
|
||||||
bool isOOS = false;
|
bool isOOS = false;
|
||||||
};
|
};
|
||||||
|
|
@ -93,10 +94,10 @@ private:
|
||||||
bool m_HasSyncError = false;
|
bool m_HasSyncError = false;
|
||||||
|
|
||||||
// Map of turn -> {Client ID -> state hash}; old indexes <= min(m_ClientsSimulated) are deleted
|
// Map of turn -> {Client ID -> state hash}; old indexes <= min(m_ClientsSimulated) are deleted
|
||||||
std::map<u32, std::map<int, std::string>> m_ClientStateHashes;
|
std::map<turn_id_t, std::map<int, std::string>> m_ClientStateHashes;
|
||||||
|
|
||||||
/// The latest turn for which we have received all commands from all clients
|
/// The latest turn for which we have received all commands from all clients
|
||||||
u32 m_ReadyTurn;
|
turn_id_t m_ReadyTurn;
|
||||||
|
|
||||||
// Current turn length
|
// Current turn length
|
||||||
u32 m_TurnLength;
|
u32 m_TurnLength;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -826,7 +826,7 @@ bool Autostart(const CmdLineArgs& args)
|
||||||
Script::GetPropertyInt(rq, pageData, 1, &pageArgs);
|
Script::GetPropertyInt(rq, pageData, 1, &pageArgs);
|
||||||
Script::StructuredClone clonedpageArgs{Script::WriteStructuredClone(rq, pageArgs)};
|
Script::StructuredClone clonedpageArgs{Script::WriteStructuredClone(rq, pageArgs)};
|
||||||
|
|
||||||
g_GUI->OpenChildPage(pageName, std::move(clonedpageArgs));
|
g_GUI->SwitchPage(pageName, &scriptInterface, pageArgs);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,10 @@ enum class Format
|
||||||
R8G8_UINT,
|
R8G8_UINT,
|
||||||
R8G8B8_UNORM,
|
R8G8B8_UNORM,
|
||||||
R8G8B8A8_UNORM,
|
R8G8B8A8_UNORM,
|
||||||
|
R8G8B8A8_SRGB,
|
||||||
R8G8B8A8_UINT,
|
R8G8B8A8_UINT,
|
||||||
B8G8R8A8_UNORM,
|
B8G8R8A8_UNORM,
|
||||||
|
B8G8R8A8_SRGB,
|
||||||
|
|
||||||
// TODO: we need to drop legacy A8 and L8 formats as soon as we have proper
|
// TODO: we need to drop legacy A8 and L8 formats as soon as we have proper
|
||||||
// channel swizzling.
|
// channel swizzling.
|
||||||
|
|
@ -65,9 +67,13 @@ enum class Format
|
||||||
D32_SFLOAT_S8_UINT,
|
D32_SFLOAT_S8_UINT,
|
||||||
|
|
||||||
BC1_RGB_UNORM,
|
BC1_RGB_UNORM,
|
||||||
|
BC1_RGB_SRGB,
|
||||||
BC1_RGBA_UNORM,
|
BC1_RGBA_UNORM,
|
||||||
|
BC1_RGBA_SRGB,
|
||||||
BC2_UNORM,
|
BC2_UNORM,
|
||||||
BC3_UNORM
|
BC2_SRGB,
|
||||||
|
BC3_UNORM,
|
||||||
|
BC3_SRGB,
|
||||||
};
|
};
|
||||||
|
|
||||||
inline bool IsDepthFormat(const Format format)
|
inline bool IsDepthFormat(const Format format)
|
||||||
|
|
|
||||||
|
|
@ -352,11 +352,11 @@ std::unique_ptr<IDevice> CDevice::Create(SDL_Window* window)
|
||||||
#if CONFIG2_GLES
|
#if CONFIG2_GLES
|
||||||
// Some GLES implementations have GL_EXT_texture_compression_dxt1
|
// Some GLES implementations have GL_EXT_texture_compression_dxt1
|
||||||
// but that only supports DXT1 so we can't use it.
|
// but that only supports DXT1 so we can't use it.
|
||||||
capabilities.S3TC = GLAD_GL_EXT_texture_compression_s3tc;
|
capabilities.S3TC = GLAD_GL_EXT_texture_compression_s3tc && GLAD_GL_EXT_texture_compression_s3tc_srgb;
|
||||||
#else
|
#else
|
||||||
// Note: we don't bother checking for GL_S3_s3tc - it is incompatible
|
// Note: we don't bother checking for GL_S3_s3tc - it is incompatible
|
||||||
// and irrelevant (was never widespread).
|
// and irrelevant (was never widespread).
|
||||||
capabilities.S3TC = GLAD_GL_EXT_texture_compression_s3tc;
|
capabilities.S3TC = GLAD_GL_EXT_texture_compression_s3tc && GLAD_GL_EXT_texture_sRGB;
|
||||||
#endif
|
#endif
|
||||||
#if CONFIG2_GLES
|
#if CONFIG2_GLES
|
||||||
capabilities.multisampling = false;
|
capabilities.multisampling = false;
|
||||||
|
|
@ -738,6 +738,7 @@ bool CDevice::IsTextureFormatSupported(const Format format) const
|
||||||
|
|
||||||
case Format::R8G8B8_UNORM:
|
case Format::R8G8B8_UNORM:
|
||||||
case Format::R8G8B8A8_UNORM:
|
case Format::R8G8B8A8_UNORM:
|
||||||
|
case Format::R8G8B8A8_SRGB:
|
||||||
case Format::A8_UNORM:
|
case Format::A8_UNORM:
|
||||||
case Format::L8_UNORM:
|
case Format::L8_UNORM:
|
||||||
supported = true;
|
supported = true;
|
||||||
|
|
@ -763,9 +764,13 @@ bool CDevice::IsTextureFormatSupported(const Format format) const
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Format::BC1_RGB_UNORM:
|
case Format::BC1_RGB_UNORM:
|
||||||
|
case Format::BC1_RGB_SRGB:
|
||||||
case Format::BC1_RGBA_UNORM:
|
case Format::BC1_RGBA_UNORM:
|
||||||
|
case Format::BC1_RGBA_SRGB:
|
||||||
case Format::BC2_UNORM:
|
case Format::BC2_UNORM:
|
||||||
|
case Format::BC2_SRGB:
|
||||||
case Format::BC3_UNORM:
|
case Format::BC3_UNORM:
|
||||||
|
case Format::BC3_SRGB:
|
||||||
supported = m_Capabilities.S3TC;
|
supported = m_Capabilities.S3TC;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -348,7 +348,8 @@ void CDeviceCommandContext::UploadTextureRegion(
|
||||||
GLenum pixelFormat = GL_RGBA;
|
GLenum pixelFormat = GL_RGBA;
|
||||||
switch (dataFormat)
|
switch (dataFormat)
|
||||||
{
|
{
|
||||||
case Format::R8G8B8A8_UNORM:
|
case Format::R8G8B8A8_UNORM: [[fallthrough]];
|
||||||
|
case Format::R8G8B8A8_SRGB:
|
||||||
break;
|
break;
|
||||||
case Format::R8G8B8_UNORM:
|
case Format::R8G8B8_UNORM:
|
||||||
pixelFormat = GL_RGB;
|
pixelFormat = GL_RGB;
|
||||||
|
|
@ -381,9 +382,13 @@ void CDeviceCommandContext::UploadTextureRegion(
|
||||||
}
|
}
|
||||||
else if (
|
else if (
|
||||||
texture->GetFormat() == Format::BC1_RGB_UNORM ||
|
texture->GetFormat() == Format::BC1_RGB_UNORM ||
|
||||||
|
texture->GetFormat() == Format::BC1_RGB_SRGB ||
|
||||||
texture->GetFormat() == Format::BC1_RGBA_UNORM ||
|
texture->GetFormat() == Format::BC1_RGBA_UNORM ||
|
||||||
|
texture->GetFormat() == Format::BC1_RGBA_SRGB ||
|
||||||
texture->GetFormat() == Format::BC2_UNORM ||
|
texture->GetFormat() == Format::BC2_UNORM ||
|
||||||
texture->GetFormat() == Format::BC3_UNORM)
|
texture->GetFormat() == Format::BC2_SRGB ||
|
||||||
|
texture->GetFormat() == Format::BC3_UNORM ||
|
||||||
|
texture->GetFormat() == Format::BC3_SRGB)
|
||||||
{
|
{
|
||||||
ENSURE(xOffset == 0 && yOffset == 0);
|
ENSURE(xOffset == 0 && yOffset == 0);
|
||||||
ENSURE(texture->GetFormat() == dataFormat);
|
ENSURE(texture->GetFormat() == dataFormat);
|
||||||
|
|
@ -392,15 +397,27 @@ void CDeviceCommandContext::UploadTextureRegion(
|
||||||
GLenum internalFormat = GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
|
GLenum internalFormat = GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
|
||||||
switch (texture->GetFormat())
|
switch (texture->GetFormat())
|
||||||
{
|
{
|
||||||
|
case Format::BC1_RGB_SRGB:
|
||||||
|
internalFormat = GL_COMPRESSED_SRGB_S3TC_DXT1_EXT;
|
||||||
|
break;
|
||||||
case Format::BC1_RGBA_UNORM:
|
case Format::BC1_RGBA_UNORM:
|
||||||
internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
|
internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
|
||||||
break;
|
break;
|
||||||
|
case Format::BC1_RGBA_SRGB:
|
||||||
|
internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;
|
||||||
|
break;
|
||||||
case Format::BC2_UNORM:
|
case Format::BC2_UNORM:
|
||||||
internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
|
internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
|
||||||
break;
|
break;
|
||||||
|
case Format::BC2_SRGB:
|
||||||
|
internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;
|
||||||
|
break;
|
||||||
case Format::BC3_UNORM:
|
case Format::BC3_UNORM:
|
||||||
internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
|
internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
|
||||||
break;
|
break;
|
||||||
|
case Format::BC3_SRGB:
|
||||||
|
internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,9 @@ std::unique_ptr<CTexture> CTexture::Create(
|
||||||
break;
|
break;
|
||||||
case Format::R8G8B8A8_UNORM:
|
case Format::R8G8B8A8_UNORM:
|
||||||
break;
|
break;
|
||||||
|
case Format::R8G8B8A8_SRGB:
|
||||||
|
internalFormat = GL_SRGB_ALPHA_EXT;
|
||||||
|
break;
|
||||||
case Format::R8G8B8_UNORM:
|
case Format::R8G8B8_UNORM:
|
||||||
internalFormat = GL_RGB;
|
internalFormat = GL_RGB;
|
||||||
pixelFormat = GL_RGB;
|
pixelFormat = GL_RGB;
|
||||||
|
|
@ -254,9 +257,13 @@ std::unique_ptr<CTexture> CTexture::Create(
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
case Format::BC1_RGB_UNORM:
|
case Format::BC1_RGB_UNORM:
|
||||||
|
case Format::BC1_RGB_SRGB:
|
||||||
case Format::BC1_RGBA_UNORM:
|
case Format::BC1_RGBA_UNORM:
|
||||||
|
case Format::BC1_RGBA_SRGB:
|
||||||
case Format::BC2_UNORM:
|
case Format::BC2_UNORM:
|
||||||
|
case Format::BC2_SRGB:
|
||||||
case Format::BC3_UNORM:
|
case Format::BC3_UNORM:
|
||||||
|
case Format::BC3_SRGB:
|
||||||
compressedFormat = true;
|
compressedFormat = true;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -847,9 +847,13 @@ bool CDevice::IsTextureFormatSupported(const Format format) const
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
case Format::BC1_RGB_UNORM:
|
case Format::BC1_RGB_UNORM:
|
||||||
|
case Format::BC1_RGB_SRGB:
|
||||||
case Format::BC1_RGBA_UNORM:
|
case Format::BC1_RGBA_UNORM:
|
||||||
|
case Format::BC1_RGBA_SRGB:
|
||||||
case Format::BC2_UNORM:
|
case Format::BC2_UNORM:
|
||||||
|
case Format::BC2_SRGB:
|
||||||
case Format::BC3_UNORM:
|
case Format::BC3_UNORM:
|
||||||
|
case Format::BC3_SRGB:
|
||||||
if (m_Capabilities.S3TC)
|
if (m_Capabilities.S3TC)
|
||||||
return true;
|
return true;
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -174,8 +174,10 @@ VkFormat FromFormat(const Format format)
|
||||||
CASE(R8G8_UNORM)
|
CASE(R8G8_UNORM)
|
||||||
CASE(R8G8_UINT)
|
CASE(R8G8_UINT)
|
||||||
CASE(R8G8B8A8_UNORM)
|
CASE(R8G8B8A8_UNORM)
|
||||||
|
CASE(R8G8B8A8_SRGB)
|
||||||
CASE(R8G8B8A8_UINT)
|
CASE(R8G8B8A8_UINT)
|
||||||
CASE(B8G8R8A8_UNORM)
|
CASE(B8G8R8A8_UNORM)
|
||||||
|
CASE(B8G8R8A8_SRGB)
|
||||||
|
|
||||||
CASE(R16_UNORM)
|
CASE(R16_UNORM)
|
||||||
CASE(R16_UINT)
|
CASE(R16_UINT)
|
||||||
|
|
@ -204,9 +206,13 @@ VkFormat FromFormat(const Format format)
|
||||||
CASE(D32_SFLOAT)
|
CASE(D32_SFLOAT)
|
||||||
|
|
||||||
CASE2(BC1_RGB_UNORM, BC1_RGB_UNORM_BLOCK)
|
CASE2(BC1_RGB_UNORM, BC1_RGB_UNORM_BLOCK)
|
||||||
|
CASE2(BC1_RGB_SRGB, BC1_RGB_SRGB_BLOCK)
|
||||||
CASE2(BC1_RGBA_UNORM, BC1_RGBA_UNORM_BLOCK)
|
CASE2(BC1_RGBA_UNORM, BC1_RGBA_UNORM_BLOCK)
|
||||||
|
CASE2(BC1_RGBA_SRGB, BC1_RGBA_SRGB_BLOCK)
|
||||||
CASE2(BC2_UNORM, BC2_UNORM_BLOCK)
|
CASE2(BC2_UNORM, BC2_UNORM_BLOCK)
|
||||||
|
CASE2(BC2_SRGB, BC2_SRGB_BLOCK)
|
||||||
CASE2(BC3_UNORM, BC3_UNORM_BLOCK)
|
CASE2(BC3_UNORM, BC3_UNORM_BLOCK)
|
||||||
|
CASE2(BC3_SRGB, BC3_SRGB_BLOCK)
|
||||||
|
|
||||||
#undef CASE
|
#undef CASE
|
||||||
#undef CASE2
|
#undef CASE2
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -179,13 +179,18 @@ void CRingCommandContext::ScheduleUpload(
|
||||||
|
|
||||||
const bool isCompressedFormat =
|
const bool isCompressedFormat =
|
||||||
format == Format::BC1_RGB_UNORM ||
|
format == Format::BC1_RGB_UNORM ||
|
||||||
|
format == Format::BC1_RGB_SRGB ||
|
||||||
format == Format::BC1_RGBA_UNORM ||
|
format == Format::BC1_RGBA_UNORM ||
|
||||||
|
format == Format::BC1_RGBA_SRGB ||
|
||||||
format == Format::BC2_UNORM ||
|
format == Format::BC2_UNORM ||
|
||||||
format == Format::BC3_UNORM;
|
format == Format::BC2_SRGB ||
|
||||||
|
format == Format::BC3_UNORM ||
|
||||||
|
format == Format::BC3_SRGB;
|
||||||
ENSURE(
|
ENSURE(
|
||||||
format == Format::R8_UNORM ||
|
format == Format::R8_UNORM ||
|
||||||
format == Format::R8G8_UNORM ||
|
format == Format::R8G8_UNORM ||
|
||||||
format == Format::R8G8B8A8_UNORM ||
|
format == Format::R8G8B8A8_UNORM ||
|
||||||
|
format == Format::R8G8B8A8_SRGB ||
|
||||||
format == Format::A8_UNORM ||
|
format == Format::A8_UNORM ||
|
||||||
format == Format::L8_UNORM ||
|
format == Format::L8_UNORM ||
|
||||||
isCompressedFormat);
|
isCompressedFormat);
|
||||||
|
|
|
||||||
|
|
@ -92,13 +92,13 @@ public:
|
||||||
return serializationTestOption ? serializationTestOption->turn :
|
return serializationTestOption ? serializationTestOption->turn :
|
||||||
std::max(CConfigDB::GetIfInitialised("serializationtest", -1), -1);
|
std::max(CConfigDB::GetIfInitialised("serializationtest", -1), -1);
|
||||||
}()},
|
}()},
|
||||||
m_RejoinTestTurn{[&]() -> std::optional<int>
|
m_RejoinTestTurn{[&]() -> std::optional<turn_id_t>
|
||||||
{
|
{
|
||||||
const auto* rejoinTestOption{
|
const auto* rejoinTestOption{
|
||||||
std::get_if<SimulationDebugOptions::RejoinTest>(&debugOptions.test)};
|
std::get_if<SimulationDebugOptions::RejoinTest>(&debugOptions.test)};
|
||||||
if (rejoinTestOption)
|
if (rejoinTestOption)
|
||||||
return rejoinTestOption->turn;
|
return rejoinTestOption->turn;
|
||||||
const int configVal{CConfigDB::GetIfInitialised("rejointest", -1)};
|
const turn_id_t configVal{CConfigDB::GetIfInitialised("rejointest", -1)};
|
||||||
if (configVal >= 0)
|
if (configVal >= 0)
|
||||||
return configVal;
|
return configVal;
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
@ -163,17 +163,17 @@ public:
|
||||||
|
|
||||||
std::set<VfsPath> m_LoadedScripts;
|
std::set<VfsPath> m_LoadedScripts;
|
||||||
|
|
||||||
uint32_t m_TurnNumber;
|
turn_id_t m_TurnNumber;
|
||||||
|
|
||||||
bool m_EnableOOSLog{false};
|
bool m_EnableOOSLog{false};
|
||||||
OsPath m_OOSLogPath;
|
OsPath m_OOSLogPath;
|
||||||
|
|
||||||
// Functions and data for the serialization test mode: (see Update() for relevant comments)
|
// Functions and data for the serialization test mode: (see Update() for relevant comments)
|
||||||
|
|
||||||
std::optional<int> m_SerializationTestTurn;
|
std::optional<turn_id_t> m_SerializationTestTurn;
|
||||||
bool m_TestingSerialization{false};
|
bool m_TestingSerialization{false};
|
||||||
bool m_EnableSerializationTest{false};
|
bool m_EnableSerializationTest{false};
|
||||||
std::optional<int> m_RejoinTestTurn;
|
std::optional<turn_id_t> m_RejoinTestTurn;
|
||||||
bool m_TestingRejoin{false};
|
bool m_TestingRejoin{false};
|
||||||
|
|
||||||
// Secondary simulation (NB: order matters for destruction).
|
// Secondary simulation (NB: order matters for destruction).
|
||||||
|
|
@ -383,7 +383,7 @@ void CSimulation2Impl::InitRNGSeedAI()
|
||||||
void CSimulation2Impl::Update(int turnLength, const std::vector<SimulationCommand>& commands)
|
void CSimulation2Impl::Update(int turnLength, const std::vector<SimulationCommand>& commands)
|
||||||
{
|
{
|
||||||
PROFILE3("sim update");
|
PROFILE3("sim update");
|
||||||
PROFILE2_ATTR("turn %d", (int)m_TurnNumber);
|
PROFILE2_ATTR("turn %d", m_TurnNumber);
|
||||||
|
|
||||||
fixed turnLengthFixed = fixed::FromInt(turnLength) / 1000;
|
fixed turnLengthFixed = fixed::FromInt(turnLength) / 1000;
|
||||||
|
|
||||||
|
|
@ -408,11 +408,11 @@ void CSimulation2Impl::Update(int turnLength, const std::vector<SimulationComman
|
||||||
const Script::Interface& scriptInterface = m_ComponentManager.GetScriptInterface();
|
const Script::Interface& scriptInterface = m_ComponentManager.GetScriptInterface();
|
||||||
|
|
||||||
const bool startSerializationTest = m_SerializationTestTurn.has_value() &&
|
const bool startSerializationTest = m_SerializationTestTurn.has_value() &&
|
||||||
std::cmp_equal(m_SerializationTestTurn.value(), m_TurnNumber);
|
m_SerializationTestTurn.value() == m_TurnNumber;
|
||||||
if (startSerializationTest)
|
if (startSerializationTest)
|
||||||
m_TestingSerialization = true;
|
m_TestingSerialization = true;
|
||||||
const bool startRejoinTest = m_RejoinTestTurn.has_value() &&
|
const bool startRejoinTest = m_RejoinTestTurn.has_value() &&
|
||||||
static_cast<int64_t>(m_RejoinTestTurn.value()) == m_TurnNumber;
|
m_RejoinTestTurn.value() == m_TurnNumber;
|
||||||
if (startRejoinTest)
|
if (startRejoinTest)
|
||||||
m_TestingRejoin = true;
|
m_TestingRejoin = true;
|
||||||
|
|
||||||
|
|
@ -891,11 +891,11 @@ bool CSimulation2::DeserializeState(std::istream& stream)
|
||||||
return m->m_ComponentManager.DeserializeState(stream);
|
return m->m_ComponentManager.DeserializeState(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSimulation2::ActivateRejoinTest(int turn)
|
void CSimulation2::ActivateRejoinTest(turn_id_t turn)
|
||||||
{
|
{
|
||||||
if (m->m_RejoinTestTurn.has_value())
|
if (m->m_RejoinTestTurn.has_value())
|
||||||
return;
|
return;
|
||||||
LOGMESSAGERENDER("Rejoin test will activate in %i turns", turn - m->m_TurnNumber);
|
LOGMESSAGERENDER("Rejoin test will activate in %d turns", turn - m->m_TurnNumber);
|
||||||
m->m_RejoinTestTurn = turn;
|
m->m_RejoinTestTurn = turn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
#include "lib/file/vfs/vfs_path.h"
|
#include "lib/file/vfs/vfs_path.h"
|
||||||
#include "lib/status.h"
|
#include "lib/status.h"
|
||||||
#include "ps/Loader.h"
|
#include "ps/Loader.h"
|
||||||
|
#include "simulation2/helpers/SimulationCommand.h"
|
||||||
#include "simulation2/system/DebugOptions.h"
|
#include "simulation2/system/DebugOptions.h"
|
||||||
#include "simulation2/system/Entity.h"
|
#include "simulation2/system/Entity.h"
|
||||||
|
|
||||||
|
|
@ -240,7 +241,7 @@ public:
|
||||||
/**
|
/**
|
||||||
* Activate the rejoin-test feature for turn @param turn.
|
* Activate the rejoin-test feature for turn @param turn.
|
||||||
*/
|
*/
|
||||||
void ActivateRejoinTest(int turn);
|
void ActivateRejoinTest(turn_id_t turn);
|
||||||
|
|
||||||
std::string GenerateSchema();
|
std::string GenerateSchema();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,35 @@ struct EntityData
|
||||||
|
|
||||||
static_assert(sizeof(EntityData) == 24);
|
static_assert(sizeof(EntityData) == 24);
|
||||||
|
|
||||||
|
// Convert world-space circle to tile-space bounds
|
||||||
|
struct TileCircle
|
||||||
|
{
|
||||||
|
std::int32_t centerX, centerZ;
|
||||||
|
std::int32_t radius;
|
||||||
|
std::int32_t squaredRadius;
|
||||||
|
std::int32_t minX, maxX, minZ, maxZ;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply an action to every tile within the circle
|
||||||
|
template<typename Func>
|
||||||
|
void ForEachTileInCircle(const TileCircle& circle, Func&& func)
|
||||||
|
{
|
||||||
|
for (std::int32_t tileZ = circle.minZ; tileZ <= circle.maxZ; ++tileZ)
|
||||||
|
{
|
||||||
|
const std::int32_t deltaZ{tileZ - circle.centerZ};
|
||||||
|
const std::int32_t squaredDeltaZ{deltaZ * deltaZ};
|
||||||
|
|
||||||
|
for (std::int32_t tileX = circle.minX; tileX <= circle.maxX; ++tileX)
|
||||||
|
{
|
||||||
|
const std::int32_t deltaX{tileX - circle.centerX};
|
||||||
|
if (deltaX * deltaX + squaredDeltaZ > circle.squaredRadius)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
func(tileX, tileZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Functor for sorting entities by distance from a source point.
|
* Functor for sorting entities by distance from a source point.
|
||||||
* It must only be passed entities that are in 'entities'
|
* It must only be passed entities that are in 'entities'
|
||||||
|
|
@ -2130,6 +2159,50 @@ public:
|
||||||
SeeExploredEntities(p);
|
SeeExploredEntities(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TileCircle WorldToTileCircle(entity_pos_t worldX, entity_pos_t worldZ, fixed radius)
|
||||||
|
{
|
||||||
|
const std::int32_t centerTileX{(worldX / LOS_TILE_SIZE).ToInt_RoundToNearest()};
|
||||||
|
const std::int32_t centerTileZ{(worldZ / LOS_TILE_SIZE).ToInt_RoundToNearest()};
|
||||||
|
const std::int32_t tileRadius{(radius / LOS_TILE_SIZE).ToInt_RoundToInfinity()};
|
||||||
|
|
||||||
|
return TileCircle{
|
||||||
|
.centerX = centerTileX,
|
||||||
|
.centerZ = centerTileZ,
|
||||||
|
.radius = tileRadius,
|
||||||
|
.squaredRadius = tileRadius * tileRadius,
|
||||||
|
.minX = std::max(centerTileX - tileRadius, 0),
|
||||||
|
.maxX = std::min(centerTileX + tileRadius, m_LosVerticesPerSide - 1),
|
||||||
|
.minZ = std::max(centerTileZ - tileRadius, 0),
|
||||||
|
.maxZ = std::min(centerTileZ + tileRadius, m_LosVerticesPerSide - 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExploreCircle(player_id_t playerId, entity_pos_t worldX, entity_pos_t worldZ, fixed radius) override
|
||||||
|
{
|
||||||
|
if (playerId <= 0 || playerId > MAX_LOS_PLAYER_ID)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const TileCircle circle{WorldToTileCircle(worldX, worldZ, radius)};
|
||||||
|
const u32 exploredBitMask{static_cast<u32>(LosState::EXPLORED) << (2 * (playerId - 1))};
|
||||||
|
u32& exploredCount = m_ExploredVertices.at(playerId);
|
||||||
|
|
||||||
|
ForEachTileInCircle(circle,
|
||||||
|
[&](std::int32_t tileX, std::int32_t tileZ)
|
||||||
|
{
|
||||||
|
if (LosIsOffWorld(tileX, tileZ))
|
||||||
|
return;
|
||||||
|
|
||||||
|
u32& losState = m_LosState.get(tileX, tileZ);
|
||||||
|
if (losState & exploredBitMask)
|
||||||
|
return;
|
||||||
|
|
||||||
|
losState |= exploredBitMask;
|
||||||
|
++exploredCount;
|
||||||
|
MarkVisibilityDirtyAroundTile(playerId, tileX, tileZ);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force any entity in explored territory to appear for player p.
|
* Force any entity in explored territory to appear for player p.
|
||||||
* This is useful for miraging entities inside the territory borders at the beginning of a game,
|
* This is useful for miraging entities inside the territory borders at the beginning of a game,
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ DEFINE_INTERFACE_METHOD("GetGaiaAndNonGaiaEntities", ICmpRangeManager, GetGaiaAn
|
||||||
DEFINE_INTERFACE_METHOD("SetDebugOverlay", ICmpRangeManager, SetDebugOverlay)
|
DEFINE_INTERFACE_METHOD("SetDebugOverlay", ICmpRangeManager, SetDebugOverlay)
|
||||||
DEFINE_INTERFACE_METHOD("ExploreMap", ICmpRangeManager, ExploreMap)
|
DEFINE_INTERFACE_METHOD("ExploreMap", ICmpRangeManager, ExploreMap)
|
||||||
DEFINE_INTERFACE_METHOD("ExploreTerritories", ICmpRangeManager, ExploreTerritories)
|
DEFINE_INTERFACE_METHOD("ExploreTerritories", ICmpRangeManager, ExploreTerritories)
|
||||||
|
DEFINE_INTERFACE_METHOD("ExploreCircle", ICmpRangeManager, ExploreCircle)
|
||||||
DEFINE_INTERFACE_METHOD("SetLosRevealWholeMap", ICmpRangeManager, SetLosRevealWholeMap)
|
DEFINE_INTERFACE_METHOD("SetLosRevealWholeMap", ICmpRangeManager, SetLosRevealWholeMap)
|
||||||
DEFINE_INTERFACE_METHOD("GetLosRevealWholeMap", ICmpRangeManager, GetLosRevealWholeMap)
|
DEFINE_INTERFACE_METHOD("GetLosRevealWholeMap", ICmpRangeManager, GetLosRevealWholeMap)
|
||||||
DEFINE_INTERFACE_METHOD("SetLosRevealWholeMapForAll", ICmpRangeManager, SetLosRevealWholeMapForAll)
|
DEFINE_INTERFACE_METHOD("SetLosRevealWholeMapForAll", ICmpRangeManager, SetLosRevealWholeMapForAll)
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,12 @@ public:
|
||||||
*/
|
*/
|
||||||
virtual void ExploreTerritories() = 0;
|
virtual void ExploreTerritories() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explore a circular area of the map for the given player.
|
||||||
|
* This marks the tiles as explored (fogged) but not necessarily visible.
|
||||||
|
*/
|
||||||
|
virtual void ExploreCircle(player_id_t p, entity_pos_t x, entity_pos_t z, fixed radius) = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reveal the shore for specified player p.
|
* Reveal the shore for specified player p.
|
||||||
* This works like for entities: if RevealShore is called multiple times with enabled, it
|
* This works like for entities: if RevealShore is called multiple times with enabled, it
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
#include "simulation2/components/ICmpVision.h"
|
#include "simulation2/components/ICmpVision.h"
|
||||||
#include "simulation2/helpers/Player.h"
|
#include "simulation2/helpers/Player.h"
|
||||||
#include "simulation2/helpers/Position.h"
|
#include "simulation2/helpers/Position.h"
|
||||||
|
#include "simulation2/helpers/Los.h"
|
||||||
#include "simulation2/system/Component.h"
|
#include "simulation2/system/Component.h"
|
||||||
#include "simulation2/system/ComponentTest.h"
|
#include "simulation2/system/ComponentTest.h"
|
||||||
#include "simulation2/system/Entity.h"
|
#include "simulation2/system/Entity.h"
|
||||||
|
|
@ -331,4 +332,39 @@ public:
|
||||||
range = fixed::FromInt(260);
|
range = fixed::FromInt(260);
|
||||||
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), fixed::FromFloat(264.952820f));
|
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), fixed::FromFloat(264.952820f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_ExploreCircle()
|
||||||
|
{
|
||||||
|
ComponentTestHelper test(*g_ScriptContext);
|
||||||
|
|
||||||
|
ICmpRangeManager* cmp = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
||||||
|
|
||||||
|
// Set up map bounds (256x256)
|
||||||
|
cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(256), entity_pos_t::FromInt(256));
|
||||||
|
cmp->Verify();
|
||||||
|
|
||||||
|
// Initially, no tiles should be explored
|
||||||
|
TS_ASSERT_EQUALS(cmp->GetPercentMapExplored(1), 0);
|
||||||
|
|
||||||
|
// Explore a circle at the center of the map with radius 50
|
||||||
|
cmp->ExploreCircle(1, entity_pos_t::FromInt(128), entity_pos_t::FromInt(128), fixed::FromInt(50));
|
||||||
|
|
||||||
|
// Check that some tiles were explored (not 0%, not 100%)
|
||||||
|
u8 percentExplored = cmp->GetPercentMapExplored(1);
|
||||||
|
TS_ASSERT(percentExplored > 0 && percentExplored < 100);
|
||||||
|
|
||||||
|
// Test invalid player (should do nothing)
|
||||||
|
cmp->ExploreCircle(0, entity_pos_t::FromInt(128), entity_pos_t::FromInt(128), fixed::FromInt(50));
|
||||||
|
cmp->ExploreCircle(99, entity_pos_t::FromInt(128), entity_pos_t::FromInt(128), fixed::FromInt(50));
|
||||||
|
// No crash, no changes - verify still works
|
||||||
|
cmp->Verify();
|
||||||
|
|
||||||
|
// Explore the entire map
|
||||||
|
cmp->ExploreCircle(1, entity_pos_t::FromInt(128), entity_pos_t::FromInt(128), fixed::FromInt(1000));
|
||||||
|
|
||||||
|
// The entire map should be explored
|
||||||
|
TS_ASSERT_EQUALS(cmp->GetPercentMapExplored(1), 100);
|
||||||
|
|
||||||
|
cmp->Verify();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue