mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
Compare commits
55 commits
4f2a8c5744
...
463a2e83f3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
463a2e83f3 | ||
|
|
61bcc38458 | ||
|
|
1c2dbf2eb0 | ||
|
|
df72c1aad1 | ||
|
|
40ab70b804 | ||
|
|
f90804c78f | ||
|
|
d6bdf51d83 | ||
|
|
ae936e8177 | ||
|
|
06e199b12a | ||
|
|
84567e66c2 | ||
|
|
c4e7364802 | ||
|
|
867bdf318b | ||
|
|
e64a138f92 | ||
|
|
4a7b7e7df1 | ||
|
|
fa90ddc8b6 | ||
|
|
764a420797 | ||
|
|
8a2bb10827 | ||
|
|
af9d102126 | ||
|
|
822092fcb4 | ||
|
|
6e79dc2f70 | ||
|
|
6fdcd68172 | ||
|
|
e4f4030a5e | ||
|
|
2fb6347125 | ||
|
|
7aa756f6a4 | ||
|
|
e712601602 | ||
|
|
865ca4cd55 | ||
|
|
dcb7cef308 | ||
|
|
71be79791f | ||
|
|
9ab89b01c2 | ||
|
|
dcce6666ae | ||
|
|
202bdb133a | ||
|
|
46981d2b90 | ||
|
|
49c887b7ca | ||
|
|
91a37e32f6 | ||
|
|
139b08fe6d | ||
|
|
dc6da217fd | ||
|
|
e41dd2d3a4 | ||
|
|
c7953026dd | ||
|
|
0340cc9abd | ||
|
|
aeeda05433 | ||
|
|
0da1e0c398 | ||
|
|
dc1e7179a3 | ||
|
|
b6d9d87ba9 | ||
|
|
839226d542 | ||
|
|
4125e5b645 | ||
|
|
f781181899 | ||
|
|
e2f5b20922 | ||
|
|
b3165505b6 | ||
|
|
15b9a39b00 | ||
|
|
cf7b8aab39 | ||
|
|
df06b6895e | ||
|
|
a80446eb59 | ||
|
|
4122f0bfb3 | ||
|
|
aae1bbbab2 | ||
|
|
8523fa5ab4 |
175 changed files with 2624 additions and 1269 deletions
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
lfscheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Fetch the base branch
|
||||
run: git fetch origin ${{ env.BASE_SHA }}
|
||||
|
|
@ -24,27 +24,14 @@ jobs:
|
|||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Add remote fork origin for LFS
|
||||
run: |
|
||||
PR_REPO="${{ gitea.event.pull_request.head.repo.full_name || gitea.repository }}"
|
||||
git remote add ${{ gitea.actor }} https://gitea.wildfiregames.com/${PR_REPO}.git
|
||||
- name: Workaround for authentication problem with LFS
|
||||
# https://gitea.com/gitea/act_runner/issues/164
|
||||
run: |
|
||||
git config --local \
|
||||
http.${{ gitea.server_url }}/${{ gitea.repository }}.git/info/lfs/objects/.extraheader ''
|
||||
|
||||
PR_REPO="${{ gitea.event.pull_request.head.repo.full_name || gitea.repository }}"
|
||||
EXTRAHEADER="$(git config --get --local http.${{ gitea.server_url }}/.extraheader)"
|
||||
git config --local \
|
||||
http.${{ gitea.server_url }}/${PR_REPO}.git/info/lfs/objects/batch.extraheader \
|
||||
'${EXTRAHEADER}'
|
||||
git config --local \
|
||||
http.${{ gitea.server_url }}/${PR_REPO}.git/info/lfs/objects/.extraheader ''
|
||||
- name: Download necessary LFS assets
|
||||
shell: sh {0}
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
cppcheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# cache only works for items in workspace, so configure apt to allow for caching.
|
||||
- name: Setup apt cache locations
|
||||
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
|
||||
- name: Cache apt pkg db and and deb files
|
||||
id: apt-cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
apt-cache
|
||||
|
|
@ -49,11 +49,11 @@ jobs:
|
|||
copyright:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 100
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
|
@ -66,10 +66,10 @@ jobs:
|
|||
jenkinsfiles:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
|
|
|
|||
|
|
@ -3,22 +3,27 @@ name: pre-commit
|
|||
on:
|
||||
- push
|
||||
- pull_request
|
||||
env:
|
||||
PRE_COMMIT_VERSION: 4.6.0
|
||||
jobs:
|
||||
build:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- id: restore-pip-cache
|
||||
uses: actions/cache/restore@v4
|
||||
- uses: actions/cache@v6
|
||||
with:
|
||||
key: pip-cache-v1-${{ github.workflow }}
|
||||
key: pip-cache-v1-${{github.workflow}}-${{env.pythonLocation}}-${{env.PRE_COMMIT_VERSION}}
|
||||
path: ~/.cache/pip
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
- uses: actions/cache/save@v4
|
||||
if: steps.restore-pip-cache.outcome == 'success'
|
||||
- run: python -m pip install pre-commit=="${{ env.PRE_COMMIT_VERSION }}"
|
||||
shell: bash
|
||||
- run: python -m pip freeze --local
|
||||
shell: bash
|
||||
- uses: actions/cache@v6
|
||||
with:
|
||||
key: pip-cache-v1-${{ github.workflow }}
|
||||
path: ~/.cache/pip
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- run: pre-commit run --show-diff-on-failure --color=always --all-files
|
||||
shell: bash
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ repos:
|
|||
\.patch$
|
||||
)
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.9
|
||||
rev: v0.16.0
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args:
|
||||
|
|
@ -56,7 +56,7 @@ repos:
|
|||
files: ^binaries/
|
||||
exclude: (^binaries/data/mods/(mod|public)/art/.*\.xml|\.dae$)
|
||||
- repo: https://github.com/scop/pre-commit-shfmt
|
||||
rev: v3.12.0-2
|
||||
rev: v3.13.1-1
|
||||
hooks:
|
||||
- id: shfmt
|
||||
args:
|
||||
|
|
@ -67,7 +67,7 @@ repos:
|
|||
hooks:
|
||||
- id: shellcheck
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.45.0
|
||||
rev: v0.47.0
|
||||
hooks:
|
||||
- id: markdownlint
|
||||
language_version: 22.14.0
|
||||
|
|
@ -77,13 +77,13 @@ repos:
|
|||
^source/third_party/
|
||||
)
|
||||
- repo: https://github.com/adrienverge/yamllint
|
||||
rev: v1.37.1
|
||||
rev: v1.38.0
|
||||
hooks:
|
||||
- id: yamllint
|
||||
args:
|
||||
- --strict
|
||||
- repo: https://github.com/eslint/eslint
|
||||
rev: v9.39.2
|
||||
rev: v10.8.0
|
||||
hooks:
|
||||
- id: eslint
|
||||
language_version: 22.14.0
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@
|
|||
<optional>
|
||||
<attribute name="define"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="srgb"/>
|
||||
</optional>
|
||||
</element>
|
||||
</zeroOrMore>
|
||||
</interleave>
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@
|
|||
layout(set = 2, binding = LOCATION, FORMAT) uniform image2D NAME
|
||||
#if USE_DESCRIPTOR_INDEXING
|
||||
#define STORAGE_BUFFER(LOCATION) \
|
||||
layout(set = 2, binding = LOCATION)
|
||||
layout(std430, set = 2, binding = LOCATION)
|
||||
#else
|
||||
#define STORAGE_BUFFER(LOCATION) \
|
||||
layout(set = 1, binding = LOCATION)
|
||||
layout(std430, set = 1, binding = LOCATION)
|
||||
#endif
|
||||
#else
|
||||
// We use offset to the binding slot for OpenGL to avoid overlapping with other
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
#define STORAGE_2D(LOCATION, FORMAT, NAME) \
|
||||
layout(binding = LOCATION, FORMAT) uniform image2D NAME
|
||||
#define STORAGE_BUFFER(LOCATION) \
|
||||
layout(binding = LOCATION)
|
||||
layout(std430, binding = LOCATION)
|
||||
#endif
|
||||
|
||||
#endif // STAGE_COMPUTE
|
||||
|
|
|
|||
|
|
@ -51,6 +51,18 @@
|
|||
</textures>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="ungarrisoned" frequency="1" />
|
||||
<variant name="garrisoned">
|
||||
<props>
|
||||
<prop
|
||||
actor="props/special/common/garrison_flag_achaemenids.xml"
|
||||
attachpoint="garrisoned"
|
||||
selectable="false"
|
||||
/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="alive" frequency="1"/>
|
||||
<variant file="structures/destruction_small.xml"/>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,18 @@
|
|||
</textures>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant frequency="1" name="ungarrisoned" />
|
||||
<variant name="garrisoned">
|
||||
<props>
|
||||
<prop
|
||||
actor="props/special/common/garrison_flag_celt.xml"
|
||||
attachpoint="garrisoned"
|
||||
selectable="false"
|
||||
/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="alive" frequency="1"/>
|
||||
<variant file="structures/destruction_small.xml"/>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,18 @@
|
|||
</textures>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="ungarrisoned" frequency="1" />
|
||||
<variant name="garrisoned">
|
||||
<props>
|
||||
<prop
|
||||
actor="props/special/common/garrison_flag_celt.xml"
|
||||
attachpoint="garrisoned"
|
||||
selectable="false"
|
||||
/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="alive" frequency="1"/>
|
||||
<variant file="structures/destruction_small.xml"/>
|
||||
|
|
|
|||
|
|
@ -134,6 +134,20 @@
|
|||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
|
||||
<group>
|
||||
<variant frequency="1" name="ungarrisoned" />
|
||||
<variant name="garrisoned">
|
||||
<props>
|
||||
<prop
|
||||
actor="props/special/common/garrison_flag_germ.xml"
|
||||
attachpoint="garrisoned"
|
||||
selectable="false"
|
||||
/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
|
||||
<group>
|
||||
<variant name="alive" frequency="1"/>
|
||||
<variant file="structures/destruction_small.xml"/>
|
||||
|
|
|
|||
|
|
@ -106,6 +106,18 @@
|
|||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="ungarrisoned" frequency="1" />
|
||||
<variant name="garrisoned">
|
||||
<props>
|
||||
<prop
|
||||
actor="props/special/common/garrison_flag_iber.xml"
|
||||
attachpoint="garrisoned"
|
||||
selectable="false"
|
||||
/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="alive" frequency="1"/>
|
||||
<variant file="structures/destruction_small.xml"/>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,18 @@
|
|||
</textures>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="ungarrisoned" frequency="1" />
|
||||
<variant name="garrisoned">
|
||||
<props>
|
||||
<prop
|
||||
actor="props/special/common/garrison_flag_rome.xml"
|
||||
attachpoint="garrisoned"
|
||||
selectable="false"
|
||||
/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="alive" frequency="1"/>
|
||||
<variant file="structures/destruction_small.xml"/>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,18 @@
|
|||
</textures>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant frequency="1" name="ungarrisoned" />
|
||||
<variant name="garrisoned">
|
||||
<props>
|
||||
<prop
|
||||
actor="props/special/common/garrison_flag_seleucids.xml"
|
||||
attachpoint="garrisoned"
|
||||
selectable="false"
|
||||
/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="alive" frequency="1"/>
|
||||
<variant file="structures/sele/light_damage.xml"/>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:995da95e21261ae0a2718baee69d76ae5f6823d21eaefaa708a89651f2099bb0
|
||||
size 586243
|
||||
oid sha256:ed243a229225ec51ee090588a63307cbd2efedecca06f4f1a0922a4d51ee3967
|
||||
size 586427
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d8e022a3ac132a32061583743d1322fc8792cdd6e7be57857547a8ed3cefbed6
|
||||
size 525300
|
||||
oid sha256:3ce649aa943771357d7c19f48294af719c50ca14178c32be60992f17845d685d
|
||||
size 525483
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:21971f20f5567f820e0f3fffdd21015e2a5ffaee309be7b6efc38d3fc56aecd3
|
||||
size 503936
|
||||
oid sha256:d1d867c2e9f3db233847da547c466325ba9f0a9e7478175281f3538cbb694709
|
||||
size 504112
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3c37b1b7f12a429cd07873d0fdb7cc8692f6578b3e5320a6be0e68420fb4688e
|
||||
size 780868
|
||||
oid sha256:cc9c95992889a4c2930a39bd20f7c86b73f7c7ddb3b36492650afae2ae424641
|
||||
size 781051
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1230a92373c4e4684f791594efb35bb7d6c091377a11bd8f0daafd13816d04ec
|
||||
size 56755
|
||||
oid sha256:6779d6ca2067d6dd5594b3d2e5ef82d08de97ad058ce7084eb0bf8ae3290f4c2
|
||||
size 56953
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:30a395c3b5a50214a689abe968ad15edb60212c3ea14041a2edc07295a4462c6
|
||||
size 45317
|
||||
oid sha256:9e6aca91d66df31b5820f39025c57937ce6a1593199b8a068722fdec5b0675f6
|
||||
size 45523
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ce554ad452cf75bd4dbb9da5de9830309a1dccf72f72f6a74475078af7f0e1c4
|
||||
size 42851
|
||||
oid sha256:07013561c7e46b020acbd03d4f371300c38d3e364c99a1e7bbcf7467aa8a34e6
|
||||
size 43041
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:60f0614c1092395065c4ba8fe6bed64da873d38c579c9cbc400e9a9b8f30f3a8
|
||||
size 272099
|
||||
oid sha256:1416efa429cd64471d50e969c43ee723382c031c676414433f8fcd58176cfa77
|
||||
size 272274
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c6e7d6264d7b3e03466bf1de58ab366a023120c6f78142aebc6570218a4803d9
|
||||
size 274339
|
||||
oid sha256:9f9e6debd5f2a9659e19e74b5826d3488167edb9a897ae1ed6e4db10742d94e8
|
||||
size 274543
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0624fc07116aa70c14527457fced3afcf7b36df1535bbf10250455e6254aeed6
|
||||
size 372821
|
||||
oid sha256:091d68b7800d9141e87ba8aaf0fa93c5bc7d260dd459774075de610ca34855e2
|
||||
size 372997
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6ef199b28b9d9ef19e0817b189d9e6f1e2f9861ca211890b8d714f9fe05f5c61
|
||||
size 420601
|
||||
oid sha256:c1413de5371443f1249d87fa40a821c880c749513a0347d9128a99472bec7b33
|
||||
size 420808
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:adf219c08e65bd7a149726d325b6f2b4fd67effc3dd1bc211c3163774ac8308e
|
||||
size 201143
|
||||
oid sha256:8aba3f3dc5a909ddadd9af5e7abfe9094f55ed6c97b39e7f97b09dc1cb951f4d
|
||||
size 201315
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f8c688df60c4880dc92b6898728d4cef443c86831a2a2e87e61dd44661b58240
|
||||
size 205000
|
||||
oid sha256:0c3298efebf687a07d4693d9014c51e1e870e95fb9d9024c9126b40adfec75a5
|
||||
size 205172
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9d86be00b7e911108e6666422a56eb061a996e6cf0bfe489d6c99f84be752444
|
||||
size 216525
|
||||
oid sha256:b97f6dff13f4639e746e1ce79c120169bccb2e09d4507dc900075cf85e788590
|
||||
size 216697
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5e258d0b3a7b5c3fe2275c2816c636b7478ee1051df31a2cbe2f516465c692f9
|
||||
size 239166
|
||||
oid sha256:8d764a2d5a712f53f08560504f6841ed00b5268fea2a283d2d5254d57ce1df76
|
||||
size 239339
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:82ff53473c25d6e17aeb97f795b543df3ec1c423e8ecb8398d0d014beac57f4e
|
||||
size 101895
|
||||
oid sha256:dccc1521e6e626e75ac2f05d2c8b2c2963ee751da08a9e819e4c28954b19f01a
|
||||
size 102099
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2377bddedc6251acbe8f03bf1820d368b21cbf88fc2783695ff31036af8ed4e3
|
||||
size 82812
|
||||
oid sha256:ae1042461639bca3d3d73fb3e723ca404c8a897ee0e2ad417af1eebb5849d23f
|
||||
size 83027
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b5f1530adecb42af05b6b81719a5c4d00fa06a5eff33f50eda8312ce2e0fcd5f
|
||||
size 77866
|
||||
oid sha256:b71b8848a0a2e503adbff9c1e09c06c1e76e16537f38af5b35395a04f11b365a
|
||||
size 78066
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c393e007e24473c1332c7d0c3a8093e0b542974da00b51b853ee413e45818ade
|
||||
size 91109
|
||||
oid sha256:256b3864edc754955814d5eec113e73fded49525d2e855a1171a4e4b495ffcc4
|
||||
size 91304
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e8fb0f356f48284704d81b601c7947f32e91995757e8c2fc390ecaf772fbd950
|
||||
size 84604
|
||||
oid sha256:4ef0415cce15f4d1ff4e342c23a995ee5081d21b7f84eb6c19fb89c1d63f84a7
|
||||
size 84789
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7c2b0d0a461535e0802d2a07395afb4546515ef4d3ccc3eab18d55a0993bc165
|
||||
size 184407
|
||||
oid sha256:2ab540ad518de662ba9a4dbe4d0d0590b6ee0b771bc482073296c949ee33d571
|
||||
size 184610
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:828e2e63b9686a601b3641be10f26e3b099ced5b8dc8d453ab94d41270f22b39
|
||||
size 213385
|
||||
oid sha256:39a6a575b8bbb0f82ed8cf4056e0df932a59fd6e3343005625b3b76644c6879c
|
||||
size 213561
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4825c81a245ddae5637f3b2dee78f7749e27a48d2efa244bc95af075ddabcd1a
|
||||
size 144598
|
||||
oid sha256:93341f17ad185e28ce62a533f1db457aec4b69bb2f67c98e4b89e4bcf9503b6a
|
||||
size 144784
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:427f9512af696afaae0721374a86ceb0e1432602013cb3dde818bfcd2791c163
|
||||
size 212264
|
||||
oid sha256:dc27dba302816891bb8f33bc65d9d68b2b330f946256aa966e5d684714af9fe2
|
||||
size 212447
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:19ca2828e5e6b99065d3d901c1a90f1b73df065dc94b3a4b76b4e5804890c137
|
||||
size 349847
|
||||
oid sha256:270fe9095c3894fb448cb91668ad48d886088006c689f0efa148e8a78a3de043
|
||||
size 350053
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:57039fff36fbba71de79b89614fea988a98bdc85b2bdc5ef88ce3a632da95a55
|
||||
size 340693
|
||||
oid sha256:977b2ffb5dc9cf0364d125f0b3dd17e1fb0cd482b5c96fbf7c61b6e6a030db86
|
||||
size 340906
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:221eeca4ee2e46023d346e80c7a3b4b3852699e0c193e31fa58e0619e858f7a2
|
||||
size 272252
|
||||
oid sha256:f177172ef923455050064af60377f2e1b171ff4ec72fa9aa5fdc5c4c28000550
|
||||
size 272435
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:afc01d8e7e7efc988b2f78b58cfb6b48b5453b8786609e9ce5db38bc617a7984
|
||||
size 279942
|
||||
oid sha256:acdaaf09848dc5cb7cf207c7bb535865aa4199a5a50609ca3b632ce5d88d95a2
|
||||
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));
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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": [
|
||||
{ "nick": "Alexandermb", "name": "Daniel Morgado" },
|
||||
{ "nick": "Athos" },
|
||||
{ "nick": "Atrik" },
|
||||
{ "nick": "Basshunter", "name": "Sebastián Gómez" },
|
||||
{ "nick": "BigTiger", "name": "Nathanael P. Moore" },
|
||||
{ "nick": "Brightgalrs", "name": "Robert D. Schultz" },
|
||||
|
|
@ -80,6 +81,7 @@
|
|||
{ "nick": "Enrique", "name": "Enrique Keykens Espolio" },
|
||||
{ "nick": "Gen.Kenobi", "name": "Daniel Schubert" },
|
||||
{ "nick": "Historicity", "name": "Shane" },
|
||||
{ "nick": "Langbart" },
|
||||
{ "nick": "Leyto", "name": "Valentin Levalet" },
|
||||
{ "nick": "LordGood", "name": "Johnathan B. Good" },
|
||||
{ "nick": "m7600" },
|
||||
|
|
|
|||
|
|
@ -507,6 +507,49 @@ var unitFilters = {
|
|||
}
|
||||
};
|
||||
|
||||
// Choose, inside a list of entities, which ones are targetable foes for boxtargeting.
|
||||
// We also filter out entities that aren't visible, or low priority like livestocks
|
||||
function getTargetableEntities(ents)
|
||||
{
|
||||
const player = g_ViewedPlayer;
|
||||
const simState = GetSimState();
|
||||
const isEnemy = simState.players[player].isEnemy;
|
||||
|
||||
// Filter valid target candidates
|
||||
const candidates = ents.filter(entity =>
|
||||
{
|
||||
const entState = GetEntityState(entity);
|
||||
return entState &&
|
||||
unitFilters.isUnit(entity) &&
|
||||
entState.visibility != "hidden";
|
||||
});
|
||||
|
||||
// Enemy players
|
||||
const enemyPlayers = candidates.filter(entity =>
|
||||
{
|
||||
const entState = GetEntityState(entity);
|
||||
return isEnemy[entState.player] && entState.player !== 0 && !hasClass(entState, "Domestic");
|
||||
});
|
||||
if (enemyPlayers.length > 0)
|
||||
return enemyPlayers;
|
||||
|
||||
// Gaia
|
||||
const gaiaUnits = candidates.filter(entity =>
|
||||
{
|
||||
const entState = GetEntityState(entity);
|
||||
return entState.player === 0;
|
||||
});
|
||||
if (gaiaUnits.length > 0)
|
||||
return gaiaUnits;
|
||||
|
||||
// Domestic animals
|
||||
return candidates.filter(entity =>
|
||||
{
|
||||
const entState = GetEntityState(entity);
|
||||
return hasClass(entState, "Domestic");
|
||||
});
|
||||
}
|
||||
|
||||
// Choose, inside a list of entities, which ones will be selected.
|
||||
// We may use several entity filters, until one returns at least one element.
|
||||
function getPreferredEntities(ents)
|
||||
|
|
@ -572,16 +615,19 @@ function handleInputBeforeGui(ev, hoveredObject)
|
|||
case INPUT_BANDBOXING:
|
||||
{
|
||||
const bandbox = Engine.GetGUIObjectByName("bandbox");
|
||||
const isAttackMode = Engine.HotkeyIsPressed("session.attack") && !g_IsObserver;
|
||||
switch (ev.type)
|
||||
{
|
||||
case "mousemotion":
|
||||
{
|
||||
const rect = updateBandbox(bandbox, ev, false);
|
||||
|
||||
const ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer);
|
||||
const preferredEntities = getPreferredEntities(ents);
|
||||
g_Selection.setHighlightList(preferredEntities);
|
||||
const player = isAttackMode ? -1 : g_ViewedPlayer;
|
||||
|
||||
const ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], player);
|
||||
const highlightEntities = isAttackMode ? getTargetableEntities(ents) : getPreferredEntities(ents);
|
||||
|
||||
g_Selection.setHighlightList(highlightEntities);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -589,6 +635,30 @@ function handleInputBeforeGui(ev, hoveredObject)
|
|||
if (ev.button == SDL_BUTTON_LEFT)
|
||||
{
|
||||
const rect = updateBandbox(bandbox, ev, true);
|
||||
if (isAttackMode)
|
||||
{
|
||||
// Box attack behavior
|
||||
const ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], -1);
|
||||
|
||||
// Filter to only enemy units
|
||||
const enemyUnits = getTargetableEntities(ents);
|
||||
|
||||
// Get currently selected friendly units
|
||||
const selectedUnits = g_Selection.toList();
|
||||
|
||||
if (selectedUnits.length && enemyUnits.length)
|
||||
{
|
||||
// Sort entities by position
|
||||
const sortedCombattants = sortEntitiesForEngagement(selectedUnits, enemyUnits);
|
||||
// Distribute attack orders
|
||||
distributeAttackOrders(sortedCombattants.attackers, sortedCombattants.targets);
|
||||
}
|
||||
g_Selection.setHighlightList([]);
|
||||
inputState = INPUT_NORMAL;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer));
|
||||
g_Selection.setHighlightList([]);
|
||||
|
||||
|
|
@ -1371,6 +1441,7 @@ function positionUnitsFreehandSelectionMouseUp(ev)
|
|||
for (let i = 1; i < inputLine.length; ++i)
|
||||
lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]);
|
||||
|
||||
// Filter units that can move
|
||||
const selection = g_Selection.filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b);
|
||||
|
||||
// Checking the line for a minimum length to save performance.
|
||||
|
|
@ -1380,39 +1451,43 @@ function positionUnitsFreehandSelectionMouseUp(ev)
|
|||
return !!action && doAction(action, ev);
|
||||
}
|
||||
|
||||
// Even distribution of the units on the line.
|
||||
let p0 = inputLine[0];
|
||||
let entityDistribution = [p0];
|
||||
const distanceBetweenEnts = lengthOfLine / (selection.length - 1);
|
||||
let freeDist = -distanceBetweenEnts;
|
||||
// Sort units by their projection onto the line direction (parallel sorting)
|
||||
const sortedSelection = sortEntitiesAlongLine(selection, inputLine[0], inputLine[inputLine.length - 1], true);
|
||||
|
||||
for (let i = 1; i < inputLine.length; ++i)
|
||||
const selectionCount = sortedSelection.length;
|
||||
const targetPositions = [];
|
||||
|
||||
// Distribute units evenly along the polyline
|
||||
for (let i = 0; i < selectionCount; ++i)
|
||||
{
|
||||
const p1 = inputLine[i];
|
||||
freeDist += inputLine[i - 1].distanceTo(p1);
|
||||
const t = i / (selectionCount - 1);
|
||||
const targetDistance = t * lengthOfLine;
|
||||
|
||||
while (freeDist >= 0)
|
||||
let accumulated = 0;
|
||||
let targetPoint = inputLine[0];
|
||||
for (let j = 1; j < inputLine.length; ++j)
|
||||
{
|
||||
p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1);
|
||||
entityDistribution.push(p0);
|
||||
freeDist -= distanceBetweenEnts;
|
||||
const segStart = inputLine[j - 1];
|
||||
const segEnd = inputLine[j];
|
||||
const segLength = segEnd.distanceTo(segStart);
|
||||
|
||||
if (accumulated + segLength >= targetDistance)
|
||||
{
|
||||
// Interpolate within this segment
|
||||
const remaining = targetDistance - accumulated;
|
||||
const frac = remaining / segLength;
|
||||
targetPoint = segStart.lerp(segEnd, frac);
|
||||
break;
|
||||
}
|
||||
accumulated += segLength;
|
||||
}
|
||||
targetPositions.push(targetPoint);
|
||||
}
|
||||
|
||||
// Rounding errors can lead to missing or too many points.
|
||||
entityDistribution = entityDistribution.slice(0, selection.length);
|
||||
entityDistribution = entityDistribution.concat(new Array(selection.length - entityDistribution.length).fill(inputLine[inputLine.length - 1]));
|
||||
|
||||
if (Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[0]) +
|
||||
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[selection.length - 1]) >
|
||||
Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[selection.length - 1]) +
|
||||
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[0]))
|
||||
entityDistribution.reverse();
|
||||
|
||||
Engine.PostNetworkCommand({
|
||||
"type": isAttackMovePressed() ? "attack-walk-custom" : "walk-custom",
|
||||
"entities": selection,
|
||||
"targetPositions": entityDistribution.map(pos => pos.toFixed(2)),
|
||||
"entities": sortedSelection,
|
||||
"targetPositions": targetPositions.map(pos => pos.toFixed(2)),
|
||||
"targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] },
|
||||
"queued": Engine.HotkeyIsPressed("session.queue"),
|
||||
"pushFront": Engine.HotkeyIsPressed("session.pushorderfront"),
|
||||
|
|
@ -1420,9 +1495,10 @@ function positionUnitsFreehandSelectionMouseUp(ev)
|
|||
});
|
||||
|
||||
// Add target markers with a minimum distance of 5 to each other.
|
||||
const entitiesBetweenMarker = Math.ceil(5 / distanceBetweenEnts);
|
||||
for (let i = 0; i < entityDistribution.length; i += entitiesBetweenMarker)
|
||||
DrawTargetMarker({ "x": entityDistribution[i].x, "z": entityDistribution[i].y });
|
||||
const stepDistance = lengthOfLine / (selectionCount - 1);
|
||||
const entitiesBetweenMarker = Math.max(1, Math.ceil(5 / stepDistance));
|
||||
for (let i = 0; i < targetPositions.length; i += entitiesBetweenMarker)
|
||||
DrawTargetMarker({ "x": targetPositions[i].x, "z": targetPositions[i].y });
|
||||
|
||||
Engine.GuiInterfaceCall("PlaySound", {
|
||||
"name": "order_walk",
|
||||
|
|
@ -1929,3 +2005,108 @@ function clearSelection()
|
|||
g_Selection.reset();
|
||||
preSelectedAction = ACTION_NONE;
|
||||
}
|
||||
|
||||
function distributeAttackOrders(attackers, targets)
|
||||
{
|
||||
if (targets.length < 1)
|
||||
return;
|
||||
|
||||
// Play attack sounds from a sample of units in selection
|
||||
const soundCount = Math.min(3, attackers.length);
|
||||
for (let i = 0; i < soundCount; i++)
|
||||
{
|
||||
const t = soundCount === 1 ? 0 : i / (soundCount - 1);
|
||||
const attackerIndex = Math.floor(t * (attackers.length - 1));
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
Engine.GuiInterfaceCall("PlaySound", {
|
||||
"name": "order_attack",
|
||||
"entity": attackers[attackerIndex]
|
||||
});
|
||||
}, i * 180);
|
||||
}
|
||||
|
||||
Engine.PostNetworkCommand({
|
||||
"type": "attack-group",
|
||||
"entities": attackers,
|
||||
"targets": targets,
|
||||
"queued": Engine.HotkeyIsPressed("session.queue"),
|
||||
"allowCapture": false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts attackers and targets for left-to-right engagement across the battle line.
|
||||
*
|
||||
* Entities are sorted by their perpendicular distance to the line connecting the
|
||||
* two army centers, creating natural pairings across the battle front.
|
||||
*
|
||||
* @param {number[]} attackers - Array of attacking entity IDs.
|
||||
* @param {number[]} targets - Array of target entity IDs.
|
||||
* @returns {Object} { attackers: number[], targets: number[] } - Both arrays sorted
|
||||
* for cross-line engagement (targets automatically reversed).
|
||||
*/
|
||||
function sortEntitiesForEngagement(attackers, targets)
|
||||
{
|
||||
const getPosition = (id) => GetEntityState(id).position;
|
||||
|
||||
const computeAveragePosition = (entities) =>
|
||||
{
|
||||
if (entities.length === 0) return new Vector2D(0, 0);
|
||||
const vectors = entities.map(id => Vector2D.from3D(getPosition(id)));
|
||||
return Vector2D.average(vectors);
|
||||
};
|
||||
|
||||
// Calculate the axis of engagement - the line between the groups' average positions
|
||||
const avgAttackers = computeAveragePosition(attackers);
|
||||
const avgTargets = computeAveragePosition(targets);
|
||||
|
||||
// Sort each group by perpendicular distance to the engagement line
|
||||
const sortedAttackers = sortEntitiesAlongLine(attackers, avgAttackers, avgTargets);
|
||||
const sortedTargets = sortEntitiesAlongLine(targets, avgAttackers, avgTargets);
|
||||
|
||||
// Reverse targets so the leftmost attacker pairs with leftmost target
|
||||
return {
|
||||
"attackers": sortedAttackers,
|
||||
"targets": sortedTargets.reverse()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts entity IDs by their projection onto a line's direction or perpendicular axis.
|
||||
*
|
||||
* @param {number[]} entities - Array of entity IDs.
|
||||
* @param {Vector2D} lineStart - Start point of the reference line.
|
||||
* @param {Vector2D} lineEnd - End point of the reference line.
|
||||
* @param {boolean} sortByDirection - If true, sort by parallel projection (along line);
|
||||
* If false, sort by perpendicular distance (default).
|
||||
* @returns {number[]} Sorted entity IDs.
|
||||
*/
|
||||
function sortEntitiesAlongLine(entities, lineStart, lineEnd, sortByDirection = false)
|
||||
{
|
||||
const dir = lineEnd.sub(lineStart);
|
||||
const length = dir.length();
|
||||
if (length === 0)
|
||||
return entities.slice();
|
||||
|
||||
// Choose basis vector: either parallel or perpendicular to the line
|
||||
let basis;
|
||||
if (sortByDirection)
|
||||
basis = dir.mult(1 / length); // Unit direction vector
|
||||
else
|
||||
basis = new Vector2D(-dir.y, dir.x).normalize(); // Perpendicular
|
||||
|
||||
const withCoord = entities.map(id =>
|
||||
{
|
||||
const pos = GetEntityState(id).position;
|
||||
const point2D = Vector2D.from3D(pos);
|
||||
const rel = point2D.sub(lineStart);
|
||||
const coord = rel.dot(basis); // Project onto basis vector
|
||||
return { id, coord };
|
||||
});
|
||||
|
||||
// Sort by that coordinate
|
||||
withCoord.sort((a, b) => a.coord - b.coord);
|
||||
return withCoord.map(item => item.id);
|
||||
}
|
||||
|
|
@ -208,3 +208,13 @@
|
|||
“This is a woman's resolve; as for men, they may live and be slaves.” \n— Boudicca, rallying against Roman occupation (Tacitus's Annals, Book 14, Chapter 35)
|
||||
“There are two things, knowledge and opinion; one makes its possessor know, the other to be ignorant.” \n— (Hippocratic treatise known as “The Law”, part of the Hippocratic Corpus)
|
||||
“Let them hate me, so long as they fear me.” \n— Caligula (Suetonius, The Twelve Caesars, Book 30)
|
||||
“There is no instance of a nation benefiting from prolonged warfare.” \n— Sun Tzu (“The Art of War”, Chapter 2)
|
||||
“To subdue the enemy without fighting is the acme of skill.” \n— Sun Tzu (“The Art of War”, Chapter 3)
|
||||
“One defends when his strength is inadequate; he attacks when it is abundant.” \n— Sun Tzu (“The Art of War”, Chapter 4)
|
||||
“When torrential water tosses boulders, it is because of its momentum. When the strike of a hawk breaks the body of its prey, it is because of timing.” \n— Sun Tzu (“The Art of War”, Chapter 5)
|
||||
“Treat your men as you would your own beloved sons. And they will follow you into the deepest valley.” \n— Sun Tzu (“The Art of War”, Chapter 10)
|
||||
“Woe to the defeated!” \n— Brennus disputing the weight of gold ransom while sacking Rome c. 387 BC (Livy, “Ab Urbe Condita”, Book 5, Chapter 48)
|
||||
“\[Brutus] found the women fighting and perishing in company with the men with such bravery that they uttered no cry even in the midst of slaughter.” \n— Brutus in Lusitania during the battles with Viriathus (Appian of Alexandria, “The Spanish Wars”, 15)
|
||||
“What I desire for my own children, I desire for all men.” \n— Ashoka the Great (“The Kalinga Rock Edicts”)
|
||||
“If you Romans choose to lord it over the world, does it follow that the world is to accept slavery ?” \n— Caratacus pleading for pardon from Roman Emperor Claudius in Rome (Tacitus, “Annals”, 12.37)
|
||||
“And where am I to get so many soldiers ?” \n— Septimius Severus, frustrated with the challenge of controlling his own men and the harsh realities of frontier warfare against the northern tribes in Britannia. (Cassius Dio, “Roman History”, Vol IX, Book LXXVI, Section 11)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
function RandomMapLogger()
|
||||
{
|
||||
this.lastTime = undefined;
|
||||
this.taskStarted = false;
|
||||
this.startTime = Engine.GetMicroseconds ? Engine.GetMicroseconds() : 0;
|
||||
this.prefix = ""; // seems noisy
|
||||
|
||||
|
|
@ -21,27 +21,18 @@ RandomMapLogger.prototype.printDirectly = function(string)
|
|||
|
||||
RandomMapLogger.prototype.print = function(string)
|
||||
{
|
||||
this.printDuration();
|
||||
this.printDirectly(this.prefix + string + "...");
|
||||
this.lastTime = Engine.GetMicroseconds();
|
||||
};
|
||||
if (this.taskStarted)
|
||||
Engine.ProfileStop();
|
||||
|
||||
RandomMapLogger.prototype.printDuration = function()
|
||||
{
|
||||
if (!this.lastTime)
|
||||
return;
|
||||
|
||||
this.printDurationDirectly("", this.lastTime);
|
||||
this.lastTime = Engine.GetMicroseconds();
|
||||
const text = this.prefix + string;
|
||||
this.printDirectly(text + "...\n");
|
||||
Engine.ProfileStart(text);
|
||||
this.taskStarted = true;
|
||||
};
|
||||
|
||||
RandomMapLogger.prototype.close = function()
|
||||
{
|
||||
this.printDuration();
|
||||
this.printDurationDirectly(this.prefix + "Total map generation time:", this.startTime);
|
||||
};
|
||||
|
||||
RandomMapLogger.prototype.printDurationDirectly = function(text, startTime)
|
||||
{
|
||||
this.printDirectly(text + " " + ((Engine.GetMicroseconds() - startTime) / 1000000).toFixed(6) + "s.\n");
|
||||
if (this.taskStarted)
|
||||
Engine.ProfileStop();
|
||||
this.printDirectly(this.prefix + "Map generation finished.\n");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -603,4 +603,147 @@ TriggerHelper.SpawnAndTurretAtClasses = function(playerID, classes, templates, c
|
|||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes an entity permanently visible to all players by:
|
||||
* - Setting alwaysVisible flag
|
||||
* - Deactivating fogging (no mirages)
|
||||
* - Exploring the surrounding area
|
||||
* - Forcing a visibility update
|
||||
*
|
||||
* @param {number} ent - Entity ID to make permanently visible
|
||||
*/
|
||||
TriggerHelper.MakeEntityPermanentlyVisible = function(ent)
|
||||
{
|
||||
const cmpVisibility = Engine.QueryInterface(ent, IID_Visibility);
|
||||
if (cmpVisibility)
|
||||
{
|
||||
cmpVisibility.alwaysVisible = true;
|
||||
cmpVisibility.SetActivated(true);
|
||||
}
|
||||
|
||||
const cmpFogging = Engine.QueryInterface(ent, IID_Fogging);
|
||||
if (cmpFogging)
|
||||
cmpFogging.PermanentlyReveal("all");
|
||||
|
||||
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
const cmpPosition = Engine.QueryInterface(ent, IID_Position);
|
||||
if (cmpRangeManager && cmpPosition && cmpPosition.IsInWorld())
|
||||
{
|
||||
const pos = cmpPosition.GetPosition2D();
|
||||
const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
|
||||
|
||||
const exploreRadius = TriggerHelper.CalculateExplorationRadius(ent);
|
||||
|
||||
for (let i = 1; i < numPlayers; ++i)
|
||||
cmpRangeManager.ExploreCircle(i, pos.x, pos.y, exploreRadius);
|
||||
|
||||
cmpRangeManager.RequestVisibilityUpdate(ent);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate a reasonable exploration radius from an entity's footprint.
|
||||
* Calculates the radius as the entity's full extent to reveal the surrounding area.
|
||||
* This is equivalent to (extent/2) * 2, giving a 2x margin around the entity.
|
||||
*/
|
||||
TriggerHelper.CalculateExplorationRadius = function(ent)
|
||||
{
|
||||
const cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
|
||||
if (!cmpFootprint)
|
||||
return 50;
|
||||
|
||||
const shapeInfo = cmpFootprint.GetShape();
|
||||
|
||||
let halfExtent; // The radius of the entity (half its size)
|
||||
if (shapeInfo && shapeInfo.type === "circle")
|
||||
halfExtent = shapeInfo.radius || shapeInfo.size0;
|
||||
else if (shapeInfo && shapeInfo.type === "square")
|
||||
halfExtent = Math.max(shapeInfo.width || shapeInfo.size0, shapeInfo.depth || shapeInfo.size1) / 2;
|
||||
else
|
||||
return 50;
|
||||
|
||||
// Use 2x the entity's radius to reveal surrounding area
|
||||
// This gives a comfortable margin around the entity
|
||||
const radius = halfExtent * 2;
|
||||
return radius;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a notification to specific players.
|
||||
*
|
||||
* @param {string} message - The message to display (use markForTranslation)
|
||||
* @param {number[]} players - Array of player IDs to send the notification to
|
||||
* @param {number} duration - Duration in milliseconds
|
||||
* @param {Object} parameters - Translation parameters (optional)
|
||||
*/
|
||||
TriggerHelper.SendNotification = function(message, players, duration = 30000, parameters = {}, translateParameters = [])
|
||||
{
|
||||
const cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
|
||||
if (!cmpGuiInterface)
|
||||
return;
|
||||
|
||||
// Ensure all parameter values are primitive types
|
||||
const primitiveParameters = {};
|
||||
for (const key in parameters)
|
||||
{
|
||||
const value = parameters[key];
|
||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
||||
primitiveParameters[key] = value;
|
||||
else
|
||||
primitiveParameters[key] = String(value);
|
||||
}
|
||||
|
||||
cmpGuiInterface.AddTimeNotification(
|
||||
{
|
||||
"message": message,
|
||||
"players": players,
|
||||
"parameters": primitiveParameters,
|
||||
"translateMessage": true,
|
||||
"translateParameters": translateParameters
|
||||
},
|
||||
duration
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a notification to a specific player and a different notification to all other active players.
|
||||
*
|
||||
* @param {number} player - The player ID who gets the 'owner' message
|
||||
* @param {string} ownerMessage - Message for the specified player (use markForTranslation)
|
||||
* @param {string} othersMessage - Message for all other active players (use markForTranslation)
|
||||
* @param {number} duration - Duration in milliseconds
|
||||
* @param {Object} parameters - Translation parameters shared by both messages (optional)
|
||||
* @param {Object} ownerParameters - Additional parameters for the owner message (optional)
|
||||
* @param {Object} othersParameters - Additional parameters for the others message (optional)
|
||||
*/
|
||||
TriggerHelper.SendDualNotification = function(player, ownerMessage, othersMessage, duration = 30000, parameters = {}, ownerParameters = {}, othersParameters = {})
|
||||
{
|
||||
const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
|
||||
|
||||
const others = [-1]; // Include observers (player id -1)
|
||||
for (let playerID = 1; playerID < numPlayers; ++playerID)
|
||||
{
|
||||
if (playerID == player)
|
||||
continue;
|
||||
const cmpPlayer = QueryPlayerIDInterface(playerID);
|
||||
if (cmpPlayer && cmpPlayer.GetState() == "defeated")
|
||||
continue;
|
||||
others.push(playerID);
|
||||
}
|
||||
|
||||
const mergedOwnerParams = Object.assign({}, parameters, ownerParameters);
|
||||
const mergedOthersParams = Object.assign({}, parameters, othersParameters);
|
||||
|
||||
// Auto-detect translatable parameters (those starting with "_" or named "player")
|
||||
const translateParameters = Object.keys(mergedOthersParams).filter(key => key.startsWith("_") || key == "player");
|
||||
|
||||
TriggerHelper.SendNotification(ownerMessage, [player], duration, mergedOwnerParams, translateParameters);
|
||||
TriggerHelper.SendNotification(othersMessage, others, duration, mergedOthersParams, translateParameters);
|
||||
};
|
||||
|
||||
TriggerHelper.SendWonderNotifications = function(owner, othersMessage, ownerMessage)
|
||||
{
|
||||
TriggerHelper.SendDualNotification(owner, ownerMessage, othersMessage, 30000, { "_player_": owner });
|
||||
};
|
||||
|
||||
Engine.RegisterGlobal("TriggerHelper", TriggerHelper);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,42 @@
|
|||
Trigger.prototype.WonderVictoryEntityRenamed = function(data)
|
||||
{
|
||||
if (this.wonderVictoryMessages[data.entity] && Engine.QueryInterface(data.newentity, IID_Wonder))
|
||||
{
|
||||
// When an entity is renamed, we first create a new entity,
|
||||
// which in case it is a wonder will receive a timer.
|
||||
// However on a rename we want to use the timer from the old entity,
|
||||
// so we need to remove the timer of the new entity.
|
||||
this.WonderVictoryDeleteTimer(data.newentity);
|
||||
if (!Engine.QueryInterface(data.newentity, IID_Wonder))
|
||||
return;
|
||||
|
||||
// Check if wonder victory is enabled
|
||||
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
|
||||
if (!cmpEndGameManager)
|
||||
return;
|
||||
|
||||
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
|
||||
if (!victoryConditions || !victoryConditions.includes("wonder"))
|
||||
return;
|
||||
|
||||
// Handle timer transfer (only if old entity had the timer)
|
||||
if (this.wonderVictoryMessages[data.entity])
|
||||
{
|
||||
this.WonderVictoryDeleteTimer(data.newentity);
|
||||
this.wonderVictoryMessages[data.newentity] = this.wonderVictoryMessages[data.entity];
|
||||
delete this.wonderVictoryMessages[data.entity];
|
||||
}
|
||||
|
||||
// Make the completed wonder permanently visible
|
||||
TriggerHelper.MakeEntityPermanentlyVisible(data.newentity);
|
||||
|
||||
// Send "wonder completed" notifications (only for non-Gaia wonders)
|
||||
const cmpOwnership = Engine.QueryInterface(data.newentity, IID_Ownership);
|
||||
if (cmpOwnership)
|
||||
{
|
||||
const owner = cmpOwnership.GetOwner();
|
||||
if (owner > 0)
|
||||
{
|
||||
TriggerHelper.SendWonderNotifications(
|
||||
owner,
|
||||
markForTranslation("%(_player_)s has built a Wonder!"),
|
||||
markForTranslation("You have built a Wonder!")
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Trigger.prototype.WonderVictoryOwnershipChanged = function(data)
|
||||
|
|
@ -18,6 +44,15 @@ Trigger.prototype.WonderVictoryOwnershipChanged = function(data)
|
|||
if (!Engine.QueryInterface(data.entity, IID_Wonder))
|
||||
return;
|
||||
|
||||
// Check if wonder victory is enabled
|
||||
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
|
||||
if (!cmpEndGameManager)
|
||||
return;
|
||||
|
||||
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
|
||||
if (!victoryConditions || !victoryConditions.includes("wonder"))
|
||||
return;
|
||||
|
||||
this.WonderVictoryDeleteTimer(data.entity);
|
||||
|
||||
if (data.to > 0)
|
||||
|
|
@ -146,11 +181,69 @@ Trigger.prototype.WonderVictorySetWinner = function(playerID)
|
|||
n));
|
||||
};
|
||||
|
||||
Trigger.prototype.WonderStartNotification = function(data)
|
||||
{
|
||||
// Check if wonder victory is enabled in game settings
|
||||
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
|
||||
if (!cmpEndGameManager)
|
||||
return;
|
||||
|
||||
// Don't show the wonder foundation if it's not a wonder victory game
|
||||
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
|
||||
if (!victoryConditions || !victoryConditions.includes("wonder"))
|
||||
return;
|
||||
|
||||
const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
|
||||
if (!cmpTemplateManager)
|
||||
return;
|
||||
|
||||
const template = cmpTemplateManager.GetTemplate(data.template);
|
||||
|
||||
if (!template || !("Wonder" in template))
|
||||
return;
|
||||
|
||||
const owner = TriggerHelper.GetOwner(data.foundation);
|
||||
if (owner <= 0)
|
||||
return;
|
||||
|
||||
TriggerHelper.MakeEntityPermanentlyVisible(data.foundation);
|
||||
|
||||
TriggerHelper.SendWonderNotifications(
|
||||
owner,
|
||||
markForTranslation("%(_player_)s has started building a Wonder."),
|
||||
markForTranslation("You have started building a Wonder.")
|
||||
);
|
||||
};
|
||||
|
||||
Trigger.prototype.RevealGaiaWonders = function()
|
||||
{
|
||||
Engine.ProfileStart("RevealGaiaWonders");
|
||||
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
|
||||
if (!cmpEndGameManager)
|
||||
return;
|
||||
|
||||
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
|
||||
if (!victoryConditions || !victoryConditions.includes("wonder"))
|
||||
return;
|
||||
|
||||
// Find and reveal all Gaia wonders
|
||||
const gaiaWonders = Engine.GetEntitiesWithInterface(IID_Wonder).filter(
|
||||
ent => TriggerHelper.GetOwner(ent) === 0
|
||||
);
|
||||
|
||||
for (const ent of gaiaWonders)
|
||||
TriggerHelper.MakeEntityPermanentlyVisible(ent);
|
||||
|
||||
Engine.ProfileStop();
|
||||
};
|
||||
|
||||
{
|
||||
const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
|
||||
cmpTrigger.RegisterTrigger("OnEntityRenamed", "WonderVictoryEntityRenamed", { "enabled": true });
|
||||
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "WonderVictoryOwnershipChanged", { "enabled": true });
|
||||
cmpTrigger.RegisterTrigger("OnDiplomacyChanged", "WonderVictoryDiplomacyChanged", { "enabled": true });
|
||||
cmpTrigger.RegisterTrigger("OnPlayerWon", "WonderVictoryPlayerWon", { "enabled": true });
|
||||
cmpTrigger.RegisterTrigger("OnConstructionStarted", "WonderStartNotification", { "enabled": true });
|
||||
cmpTrigger.DoAfterDelay(0, "RevealGaiaWonders", {});
|
||||
cmpTrigger.wonderVictoryMessages = {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ Fogging.prototype.Init = function()
|
|||
this.mirages = [];
|
||||
this.miraged = [];
|
||||
this.seen = [];
|
||||
this.revealed = [];
|
||||
|
||||
const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
|
||||
for (let player = 0; player < numPlayers; ++player)
|
||||
|
|
@ -36,6 +37,7 @@ Fogging.prototype.Init = function()
|
|||
this.mirages.push(INVALID_ENTITY);
|
||||
this.miraged.push(false);
|
||||
this.seen.push(false);
|
||||
this.revealed.push(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -169,6 +171,45 @@ Fogging.prototype.WasSeen = function(player)
|
|||
return this.seen[player];
|
||||
};
|
||||
|
||||
/**
|
||||
* Permanently reveal this entity to one or all players.
|
||||
* Destroys existing mirages and optionally deactivates fogging entirely.
|
||||
* @param {number|string} player - Player ID to reveal to, or "all" for all players.
|
||||
*/
|
||||
Fogging.prototype.PermanentlyReveal = function(player = "all")
|
||||
{
|
||||
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
|
||||
// Validate single player case
|
||||
if (player !== "all" && (player < 0 || player >= this.mirages.length))
|
||||
return;
|
||||
|
||||
// Determine which players to reveal
|
||||
const startPlayer = (player === "all") ? 0 : player;
|
||||
const endPlayer = (player === "all") ? this.mirages.length : player + 1;
|
||||
|
||||
// Reveal each player in the range
|
||||
for (let i = startPlayer; i < endPlayer; ++i)
|
||||
{
|
||||
if (this.mirages[i] != INVALID_ENTITY)
|
||||
{
|
||||
Engine.DestroyEntity(this.mirages[i]);
|
||||
this.mirages[i] = INVALID_ENTITY;
|
||||
this.miraged[i] = false;
|
||||
}
|
||||
this.revealed[i] = true;
|
||||
this.seen[i] = true;
|
||||
}
|
||||
|
||||
// Deactivate fogging if revealing to all players
|
||||
if (player === "all")
|
||||
this.activated = false;
|
||||
|
||||
// Request visibility update
|
||||
if (cmpRangeManager)
|
||||
cmpRangeManager.RequestVisibilityUpdate(this.entity);
|
||||
};
|
||||
|
||||
Fogging.prototype.OnOwnershipChanged = function(msg)
|
||||
{
|
||||
// Always activate fogging for non-Gaia entities.
|
||||
|
|
@ -209,7 +250,8 @@ Fogging.prototype.OnVisibilityChanged = function(msg)
|
|||
this.seen[msg.player] = true;
|
||||
}
|
||||
|
||||
if (msg.newVisibility == VIS_FOGGED && this.activated)
|
||||
// Only create a mirage if the player hasn’t been permanently revealed
|
||||
if (msg.newVisibility == VIS_FOGGED && this.activated && !this.revealed[msg.player])
|
||||
this.LoadMirage(msg.player);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -424,10 +424,42 @@ UnitAI.prototype.UnitFsmSpec = {
|
|||
|
||||
"Order.Attack": function(msg)
|
||||
{
|
||||
const type = this.GetBestAttackAgainst(msg.data.target, msg.data.allowCapture);
|
||||
if (!type)
|
||||
return this.FinishOrder();
|
||||
// Check if this is a group attack
|
||||
const isGroupAttack = !!msg.data.targetArray;
|
||||
|
||||
// For group attacks with invalid initial target, find one from the array
|
||||
if (isGroupAttack && (!msg.data.target || msg.data.target === INVALID_ENTITY))
|
||||
{
|
||||
if (!this.FindAndSetNextTarget(msg.data))
|
||||
{
|
||||
this.FinishOrder();
|
||||
return ACCEPT_ORDER;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the attack type for the current target
|
||||
const type = this.GetBestAttackAgainst(msg.data.target, msg.data.allowCapture);
|
||||
|
||||
if (!type)
|
||||
{
|
||||
// Current target can't be attacked
|
||||
if (isGroupAttack)
|
||||
{
|
||||
if (this.FindAndSetNextTarget(msg.data))
|
||||
{
|
||||
msg.data.attackType = this.GetBestAttackAgainst(msg.data.target, msg.data.allowCapture);
|
||||
if (msg.data.attackType)
|
||||
{
|
||||
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
|
||||
return ACCEPT_ORDER;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.FinishOrder();
|
||||
return ACCEPT_ORDER;
|
||||
}
|
||||
|
||||
// Set attackType before range check
|
||||
msg.data.attackType = type;
|
||||
|
||||
this.RememberTargetPosition();
|
||||
|
|
@ -1366,6 +1398,12 @@ UnitAI.prototype.UnitFsmSpec = {
|
|||
// Wait for individual members to finish
|
||||
"enter": function(msg)
|
||||
{
|
||||
if (this.order.data.targetArray)
|
||||
{
|
||||
// For group attacks, we're done once we distributed targets
|
||||
this.FinishOrder();
|
||||
return true;
|
||||
}
|
||||
const target = this.order.data.target;
|
||||
if (!this.CheckFormationTargetAttackRange(target))
|
||||
{
|
||||
|
|
@ -1602,6 +1640,26 @@ UnitAI.prototype.UnitFsmSpec = {
|
|||
"INDIVIDUAL": {
|
||||
"Attacked": function(msg)
|
||||
{
|
||||
// First, check if we have a group attack order and the attacker is in our target array
|
||||
if (this.order?.type === "Attack" &&
|
||||
this.order.data?.targetArray?.includes(msg.data.attacker))
|
||||
{
|
||||
// Switch to attack the entity that's attacking us
|
||||
this.order.data.target = msg.data.attacker;
|
||||
this.order.data.attackType = this.GetBestAttackAgainst(msg.data.attacker, this.order.data.allowCapture);
|
||||
|
||||
// Update the current target index
|
||||
const currentIndex = this.order.data.targetArray.indexOf(msg.data.attacker);
|
||||
if (currentIndex !== -1)
|
||||
this.order.data.currentTargetIndex = currentIndex;
|
||||
|
||||
// Make sure we're in the right state to handle this
|
||||
const currentState = this.GetCurrentState();
|
||||
if (currentState === "INDIVIDUAL.IDLE" || currentState === "INDIVIDUAL.WALKING")
|
||||
this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force)
|
||||
this.RespondToTargetedEntities([msg.data.attacker]);
|
||||
},
|
||||
|
|
@ -2150,6 +2208,35 @@ UnitAI.prototype.UnitFsmSpec = {
|
|||
|
||||
"Attacked": function(msg)
|
||||
{
|
||||
// Check if we're doing a group attack and the attacker is in our target array
|
||||
if (!this.order.data.targetArray ||
|
||||
!this.order.data.targetArray.includes(msg.data.attacker) ||
|
||||
msg.data.attacker === this.order.data.target)
|
||||
return;
|
||||
|
||||
// Switch to attack the entity that's attacking us
|
||||
this.order.data.target = msg.data.attacker;
|
||||
this.order.data.attackType = this.GetBestAttackAgainst(msg.data.attacker, this.order.data.allowCapture);
|
||||
|
||||
// Update the current target index
|
||||
const currentIndex = this.order.data.targetArray.indexOf(msg.data.attacker);
|
||||
if (currentIndex !== -1)
|
||||
this.order.data.currentTargetIndex = currentIndex;
|
||||
|
||||
// Re-evaluate our approach to the new target
|
||||
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
|
||||
{
|
||||
if (this.CanUnpack())
|
||||
{
|
||||
this.PushOrderFront("Unpack", { "force": true });
|
||||
return;
|
||||
}
|
||||
this.SetNextState("ATTACKING");
|
||||
}
|
||||
// Continue approaching the new target
|
||||
else if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
|
||||
this.FinishOrder();
|
||||
|
||||
// If we're already in combat mode, ignore anyone else who's attacking us
|
||||
// unless it's a melee attack since they may be blocking our way to the target
|
||||
if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || !this.order.data.force))
|
||||
|
|
@ -2209,7 +2296,23 @@ UnitAI.prototype.UnitFsmSpec = {
|
|||
|
||||
"MovementUpdate": function(msg)
|
||||
{
|
||||
if (msg.likelyFailure)
|
||||
if (!msg.likelyFailure)
|
||||
{
|
||||
if (!this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
|
||||
return;
|
||||
|
||||
if (this.CanUnpack())
|
||||
{
|
||||
this.PushOrderFront("Unpack", { "force": true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.SetNextState("ATTACKING");
|
||||
return;
|
||||
}
|
||||
|
||||
// For group attacks, try next target instead of moving to last position
|
||||
if (!this.order.data.targetArray || this.order.data.currentTargetIndex === undefined)
|
||||
{
|
||||
// This also handles hunting.
|
||||
if (this.orderQueue.length > 1)
|
||||
|
|
@ -2217,11 +2320,13 @@ UnitAI.prototype.UnitFsmSpec = {
|
|||
this.FinishOrder();
|
||||
return;
|
||||
}
|
||||
else if (!this.order.data.force || !this.order.data.lastPos)
|
||||
|
||||
if (!this.order.data.force || !this.order.data.lastPos)
|
||||
{
|
||||
this.SetNextState("COMBAT.FINDINGNEWTARGET");
|
||||
return;
|
||||
}
|
||||
|
||||
// If the order was forced, try moving to the target position,
|
||||
// under the assumption that this is desirable if the target
|
||||
// was somewhat far away - we'll likely end up closer to where
|
||||
|
|
@ -2234,21 +2339,30 @@ UnitAI.prototype.UnitFsmSpec = {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
|
||||
// Handle group attack case
|
||||
// Remove current unreachable target from array
|
||||
const currentIndex = this.order.data.currentTargetIndex;
|
||||
if (currentIndex < this.order.data.targetArray.length)
|
||||
this.order.data.targetArray.splice(currentIndex, 1);
|
||||
|
||||
// Try to find next valid target
|
||||
if (this.order.data.targetArray.length === 0 || !this.FindAndSetNextTarget(this.order.data))
|
||||
{
|
||||
if (this.CanUnpack())
|
||||
{
|
||||
this.PushOrderFront("Unpack", { "force": true });
|
||||
return;
|
||||
}
|
||||
this.SetNextState("ATTACKING");
|
||||
this.FinishOrder();
|
||||
return;
|
||||
}
|
||||
else if (msg.likelySuccess)
|
||||
// Try moving again,
|
||||
// attack range uses a height-related formula and our actual max range might have changed.
|
||||
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
|
||||
this.FinishOrder();
|
||||
},
|
||||
|
||||
this.order.data.attackType = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
|
||||
|
||||
// Try approaching the new target
|
||||
if (this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
|
||||
return;
|
||||
|
||||
// If we can't approach the new target either, remove it and continue
|
||||
this.order.data.targetArray.splice(this.order.data.currentTargetIndex, 1);
|
||||
if (this.order.data.targetArray.length === 0)
|
||||
this.FinishOrder();
|
||||
}
|
||||
},
|
||||
|
||||
"ATTACKING": {
|
||||
|
|
@ -2322,6 +2436,18 @@ UnitAI.prototype.UnitFsmSpec = {
|
|||
|
||||
"OutOfRange": function()
|
||||
{
|
||||
// Check if the target is garrisoned or in a turret (turreted entities cannot be attacked by melee units)
|
||||
const cmpTargetUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
|
||||
const isTargetGarrisoned = cmpTargetUnitAI && cmpTargetUnitAI.isGarrisoned;
|
||||
const isTargetInTurret = cmpTargetUnitAI && cmpTargetUnitAI.IsTurret();
|
||||
|
||||
// If target is garrisoned or in a turret (and we're not ranged), treat as invalid
|
||||
if (isTargetGarrisoned || (isTargetInTurret && this.order.data.attackType !== "Ranged"))
|
||||
{
|
||||
this.ProcessMessage("TargetInvalidated");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force))
|
||||
{
|
||||
if (this.CanPack())
|
||||
|
|
@ -2332,11 +2458,53 @@ UnitAI.prototype.UnitFsmSpec = {
|
|||
this.SetNextState("CHASING");
|
||||
return;
|
||||
}
|
||||
|
||||
// For group attacks, try next target instead of finding new ones
|
||||
if (this.order.data.targetArray && this.order.data.currentTargetIndex !== undefined)
|
||||
{
|
||||
const currentIndex = this.order.data.currentTargetIndex;
|
||||
|
||||
// Remove current target since it's out of range and we can't chase
|
||||
this.order.data.targetArray.splice(currentIndex, 1);
|
||||
|
||||
// Try to find next valid target in sequence
|
||||
if (this.order.data.targetArray.length > 0 && this.FindAndSetNextTarget(this.order.data))
|
||||
{
|
||||
this.order.data.attackType = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
|
||||
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.SetNextState("FINDINGNEWTARGET");
|
||||
},
|
||||
|
||||
"TargetInvalidated": function()
|
||||
{
|
||||
// For group attacks, remove current target and move to next in array
|
||||
if (this.order.data.targetArray && this.order.data.currentTargetIndex !== undefined)
|
||||
{
|
||||
// Remove the current target from the array
|
||||
const currentIndex = this.order.data.currentTargetIndex;
|
||||
if (currentIndex < this.order.data.targetArray.length)
|
||||
{
|
||||
this.order.data.targetArray.splice(currentIndex, 1);
|
||||
|
||||
// If we have more targets, continue with the next one
|
||||
if (this.order.data.targetArray.length > 0)
|
||||
{
|
||||
// Try to find next valid target in sequence
|
||||
if (this.FindAndSetNextTarget(this.order.data))
|
||||
{
|
||||
this.order.data.attackType = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
|
||||
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No more targets or not a group attack - use original behavior
|
||||
this.SetNextState("FINDINGNEWTARGET");
|
||||
},
|
||||
|
||||
|
|
@ -2360,7 +2528,26 @@ UnitAI.prototype.UnitFsmSpec = {
|
|||
|
||||
"enter": function()
|
||||
{
|
||||
// Check if we are attacking a formation.
|
||||
// First, check if we have pending targets in a group attack
|
||||
if (this.order.data.targetArray && this.order.data.currentTargetIndex !== undefined)
|
||||
{
|
||||
// Try to find next valid target from the array
|
||||
if (this.FindAndSetNextTarget(this.order.data))
|
||||
{
|
||||
this.order.data.attackType = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
|
||||
this.SetNextState("COMBAT.ATTACKING");
|
||||
return true;
|
||||
}
|
||||
|
||||
// No valid targets in array - clean up and continue with normal behavior
|
||||
this.order.data.targetArray = [];
|
||||
this.order.data.currentTargetIndex = undefined;
|
||||
}
|
||||
|
||||
if (!this.order.data.target)
|
||||
return false;
|
||||
|
||||
// Try to find the formation the target was a part of.
|
||||
let cmpFormation = Engine.QueryInterface(this.order.data.target, IID_Formation);
|
||||
if (cmpFormation)
|
||||
this.order.data.formationTarget = this.order.data.target;
|
||||
|
|
@ -2468,6 +2655,12 @@ UnitAI.prototype.UnitFsmSpec = {
|
|||
{
|
||||
if (msg.likelyFailure)
|
||||
{
|
||||
// If this is a group attack with pending targets, try the next one
|
||||
if (this.order.data.targetArray && this.order.data.targetArray.length > 0)
|
||||
{
|
||||
this.SetNextState("COMBAT.FINDINGNEWTARGET");
|
||||
return;
|
||||
}
|
||||
// This also handles hunting.
|
||||
if (this.orderQueue.length > 1)
|
||||
{
|
||||
|
|
@ -3887,6 +4080,9 @@ UnitAI.prototype.OnOwnershipChanged = function(msg)
|
|||
|
||||
UnitAI.prototype.OnDestroy = function()
|
||||
{
|
||||
// Clean up all group attack states before destruction
|
||||
this.CleanupAllGroupAttackStates();
|
||||
|
||||
// Switch to an empty state to let states execute their leave handlers.
|
||||
this.UnitFsm.SwitchToNextState(this, "");
|
||||
|
||||
|
|
@ -4093,6 +4289,9 @@ UnitAI.prototype.FsmStateNameChanged = function(state)
|
|||
*/
|
||||
UnitAI.prototype.FinishOrder = function()
|
||||
{
|
||||
// Clean up any group attack state before removing order
|
||||
this.CleanupGroupAttackState(this.order);
|
||||
|
||||
if (!this.orderQueue.length)
|
||||
{
|
||||
const stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line
|
||||
|
|
@ -4318,15 +4517,24 @@ UnitAI.prototype.ReplaceOrder = function(type, data)
|
|||
const idx = this.orderQueue.findIndex(o => o.type == "LeaveFormation");
|
||||
if (idx === -1)
|
||||
{
|
||||
// Clean up all queued orders before clearing them
|
||||
this.CleanupAllGroupAttackStates();
|
||||
this.orderQueue = [];
|
||||
this.order = undefined;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clean up orders being discarded
|
||||
for (let i = 0; i < idx; ++i)
|
||||
this.CleanupGroupAttackState(this.orderQueue[i]);
|
||||
this.orderQueue.splice(0, idx);
|
||||
}
|
||||
this.PushOrderFront(type, data);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clean up all queued orders before clearing them
|
||||
this.CleanupAllGroupAttackStates();
|
||||
this.orderQueue = [];
|
||||
this.PushOrder(type, data);
|
||||
}
|
||||
|
|
@ -4405,6 +4613,7 @@ UnitAI.prototype.BackToWork = function()
|
|||
if (this.IsTurret(cmpTurretable) && !cmpTurretable.LeaveTurret())
|
||||
return false;
|
||||
|
||||
this.CleanupAllGroupAttackStates();
|
||||
this.orderQueue = [];
|
||||
|
||||
this.AddOrders(this.workOrders);
|
||||
|
|
@ -5060,6 +5269,9 @@ UnitAI.prototype.CheckTargetRange = function(target, iid, type)
|
|||
*/
|
||||
UnitAI.prototype.CheckTargetAttackRange = function(target, type)
|
||||
{
|
||||
if (!target || target === INVALID_ENTITY || !type)
|
||||
return false;
|
||||
|
||||
// for formation members, the formation will take care of the range check
|
||||
if (this.IsFormationMember())
|
||||
{
|
||||
|
|
@ -5793,6 +6005,75 @@ UnitAI.prototype.Attack = function(target, allowCapture = this.DEFAULT_CAPTURE,
|
|||
this.AddOrder("Attack", order, queued, pushFront);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a group attack order to the queue.
|
||||
* Used when some unit(s) are ordered to attack multiple targets.
|
||||
* Each unit receives the full target list (from Commands) but starts at a different position
|
||||
* based on the startingIndex in the attacking group.
|
||||
*
|
||||
* @param {Array} targets - Array of target entity IDs to attack (pre-sorted by distance)
|
||||
* @param {number} startingIndex - This unit's position in the attacking group (0-based)
|
||||
* @param {number} unitCount - Total number of units in the attacking group
|
||||
* @param {boolean} allowCapture - Whether capturing is allowed
|
||||
* @param {boolean} queued - If true, add to end of queue instead of replacing current order
|
||||
* @param {boolean} pushFront - If true, add to front of queue (interrupt current order)
|
||||
*/
|
||||
UnitAI.prototype.AttackGroup = function(targets, startingIndex, unitCount, allowCapture = this.DEFAULT_CAPTURE, queued = false, pushFront = false)
|
||||
{
|
||||
if (!targets?.length)
|
||||
return;
|
||||
|
||||
// Check if adding these targets would exceed the limit
|
||||
const currentTotal = this.CountTotalTargetsInQueue();
|
||||
const newTargetCount = targets.length;
|
||||
|
||||
if (currentTotal + newTargetCount > this.MAX_TOTAL_TARGETS)
|
||||
{
|
||||
// Trim the targets to fit within the limit
|
||||
const allowedNewTargets = Math.max(0, this.MAX_TOTAL_TARGETS - currentTotal);
|
||||
if (allowedNewTargets === 0)
|
||||
return;
|
||||
|
||||
// Trim the target array
|
||||
targets = targets.slice(0, allowedNewTargets);
|
||||
|
||||
// If trimming results in no targets, just return
|
||||
if (targets.length === 0)
|
||||
return;
|
||||
}
|
||||
|
||||
// If this unit is in a formation, move the formation controller out of the world
|
||||
// This prevents the controller from staying at the coordinates where the units received the order
|
||||
// That can potentially be far apart from where the fighting will occur.
|
||||
// Result: The formation will reform into a more logical position once members are done (idle).
|
||||
this.MoveFormationControllerOutOfWorld();
|
||||
|
||||
const stride = targets.length / unitCount;
|
||||
const startIdx = Math.min(Math.round((startingIndex + 0.5) * stride), targets.length - 1);
|
||||
|
||||
const order = {
|
||||
"force": true,
|
||||
"allowCapture": allowCapture,
|
||||
"targetArray": targets.slice(),
|
||||
"target": INVALID_ENTITY,
|
||||
"currentTargetIndex": startIdx,
|
||||
"originalFirstTarget": targets[0],
|
||||
};
|
||||
|
||||
this.RememberTargetPosition(order);
|
||||
this.AddOrder("Attack", order, queued, pushFront);
|
||||
};
|
||||
|
||||
UnitAI.prototype.MoveFormationControllerOutOfWorld = function()
|
||||
{
|
||||
if (!this.IsFormationMember() || !this.formationController)
|
||||
return;
|
||||
|
||||
const cmpControllerPosition = Engine.QueryInterface(this.formationController, IID_Position);
|
||||
if (cmpControllerPosition?.IsInWorld())
|
||||
cmpControllerPosition.MoveOutOfWorld();
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds garrison order to the queue, forced by the player.
|
||||
*/
|
||||
|
|
@ -6726,6 +7007,72 @@ UnitAI.prototype.AttackEntitiesByPreference = function(ents)
|
|||
return this.RespondToTargetedEntities(entsWithoutPref);
|
||||
};
|
||||
|
||||
UnitAI.prototype.FindAndSetNextTarget = function(data)
|
||||
{
|
||||
if (!this.EnsureValidTargetIndex(data))
|
||||
return false;
|
||||
|
||||
const startIdx = data.currentTargetIndex;
|
||||
const currentTarget = data.targetArray[startIdx];
|
||||
|
||||
// First check the starting target
|
||||
if (this.CanAttack(currentTarget) && this.CheckTargetVisible(currentTarget))
|
||||
return this.SetNextTarget(data, startIdx);
|
||||
|
||||
// Search forward using find
|
||||
const forwardFound = data.targetArray
|
||||
.slice(startIdx + 1)
|
||||
.findIndex((target, i) =>
|
||||
this.CanAttack(target) && this.CheckTargetVisible(target)
|
||||
);
|
||||
|
||||
if (forwardFound !== -1)
|
||||
return this.SetNextTarget(data, startIdx + 1 + forwardFound);
|
||||
|
||||
// Search backward using find
|
||||
const backwardFound = data.targetArray
|
||||
.slice(0, startIdx)
|
||||
.reverse()
|
||||
.findIndex((target, i) =>
|
||||
this.CanAttack(target) && this.CheckTargetVisible(target)
|
||||
);
|
||||
|
||||
if (backwardFound !== -1)
|
||||
{
|
||||
// Convert reverse index back to original index
|
||||
const originalIndex = startIdx - 1 - backwardFound;
|
||||
return this.SetNextTarget(data, originalIndex);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
UnitAI.prototype.SetNextTarget = function(data, index)
|
||||
{
|
||||
const target = data.targetArray[index];
|
||||
data.target = target;
|
||||
data.attackType = this.GetBestAttackAgainst(target, data.allowCapture);
|
||||
data.currentTargetIndex = index;
|
||||
return true;
|
||||
};
|
||||
|
||||
UnitAI.prototype.EnsureValidTargetIndex = function(data)
|
||||
{
|
||||
if (!data.targetArray || data.currentTargetIndex === undefined)
|
||||
return false;
|
||||
|
||||
// If array is empty, clear the index
|
||||
if (data.targetArray.length === 0)
|
||||
{
|
||||
data.currentTargetIndex = undefined;
|
||||
return false;
|
||||
}
|
||||
|
||||
data.currentTargetIndex = Math.clamp(data.currentTargetIndex, 0, data.targetArray.length - 1);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Call UnitAI.funcname(args) on all formation members.
|
||||
* @param resetFinishedEntities - If true, call ResetFinishedEntities first.
|
||||
|
|
@ -6837,6 +7184,50 @@ UnitAI.prototype.ResetObstructionMitigationFlag = function()
|
|||
delete this.obstructionMitigationAttempted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Counts the total number of target entities across all queued orders.
|
||||
* This includes targetArray from group attacks and single targets.
|
||||
* Each target reference is counted separately, even if the same entity
|
||||
* appears in multiple orders or in both targetArray and target.
|
||||
* @returns {number} - Total number of target entity references in the order queue
|
||||
*/
|
||||
UnitAI.prototype.CountTotalTargetsInQueue = function()
|
||||
{
|
||||
let total = 0;
|
||||
for (const order of this.orderQueue)
|
||||
{
|
||||
if (order.data)
|
||||
{
|
||||
// Count group attack targets
|
||||
if (order.data.targetArray && Array.isArray(order.data.targetArray))
|
||||
total += order.data.targetArray.length;
|
||||
|
||||
// Add single target for consistency
|
||||
if (order.data.target && order.data.target !== INVALID_ENTITY)
|
||||
total++;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
UnitAI.prototype.CleanupGroupAttackState = function(order)
|
||||
{
|
||||
if (order?.data?.targetArray)
|
||||
{
|
||||
delete order.data.targetArray;
|
||||
delete order.data.currentTargetIndex;
|
||||
}
|
||||
};
|
||||
|
||||
UnitAI.prototype.CleanupAllGroupAttackStates = function()
|
||||
{
|
||||
for (const order of this.orderQueue)
|
||||
this.CleanupGroupAttackState(order);
|
||||
};
|
||||
|
||||
// Maximum number of target entities allowed across all queued orders
|
||||
UnitAI.prototype.MAX_TOTAL_TARGETS = 1000;
|
||||
|
||||
UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec);
|
||||
|
||||
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ function TestFormationExiting(mode)
|
|||
"ResetActiveQuery": function(id) { if (mode == 0) return []; return [enemy]; },
|
||||
"DisableActiveQuery": function(id) { },
|
||||
"GetEntityFlagMask": function(identifier) { },
|
||||
"GetLosVisibility": (ent, player) => "visible"
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
|
||||
|
|
@ -319,8 +320,11 @@ function TestMoveIntoFormationWhileAttacking()
|
|||
"ResetActiveQuery": function(id) { return [enemy]; },
|
||||
"DisableActiveQuery": function(id) { },
|
||||
"GetEntityFlagMask": function(identifier) { },
|
||||
"GetLosVisibility": (target, player) => "visible",
|
||||
"GetLosVisibilityPosition": (x, z, player) => "visible"
|
||||
});
|
||||
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
|
||||
"GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; },
|
||||
});
|
||||
|
|
@ -406,6 +410,10 @@ function TestMoveIntoFormationWhileAttacking()
|
|||
"GetHitpoints": function() { return 40; },
|
||||
});
|
||||
|
||||
AddMock(enemy, IID_Ownership, {
|
||||
"GetOwner": () => 2 // Different player ID (enemy)
|
||||
});
|
||||
|
||||
const controllerFormation = ConstructComponent(controller, "Formation", {
|
||||
"FormationShape": "square",
|
||||
"ShiftRows": "false",
|
||||
|
|
@ -421,6 +429,10 @@ function TestMoveIntoFormationWhileAttacking()
|
|||
"DefaultStance": "aggressive"
|
||||
});
|
||||
|
||||
AddMock(controller, IID_Ownership, {
|
||||
"GetOwner": () => 1
|
||||
});
|
||||
|
||||
AddMock(controller, IID_Position, {
|
||||
"GetTurretParent": () => INVALID_ENTITY,
|
||||
"JumpTo": function(x, z) { this.x = x; this.z = z; },
|
||||
|
|
@ -458,7 +470,7 @@ function TestMoveIntoFormationWhileAttacking()
|
|||
|
||||
controllerFormation.SetMembers(units);
|
||||
|
||||
controllerAI.Attack(enemy, []);
|
||||
controllerAI.Attack(enemy);
|
||||
|
||||
for (const ent of unitAIs)
|
||||
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
|
||||
|
|
@ -889,4 +901,362 @@ function TestAttemptObstructionMitigation()
|
|||
})();
|
||||
}
|
||||
|
||||
TestAttemptObstructionMitigation();
|
||||
TestAttemptObstructionMitigation();
|
||||
function TestFindAndSetNextTargetBehavior()
|
||||
{
|
||||
ResetState();
|
||||
|
||||
const attacker = 10;
|
||||
const targets = [20, 21, 22, 23, 24]; // Multiple targets for comprehensive testing
|
||||
|
||||
// Basic mocks needed for unitAI creation
|
||||
AddMock(SYSTEM_ENTITY, IID_Timer, {
|
||||
"SetInterval": () => {},
|
||||
"SetTimeout": () => {}
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
||||
"CreateActiveQuery": () => 1,
|
||||
"EnableActiveQuery": () => {},
|
||||
"ResetActiveQuery": () => [],
|
||||
"DisableActiveQuery": () => {},
|
||||
"GetEntityFlagMask": () => 0
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
|
||||
"IsInTargetRange": () => false
|
||||
});
|
||||
|
||||
// Create the unitAI component
|
||||
const unitAI = ConstructComponent(attacker, "UnitAI", {
|
||||
"FormationController": "false",
|
||||
"DefaultStance": "aggressive"
|
||||
});
|
||||
|
||||
AddMock(attacker, IID_Attack, {
|
||||
"GetBestAttackAgainst": () => "Melee"
|
||||
});
|
||||
|
||||
unitAI.OnCreate();
|
||||
|
||||
// Mock the visibility and attackability checks
|
||||
unitAI.CanAttack = (target) =>
|
||||
{
|
||||
// Target 21 is always unattackable (e.g., garrisoned, dead, etc.)
|
||||
return target !== 21;
|
||||
};
|
||||
|
||||
unitAI.CheckTargetVisible = (target) =>
|
||||
{
|
||||
// Target 22 is always not visible
|
||||
return target !== 22;
|
||||
};
|
||||
|
||||
// First target is valid
|
||||
(function()
|
||||
{
|
||||
const data = {
|
||||
"targetArray": [20, 21, 22, 23, 24],
|
||||
"currentTargetIndex": 0, // Currently targeting 20
|
||||
"target": 20,
|
||||
"allowCapture": false
|
||||
};
|
||||
|
||||
const found = unitAI.FindAndSetNextTarget(data);
|
||||
TS_ASSERT(found);
|
||||
TS_ASSERT_EQUALS(data.target, 20);
|
||||
TS_ASSERT_EQUALS(data.currentTargetIndex, 0);
|
||||
})();
|
||||
|
||||
// End of array behavior
|
||||
(function()
|
||||
{
|
||||
const data = {
|
||||
"targetArray": [18, 19, 20, 21, 22],
|
||||
"currentTargetIndex": 2, // Currently targeting 22
|
||||
"allowCapture": false
|
||||
};
|
||||
|
||||
// 21 and 22 are invalid, should go backward and find 20
|
||||
const found = unitAI.FindAndSetNextTarget(data);
|
||||
TS_ASSERT(found);
|
||||
TS_ASSERT_EQUALS(data.target, 20); // Should be assigned targetArray[initialTarget - 1]
|
||||
})();
|
||||
|
||||
// No valid targets found
|
||||
(function()
|
||||
{
|
||||
const data = {
|
||||
"targetArray": [21, 22], // Both are invalid (21 unattackable, 22 not visible)
|
||||
"currentTargetIndex": 0,
|
||||
"target": 21,
|
||||
"allowCapture": false
|
||||
};
|
||||
|
||||
const found = unitAI.FindAndSetNextTarget(data);
|
||||
TS_ASSERT(!found);
|
||||
TS_ASSERT_EQUALS(data.target, 21); // Should remain unchanged
|
||||
TS_ASSERT_EQUALS(data.currentTargetIndex, 0);
|
||||
})();
|
||||
|
||||
// Empty target array
|
||||
(function()
|
||||
{
|
||||
const data = {
|
||||
"targetArray": [],
|
||||
"currentTargetIndex": 0,
|
||||
"target": 20,
|
||||
"allowCapture": false
|
||||
};
|
||||
|
||||
const found = unitAI.FindAndSetNextTarget(data);
|
||||
TS_ASSERT(!found);
|
||||
TS_ASSERT_EQUALS(data.target, 20); // Should remain unchanged
|
||||
TS_ASSERT_EQUALS(data.currentTargetIndex, void 0); // Should nullify currentTargetIndex
|
||||
})();
|
||||
|
||||
// Current target is last in array
|
||||
(function()
|
||||
{
|
||||
const data = {
|
||||
"targetArray": [20, 21, 22, 23, 24],
|
||||
"currentTargetIndex": 4, // Currently targeting 24 (last element)
|
||||
"allowCapture": false
|
||||
};
|
||||
|
||||
const found = unitAI.FindAndSetNextTarget(data);
|
||||
TS_ASSERT(found);
|
||||
TS_ASSERT_EQUALS(data.target, 24); // Should remain unchanged
|
||||
TS_ASSERT_EQUALS(data.currentTargetIndex, 4);
|
||||
})();
|
||||
|
||||
// Single invalid target
|
||||
(function()
|
||||
{
|
||||
const data = {
|
||||
"targetArray": [21], // Only one target, which is invalid
|
||||
"currentTargetIndex": 0,
|
||||
"allowCapture": false
|
||||
};
|
||||
|
||||
const found = unitAI.FindAndSetNextTarget(data);
|
||||
TS_ASSERT(!found);
|
||||
})();
|
||||
|
||||
// Verify if we don't do more calls then necessary
|
||||
(function()
|
||||
{
|
||||
const searchOrder = [];
|
||||
|
||||
unitAI.CanAttack = (target) =>
|
||||
{
|
||||
searchOrder.push(target);
|
||||
return target == 20;
|
||||
};
|
||||
|
||||
unitAI.CheckTargetVisible = (target) => true; // All visible
|
||||
|
||||
const data = {
|
||||
"targetArray": [20, 21, 22, 23],
|
||||
"currentTargetIndex": 1, // Currently targeting 21
|
||||
"allowCapture": false
|
||||
};
|
||||
|
||||
const found = unitAI.FindAndSetNextTarget(data);
|
||||
TS_ASSERT(found);
|
||||
TS_ASSERT_EQUALS(searchOrder.length, 4); // Should find at 20 (4 calls)
|
||||
|
||||
})();
|
||||
}
|
||||
|
||||
TestFindAndSetNextTargetBehavior();
|
||||
|
||||
function TestAttackGroupBehavior()
|
||||
{
|
||||
ResetState();
|
||||
|
||||
const attacker1 = 100;
|
||||
const attacker2 = 101;
|
||||
const targets = [200, 201, 202, 203, 204];
|
||||
|
||||
// Basic mocks needed for unitAI creation
|
||||
AddMock(SYSTEM_ENTITY, IID_Timer, {
|
||||
"SetInterval": () => {},
|
||||
"SetTimeout": () => {}
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
||||
"CreateActiveQuery": () => 1,
|
||||
"EnableActiveQuery": () => {},
|
||||
"ResetActiveQuery": () => [],
|
||||
"DisableActiveQuery": () => {},
|
||||
"GetEntityFlagMask": () => 0
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
|
||||
"IsInTargetRange": () => false
|
||||
});
|
||||
|
||||
// Create two unitAI components
|
||||
const unitAI1 = ConstructComponent(attacker1, "UnitAI", {
|
||||
"FormationController": "false",
|
||||
"DefaultStance": "aggressive"
|
||||
});
|
||||
|
||||
const unitAI2 = ConstructComponent(attacker2, "UnitAI", {
|
||||
"FormationController": "false",
|
||||
"DefaultStance": "aggressive"
|
||||
});
|
||||
|
||||
AddMock(attacker1, IID_Attack, {
|
||||
"GetBestAttackAgainst": () => "Melee"
|
||||
});
|
||||
|
||||
AddMock(attacker2, IID_Attack, {
|
||||
"GetBestAttackAgainst": () => "Melee"
|
||||
});
|
||||
|
||||
unitAI1.OnCreate();
|
||||
unitAI2.OnCreate();
|
||||
|
||||
// Basic AttackGroup distribution
|
||||
(function()
|
||||
{
|
||||
let orderAdded1 = null;
|
||||
let orderAdded2 = null;
|
||||
|
||||
// Mock AddOrder to capture what orders are created
|
||||
unitAI1.AddOrder = (type, data, queued, pushFront) =>
|
||||
{
|
||||
orderAdded1 = { type, data, queued, pushFront };
|
||||
};
|
||||
|
||||
unitAI2.AddOrder = (type, data, queued, pushFront) =>
|
||||
{
|
||||
orderAdded2 = { type, data, queued, pushFront };
|
||||
};
|
||||
|
||||
// Call AttackGroup for both units
|
||||
unitAI1.AttackGroup(targets, 0, 2, false, false, false);
|
||||
unitAI2.AttackGroup(targets, 1, 2, false, false, false);
|
||||
|
||||
// Verify unit1's order
|
||||
TS_ASSERT(orderAdded1);
|
||||
TS_ASSERT_EQUALS(orderAdded1.type, "Attack");
|
||||
TS_ASSERT_EQUALS(orderAdded1.data.targetArray.length, 5);
|
||||
TS_ASSERT_EQUALS(orderAdded1.data.target, INVALID_ENTITY);
|
||||
// Unit 0 should start at position 1 ==> Math.round((0 + 0.5) * 2.5) = Math.round(1.25) = 1
|
||||
TS_ASSERT_EQUALS(orderAdded1.data.currentTargetIndex, 1);
|
||||
|
||||
// Verify unit2's order
|
||||
TS_ASSERT(orderAdded2);
|
||||
TS_ASSERT_EQUALS(orderAdded2.type, "Attack");
|
||||
TS_ASSERT_EQUALS(orderAdded2.data.targetArray.length, 5);
|
||||
TS_ASSERT_EQUALS(orderAdded2.data.target, INVALID_ENTITY);
|
||||
// Unit 1 should start at position 4 ==> Math.round((1 + 0.5) * 2.5) = Math.round(3.75) = 4
|
||||
TS_ASSERT_EQUALS(orderAdded2.data.currentTargetIndex, 4);
|
||||
})();
|
||||
|
||||
// More units than targets
|
||||
(function()
|
||||
{
|
||||
let orderAdded1 = null;
|
||||
let orderAdded2 = null;
|
||||
let orderAdded3 = null;
|
||||
|
||||
const unitAI3 = ConstructComponent(102, "UnitAI", {
|
||||
"FormationController": "false",
|
||||
"DefaultStance": "aggressive"
|
||||
});
|
||||
AddMock(102, IID_Attack, { "GetBestAttackAgainst": () => "Melee" });
|
||||
unitAI3.OnCreate();
|
||||
|
||||
unitAI1.AddOrder = (type, data, queued, pushFront) =>
|
||||
{
|
||||
orderAdded1 = { type, data, queued, pushFront };
|
||||
};
|
||||
unitAI2.AddOrder = (type, data, queued, pushFront) =>
|
||||
{
|
||||
orderAdded2 = { type, data, queued, pushFront };
|
||||
};
|
||||
unitAI3.AddOrder = (type, data, queued, pushFront) =>
|
||||
{
|
||||
orderAdded3 = { type, data, queued, pushFront };
|
||||
};
|
||||
|
||||
const smallTargets = [300, 301];
|
||||
|
||||
unitAI1.AttackGroup(smallTargets, 0, 3, true, true, false);
|
||||
unitAI2.AttackGroup(smallTargets, 1, 3, true, true, false);
|
||||
unitAI3.AttackGroup(smallTargets, 2, 3, true, true, false);
|
||||
|
||||
// Verify distribution when more units than targets
|
||||
TS_ASSERT(orderAdded1);
|
||||
TS_ASSERT_EQUALS(orderAdded1.data.currentTargetIndex, 0); // 0 * 2 / 3 = 0
|
||||
TS_ASSERT_EQUALS(orderAdded1.data.allowCapture, true);
|
||||
TS_ASSERT_EQUALS(orderAdded1.queued, true);
|
||||
|
||||
TS_ASSERT(orderAdded2);
|
||||
TS_ASSERT_EQUALS(orderAdded2.data.currentTargetIndex, 1); // 1 * 2 / 3 = 0.66, round = 1
|
||||
TS_ASSERT(orderAdded2.data.currentTargetIndex < smallTargets.length);
|
||||
|
||||
TS_ASSERT(orderAdded3);
|
||||
TS_ASSERT_EQUALS(orderAdded3.data.currentTargetIndex, 1); // 2 * 2 / 3 = 1.33, round = 1
|
||||
TS_ASSERT(orderAdded3.data.currentTargetIndex < smallTargets.length);
|
||||
})();
|
||||
|
||||
// Edge case - single target
|
||||
(function()
|
||||
{
|
||||
let orderAdded = null;
|
||||
unitAI1.AddOrder = (type, data, queued, pushFront) =>
|
||||
{
|
||||
orderAdded = { type, data, queued, pushFront };
|
||||
};
|
||||
|
||||
const singleTarget = [400];
|
||||
|
||||
unitAI1.AttackGroup(singleTarget, 0, 1, false, false, true);
|
||||
|
||||
TS_ASSERT(orderAdded);
|
||||
TS_ASSERT_EQUALS(orderAdded.data.targetArray.length, 1);
|
||||
TS_ASSERT_EQUALS(orderAdded.data.currentTargetIndex, 0);
|
||||
TS_ASSERT_EQUALS(orderAdded.pushFront, true);
|
||||
})();
|
||||
|
||||
// Empty targets array should do nothing
|
||||
(function()
|
||||
{
|
||||
let callCount = 0;
|
||||
unitAI1.AddOrder = () => { callCount++; };
|
||||
|
||||
unitAI1.AttackGroup([], 0, 1);
|
||||
TS_ASSERT_EQUALS(callCount, 0);
|
||||
|
||||
unitAI1.AttackGroup(null, 0, 1);
|
||||
TS_ASSERT_EQUALS(callCount, 0);
|
||||
|
||||
unitAI1.AttackGroup(undefined, 0, 1);
|
||||
TS_ASSERT_EQUALS(callCount, 0);
|
||||
})();
|
||||
|
||||
// Verify RememberTargetPosition is called
|
||||
(function()
|
||||
{
|
||||
let rememberedOrder = null;
|
||||
unitAI1.RememberTargetPosition = (order) =>
|
||||
{
|
||||
rememberedOrder = order;
|
||||
};
|
||||
|
||||
unitAI1.AddOrder = () => {};
|
||||
|
||||
const testTargets = [500, 501];
|
||||
unitAI1.AttackGroup(testTargets, 0, 1);
|
||||
|
||||
TS_ASSERT(rememberedOrder);
|
||||
TS_ASSERT_EQUALS(rememberedOrder.targetArray.length, 2);
|
||||
})();
|
||||
}
|
||||
|
||||
TestAttackGroupBehavior();
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@
|
|||
{ "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.9 }
|
||||
],
|
||||
"auraName": "Economic Fortune",
|
||||
"auraDescription": "Solon brought in a new system of weights and measures, fathers were encouraged to find trades for their sons.\nEconomic technologies −10% resource costs."
|
||||
"auraDescription": "Solon brought in a new system of weights and measures, and fathers were encouraged to find trades for their sons.\nEconomic technologies −10% resource costs."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@
|
|||
{ "value": "TerritoryInfluence/Radius", "multiply": 1.2 }
|
||||
],
|
||||
"auraName": "Territorial Expansion",
|
||||
"auraDescription": "At its height, the Empire's borders spanned from the Fergana Valley in the west, to Korea in the east, and to northern Vietnam in the south.\nTerritory influence bonus +20%."
|
||||
"auraDescription": "At its height, the Empire's borders spanned from the Fergana Valley in the west to Korea in the east and to northern Vietnam in the south.\nTerritory influence bonus +20%."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@
|
|||
{ "value": "Resistance/Entity/Damage/Crush", "add": 1 }
|
||||
],
|
||||
"auraName": "Founder and Defender of the Republic",
|
||||
"auraDescription": "Brutus was one of the key figures in the overthrow of the monarchy and the founding of the Roman Republic. Later, as consul he led a Roman army to victory against the Etruscan King Tarquinius who sought to retake the throne.\nHumans and Siege Engines +1 crush, hack, pierce resistance."
|
||||
"auraDescription": "Brutus was one of the key figures in the overthrow of the monarchy and the founding of the Roman Republic. Later, as consul, he led a Roman army to victory against the Etruscan King Tarquinius who sought to retake the throne.\nHumans and Siege Engines +1 crush, hack, and pierce resistance."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,6 +179,28 @@ var g_Commands = {
|
|||
});
|
||||
},
|
||||
|
||||
"attack-group": function(player, cmd, data)
|
||||
{
|
||||
const unitAIs = data.entities.flatMap(ent =>
|
||||
{
|
||||
const cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
|
||||
const cmpAttack = Engine.QueryInterface(ent, IID_Attack);
|
||||
|
||||
// Check if unit can move and has attack capability
|
||||
const canAttack = cmpAttack && cmpAttack.GetAttackTypes().length > 0;
|
||||
return (cmpUnitAI && cmpUnitAI.AbleToMove() && canAttack) ? [cmpUnitAI] : [];
|
||||
});
|
||||
|
||||
if (!unitAIs.length || !cmd.targets?.length)
|
||||
return;
|
||||
|
||||
// Pass the entire target array and unit count to each UnitAI
|
||||
unitAIs.forEach((cmpUnitAI, index) =>
|
||||
{
|
||||
cmpUnitAI.AttackGroup(cmd.targets, index, unitAIs.length, cmd.allowCapture, cmd.queued, cmd.pushFront);
|
||||
});
|
||||
},
|
||||
|
||||
"patrol": function(player, cmd, data)
|
||||
{
|
||||
const ents = data.entities.length;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2025 Wildfire Games.
|
||||
/* Copyright (C) 2026 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -54,7 +54,7 @@ pipeline {
|
|||
stage('Check for shader changes') {
|
||||
when {
|
||||
anyOf {
|
||||
changeset 'binaries/data/mods/**/shaders/**/*.xml'
|
||||
changeset 'binaries/data/mods/**/shaders/**/*'
|
||||
changeset 'source/tools/spirv/compile.py'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDzzCCAjegAwIBAgIUYMCyNaykR4OF8u4Bq+pesqG48LQwDQYJKoZIhvcNAQEL
|
||||
BQAwADAgFw0yNjA1MjAxOTA0NDFaGA85OTk5MTIzMTIzNTk1OVowADCCAaIwDQYJ
|
||||
KoZIhvcNAQEBBQADggGPADCCAYoCggGBAL5uhNcLTOs4Senn8zuwy8c15HGDCBk/
|
||||
tSgUFCSueBRtbZuAnPtxlkOkJDB7y/Abp+rhHe7T7sa1iYS+Q7aWK0QIaoiKJj1x
|
||||
35SsCYmniAs797tsxnzfRuP2WJe8yt/kuia6d6AL12P5Yu05JEJWaiDHljfSZsGB
|
||||
spCD5c2PyYZqGKNxdUHLlFDZ3RIWkym8h/wTKESlIfKcO0l9Ythtz0qAFWZbOJRR
|
||||
VXUIJ6djYtc3C3cIIa6P18598CkRlmk7WSRfSrUY1USrzCL5dwBnfqLNqyWuqHWk
|
||||
hJLM7Izu2ZR24V4rQpf7V32jAUkWCwRUyseqkilgYIV3MWLoLlBP5jLTRBZowIJ2
|
||||
h6EnqGNfUqKuR9W/sT21lkP+XUJPNfPEjL5paG1jpyMCwvWHrfVd63wjDUaZxa52
|
||||
APZlhDLoKPrMI4jBuV0yBnO8m2PKVHGbfytuCUSWRW/+YHtoIBVJ5GpPbZ7XRjan
|
||||
/p/Rk++XVXlKyo9b66PDQ/qBEzTrsFie2QIDAQABoz8wPTAMBgNVHRMBAf8EAjAA
|
||||
MA4GA1UdDwEB/wQEAwIFoDAdBgNVHQ4EFgQUSCH7PDVIF0erciboK/dzV1i+hC8w
|
||||
DQYJKoZIhvcNAQELBQADggGBACVS1GPwXvHxs5PFV8dobDXDbzIF984YJGkfkpTI
|
||||
eQiC2GdQZd+d2NQdYJYu+yFQrmfPyhH4FPH6uLUp2eNGexXlv58TNMkh69MekapJ
|
||||
+XBBwwCynL4vZETtozE6Dzp5S7XXL3cenYI+0MIforEhI3dtbjg2zxRQy4YiBYuh
|
||||
tecbazuNsVXw7iSN8yT0wZTfg4gGHDyzEdO4+8BcBUfzpBDOr+/TV58doIA/ZucX
|
||||
utFR8o9tVmvVEef8LaEOzmn7fgpxaEiVEbFH1PUyRhMZlQRN/eTTkDTRiTzssoXA
|
||||
dtIV5M1kJjkdn/aYxM8h+unJ6/MfPvuat+HcmKDXVhdoNJ2r23mUwRFPt7fwm75Y
|
||||
gp8WWwP9D8MglcVHhsp+KrDKfFX7MicDPhadc6LEWveo4PRRdJcKcWedJEvnWFNN
|
||||
0TbbC5xQgIB1KijxvqoBr38XQVhFry1dZRPBv642YBMdiHWGCoLE8EBssDqs5Wp+
|
||||
DMqs4E5nZzbVVSM5l5J2q6Wlbw==
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -28,7 +28,7 @@ SDL2_VERSION="SDL2-2.24.0"
|
|||
# NOTE: remember to also update LIB_URL below when changing version
|
||||
BOOST_VERSION="boost_1_81_0"
|
||||
# NOTE: remember to also update LIB_URL below when changing version
|
||||
WXWIDGETS_VERSION="wxWidgets-3.2.8"
|
||||
WXWIDGETS_VERSION="wxWidgets-3.2.10"
|
||||
# libpng was included as part of X11 but that's removed from Mountain Lion
|
||||
# (also the Snow Leopard version was ancient 1.2)
|
||||
PNG_VERSION="libpng-1.6.44"
|
||||
|
|
@ -477,7 +477,7 @@ echo "Building wxWidgets..."
|
|||
LIB_VERSION="${WXWIDGETS_VERSION}"
|
||||
LIB_ARCHIVE="$LIB_VERSION.tar.bz2"
|
||||
LIB_DIRECTORY="$LIB_VERSION"
|
||||
LIB_URL="http://github.com/wxWidgets/wxWidgets/releases/download/v3.2.8/"
|
||||
LIB_URL="http://github.com/wxWidgets/wxWidgets/releases/download/v3.2.10/"
|
||||
|
||||
mkdir -p wxwidgets
|
||||
cd wxwidgets
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ echo "Building cpp-httplib..."
|
|||
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
||||
echo "Skipping - already built (use --force-rebuild to override)"
|
||||
exit
|
||||
else
|
||||
rm -f .already-built
|
||||
fi
|
||||
|
||||
# fetch
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ done
|
|||
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
||||
echo "Skipping - already built (use --force-rebuild to override)"
|
||||
exit
|
||||
else
|
||||
rm -f .already-built
|
||||
fi
|
||||
|
||||
# fetch
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ done
|
|||
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
||||
echo "Skipping - already built (use --force-rebuild to override)"
|
||||
exit
|
||||
else
|
||||
rm -f .already-built
|
||||
fi
|
||||
|
||||
# fetch
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ done
|
|||
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
||||
echo "Skipping - already built (use --force-rebuild to override)"
|
||||
exit
|
||||
else
|
||||
rm -f .already-built
|
||||
fi
|
||||
|
||||
# fetch
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ done
|
|||
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
||||
echo "Skipping - already built (use --force-rebuild to override)"
|
||||
exit
|
||||
else
|
||||
rm -f .already-built
|
||||
fi
|
||||
|
||||
# fetch
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ echo "Building SpiderMonkey..."
|
|||
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
||||
echo "Skipping - already built (use --force-rebuild to override)"
|
||||
exit
|
||||
else
|
||||
rm -f .already-built
|
||||
fi
|
||||
|
||||
OS="${OS:=$(uname -s)}"
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ done
|
|||
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
|
||||
echo "Skipping - already built (use --force-rebuild to override)"
|
||||
exit
|
||||
else
|
||||
rm -f .already-built
|
||||
fi
|
||||
|
||||
# fetch
|
||||
|
|
|
|||
182
privatekey.pem
182
privatekey.pem
|
|
@ -1,182 +0,0 @@
|
|||
Public Key Info:
|
||||
Public Key Algorithm: RSA
|
||||
Key Security Level: High (3072 bits)
|
||||
|
||||
modulus:
|
||||
00:be:6e:84:d7:0b:4c:eb:38:49:e9:e7:f3:3b:b0:cb
|
||||
c7:35:e4:71:83:08:19:3f:b5:28:14:14:24:ae:78:14
|
||||
6d:6d:9b:80:9c:fb:71:96:43:a4:24:30:7b:cb:f0:1b
|
||||
a7:ea:e1:1d:ee:d3:ee:c6:b5:89:84:be:43:b6:96:2b
|
||||
44:08:6a:88:8a:26:3d:71:df:94:ac:09:89:a7:88:0b
|
||||
3b:f7:bb:6c:c6:7c:df:46:e3:f6:58:97:bc:ca:df:e4
|
||||
ba:26:ba:77:a0:0b:d7:63:f9:62:ed:39:24:42:56:6a
|
||||
20:c7:96:37:d2:66:c1:81:b2:90:83:e5:cd:8f:c9:86
|
||||
6a:18:a3:71:75:41:cb:94:50:d9:dd:12:16:93:29:bc
|
||||
87:fc:13:28:44:a5:21:f2:9c:3b:49:7d:62:d8:6d:cf
|
||||
4a:80:15:66:5b:38:94:51:55:75:08:27:a7:63:62:d7
|
||||
37:0b:77:08:21:ae:8f:d7:ce:7d:f0:29:11:96:69:3b
|
||||
59:24:5f:4a:b5:18:d5:44:ab:cc:22:f9:77:00:67:7e
|
||||
a2:cd:ab:25:ae:a8:75:a4:84:92:cc:ec:8c:ee:d9:94
|
||||
76:e1:5e:2b:42:97:fb:57:7d:a3:01:49:16:0b:04:54
|
||||
ca:c7:aa:92:29:60:60:85:77:31:62:e8:2e:50:4f:e6
|
||||
32:d3:44:16:68:c0:82:76:87:a1:27:a8:63:5f:52:a2
|
||||
ae:47:d5:bf:b1:3d:b5:96:43:fe:5d:42:4f:35:f3:c4
|
||||
8c:be:69:68:6d:63:a7:23:02:c2:f5:87:ad:f5:5d:eb
|
||||
7c:23:0d:46:99:c5:ae:76:00:f6:65:84:32:e8:28:fa
|
||||
cc:23:88:c1:b9:5d:32:06:73:bc:9b:63:ca:54:71:9b
|
||||
7f:2b:6e:09:44:96:45:6f:fe:60:7b:68:20:15:49:e4
|
||||
6a:4f:6d:9e:d7:46:36:a7:fe:9f:d1:93:ef:97:55:79
|
||||
4a:ca:8f:5b:eb:a3:c3:43:fa:81:13:34:eb:b0:58:9e
|
||||
d9:
|
||||
|
||||
public exponent:
|
||||
01:00:01:
|
||||
|
||||
private exponent:
|
||||
54:d4:52:a4:a0:ca:10:f6:30:26:dc:46:83:ce:8b:d8
|
||||
1f:ef:b5:89:13:30:7c:2a:ac:c1:d4:ff:4a:20:ff:a8
|
||||
87:6c:ff:eb:ee:2e:79:2b:84:91:02:70:03:36:e0:7a
|
||||
fa:ac:71:73:14:41:87:8f:12:c5:69:24:2c:cf:d4:52
|
||||
28:15:9c:e1:3d:8b:9d:90:65:60:05:97:a6:63:79:ed
|
||||
aa:bb:79:07:2a:55:23:f5:24:a5:ee:62:11:55:8f:44
|
||||
45:40:47:4d:aa:38:b6:b6:3f:15:41:a1:1f:53:f3:4e
|
||||
ca:d9:e5:df:fa:1a:35:36:60:1e:01:5e:82:b0:d9:09
|
||||
a6:14:18:d4:8d:0c:ac:f3:1f:39:d5:76:ec:f3:68:a2
|
||||
82:ed:dd:c0:46:77:4a:e5:c1:9b:49:19:a1:23:b9:75
|
||||
8c:7c:fb:ed:a2:d4:9f:2e:9c:45:97:b4:7f:17:66:9b
|
||||
84:a5:ef:9f:61:6d:7e:4a:e3:da:f9:d0:75:da:46:ae
|
||||
f6:e9:ae:cd:66:11:11:62:5a:3e:22:55:9f:ac:2c:bd
|
||||
a8:5a:62:1e:fe:c6:78:04:1d:69:f3:12:53:c7:5a:db
|
||||
c3:19:a4:d9:d6:5a:25:55:f7:ba:30:d1:f4:bb:8a:b6
|
||||
8a:c5:1d:4c:66:c3:e6:c1:05:4a:d8:9b:2e:f3:b9:3f
|
||||
8a:b6:6e:04:cb:5f:32:65:a0:1c:32:5a:f5:b0:36:a3
|
||||
ed:96:1b:cf:b0:1d:32:3e:8b:a4:53:b2:c0:6d:30:d3
|
||||
93:43:d6:fe:29:fd:9e:e2:72:30:7a:01:2b:b2:ad:39
|
||||
b5:6c:8b:41:67:77:bb:09:8e:41:41:e2:a9:5a:76:4b
|
||||
51:56:43:72:db:02:5e:d9:ef:93:16:c7:64:18:3a:f7
|
||||
73:d8:b8:d0:8f:af:e3:cd:06:26:8f:af:b0:b6:57:f6
|
||||
f0:c7:4f:f0:6f:f5:49:07:96:83:ae:44:60:5b:1f:df
|
||||
0f:70:07:c4:06:03:bf:47:f3:e8:a8:1e:54:ba:46:e9
|
||||
|
||||
|
||||
prime1:
|
||||
00:f8:6f:14:90:ad:01:c6:ea:8e:e0:ad:bd:27:fd:af
|
||||
e5:09:6b:24:02:c5:18:aa:69:d1:d4:0f:78:06:89:c1
|
||||
1e:5f:5a:3c:21:ca:1e:a3:73:0d:5e:d6:42:e9:6c:b0
|
||||
81:85:85:b7:d7:57:b2:27:42:67:5b:06:fa:e7:32:64
|
||||
bd:f5:fe:31:44:fb:a2:8e:b9:e8:3a:da:42:1a:13:20
|
||||
47:ac:8e:e1:f0:7e:81:1d:c9:de:ae:81:f5:fc:07:be
|
||||
4d:df:a1:a4:36:bb:c8:b4:f5:96:bb:bc:cf:b8:53:28
|
||||
15:28:32:f8:82:0d:9e:ed:0c:75:d5:83:a6:9f:cd:d1
|
||||
37:42:ad:6d:a4:75:0c:e6:9e:dd:16:6d:f9:57:56:9a
|
||||
0c:9c:f8:90:86:f8:a9:92:c8:61:81:09:82:a8:89:a9
|
||||
8f:c6:71:77:63:40:77:46:ed:c8:78:41:7b:ac:95:c2
|
||||
23:b2:64:44:26:00:d9:6b:b1:a8:99:28:78:7e:71:ed
|
||||
a3:
|
||||
|
||||
prime2:
|
||||
00:c4:3b:39:2f:33:08:d4:22:43:0f:4a:69:a9:89:51
|
||||
c9:1a:9f:a2:e2:82:bb:fd:77:76:c0:5a:36:38:d0:36
|
||||
ee:f2:aa:8b:4a:2a:56:df:eb:88:7f:50:bf:55:bc:9d
|
||||
cb:e2:83:58:9d:60:3e:b5:6e:6f:89:61:4c:09:a1:99
|
||||
59:78:77:38:fe:b7:87:45:6e:54:5d:52:ef:18:25:e2
|
||||
39:f4:04:9d:0e:bd:05:c3:a1:06:11:d8:93:8e:59:85
|
||||
0e:18:bf:80:fd:aa:a8:c6:20:3e:98:ea:82:6e:2f:37
|
||||
64:ec:19:e8:ab:f5:c8:b1:f6:ff:af:ef:f5:37:e0:d2
|
||||
4c:2f:eb:ca:22:fc:56:7e:9e:f5:41:df:f5:c2:06:17
|
||||
5a:e4:0a:c3:fb:fe:4f:00:0c:f5:36:8b:1b:33:e9:11
|
||||
d2:44:ac:3e:d0:d9:e7:c7:17:e5:3a:4e:ed:5c:73:7e
|
||||
73:83:4c:28:a0:1b:78:7c:82:f9:20:7b:8b:29:68:51
|
||||
53:
|
||||
|
||||
coefficient:
|
||||
00:aa:69:84:80:d9:0d:3a:12:90:d8:4e:cf:9c:24:5a
|
||||
5b:40:c0:41:b4:73:db:51:4a:7a:3b:91:9c:51:01:6c
|
||||
41:de:a3:33:e9:2e:ef:3b:2c:42:fd:75:98:f4:47:f0
|
||||
b3:12:f6:d6:ff:88:3e:d2:68:b7:ed:10:6e:38:c6:f4
|
||||
be:0a:82:17:b3:62:b5:25:d2:c2:54:b9:5b:e8:a1:6a
|
||||
04:2d:6a:bb:73:b7:9d:5f:d2:7f:5d:7c:c0:50:4a:8e
|
||||
af:e9:a8:68:b3:f7:a3:d9:71:0c:84:e4:c8:68:0b:10
|
||||
5a:71:f0:f5:92:ee:20:2f:0c:a5:24:6b:1a:7e:c0:3c
|
||||
28:4a:37:fd:ad:2f:fa:15:87:97:a1:d0:b1:62:26:f3
|
||||
96:a2:33:8d:41:d8:6a:0b:ce:c9:bf:c2:0d:8e:94:84
|
||||
66:27:a4:c1:00:32:64:ad:8a:3e:45:45:4b:c1:d1:c4
|
||||
7d:f2:d1:3a:f6:65:36:46:2e:52:35:b9:a2:ab:87:3c
|
||||
66:
|
||||
|
||||
exp1:
|
||||
00:8c:40:f1:e4:47:b7:f4:1b:e1:f3:d8:42:2c:fc:9c
|
||||
bb:fa:58:41:69:4a:ea:84:f2:de:e2:10:a5:9b:53:53
|
||||
f2:98:b4:71:b4:45:ce:8c:4a:5c:e7:08:a1:97:f4:a3
|
||||
a5:4b:c3:55:29:be:b9:b6:4e:57:d9:5d:14:73:47:d0
|
||||
f6:29:95:8b:2d:3d:be:e5:42:f1:67:a0:66:a7:1f:db
|
||||
1e:7b:bd:e2:b2:8a:48:cc:8f:76:27:20:f9:c5:82:7d
|
||||
9e:ab:3d:2f:5f:33:1e:b9:82:d8:c9:3e:6c:2a:cc:cf
|
||||
99:3a:2b:a4:7e:8f:c0:04:65:ff:74:3e:31:e8:90:22
|
||||
a8:46:fd:70:23:e3:6d:18:19:e2:09:52:a6:ec:f6:d8
|
||||
5b:7c:97:1b:c9:07:43:7c:b9:a1:ca:5e:9b:24:19:2b
|
||||
e0:1e:91:5a:6c:6f:2d:a7:9e:80:89:db:b6:3d:96:02
|
||||
97:72:94:06:a1:49:e3:75:58:44:2d:cb:5a:53:50:70
|
||||
6f:
|
||||
|
||||
exp2:
|
||||
25:93:05:87:21:29:8e:9d:24:e4:17:a6:95:dd:02:79
|
||||
14:8c:fe:be:8a:b3:fe:7b:d2:94:50:71:d3:7d:23:17
|
||||
ac:05:b5:f8:34:95:3f:f9:34:c0:d4:30:5e:f5:67:ed
|
||||
b3:68:dd:1d:fd:60:e4:92:c9:ee:af:5f:c4:f4:59:8d
|
||||
c5:40:66:fc:77:1e:02:d0:76:7d:0c:35:56:15:62:f5
|
||||
1f:e1:86:45:5d:32:6e:5c:35:f2:52:db:26:45:c3:f1
|
||||
88:11:9b:5c:77:42:2b:f5:de:a6:9f:38:ec:6a:44:1c
|
||||
22:0d:6f:fd:05:6a:31:91:8f:32:1c:2b:83:50:9c:54
|
||||
14:54:fc:f6:a8:04:d3:e8:12:24:54:03:15:ec:de:a9
|
||||
fb:c2:87:f6:87:a2:8e:ea:ec:45:4e:6b:9e:0c:01:ea
|
||||
96:55:b9:0d:7a:bc:23:e6:52:71:50:cd:a8:87:40:ee
|
||||
53:74:d0:ce:9f:93:f9:9b:86:a2:8e:a5:7d:ff:48:9b
|
||||
|
||||
|
||||
|
||||
Public Key PIN:
|
||||
pin-sha256:mTYJ3eMVIy4Zcqoo22QDSav9huivaJ8VbglPlInerS8=
|
||||
Public Key ID:
|
||||
sha256:993609dde315232e1972aa28db640349abfd86e8af689f156e094f9489dead2f
|
||||
sha1:4821fb3c35481747ab7226e82bf7735758be842f
|
||||
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIG5AIBAAKCAYEAvm6E1wtM6zhJ6efzO7DLxzXkcYMIGT+1KBQUJK54FG1tm4Cc
|
||||
+3GWQ6QkMHvL8Bun6uEd7tPuxrWJhL5DtpYrRAhqiIomPXHflKwJiaeICzv3u2zG
|
||||
fN9G4/ZYl7zK3+S6Jrp3oAvXY/li7TkkQlZqIMeWN9JmwYGykIPlzY/JhmoYo3F1
|
||||
QcuUUNndEhaTKbyH/BMoRKUh8pw7SX1i2G3PSoAVZls4lFFVdQgnp2Ni1zcLdwgh
|
||||
ro/Xzn3wKRGWaTtZJF9KtRjVRKvMIvl3AGd+os2rJa6odaSEkszsjO7ZlHbhXitC
|
||||
l/tXfaMBSRYLBFTKx6qSKWBghXcxYuguUE/mMtNEFmjAgnaHoSeoY19Soq5H1b+x
|
||||
PbWWQ/5dQk8188SMvmlobWOnIwLC9Yet9V3rfCMNRpnFrnYA9mWEMugo+swjiMG5
|
||||
XTIGc7ybY8pUcZt/K24JRJZFb/5ge2ggFUnkak9tntdGNqf+n9GT75dVeUrKj1vr
|
||||
o8ND+oETNOuwWJ7ZAgMBAAECggGAVNRSpKDKEPYwJtxGg86L2B/vtYkTMHwqrMHU
|
||||
/0og/6iHbP/r7i55K4SRAnADNuB6+qxxcxRBh48SxWkkLM/UUigVnOE9i52QZWAF
|
||||
l6Zjee2qu3kHKlUj9SSl7mIRVY9ERUBHTao4trY/FUGhH1PzTsrZ5d/6GjU2YB4B
|
||||
XoKw2QmmFBjUjQys8x851Xbs82iigu3dwEZ3SuXBm0kZoSO5dYx8++2i1J8unEWX
|
||||
tH8XZpuEpe+fYW1+SuPa+dB12kau9umuzWYREWJaPiJVn6wsvahaYh7+xngEHWnz
|
||||
ElPHWtvDGaTZ1lolVfe6MNH0u4q2isUdTGbD5sEFStibLvO5P4q2bgTLXzJloBwy
|
||||
WvWwNqPtlhvPsB0yPoukU7LAbTDTk0PW/in9nuJyMHoBK7KtObVsi0Fnd7sJjkFB
|
||||
4qladktRVkNy2wJe2e+TFsdkGDr3c9i40I+v480GJo+vsLZX9vDHT/Bv9UkHloOu
|
||||
RGBbH98PcAfEBgO/R/PoqB5UukbpAoHBAPhvFJCtAcbqjuCtvSf9r+UJayQCxRiq
|
||||
adHUD3gGicEeX1o8Icoeo3MNXtZC6WywgYWFt9dXsidCZ1sG+ucyZL31/jFE+6KO
|
||||
ueg62kIaEyBHrI7h8H6BHcneroH1/Ae+Td+hpDa7yLT1lru8z7hTKBUoMviCDZ7t
|
||||
DHXVg6afzdE3Qq1tpHUM5p7dFm35V1aaDJz4kIb4qZLIYYEJgqiJqY/GcXdjQHdG
|
||||
7ch4QXuslcIjsmREJgDZa7GomSh4fnHtowKBwQDEOzkvMwjUIkMPSmmpiVHJGp+i
|
||||
4oK7/Xd2wFo2ONA27vKqi0oqVt/riH9Qv1W8ncvig1idYD61bm+JYUwJoZlZeHc4
|
||||
/reHRW5UXVLvGCXiOfQEnQ69BcOhBhHYk45ZhQ4Yv4D9qqjGID6Y6oJuLzdk7Bno
|
||||
q/XIsfb/r+/1N+DSTC/ryiL8Vn6e9UHf9cIGF1rkCsP7/k8ADPU2ixsz6RHSRKw+
|
||||
0NnnxxflOk7tXHN+c4NMKKAbeHyC+SB7iyloUVMCgcEAjEDx5Ee39Bvh89hCLPyc
|
||||
u/pYQWlK6oTy3uIQpZtTU/KYtHG0Rc6MSlznCKGX9KOlS8NVKb65tk5X2V0Uc0fQ
|
||||
9imViy09vuVC8WegZqcf2x57veKyikjMj3YnIPnFgn2eqz0vXzMeuYLYyT5sKszP
|
||||
mTorpH6PwARl/3Q+MeiQIqhG/XAj420YGeIJUqbs9thbfJcbyQdDfLmhyl6bJBkr
|
||||
4B6RWmxvLaeegInbtj2WApdylAahSeN1WEQty1pTUHBvAoHAJZMFhyEpjp0k5Bem
|
||||
ld0CeRSM/r6Ks/570pRQcdN9IxesBbX4NJU/+TTA1DBe9Wfts2jdHf1g5JLJ7q9f
|
||||
xPRZjcVAZvx3HgLQdn0MNVYVYvUf4YZFXTJuXDXyUtsmRcPxiBGbXHdCK/Xepp84
|
||||
7GpEHCINb/0FajGRjzIcK4NQnFQUVPz2qATT6BIkVAMV7N6p+8KH9oeijursRU5r
|
||||
ngwB6pZVuQ16vCPmUnFQzaiHQO5TdNDOn5P5m4aijqV9/0ibAoHBAKpphIDZDToS
|
||||
kNhOz5wkWltAwEG0c9tRSno7kZxRAWxB3qMz6S7vOyxC/XWY9EfwsxL21v+IPtJo
|
||||
t+0QbjjG9L4KghezYrUl0sJUuVvooWoELWq7c7edX9J/XXzAUEqOr+moaLP3o9lx
|
||||
DITkyGgLEFpx8PWS7iAvDKUkaxp+wDwoSjf9rS/6FYeXodCxYibzlqIzjUHYagvO
|
||||
yb/CDY6UhGYnpMEAMmStij5FRUvB0cR98tE69mU2Ri5SNbmiq4c8Zg==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
|
|
@ -33,6 +33,7 @@ ignore = [
|
|||
"ANN",
|
||||
"C90",
|
||||
"COM812",
|
||||
"CPY001",
|
||||
"D10",
|
||||
"EM",
|
||||
"FIX002",
|
||||
|
|
|
|||
|
|
@ -92,12 +92,12 @@ void CCinemaManager::DrawSpline(Renderer::Backend::IDeviceCommandContext& device
|
|||
if (spline.GetAllNodes().size() == 2)
|
||||
smoothness = 2;
|
||||
|
||||
const float start = spline.MaxDistance.ToFloat() / smoothness;
|
||||
const float start = spline.GetMaxDistance().ToFloat() / smoothness;
|
||||
|
||||
std::vector<CVector3D> line;
|
||||
for (int i = 0; i <= smoothness; ++i)
|
||||
{
|
||||
const float time = start * i / spline.MaxDistance.ToFloat();
|
||||
const float time = start * i / spline.GetMaxDistance().ToFloat();
|
||||
line.emplace_back(spline.GetPosition(time));
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ void CCinemaManager::DrawSpline(Renderer::Backend::IDeviceCommandContext& device
|
|||
{
|
||||
for (int i = 0; i <= smoothness; ++i)
|
||||
{
|
||||
const float time = start * i / spline.MaxDistance.ToFloat();
|
||||
const float time = start * i / spline.GetMaxDistance().ToFloat();
|
||||
const CVector3D tmp = spline.GetPosition(time);
|
||||
const float groundY = g_Game->GetWorld()->GetTerrain().GetExactGroundLevel(tmp.X, tmp.Z);
|
||||
g_Renderer.GetDebugRenderer().DrawLine(deviceCommandContext, tmp, CVector3D(tmp.X, groundY, tmp.Z), splineColor, 0.1f, false);
|
||||
|
|
|
|||
|
|
@ -53,25 +53,6 @@ public:
|
|||
void SetElevation(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
|
||||
bool operator==(const CLightEnv& o) const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -62,8 +62,7 @@ void CMaterial::AddRenderQuery(const char* key)
|
|||
m_RenderQueries.Add(key);
|
||||
}
|
||||
|
||||
void CMaterial::AddRequiredSampler(const CStr& samplerName)
|
||||
void CMaterial::AddRequiredSampler(RequiredSampler sampler)
|
||||
{
|
||||
CStrIntern string(samplerName);
|
||||
m_RequiredSamplers.push_back(string);
|
||||
m_RequiredSamplers.emplace_back(std::move(sampler));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ public:
|
|||
CTexturePtr Sampler;
|
||||
};
|
||||
|
||||
struct RequiredSampler
|
||||
{
|
||||
CStrIntern name;
|
||||
bool sRGB;
|
||||
};
|
||||
|
||||
typedef std::vector<TextureSampler> SamplersVector;
|
||||
|
||||
CMaterial();
|
||||
|
|
@ -82,8 +88,8 @@ public:
|
|||
void AddRenderQuery(const char* key);
|
||||
const CShaderRenderQueries& GetRenderQueries() const { return m_RenderQueries; }
|
||||
|
||||
void AddRequiredSampler(const CStr& samplerName);
|
||||
const std::vector<CStrIntern>& GetRequiredSampler() const { return m_RequiredSamplers; }
|
||||
void AddRequiredSampler(RequiredSampler sampler);
|
||||
const std::vector<RequiredSampler>& GetRequiredSamplers() const { return m_RequiredSamplers; }
|
||||
|
||||
private:
|
||||
|
||||
|
|
@ -92,7 +98,7 @@ private:
|
|||
CTexturePtr m_DiffuseTexture;
|
||||
|
||||
SamplersVector m_Samplers;
|
||||
std::vector<CStrIntern> m_RequiredSamplers;
|
||||
std::vector<RequiredSampler> m_RequiredSamplers;
|
||||
|
||||
std::array<CStrIntern, static_cast<size_t>(Pass::COUNT)> m_ShaderEffects;
|
||||
CShaderDefines m_ShaderDefines;
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ CMaterial CMaterialManager::LoadMaterial(const VfsPath& pathname)
|
|||
AT(material);
|
||||
AT(name);
|
||||
AT(pass);
|
||||
AT(srgb);
|
||||
AT(value);
|
||||
#undef AT
|
||||
#undef EL
|
||||
|
|
@ -161,7 +162,8 @@ CMaterial CMaterialManager::LoadMaterial(const VfsPath& pathname)
|
|||
}
|
||||
else if (token == el_required_texture)
|
||||
{
|
||||
material.AddRequiredSampler(attrs.GetNamedItem(at_name));
|
||||
const bool sRGB{attrs.GetNamedItem(at_srgb) == "true"};
|
||||
material.AddRequiredSampler({CStrIntern{attrs.GetNamedItem(at_name)}, sRGB});
|
||||
if (!attrs.GetNamedItem(at_define).empty())
|
||||
material.AddShaderDefine(CStrIntern(attrs.GetNamedItem(at_define)), str_1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2025 Wildfire Games.
|
||||
/* Copyright (C) 2026 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -69,7 +69,7 @@ CObjectBase::CObjectBase(CObjectManager& objectManager, CActorDef& actorDef, u8
|
|||
m_Properties.m_FloatOnWater = false;
|
||||
|
||||
// Remove leading art/actors/ & include quality level.
|
||||
m_Identifier = m_ActorDef.m_Pathname.string8().substr(11) + CStr::FromInt(m_QualityLevel);
|
||||
m_Identifier = fmt::format("{}{}", m_ActorDef.m_Pathname.string8().substr(11), m_QualityLevel);
|
||||
}
|
||||
|
||||
std::unique_ptr<CObjectBase> CObjectBase::CopyWithQuality(u8 newQualityLevel) const
|
||||
|
|
|
|||
|
|
@ -51,6 +51,22 @@
|
|||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
bool ShouldInterpretTextureAsSRGB(const CObjectBase::Samp& sampler, const CMaterial& material)
|
||||
{
|
||||
const auto requiredSamplers{material.GetRequiredSamplers()};
|
||||
auto it{std::find_if(requiredSamplers.begin(), requiredSamplers.end(),
|
||||
[&](const CMaterial::RequiredSampler& requiredSampler)
|
||||
{
|
||||
return requiredSampler.name == sampler.m_SamplerName;
|
||||
})};
|
||||
return it != requiredSamplers.end() && it->sRGB;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
CObjectEntry::CObjectEntry(const std::shared_ptr<CObjectBase>& base, const CSimulation2& simulation) :
|
||||
m_Base(base), m_Color(1.0f, 1.0f, 1.0f, 1.0f), m_Simulation(simulation)
|
||||
{
|
||||
|
|
@ -155,6 +171,7 @@ bool CObjectEntry::BuildVariation(const std::vector<const std::set<CStr>*>& comp
|
|||
{
|
||||
CTextureProperties textureProps(samp.m_SamplerFile);
|
||||
textureProps.SetAddressMode(Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE);
|
||||
textureProps.SetSRGB(ShouldInterpretTextureAsSRGB(samp, material));
|
||||
CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps);
|
||||
// if we've loaded this model we're probably going to render it soon, so prefetch its texture.
|
||||
// All textures are prefetched even in the fixed pipeline, including the normal maps etc.
|
||||
|
|
@ -167,11 +184,14 @@ bool CObjectEntry::BuildVariation(const std::vector<const std::set<CStr>*>& comp
|
|||
CModel* model = newModel.get();
|
||||
m_Model = std::move(newModel);
|
||||
|
||||
for (const CStrIntern& requSampName : model->GetMaterial().GetRequiredSampler())
|
||||
for (const auto& requiredSampler : model->GetMaterial().GetRequiredSamplers())
|
||||
{
|
||||
if (std::find_if(m_Samplers.begin(), m_Samplers.end(),
|
||||
[&](const CObjectBase::Samp& sampler) { return sampler.m_SamplerName == requSampName; }) == m_Samplers.end())
|
||||
LOGERROR("Actor %s: required texture sampler %s not found (material %s)", m_Base->GetIdentifier(), requSampName.string().c_str(), m_Base->m_Material.string8().c_str());
|
||||
[&](const CObjectBase::Samp& sampler) { return sampler.m_SamplerName == requiredSampler.name; }) == m_Samplers.end())
|
||||
{
|
||||
LOGERROR("Actor %s: required texture sampler %s not found (material %s)",
|
||||
m_Base->GetIdentifier(), requiredSampler.name.string().c_str(), m_Base->m_Material.string8().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// calculate initial object space bounds, based on vertex positions
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2025 Wildfire Games.
|
||||
/* Copyright (C) 2026 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -156,7 +156,8 @@ bool ResolveIncludesImpl(
|
|||
it = includeCache.emplace(path, std::move(includeContent)).first;
|
||||
}
|
||||
// We need to insert #line directives to have correct line numbers in errors.
|
||||
chunks.emplace_back(lineDirective + "1\n" + it->second + "\n" + lineDirective + CStr::FromUInt(line + 1) + "\n");
|
||||
chunks.emplace_back(fmt::format("{}1\n{}\n{}{}\n", lineDirective, it->second, lineDirective,
|
||||
line + 1));
|
||||
processedParts.emplace_back(currentPart.substr(0, lineStart));
|
||||
if (!ResolveIncludesImpl(chunks.back(), includeCache, includeCallback, chunks, processedParts))
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2025 Wildfire Games.
|
||||
/* Copyright (C) 2026 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -60,7 +60,7 @@ namespace
|
|||
{
|
||||
|
||||
Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded(
|
||||
Renderer::Backend::IDevice* device, Tex& textureData, const bool hasS3TC)
|
||||
Renderer::Backend::IDevice* device, Tex& textureData, const bool hasS3TC, const bool sRGB)
|
||||
{
|
||||
const bool alpha = (textureData.m_Flags & TEX_ALPHA) != 0;
|
||||
const bool grey = (textureData.m_Flags & TEX_GREY) != 0;
|
||||
|
|
@ -81,13 +81,13 @@ Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded(
|
|||
switch (dxt)
|
||||
{
|
||||
case DXT1A:
|
||||
return Renderer::Backend::Format::BC1_RGBA_UNORM;
|
||||
return sRGB ? Renderer::Backend::Format::BC1_RGBA_SRGB : Renderer::Backend::Format::BC1_RGBA_UNORM;
|
||||
case 1:
|
||||
return Renderer::Backend::Format::BC1_RGB_UNORM;
|
||||
return sRGB ? Renderer::Backend::Format::BC1_RGB_SRGB : Renderer::Backend::Format::BC1_RGB_UNORM;
|
||||
case 3:
|
||||
return Renderer::Backend::Format::BC2_UNORM;
|
||||
return sRGB ? Renderer::Backend::Format::BC2_SRGB : Renderer::Backend::Format::BC2_UNORM;
|
||||
case 5:
|
||||
return Renderer::Backend::Format::BC3_UNORM;
|
||||
return sRGB ? Renderer::Backend::Format::BC3_SRGB : Renderer::Backend::Format::BC3_UNORM;
|
||||
default:
|
||||
LOGERROR("Unknown DXT compression.");
|
||||
return Renderer::Backend::Format::UNDEFINED;
|
||||
|
|
@ -101,9 +101,12 @@ Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded(
|
|||
{
|
||||
case 8:
|
||||
ENSURE(grey);
|
||||
ENSURE(!sRGB);
|
||||
return Renderer::Backend::Format::L8_UNORM;
|
||||
case 24:
|
||||
ENSURE(grey);
|
||||
ENSURE(!alpha);
|
||||
ENSURE(!sRGB);
|
||||
if (device->IsTextureFormatSupported(Renderer::Backend::Format::R8G8B8_UNORM))
|
||||
return Renderer::Backend::Format::R8G8B8_UNORM;
|
||||
else
|
||||
|
|
@ -114,7 +117,7 @@ Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded(
|
|||
}
|
||||
case 32:
|
||||
ENSURE(alpha);
|
||||
return Renderer::Backend::Format::R8G8B8A8_UNORM;
|
||||
return sRGB ? Renderer::Backend::Format::R8G8B8A8_SRGB : Renderer::Backend::Format::R8G8B8A8_UNORM;
|
||||
default:
|
||||
LOGERROR("Unsupported BPP: %zu", textureData.m_Bpp);
|
||||
}
|
||||
|
|
@ -336,6 +339,7 @@ struct TPhash
|
|||
hash_combine(seed, textureProperties.m_AnisotropicFilterEnabled);
|
||||
hash_combine(seed, textureProperties.m_FormatOverride);
|
||||
hash_combine(seed, textureProperties.m_IgnoreQuality);
|
||||
hash_combine(seed, textureProperties.m_SRGB);
|
||||
return seed;
|
||||
}
|
||||
|
||||
|
|
@ -358,7 +362,8 @@ struct TPequal_to
|
|||
lhs.m_AddressModeV == rhs.m_AddressModeV &&
|
||||
lhs.m_AnisotropicFilterEnabled == rhs.m_AnisotropicFilterEnabled &&
|
||||
lhs.m_FormatOverride == rhs.m_FormatOverride &&
|
||||
lhs.m_IgnoreQuality == rhs.m_IgnoreQuality;
|
||||
lhs.m_IgnoreQuality == rhs.m_IgnoreQuality &&
|
||||
lhs.m_SRGB == rhs.m_SRGB;
|
||||
}
|
||||
|
||||
bool operator()(const CTexturePtr& lhs, const CTexturePtr& rhs) const
|
||||
|
|
@ -526,7 +531,8 @@ public:
|
|||
}
|
||||
else
|
||||
{
|
||||
format = ChooseFormatAndTransformTextureDataIfNeeded(m_Device, textureData, m_HasS3TC);
|
||||
format = ChooseFormatAndTransformTextureDataIfNeeded(
|
||||
m_Device, textureData, m_HasS3TC, texture->m_Properties.m_SRGB);
|
||||
}
|
||||
|
||||
if (format == Renderer::Backend::Format::UNDEFINED)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2025 Wildfire Games.
|
||||
/* Copyright (C) 2026 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -245,6 +245,8 @@ public:
|
|||
|
||||
void SetIgnoreQuality(bool ignore) { m_IgnoreQuality = ignore; }
|
||||
|
||||
void SetSRGB(const bool sRGB) { m_SRGB = sRGB; }
|
||||
|
||||
private:
|
||||
// Must update TPhash, TPequal_to when changing these fields
|
||||
VfsPath m_Path;
|
||||
|
|
@ -257,6 +259,7 @@ private:
|
|||
Renderer::Backend::Format m_FormatOverride =
|
||||
Renderer::Backend::Format::UNDEFINED;
|
||||
bool m_IgnoreQuality = false;
|
||||
bool m_SRGB{false};
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -755,7 +755,7 @@ void CGUI::Xeromyces_ReadObject(const XMBData& xmb, XMBElement element, IGUIObje
|
|||
// Check if name isn't set, generate an internal name in that case.
|
||||
if (!NameSet)
|
||||
{
|
||||
object->SetName("__internal(" + CStr::FromInt(m_InternalNameNumber) + ")");
|
||||
object->SetName(fmt::format("__internal({})", m_InternalNameNumber));
|
||||
++m_InternalNameNumber;
|
||||
}
|
||||
|
||||
|
|
@ -977,7 +977,7 @@ void CGUI::Xeromyces_ReadRepeat(const XMBData& xmb, XMBElement element, IGUIObje
|
|||
|
||||
for (int n = 0; n < count; ++n)
|
||||
{
|
||||
NameSubst.emplace_back(var, "[" + CStr::FromInt(n) + "]");
|
||||
NameSubst.emplace_back(var, fmt::format("[{}]", n));
|
||||
|
||||
XERO_ITER_EL(element, child)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2025 Wildfire Games.
|
||||
/* Copyright (C) 2026 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -52,114 +52,111 @@ CVector3D GetPositionOnCubic(const CVector3D& startPos, const CVector3D& startVe
|
|||
|
||||
/*********************************** R N S **************************************************/
|
||||
|
||||
RNSpline::RNSpline()
|
||||
: NodeCount(0)
|
||||
{
|
||||
}
|
||||
RNSpline::RNSpline() = default;
|
||||
|
||||
RNSpline::~RNSpline() = default;
|
||||
|
||||
// adds node and updates segment length
|
||||
void RNSpline::AddNode(const CFixedVector3D& pos)
|
||||
{
|
||||
if (NodeCount >= MAX_SPLINE_NODES)
|
||||
if (m_NodeCount >= MAX_SPLINE_NODES)
|
||||
return;
|
||||
if (NodeCount == 0)
|
||||
MaxDistance = fixed::Zero();
|
||||
if (m_NodeCount == 0)
|
||||
m_MaxDistance = fixed::Zero();
|
||||
else
|
||||
{
|
||||
Node[NodeCount-1].Distance = (Node[NodeCount-1].Position - pos).Length();
|
||||
MaxDistance += Node[NodeCount-1].Distance;
|
||||
m_Nodes[m_NodeCount-1].Distance = (m_Nodes[m_NodeCount-1].Position - pos).Length();
|
||||
m_MaxDistance += m_Nodes[m_NodeCount-1].Distance;
|
||||
}
|
||||
SplineData temp;
|
||||
temp.Position = pos;
|
||||
Node.push_back(temp);
|
||||
++NodeCount;
|
||||
m_Nodes.push_back(temp);
|
||||
++m_NodeCount;
|
||||
}
|
||||
|
||||
|
||||
// called after all nodes added. This function calculates the node velocities
|
||||
void RNSpline::BuildSpline()
|
||||
{
|
||||
if (NodeCount == 2)
|
||||
if (m_NodeCount == 2)
|
||||
{
|
||||
Node[0].Velocity = GetStartVelocity(0);
|
||||
Node[NodeCount-1].Velocity = GetEndVelocity(NodeCount-1);
|
||||
m_Nodes[0].Velocity = GetStartVelocity(0);
|
||||
m_Nodes[m_NodeCount-1].Velocity = GetEndVelocity(m_NodeCount-1);
|
||||
return;
|
||||
}
|
||||
else if (NodeCount < 2)
|
||||
else if (m_NodeCount < 2)
|
||||
return;
|
||||
|
||||
for (int i = 1; i < NodeCount-1; ++i)
|
||||
for (int i = 1; i < m_NodeCount-1; ++i)
|
||||
{
|
||||
CVector3D Next = Node[i+1].Position - Node[i].Position;
|
||||
CVector3D Previous = Node[i-1].Position - Node[i].Position;
|
||||
CVector3D Next = m_Nodes[i+1].Position - m_Nodes[i].Position;
|
||||
CVector3D Previous = m_Nodes[i-1].Position - m_Nodes[i].Position;
|
||||
Next.Normalize();
|
||||
Previous.Normalize();
|
||||
|
||||
// split the angle (figure 4)
|
||||
Node[i].Velocity = Next - Previous;
|
||||
Node[i].Velocity.Normalize();
|
||||
m_Nodes[i].Velocity = Next - Previous;
|
||||
m_Nodes[i].Velocity.Normalize();
|
||||
}
|
||||
// calculate start and end velocities
|
||||
Node[0].Velocity = GetStartVelocity(0);
|
||||
Node[NodeCount-1].Velocity = GetEndVelocity(NodeCount-1);
|
||||
m_Nodes[0].Velocity = GetStartVelocity(0);
|
||||
m_Nodes[m_NodeCount-1].Velocity = GetEndVelocity(m_NodeCount-1);
|
||||
}
|
||||
|
||||
// spline access function. time is 0 -> 1
|
||||
CVector3D RNSpline::GetPosition(float time) const
|
||||
{
|
||||
if (NodeCount < 2)
|
||||
if (m_NodeCount < 2)
|
||||
return CVector3D(0.0f, 0.0f, 0.0f);
|
||||
if (time < 0.0f)
|
||||
time = 0.0f;
|
||||
if (time > 1.0f)
|
||||
time = 1.0f;
|
||||
float Distance = time * MaxDistance.ToFloat();
|
||||
float Distance = time * m_MaxDistance.ToFloat();
|
||||
float CurrentDistance = 0.f;
|
||||
int i = 0;
|
||||
|
||||
// Find which node we're on
|
||||
while (CurrentDistance + Node[i].Distance.ToFloat() < Distance && i < NodeCount - 2)
|
||||
while (CurrentDistance + m_Nodes[i].Distance.ToFloat() < Distance && i < m_NodeCount - 2)
|
||||
{
|
||||
CurrentDistance += Node[i].Distance.ToFloat();
|
||||
CurrentDistance += m_Nodes[i].Distance.ToFloat();
|
||||
++i;
|
||||
}
|
||||
ENSURE(i < NodeCount - 1);
|
||||
ENSURE(i < m_NodeCount - 1);
|
||||
float t = Distance - CurrentDistance;
|
||||
// TODO: reimplement CVector3D comparator (float comparing is bad without EPS)
|
||||
if (Node[i].Position == Node[i+1].Position || Node[i].Distance.ToFloat() < 1e-7) // distance too small or zero
|
||||
if (m_Nodes[i].Position == m_Nodes[i+1].Position || m_Nodes[i].Distance.ToFloat() < 1e-7) // distance too small or zero
|
||||
{
|
||||
return Node[i+1].Position;
|
||||
return m_Nodes[i+1].Position;
|
||||
}
|
||||
t /= Node[i].Distance.ToFloat(); // scale t in range 0 - 1
|
||||
CVector3D startVel = Node[i].Velocity * Node[i].Distance.ToFloat();
|
||||
CVector3D endVel = Node[i+1].Velocity * Node[i].Distance.ToFloat();
|
||||
return GetPositionOnCubic(Node[i].Position, startVel,
|
||||
Node[i+1].Position, endVel, t);
|
||||
t /= m_Nodes[i].Distance.ToFloat(); // scale t in range 0 - 1
|
||||
CVector3D startVel = m_Nodes[i].Velocity * m_Nodes[i].Distance.ToFloat();
|
||||
CVector3D endVel = m_Nodes[i+1].Velocity * m_Nodes[i].Distance.ToFloat();
|
||||
return GetPositionOnCubic(m_Nodes[i].Position, startVel,
|
||||
m_Nodes[i+1].Position, endVel, t);
|
||||
}
|
||||
|
||||
const std::vector<SplineData>& RNSpline::GetAllNodes() const
|
||||
{
|
||||
return Node;
|
||||
return m_Nodes;
|
||||
}
|
||||
|
||||
// internal. Based on Equation 14
|
||||
CVector3D RNSpline::GetStartVelocity(int index)
|
||||
{
|
||||
if (index >= NodeCount - 1 || index < 0)
|
||||
if (index >= m_NodeCount - 1 || index < 0)
|
||||
return CVector3D(0.0f, 0.0f, 0.0f);
|
||||
CVector3D temp = CVector3D(Node[index+1].Position - Node[index].Position) * 3.0f * (1.0f / Node[index].Distance.ToFloat());
|
||||
return (temp - Node[index+1].Velocity)*0.5f;
|
||||
CVector3D temp = CVector3D(m_Nodes[index+1].Position - m_Nodes[index].Position) * 3.0f * (1.0f / m_Nodes[index].Distance.ToFloat());
|
||||
return (temp - m_Nodes[index+1].Velocity)*0.5f;
|
||||
}
|
||||
|
||||
// internal. Based on Equation 15
|
||||
CVector3D RNSpline::GetEndVelocity(int index)
|
||||
{
|
||||
if (index >= NodeCount || index < 1)
|
||||
if (index >= m_NodeCount || index < 1)
|
||||
return CVector3D(0.0f, 0.0f, 0.0f);
|
||||
CVector3D temp = CVector3D(Node[index].Position - Node[index-1].Position) * 3.0f * (1.0f / Node[index-1].Distance.ToFloat());
|
||||
return (temp - Node[index-1].Velocity) * 0.5f;
|
||||
CVector3D temp = CVector3D(m_Nodes[index].Position - m_Nodes[index-1].Position) * 3.0f * (1.0f / m_Nodes[index-1].Distance.ToFloat());
|
||||
return (temp - m_Nodes[index-1].Velocity) * 0.5f;
|
||||
}
|
||||
|
||||
/*********************************** S N S **************************************************/
|
||||
|
|
@ -176,21 +173,21 @@ void SNSpline::BuildSpline()
|
|||
// smoothing filter.
|
||||
void SNSpline::Smooth()
|
||||
{
|
||||
if (NodeCount < 3)
|
||||
if (m_NodeCount < 3)
|
||||
return;
|
||||
|
||||
CVector3D newVel;
|
||||
CVector3D oldVel = GetStartVelocity(0);
|
||||
for (int i = 1; i < NodeCount-1; ++i)
|
||||
for (int i = 1; i < m_NodeCount-1; ++i)
|
||||
{
|
||||
// Equation 12
|
||||
newVel = GetEndVelocity(i) * Node[i].Distance.ToFloat() + GetStartVelocity(i) * Node[i-1].Distance.ToFloat();
|
||||
newVel = newVel * (1 / (Node[i-1].Distance + Node[i].Distance).ToFloat());
|
||||
Node[i-1].Velocity = oldVel;
|
||||
newVel = GetEndVelocity(i) * m_Nodes[i].Distance.ToFloat() + GetStartVelocity(i) * m_Nodes[i-1].Distance.ToFloat();
|
||||
newVel = newVel * (1 / (m_Nodes[i-1].Distance + m_Nodes[i].Distance).ToFloat());
|
||||
m_Nodes[i-1].Velocity = oldVel;
|
||||
oldVel = newVel;
|
||||
}
|
||||
Node[NodeCount-1].Velocity = GetEndVelocity(NodeCount-1);
|
||||
Node[NodeCount-2].Velocity = oldVel;
|
||||
m_Nodes[m_NodeCount-1].Velocity = GetEndVelocity(m_NodeCount-1);
|
||||
m_Nodes[m_NodeCount-2].Velocity = oldVel;
|
||||
}
|
||||
|
||||
/*********************************** T N S **************************************************/
|
||||
|
|
@ -201,15 +198,15 @@ TNSpline::~TNSpline() = default;
|
|||
// ie time period is time from last node to this node
|
||||
void TNSpline::AddNode(const CFixedVector3D& pos, const CFixedVector3D& rotation, fixed timePeriod)
|
||||
{
|
||||
if (NodeCount >= MAX_SPLINE_NODES)
|
||||
if (m_NodeCount >= MAX_SPLINE_NODES)
|
||||
return;
|
||||
|
||||
if (NodeCount == 0)
|
||||
MaxDistance = fixed::Zero();
|
||||
if (m_NodeCount == 0)
|
||||
m_MaxDistance = fixed::Zero();
|
||||
else
|
||||
{
|
||||
Node[NodeCount-1].Distance = timePeriod;
|
||||
MaxDistance += Node[NodeCount-1].Distance;
|
||||
m_Nodes[m_NodeCount-1].Distance = timePeriod;
|
||||
m_MaxDistance += m_Nodes[m_NodeCount-1].Distance;
|
||||
}
|
||||
|
||||
SplineData temp;
|
||||
|
|
@ -219,56 +216,56 @@ void TNSpline::AddNode(const CFixedVector3D& pos, const CFixedVector3D& rotation
|
|||
temp.Distance = fixed::Zero();
|
||||
temp.Velocity = CVector3D(0.0f, 0.0f, 0.0f);
|
||||
temp.Rotation = rotation;
|
||||
Node.push_back(temp);
|
||||
++NodeCount;
|
||||
m_Nodes.push_back(temp);
|
||||
++m_NodeCount;
|
||||
}
|
||||
|
||||
//Inserts node before position
|
||||
void TNSpline::InsertNode(const int index, const CFixedVector3D& pos, const CFixedVector3D& /*rotation*/,
|
||||
fixed timePeriod)
|
||||
{
|
||||
if (NodeCount >= MAX_SPLINE_NODES || index < 0 || index > NodeCount)
|
||||
if (m_NodeCount >= MAX_SPLINE_NODES || index < 0 || index > m_NodeCount)
|
||||
return;
|
||||
|
||||
if (NodeCount == 0)
|
||||
MaxDistance = fixed::Zero();
|
||||
if (m_NodeCount == 0)
|
||||
m_MaxDistance = fixed::Zero();
|
||||
else
|
||||
MaxDistance += timePeriod;
|
||||
m_MaxDistance += timePeriod;
|
||||
|
||||
SplineData temp;
|
||||
temp.Position = pos;
|
||||
temp.Distance = timePeriod;
|
||||
Node.insert(Node.begin() + index, temp);
|
||||
m_Nodes.insert(m_Nodes.begin() + index, temp);
|
||||
if (index > 0)
|
||||
std::swap(Node[index].Distance, Node[index - 1].Distance);
|
||||
++NodeCount;
|
||||
std::swap(m_Nodes[index].Distance, m_Nodes[index - 1].Distance);
|
||||
++m_NodeCount;
|
||||
}
|
||||
|
||||
//Removes node at index
|
||||
void TNSpline::RemoveNode(const int index)
|
||||
{
|
||||
if (NodeCount == 0 || index > NodeCount - 1)
|
||||
if (m_NodeCount == 0 || index > m_NodeCount - 1)
|
||||
return;
|
||||
|
||||
MaxDistance -= Node[index].Distance;
|
||||
Node.erase(Node.begin() + index);
|
||||
--NodeCount;
|
||||
m_MaxDistance -= m_Nodes[index].Distance;
|
||||
m_Nodes.erase(m_Nodes.begin() + index);
|
||||
--m_NodeCount;
|
||||
}
|
||||
|
||||
void TNSpline::UpdateNodeTime(const int index, fixed time)
|
||||
{
|
||||
if (NodeCount == 0 || index > NodeCount - 1)
|
||||
if (m_NodeCount == 0 || index > m_NodeCount - 1)
|
||||
return;
|
||||
|
||||
Node[index].Distance = time;
|
||||
m_Nodes[index].Distance = time;
|
||||
}
|
||||
|
||||
void TNSpline::UpdateNodePos(const int index, const CFixedVector3D& pos)
|
||||
{
|
||||
if (NodeCount == 0 || index > NodeCount - 1)
|
||||
if (m_NodeCount == 0 || index > m_NodeCount - 1)
|
||||
return;
|
||||
|
||||
Node[index].Position = pos;
|
||||
m_Nodes[index].Position = pos;
|
||||
}
|
||||
|
||||
void TNSpline::BuildSpline()
|
||||
|
|
@ -289,15 +286,15 @@ void TNSpline::Smooth()
|
|||
|
||||
void TNSpline::Constrain()
|
||||
{
|
||||
if (NodeCount < 3)
|
||||
if (m_NodeCount < 3)
|
||||
return;
|
||||
|
||||
for (int i = 1; i < NodeCount-1; ++i)
|
||||
for (int i = 1; i < m_NodeCount-1; ++i)
|
||||
{
|
||||
// Equation 13
|
||||
float r0 = (Node[i].Position - Node[i - 1].Position).Length().ToFloat() / Node[i-1].Distance.ToFloat();
|
||||
float r1 = (Node[i+1].Position - Node[i].Position).Length().ToFloat() / Node[i].Distance.ToFloat();
|
||||
Node[i].Velocity *= 4.0f*r0*r1 / ((r0 + r1)*(r0 + r1));
|
||||
float r0 = (m_Nodes[i].Position - m_Nodes[i - 1].Position).Length().ToFloat() / m_Nodes[i-1].Distance.ToFloat();
|
||||
float r1 = (m_Nodes[i+1].Position - m_Nodes[i].Position).Length().ToFloat() / m_Nodes[i].Distance.ToFloat();
|
||||
m_Nodes[i].Velocity *= 4.0f*r0*r1 / ((r0 + r1)*(r0 + r1));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2025 Wildfire Games.
|
||||
/* Copyright (C) 2026 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -63,12 +63,12 @@ public:
|
|||
CVector3D GetRotation(float time) const;
|
||||
const std::vector<SplineData>& GetAllNodes() const;
|
||||
|
||||
fixed MaxDistance;
|
||||
int NodeCount;
|
||||
fixed GetMaxDistance() const { return m_MaxDistance; }
|
||||
|
||||
protected:
|
||||
|
||||
std::vector<SplineData> Node;
|
||||
fixed m_MaxDistance;
|
||||
int m_NodeCount{0};
|
||||
std::vector<SplineData> m_Nodes;
|
||||
CVector3D GetStartVelocity(int index);
|
||||
CVector3D GetEndVelocity(int index);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ void CNetClientTurnManager::PostCommand(JS::HandleValue data)
|
|||
// TODO: we should do this when the server stops sending our commands back to us
|
||||
}
|
||||
|
||||
void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn)
|
||||
void CNetClientTurnManager::NotifyFinishedOwnCommands(turn_id_t turn)
|
||||
{
|
||||
NETCLIENTTURN_LOG("NotifyFinishedOwnCommands(%d)\n", turn);
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn)
|
|||
m_NetClient.SendMessage(&msg);
|
||||
}
|
||||
|
||||
void CNetClientTurnManager::NotifyFinishedUpdate(u32 turn, const UpdateCallback&)
|
||||
void CNetClientTurnManager::NotifyFinishedUpdate(turn_id_t turn, const UpdateCallback&)
|
||||
{
|
||||
bool quick = !TurnNeedsFullHash(turn);
|
||||
std::string hash;
|
||||
|
|
@ -110,10 +110,10 @@ void CNetClientTurnManager::OnDestroyConnection()
|
|||
void CNetClientTurnManager::OnSimulationMessage(CSimulationMessage* msg)
|
||||
{
|
||||
// Command received from the server - store it for later execution
|
||||
AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, msg->m_Turn);
|
||||
AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, static_cast<turn_id_t>(msg->m_Turn));
|
||||
}
|
||||
|
||||
void CNetClientTurnManager::OnSyncError(u32 turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames)
|
||||
void CNetClientTurnManager::OnSyncError(turn_id_t turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames)
|
||||
{
|
||||
CStr expectedHashHex(Hexify(expectedHash));
|
||||
NETCLIENTTURN_LOG("OnSyncError(%d, %hs)\n", turn, expectedHashHex.c_str());
|
||||
|
|
|
|||
|
|
@ -49,12 +49,12 @@ public:
|
|||
*/
|
||||
void OnDestroyConnection();
|
||||
|
||||
void OnSyncError(u32 turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames);
|
||||
void OnSyncError(turn_id_t turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames);
|
||||
|
||||
private:
|
||||
void NotifyFinishedOwnCommands(u32 turn) override;
|
||||
void NotifyFinishedOwnCommands(turn_id_t turn) override;
|
||||
|
||||
void NotifyFinishedUpdate(u32 turn, const UpdateCallback& sendEventToAll) override;
|
||||
void NotifyFinishedUpdate(turn_id_t turn, const UpdateCallback& sendEventToAll) override;
|
||||
|
||||
CNetClient& m_NetClient;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ CStr CNetMessage::ToString() const
|
|||
if (GetType() == NMT_INVALID)
|
||||
return "MESSAGE_TYPE_NONE { Undefined Message }";
|
||||
else
|
||||
return "Unknown Message " + CStr::FromInt(GetType());
|
||||
return fmt::format("Unknown Message {}", static_cast<int>(GetType()));
|
||||
}
|
||||
|
||||
CNetMessage* CNetMessageFactory::CreateMessage(const std::span<const std::uint8_t> data,
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ public:
|
|||
|
||||
u32 m_Client;
|
||||
i32 m_Player;
|
||||
u32 m_Turn;
|
||||
i32 m_Turn;
|
||||
JS::PersistentRooted<JS::Value> m_Data;
|
||||
private:
|
||||
const Script::Interface& m_ScriptInterface;
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ u8* CSimulationMessage::Serialize(u8* pBuffer) const
|
|||
CBufferBinarySerializer serializer(m_ScriptInterface, pos);
|
||||
serializer.NumberU32_Unbounded("client", m_Client);
|
||||
serializer.NumberI32_Unbounded("player", m_Player);
|
||||
serializer.NumberU32_Unbounded("turn", m_Turn);
|
||||
serializer.NumberI32_Unbounded("turn", m_Turn);
|
||||
|
||||
serializer.ScriptVal("command", const_cast<JS::PersistentRootedValue*>(&m_Data));
|
||||
return serializer.GetBuffer();
|
||||
|
|
@ -166,7 +166,7 @@ const u8* CSimulationMessage::Deserialize(const u8* pStart, const u8* pEnd)
|
|||
CStdDeserializer deserializer(m_ScriptInterface, stream);
|
||||
deserializer.NumberU32_Unbounded("client", m_Client);
|
||||
deserializer.NumberI32_Unbounded("player", m_Player);
|
||||
deserializer.NumberU32_Unbounded("turn", m_Turn);
|
||||
deserializer.NumberI32_Unbounded("turn", m_Turn);
|
||||
deserializer.ScriptVal("command", &m_Data);
|
||||
return pEnd;
|
||||
}
|
||||
|
|
@ -178,7 +178,7 @@ size_t CSimulationMessage::GetSerializedLength() const
|
|||
CLengthBinarySerializer serializer(m_ScriptInterface);
|
||||
serializer.NumberU32_Unbounded("client", m_Client);
|
||||
serializer.NumberI32_Unbounded("player", m_Player);
|
||||
serializer.NumberU32_Unbounded("turn", m_Turn);
|
||||
serializer.NumberI32_Unbounded("turn", m_Turn);
|
||||
|
||||
// TODO: The cast can probably be removed if and when ScriptVal can take a JS::HandleValue instead of
|
||||
// a JS::MutableHandleValue (relies on JSAPI change). Also search for other casts like this one in that case.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2025 Wildfire Games.
|
||||
/* Copyright (C) 2026 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
#define PS_PROTOCOL_MAGIC 0x5073013f // 'P', 's', 0x01, '?'
|
||||
#define PS_PROTOCOL_MAGIC_RESPONSE 0x50630121 // 'P', 'c', 0x01, '!'
|
||||
#define PS_PROTOCOL_VERSION 0x01010019 // Arbitrary protocol
|
||||
#define PS_PROTOCOL_VERSION 0x0101001A // Arbitrary protocol
|
||||
#define PS_DEFAULT_PORT 0x5073 // 'P', 's'
|
||||
|
||||
// Set when lobby authentication is required. Used in the SrvHandshakeResponseMessage.
|
||||
|
|
@ -231,7 +231,7 @@ START_NMT_CLASS_(ClientPaused, NMT_CLIENT_PAUSED)
|
|||
END_NMT_CLASS()
|
||||
|
||||
START_NMT_CLASS_(LoadedGame, NMT_LOADED_GAME)
|
||||
NMT_FIELD_INT(m_CurrentTurn, u32, 4)
|
||||
NMT_FIELD_INT(m_CurrentTurn, i32, 4)
|
||||
END_NMT_CLASS()
|
||||
|
||||
START_NMT_CLASS_(GameStart, NMT_GAME_START)
|
||||
|
|
@ -243,17 +243,17 @@ START_NMT_CLASS_(GameSavedStart, NMT_SAVED_GAME_START)
|
|||
END_NMT_CLASS()
|
||||
|
||||
START_NMT_CLASS_(EndCommandBatch, NMT_END_COMMAND_BATCH)
|
||||
NMT_FIELD_INT(m_Turn, u32, 4)
|
||||
NMT_FIELD_INT(m_Turn, i32, 4)
|
||||
NMT_FIELD_INT(m_TurnLength, u32, 2)
|
||||
END_NMT_CLASS()
|
||||
|
||||
START_NMT_CLASS_(SyncCheck, NMT_SYNC_CHECK)
|
||||
NMT_FIELD_INT(m_Turn, u32, 4)
|
||||
NMT_FIELD_INT(m_Turn, i32, 4)
|
||||
NMT_FIELD(CStr, m_Hash)
|
||||
END_NMT_CLASS()
|
||||
|
||||
START_NMT_CLASS_(SyncError, NMT_SYNC_ERROR)
|
||||
NMT_FIELD_INT(m_Turn, u32, 4)
|
||||
NMT_FIELD_INT(m_Turn, i32, 4)
|
||||
NMT_FIELD(CStr, m_HashExpected)
|
||||
NMT_START_ARRAY(m_PlayerNames)
|
||||
NMT_FIELD(CStrW, m_Name)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@
|
|||
#include "ps/CLogger.h"
|
||||
#include "ps/ConfigDB.h"
|
||||
#include "ps/GUID.h"
|
||||
#include "ps/GameSetup/CmdLineArgs.h"
|
||||
#include "ps/GameSetup/Paths.h"
|
||||
#include "ps/Hashing.h"
|
||||
#include "ps/ProfileViewer.h"
|
||||
#include "ps/Profiler2.h"
|
||||
|
|
@ -50,13 +52,16 @@
|
|||
#include "scriptinterface/Context.h"
|
||||
#include "scriptinterface/Interface.h"
|
||||
#include "scriptinterface/Request.h"
|
||||
#include "simulation2/helpers/SimulationCommand.h"
|
||||
#include "simulation2/system/TurnManager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <fmt/format.h>
|
||||
#include <functional>
|
||||
#include <gnutls/crypto.h>
|
||||
#include <gnutls/gnutls.h>
|
||||
#include <gnutls/x509.h>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <netdb.h>
|
||||
|
|
@ -65,6 +70,7 @@
|
|||
#include <ngtcp2/ngtcp2_crypto_gnutls.h>
|
||||
#include <numeric>
|
||||
#include <poll.h>
|
||||
#include <random>
|
||||
#include <ranges>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
|
@ -79,6 +85,10 @@
|
|||
#include <miniupnpc/upnperrors.h>
|
||||
#endif
|
||||
|
||||
#if FMT_VERSION >= 80000
|
||||
#include <fmt/xchar.h>
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Number of peers to allocate for the enet host.
|
||||
* Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
|
||||
|
|
@ -214,6 +224,99 @@ int ResolveAndBind(const char *port, AddressStorage& localAddress)
|
|||
return fd;
|
||||
}
|
||||
|
||||
void GenerateKeyfiles(const std::filesystem::path& privatekey, const std::filesystem::path& certificate)
|
||||
{
|
||||
constexpr auto binaryOverrideMode = std::ios::binary | std::ios::trunc;
|
||||
gnutls_x509_privkey_t newKey;
|
||||
if (const int ret{gnutls_x509_privkey_init(&newKey)})
|
||||
{
|
||||
throw std::runtime_error{fmt::format("Error initializing privatkey: {}",
|
||||
gnutls_strerror(ret))};
|
||||
}
|
||||
std::unique_ptr<gnutls_x509_privkey_int, decltype(&gnutls_x509_privkey_deinit)> keyDeleter(
|
||||
newKey, gnutls_x509_privkey_deinit);
|
||||
|
||||
if (const int ret{gnutls_x509_privkey_generate(newKey, GNUTLS_PK_RSA, 3072, 0)})
|
||||
{
|
||||
throw std::runtime_error{fmt::format("Error generating privatkey: {}",
|
||||
gnutls_strerror(ret))};
|
||||
}
|
||||
|
||||
gnutls_datum_t exportedKey;
|
||||
if (const int ret{gnutls_x509_privkey_export2_pkcs8(newKey, GNUTLS_X509_FMT_DER, nullptr, 0,
|
||||
&exportedKey)})
|
||||
{
|
||||
throw std::runtime_error{fmt::format("Error exporting privatkey: {}",
|
||||
gnutls_strerror(ret))};
|
||||
}
|
||||
const std::unique_ptr<unsigned char, decltype(gnutls_free)> exportedKeyDeleter{exportedKey.data,
|
||||
gnutls_free};
|
||||
|
||||
std::ofstream{privatekey, binaryOverrideMode}.write(
|
||||
reinterpret_cast<const char*>(exportedKey.data), exportedKey.size);
|
||||
|
||||
|
||||
gnutls_x509_crt_t newCertificate;
|
||||
if (const int ret{gnutls_x509_crt_init(&newCertificate)})
|
||||
{
|
||||
throw std::runtime_error{fmt::format("Error initializing certificate: {}",
|
||||
gnutls_strerror(ret))};
|
||||
}
|
||||
std::unique_ptr<gnutls_x509_crt_int, decltype(&gnutls_x509_crt_deinit)> certificateDeleter(
|
||||
newCertificate, gnutls_x509_crt_deinit);
|
||||
|
||||
std::array<unsigned char, 20> serialNumber;
|
||||
auto generator = [&, randomDevice = std::random_device{},
|
||||
distribution = std::uniform_int_distribution<unsigned char>{}]() mutable
|
||||
{
|
||||
return distribution(randomDevice);
|
||||
};
|
||||
std::ranges::generate(serialNumber, std::ref(generator));
|
||||
std::get<0>(serialNumber) &= ~(1 << 8);
|
||||
if (const int ret{gnutls_x509_crt_set_serial(newCertificate, serialNumber.data(),
|
||||
serialNumber.size())})
|
||||
{
|
||||
throw std::runtime_error{fmt::format("Error setting serial number: {}",
|
||||
gnutls_strerror(ret))};
|
||||
}
|
||||
|
||||
if (const int ret{gnutls_x509_crt_set_activation_time(newCertificate, 0)})
|
||||
{
|
||||
throw std::runtime_error{fmt::format("Error setting activation time: {}",
|
||||
gnutls_strerror(ret))};
|
||||
}
|
||||
if (const int ret{gnutls_x509_crt_set_expiration_time(newCertificate, -1)})
|
||||
{
|
||||
throw std::runtime_error{fmt::format("Error setting expiration time: {}",
|
||||
gnutls_strerror(ret))};
|
||||
}
|
||||
|
||||
if (const int ret{gnutls_x509_crt_set_key(newCertificate, newKey)})
|
||||
{
|
||||
throw std::runtime_error{fmt::format("Error setting certificate key: {}",
|
||||
gnutls_strerror(ret))};
|
||||
}
|
||||
|
||||
if (const int ret{gnutls_x509_crt_sign(newCertificate, newCertificate, newKey)})
|
||||
{
|
||||
throw std::runtime_error{fmt::format("Error signing certificate: {}",
|
||||
gnutls_strerror(ret))};
|
||||
}
|
||||
|
||||
gnutls_datum_t exportedCertificate;
|
||||
if (const int ret{gnutls_x509_crt_export2(newCertificate, GNUTLS_X509_FMT_DER,
|
||||
&exportedCertificate)})
|
||||
{
|
||||
throw std::runtime_error{fmt::format("Error exporting certificate: {}",
|
||||
gnutls_strerror(ret))};
|
||||
}
|
||||
const std::unique_ptr<unsigned char, decltype(gnutls_free)> exportedCertificateDeleter{
|
||||
exportedCertificate.data, gnutls_free};
|
||||
|
||||
std::ofstream{certificate, binaryOverrideMode}.write(
|
||||
reinterpret_cast<const char*>(exportedCertificate.data), exportedCertificate.size);
|
||||
}
|
||||
|
||||
std::unique_ptr<gnutls_certificate_credentials_st, CredentialsDeleter> CreateTlsServerCredentials()
|
||||
{
|
||||
gnutls_certificate_credentials_t temp;
|
||||
|
|
@ -224,8 +327,20 @@ std::unique_ptr<gnutls_certificate_credentials_st, CredentialsDeleter> CreateTls
|
|||
}
|
||||
std::unique_ptr<gnutls_certificate_credentials_st, CredentialsDeleter> cred{temp};
|
||||
|
||||
if (const int ret{gnutls_certificate_set_x509_key_file(cred.get(), "certificate.pem",
|
||||
"privatekey.pem", GNUTLS_X509_FMT_PEM)})
|
||||
const auto folder = (Paths(g_CmdLineArgs).UserData() / "encryption").fileSystemPath();
|
||||
std::filesystem::create_directory(folder);
|
||||
|
||||
const auto privatekeyPath{folder / "privatekey.der"};
|
||||
const auto certificatePath{folder / "certificate.der"};
|
||||
|
||||
if (!std::filesystem::is_regular_file(privatekeyPath) ||
|
||||
!std::filesystem::is_regular_file(certificatePath))
|
||||
{
|
||||
GenerateKeyfiles(privatekeyPath, certificatePath);
|
||||
}
|
||||
|
||||
if (const int ret{gnutls_certificate_set_x509_key_file(cred.get(), certificatePath.c_str(),
|
||||
privatekeyPath.c_str(), GNUTLS_X509_FMT_DER)})
|
||||
{
|
||||
throw std::runtime_error{fmt::format("gnutls_certificate_set_x509_key_file: {}",
|
||||
gnutls_strerror(ret))};
|
||||
|
|
@ -1362,9 +1477,9 @@ bool CNetServerWorker::OnSimulationCommand(CNetServerSession* session, CFsmEvent
|
|||
server.Multicast(message, { NSS_INGAME });
|
||||
|
||||
// Save all the received commands
|
||||
if (server.m_SavedCommands.size() < message->m_Turn + 1)
|
||||
server.m_SavedCommands.resize(message->m_Turn + 1);
|
||||
server.m_SavedCommands[message->m_Turn].push_back(*message);
|
||||
if (server.m_SavedCommands.size() < static_cast<size_t>(message->m_Turn) + 1)
|
||||
server.m_SavedCommands.resize(static_cast<size_t>(message->m_Turn) + 1);
|
||||
server.m_SavedCommands[static_cast<size_t>(message->m_Turn)].push_back(*message);
|
||||
|
||||
// TODO: we shouldn't send the message back to the client that first sent it
|
||||
return true;
|
||||
|
|
@ -1586,14 +1701,14 @@ bool CNetServerWorker::OnJoinSyncingLoadedGame(CNetServerSession* session, CFsmE
|
|||
|
||||
CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
|
||||
|
||||
u32 turn = message->m_CurrentTurn;
|
||||
u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
|
||||
turn_id_t turn = message->m_CurrentTurn;
|
||||
turn_id_t readyTurn = server.m_ServerTurnManager->GetReadyTurn();
|
||||
|
||||
// Send them all commands received since their saved state,
|
||||
// and turn-ended messages for any turns that have already been processed
|
||||
for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i)
|
||||
for (turn_id_t i = turn + 1; i < std::max(readyTurn + 1, static_cast<turn_id_t>(server.m_SavedCommands.size())); ++i)
|
||||
{
|
||||
if (i < server.m_SavedCommands.size())
|
||||
if (static_cast<size_t>(i) < server.m_SavedCommands.size())
|
||||
for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
|
||||
session->SendMessage(&server.m_SavedCommands[i][j]);
|
||||
|
||||
|
|
@ -1830,7 +1945,7 @@ CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
|
|||
if (unique)
|
||||
return name;
|
||||
|
||||
name = original + L" (" + CStrW::FromUInt(id++) + L")";
|
||||
name = fmt::format(L"{}({})", original, id++);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
#include "network/NetServerSession.h"
|
||||
#include "ps/CLogger.h"
|
||||
#include "ps/ConfigDB.h"
|
||||
#include "simulation2/helpers/SimulationCommand.h"
|
||||
#include "simulation2/system/TurnManager.h"
|
||||
|
||||
#include <limits>
|
||||
|
|
@ -50,7 +51,7 @@ CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server)
|
|||
m_SavedTurnLengths.push_back(m_TurnLength);
|
||||
}
|
||||
|
||||
void CNetServerTurnManager::NotifyFinishedClientCommands(CNetServerSession& session, u32 turn)
|
||||
void CNetServerTurnManager::NotifyFinishedClientCommands(CNetServerSession& session, turn_id_t turn)
|
||||
{
|
||||
int client = session.GetHostID();
|
||||
|
||||
|
|
@ -105,11 +106,11 @@ void CNetServerTurnManager::CheckClientsReady()
|
|||
msg.m_Turn = m_ReadyTurn;
|
||||
m_NetServer.Multicast(&msg, { NSS_INGAME });
|
||||
|
||||
ENSURE(m_SavedTurnLengths.size() == m_ReadyTurn);
|
||||
ENSURE(std::cmp_equal(m_SavedTurnLengths.size(), m_ReadyTurn));
|
||||
m_SavedTurnLengths.push_back(m_TurnLength);
|
||||
}
|
||||
|
||||
void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& session, u32 turn, const CStr& hash)
|
||||
void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& session, turn_id_t turn, const CStr& hash)
|
||||
{
|
||||
|
||||
int client = session.GetHostID();
|
||||
|
|
@ -138,13 +139,13 @@ void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& sessio
|
|||
m_ClientStateHashes[turn][client] = hash;
|
||||
|
||||
// Find the newest turn which we know all clients have simulated
|
||||
u32 newest = std::numeric_limits<u32>::max();
|
||||
turn_id_t newest = std::numeric_limits<turn_id_t>::max();
|
||||
for (const std::pair<const int, Client>& clientData : m_ClientsData)
|
||||
if (clientData.second.simulatedTurn < newest)
|
||||
newest = clientData.second.simulatedTurn;
|
||||
|
||||
// For every set of state hashes that all clients have simulated, check for OOS
|
||||
for (const std::pair<const u32, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
|
||||
for (const std::pair<const turn_id_t, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
|
||||
{
|
||||
if (clientStateHash.first > newest)
|
||||
break;
|
||||
|
|
@ -187,7 +188,7 @@ void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& sessio
|
|||
m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1));
|
||||
}
|
||||
|
||||
void CNetServerTurnManager::InitialiseClient(int client, u32 turn, bool observer)
|
||||
void CNetServerTurnManager::InitialiseClient(int client, turn_id_t turn, bool observer)
|
||||
{
|
||||
NETSERVERTURN_LOG("InitialiseClient(client=%d, turn=%d)\n", client, turn);
|
||||
|
||||
|
|
@ -206,7 +207,7 @@ void CNetServerTurnManager::UninitialiseClient(int client)
|
|||
bool checkOOS = m_ClientsData[client].isOOS;
|
||||
m_ClientsData.erase(client);
|
||||
|
||||
for (std::pair<const u32, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
|
||||
for (std::pair<const turn_id_t, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
|
||||
clientStateHash.second.erase(client);
|
||||
|
||||
// Check whether we're ready for the next turn now that we're not
|
||||
|
|
@ -228,8 +229,8 @@ void CNetServerTurnManager::SetTurnLength(u32 msecs)
|
|||
m_TurnLength = msecs;
|
||||
}
|
||||
|
||||
u32 CNetServerTurnManager::GetSavedTurnLength(u32 turn)
|
||||
u32 CNetServerTurnManager::GetSavedTurnLength(turn_id_t turn)
|
||||
{
|
||||
ENSURE(turn <= m_ReadyTurn);
|
||||
return m_SavedTurnLengths.at(turn);
|
||||
return m_SavedTurnLengths.at(static_cast<size_t>(turn));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2025 Wildfire Games.
|
||||
/* Copyright (C) 2026 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
#include "lib/code_annotation.h"
|
||||
#include "lib/types.h"
|
||||
#include "ps/CStr.h"
|
||||
#include "simulation2/helpers/SimulationCommand.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
|
@ -44,15 +45,15 @@ class CNetServerTurnManager
|
|||
public:
|
||||
CNetServerTurnManager(CNetServerWorker& server);
|
||||
|
||||
void NotifyFinishedClientCommands(CNetServerSession& session, u32 turn);
|
||||
void NotifyFinishedClientCommands(CNetServerSession& session, turn_id_t turn);
|
||||
|
||||
void NotifyFinishedClientUpdate(CNetServerSession& session, u32 turn, const CStr& hash);
|
||||
void NotifyFinishedClientUpdate(CNetServerSession& session, turn_id_t turn, const CStr& hash);
|
||||
|
||||
/**
|
||||
* Inform the turn manager of a new client
|
||||
* @param observer - whether this client is an observer.
|
||||
*/
|
||||
void InitialiseClient(int client, u32 turn, bool observer);
|
||||
void InitialiseClient(int client, turn_id_t turn, bool observer);
|
||||
|
||||
/**
|
||||
* Inform the turn manager that a previously-initialised client has left the game.
|
||||
|
|
@ -65,13 +66,13 @@ public:
|
|||
* Returns the latest turn for which all clients are ready;
|
||||
* they will have already been told to execute this turn.
|
||||
*/
|
||||
u32 GetReadyTurn() { return m_ReadyTurn; }
|
||||
turn_id_t GetReadyTurn() { return m_ReadyTurn; }
|
||||
|
||||
/**
|
||||
* Returns the turn length that was used for the given turn.
|
||||
* Requires turn <= GetReadyTurn().
|
||||
*/
|
||||
u32 GetSavedTurnLength(u32 turn);
|
||||
u32 GetSavedTurnLength(turn_id_t turn);
|
||||
|
||||
private:
|
||||
void CheckClientsReady();
|
||||
|
|
@ -80,9 +81,9 @@ private:
|
|||
{
|
||||
CStrW playerName;
|
||||
// Latest turn for which all commands have been received.
|
||||
u32 readyTurn;
|
||||
turn_id_t readyTurn;
|
||||
// Last known simulated turn.
|
||||
u32 simulatedTurn;
|
||||
turn_id_t simulatedTurn;
|
||||
bool isObserver;
|
||||
bool isOOS = false;
|
||||
};
|
||||
|
|
@ -93,10 +94,10 @@ private:
|
|||
bool m_HasSyncError = false;
|
||||
|
||||
// Map of turn -> {Client ID -> state hash}; old indexes <= min(m_ClientsSimulated) are deleted
|
||||
std::map<u32, std::map<int, std::string>> m_ClientStateHashes;
|
||||
std::map<turn_id_t, std::map<int, std::string>> m_ClientStateHashes;
|
||||
|
||||
/// The latest turn for which we have received all commands from all clients
|
||||
u32 m_ReadyTurn;
|
||||
turn_id_t m_ReadyTurn;
|
||||
|
||||
// Current turn length
|
||||
u32 m_TurnLength;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
#include "network/NetServerSession.h"
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <ngtcp2/ngtcp2.h>
|
||||
#include <string>
|
||||
|
||||
|
|
@ -75,7 +76,7 @@ const std::vector<ProfileColumn>& CNetStatsTable::GetColumns()
|
|||
std::lock_guard<std::mutex> lock(m_Mutex);
|
||||
|
||||
for (size_t i = 0; i < m_LatchedData.size(); ++i)
|
||||
m_ColumnDescriptions.push_back(ProfileColumn("Peer "+CStr::FromUInt(i), 80));
|
||||
m_ColumnDescriptions.push_back(ProfileColumn(fmt::format("Peer {}", i), 80));
|
||||
}
|
||||
|
||||
return m_ColumnDescriptions;
|
||||
|
|
@ -106,13 +107,13 @@ CStr CNetStatsTable::GetCellText(size_t row, size_t col)
|
|||
ROW(Row_OutData, "outgoing bytes", {});
|
||||
ROW(Row_LastSendTime, "last send time", {});
|
||||
ROW(Row_LastRecvTime, "last receive time", {});
|
||||
ROW(Row_NextTimeout, "next timeout", CStr::FromUInt(ngtcp2_conn_get_expiry(m_Connection)));
|
||||
ROW(Row_NextTimeout, "next timeout", std::to_string(ngtcp2_conn_get_expiry(m_Connection)));
|
||||
ROW(Row_PacketsSent, "packets sent", {});
|
||||
ROW(Row_PacketsLost, "packets lost", {});
|
||||
ROW(Row_LastRTT, "last RTT", CStr::FromUInt(info.latest_rtt));
|
||||
ROW(Row_RTT, "mean RTT", CStr::FromUInt(info.smoothed_rtt));
|
||||
ROW(Row_MTU, "MTU", CStr::FromUInt(ngtcp2_conn_get_path_max_tx_udp_payload_size(m_Connection)));
|
||||
ROW(Row_ReliableInTransit, "reliable data in transit", CStr::FromUInt(info.bytes_in_flight));
|
||||
ROW(Row_LastRTT, "last RTT", std::to_string(info.latest_rtt));
|
||||
ROW(Row_RTT, "mean RTT", std::to_string(info.smoothed_rtt));
|
||||
ROW(Row_MTU, "MTU", std::to_string(ngtcp2_conn_get_path_max_tx_udp_payload_size(m_Connection)));
|
||||
ROW(Row_ReliableInTransit, "reliable data in transit", std::to_string(info.bytes_in_flight));
|
||||
|
||||
default:
|
||||
return "???";
|
||||
|
|
@ -131,7 +132,7 @@ void CNetStatsTable::LatchHostState(const std::span<std::unique_ptr<CNetServerSe
|
|||
std::lock_guard<std::mutex> lock(m_Mutex);
|
||||
|
||||
#define ROW(id, title, member) \
|
||||
m_LatchedData[i].push_back(CStr::FromUInt(info.member));
|
||||
m_LatchedData[i].push_back(std::to_string(info.member));
|
||||
|
||||
m_LatchedData.clear();
|
||||
m_LatchedData.resize(sessions.size());
|
||||
|
|
@ -148,7 +149,7 @@ void CNetStatsTable::LatchHostState(const std::span<std::unique_ptr<CNetServerSe
|
|||
m_LatchedData[i].push_back({});
|
||||
// ROW(Row_LastRecvTime, "last receive time", lastReceiveTime);
|
||||
m_LatchedData[i].push_back({});
|
||||
m_LatchedData[i].push_back(CStr::FromUInt(ngtcp2_conn_get_expiry(
|
||||
m_LatchedData[i].push_back(std::to_string(ngtcp2_conn_get_expiry(
|
||||
sessions[i]->m_Connection.m_QuicConnection.get())));
|
||||
// ROW(Row_PacketsSent, "packets sent", pkt_sent);
|
||||
m_LatchedData[i].push_back({});
|
||||
|
|
@ -156,7 +157,7 @@ void CNetStatsTable::LatchHostState(const std::span<std::unique_ptr<CNetServerSe
|
|||
m_LatchedData[i].push_back({});
|
||||
ROW(Row_LastRTT, "last RTT", latest_rtt);
|
||||
ROW(Row_RTT, "mean RTT", smoothed_rtt);
|
||||
m_LatchedData[i].push_back(CStr::FromUInt(ngtcp2_conn_get_path_max_tx_udp_payload_size(
|
||||
m_LatchedData[i].push_back(std::to_string(ngtcp2_conn_get_path_max_tx_udp_payload_size(
|
||||
sessions[i]->m_Connection.m_QuicConnection.get())));
|
||||
ROW(Row_ReliableInTransit, "reliable data in transit", bytes_in_flight);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2025 Wildfire Games.
|
||||
/* Copyright (C) 2026 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
static inline CStr NetMessageStringConvert(u32 arg)
|
||||
{
|
||||
return CStr::FromUInt(arg);
|
||||
return std::to_string(arg);
|
||||
}
|
||||
|
||||
static inline CStr NetMessageStringConvert(const CStr8& arg)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2021 Wildfire Games.
|
||||
/* Copyright (C) 2026 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -202,27 +202,6 @@ CStr CStr::Repeat(const CStr& str, size_t reps)
|
|||
|
||||
// Construction from numbers:
|
||||
|
||||
CStr CStr::FromInt(int n)
|
||||
{
|
||||
tstringstream<StrBase> ss;
|
||||
ss << n;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
CStr CStr::FromUInt(unsigned int n)
|
||||
{
|
||||
tstringstream<StrBase> ss;
|
||||
ss << n;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
CStr CStr::FromInt64(i64 n)
|
||||
{
|
||||
tstringstream<StrBase> ss;
|
||||
ss << n;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
CStr CStr::FromDouble(double n)
|
||||
{
|
||||
tstringstream<StrBase> ss;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2025 Wildfire Games.
|
||||
/* Copyright (C) 2026 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -95,9 +95,6 @@ public:
|
|||
|
||||
// Conversions:
|
||||
|
||||
static CStr FromInt(int n);
|
||||
static CStr FromUInt(unsigned int n);
|
||||
static CStr FromInt64(i64 n);
|
||||
static CStr FromDouble(double n);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ CGame::CGame(bool replayLog, const SimulationDebugOptions debugOptions):
|
|||
m_SimRate(1.0f),
|
||||
m_PlayerID(-1),
|
||||
m_ViewedPlayerID(-1),
|
||||
m_IsSavedGame(false),
|
||||
m_IsVisualReplay(false),
|
||||
m_ReplayStream(NULL)
|
||||
{
|
||||
|
|
@ -224,7 +223,7 @@ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& saved
|
|||
const Script::Interface& scriptInterface = m_Simulation2->GetScriptInterface();
|
||||
Script::Request rq(scriptInterface);
|
||||
|
||||
m_IsSavedGame = !savedState.empty();
|
||||
const bool isSavedGame{!savedState.empty()};
|
||||
|
||||
m_Simulation2->SetInitAttributes(attribs);
|
||||
|
||||
|
|
@ -285,7 +284,7 @@ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& saved
|
|||
co_return g_Renderer.GetSceneRenderer().GetWaterManager().LoadWaterTextures();
|
||||
}, L"LoadWaterTextures", 80);
|
||||
|
||||
if (m_IsSavedGame)
|
||||
if (isSavedGame)
|
||||
PS::Loader::Register(std::bind_front(
|
||||
[](CGame* game, const std::string& state) -> PS::Loader::Task
|
||||
{
|
||||
|
|
@ -298,13 +297,32 @@ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& saved
|
|||
co_return game->LoadVisualReplayData();
|
||||
}, this), L"Loading visual replay data", 1000);
|
||||
|
||||
// Call the script function InitGame only for new games, not saved games
|
||||
if (!isSavedGame)
|
||||
{
|
||||
// Perform some simulation initializations (replace skirmish entities, explore territories, etc.)
|
||||
// that needs to be done before setting up the AI and shouldn't be done in Atlas
|
||||
if (!g_AtlasGameLoop->running)
|
||||
{
|
||||
PS::Loader::Register(std::bind_front([](CGame* game) -> PS::Loader::Task
|
||||
{
|
||||
game->m_Simulation2->PreInitGame();
|
||||
co_return 0;
|
||||
}, this), L"PreInitGame", 5000);
|
||||
}
|
||||
|
||||
PS::Loader::Register(std::bind_front([](CGame* game) -> PS::Loader::Task
|
||||
{
|
||||
game->m_Simulation2->InitGame();
|
||||
co_return 0;
|
||||
}, this), L"InitGame", 4000);
|
||||
}
|
||||
|
||||
PS::Loader::EndRegistering();
|
||||
}
|
||||
|
||||
int CGame::LoadInitialState(const std::string& savedState)
|
||||
{
|
||||
ENSURE(m_IsSavedGame);
|
||||
|
||||
std::stringstream stream(savedState);
|
||||
|
||||
bool ok = m_Simulation2->DeserializeState(stream);
|
||||
|
|
@ -324,17 +342,6 @@ int CGame::LoadInitialState(const std::string& savedState)
|
|||
**/
|
||||
PSRETURN CGame::ReallyStartGame()
|
||||
{
|
||||
// Call the script function InitGame only for new games, not saved games
|
||||
if (!m_IsSavedGame)
|
||||
{
|
||||
// Perform some simulation initializations (replace skirmish entities, explore territories, etc.)
|
||||
// that needs to be done before setting up the AI and shouldn't be done in Atlas
|
||||
if (!g_AtlasGameLoop->running)
|
||||
m_Simulation2->PreInitGame();
|
||||
|
||||
m_Simulation2->InitGame();
|
||||
}
|
||||
|
||||
// We need to do an initial Interpolate call to set up all the models etc,
|
||||
// because Update might never interpolate (e.g. if the game starts paused)
|
||||
// and we could end up rendering before having set up any models (so they'd
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2025 Wildfire Games.
|
||||
/* Copyright (C) 2026 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -226,7 +226,6 @@ private:
|
|||
std::vector<CColor> m_PlayerColors;
|
||||
|
||||
int LoadInitialState(const std::string& savedState);
|
||||
bool m_IsSavedGame; // true if loading a saved game; false for a new game
|
||||
|
||||
bool m_CheatsEnabled;
|
||||
|
||||
|
|
|
|||
|
|
@ -826,7 +826,7 @@ bool Autostart(const CmdLineArgs& args)
|
|||
Script::GetPropertyInt(rq, pageData, 1, &pageArgs);
|
||||
Script::StructuredClone clonedpageArgs{Script::WriteStructuredClone(rq, pageArgs)};
|
||||
|
||||
g_GUI->OpenChildPage(pageName, std::move(clonedpageArgs));
|
||||
g_GUI->SwitchPage(pageName, &scriptInterface, pageArgs);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2025 Wildfire Games.
|
||||
/* Copyright (C) 2026 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
#include <SDL_keycode.h>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <fmt/format.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
|
@ -122,7 +123,7 @@ CStr FindScancodeName(SDL_Scancode scancode)
|
|||
const char* name = SDL_GetScancodeName(scancode);
|
||||
// Some scancodes have no name, but we must have something to save/load/recognize it, so parse it as SYM_XX
|
||||
if (strlen(name) == 0)
|
||||
return CStr("SYM_") + CStr::FromInt(scancode);
|
||||
return fmt::format("SYM_{}", static_cast<int>(scancode));
|
||||
return name;
|
||||
}
|
||||
|
||||
|
|
@ -221,7 +222,7 @@ CStr FindKeyName(SDL_Scancode scancode)
|
|||
return name;
|
||||
|
||||
// Else, show something regardless, so the player knows it's at least recognized.
|
||||
return CStr("SYM_") + CStr::FromInt(scancode);
|
||||
return fmt::format("SYM_{}", static_cast<int>(scancode));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ void CProfiler2::RemoveThreadStorage(ThreadStorage* storage)
|
|||
}
|
||||
|
||||
CProfiler2::ThreadStorage::ThreadStorage(CProfiler2& profiler, const std::string& name) :
|
||||
m_Profiler(profiler), m_Name(name), m_BufferPos0(0), m_BufferPos1(0), m_LastTime(timer_Time())
|
||||
m_Profiler(profiler), m_Name(name), m_LastTime(timer_Time())
|
||||
{
|
||||
m_Buffer = new u8[BUFFER_SIZE];
|
||||
memset(m_Buffer, ITEM_NOP, BUFFER_SIZE);
|
||||
|
|
@ -268,50 +268,45 @@ CProfiler2::ThreadStorage::~ThreadStorage()
|
|||
|
||||
void CProfiler2::ThreadStorage::Write(EItem type, const void* item, u32 itemSize)
|
||||
{
|
||||
// See m_BufferPos0 etc for comments on synchronisation
|
||||
// See RingBufferSpan for comments on synchronisation
|
||||
|
||||
u32 size = 1 + itemSize;
|
||||
u32 start = m_BufferPos0;
|
||||
std::uint32_t start{m_ValidRange.begin.load()};
|
||||
if (start + size > BUFFER_SIZE)
|
||||
{
|
||||
// The remainder of the buffer is too small - fill the rest
|
||||
// with NOPs then start from offset 0, so we don't have to
|
||||
// bother splitting the real item across the end of the buffer
|
||||
|
||||
m_BufferPos0 = size;
|
||||
COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer
|
||||
m_ValidRange.begin.store(size);
|
||||
|
||||
memset(m_Buffer + start, 0, BUFFER_SIZE - start);
|
||||
start = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_BufferPos0 = start + size;
|
||||
COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer
|
||||
m_ValidRange.begin.store(start + size);
|
||||
}
|
||||
|
||||
m_Buffer[start] = (u8)type;
|
||||
memcpy(&m_Buffer[start + 1], item, itemSize);
|
||||
|
||||
COMPILER_FENCE; // must write m_BufferPos1 after m_Buffer
|
||||
m_BufferPos1 = start + size;
|
||||
m_ValidRange.end.store(start + size);
|
||||
}
|
||||
|
||||
std::string CProfiler2::ThreadStorage::GetBuffer()
|
||||
{
|
||||
// Called from an arbitrary thread (not the one writing to the buffer).
|
||||
//
|
||||
// See comments on m_BufferPos0 etc.
|
||||
// See comments on RingBufferSpan.
|
||||
|
||||
std::unique_ptr<u8[]> buffer{std::make_unique<u8[]>(BUFFER_SIZE)};
|
||||
|
||||
u32 pos1 = m_BufferPos1;
|
||||
COMPILER_FENCE; // must read m_BufferPos1 before m_Buffer
|
||||
const std::uint32_t pos1{m_ValidRange.end.load()};
|
||||
|
||||
memcpy(buffer.get(), m_Buffer, BUFFER_SIZE);
|
||||
|
||||
COMPILER_FENCE; // must read m_BufferPos0 after m_Buffer
|
||||
u32 pos0 = m_BufferPos0;
|
||||
const std::uint32_t pos0{m_ValidRange.begin.load()};
|
||||
|
||||
// The range [pos1, pos0) modulo BUFFER_SIZE is invalid, so concatenate the rest of the buffer
|
||||
|
||||
|
|
@ -522,7 +517,7 @@ void rewriteBuffer(u8* buffer, u32& bufferSize)
|
|||
std::string basic = attrib;
|
||||
std::map<std::string, double>::iterator time_attrib = time_per_attribute.find(attrib);
|
||||
if (time_attrib != time_per_attribute.end())
|
||||
basic += " " + CStr::FromInt(1000000*time_attrib->second) + "us";
|
||||
basic = fmt::format("{} {}us", basic, 1000000 * time_attrib->second);
|
||||
|
||||
u32 length = static_cast<u32>(basic.size());
|
||||
memcpy(buffer + writePos, &length, sizeof(length));
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@
|
|||
#include "lib/types.h"
|
||||
#include "ps/ThreadUtil.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdarg>
|
||||
#include <cstring>
|
||||
#include <iosfwd>
|
||||
|
|
@ -223,16 +224,18 @@ private:
|
|||
// To allow hopefully-safe reading of the buffer from a separate thread,
|
||||
// without any expensive synchronisation in the recording thread,
|
||||
// two copies of the current buffer write position are stored.
|
||||
// BufferPos0 is updated before writing; BufferPos1 is updated after writing.
|
||||
// GetBuffer can read Pos1, copy the buffer, read Pos0, then assume any bytes
|
||||
// outside the range Pos1 <= x < Pos0 are safe to use. (Any in that range might
|
||||
// begin is updated before writing; end is updated after writing.
|
||||
// GetBuffer can load end, copy the buffer, load begin, then assume any bytes
|
||||
// outside the range end <= x < begin are safe to use. (Any in that range might
|
||||
// be half-written and corrupted.) (All ranges are modulo BUFFER_SIZE.)
|
||||
// Outside of Write(), these will always be equal.
|
||||
//
|
||||
// TODO: does this attempt at synchronisation (plus use of COMPILER_FENCE etc)
|
||||
// actually work in practice?
|
||||
u32 m_BufferPos0;
|
||||
u32 m_BufferPos1;
|
||||
|
||||
struct RingBufferSpan
|
||||
{
|
||||
std::atomic<std::uint32_t> begin{0};
|
||||
std::atomic<std::uint32_t> end{0};
|
||||
};
|
||||
RingBufferSpan m_ValidRange;
|
||||
};
|
||||
|
||||
public:
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue